At first, let’s refresh in memory what is Promise. A promise represents a result of an asynchronous operation. You can add fulfillment and error handlers to a promise object and they will be invoked once this operation has completed or failed. Check this article to learn more about promises.
Promise is a very powerful tool which allows us to pass around the code the eventual results of some deferred operation. But there is one problem with promises: they don’t give us much control.
Sometimes it may take too long for them to be resolved or rejected and we can’t wait for it.
To cancel a promise at first we need to go and create one. A promise can be created with the
React\Promise\Promise class. Its constructor accepts two arguments:
callable $resolver- a handler being triggered immediately after creating a promise.
callable $canceller- a handler being triggered when a promise is cancelled via
Both handlers accept
$resolve($value) fulfills the promise with the
$reject($reason) simply rejects a promise.
This is a very trivial example. The promise above will be immediately resolved after creation. Not very useful. The simple timer can delay this resolving a bit. To run a timer we need to create an instance of the event loop and then
Now the promise resolves only in 5 seconds. Exactly what we need. So, we can try to cancel this promise. For example, for some reason, we can’t wait 5 seconds and if we haven’t received the result in 2 seconds we don’t care anymore about this promise. How to handle this scenario?
ReactPHP PromiseTimer is a nice component which provides timeouts implementation for promises. To set a timer for the promise there is a simple
timeout(PromiseInterface $promise, $time, LoopInterface $loop) accepts three arguments:
$promiseto be cancelled after timeout.
$timeto wait for a promise to be resolved or rejected.
- an instance of the event
The function itself returns a new promise (a wrapper over the the input promise). The relations between the principal promise and its wrapper are the following:
- When the principal promise resolves before the specified
$time, the wrapper promise also resolves with this fulfillment value.
- If the principal promise rejects before the specified
$timethe wrapper also rejects with the same rejection value.
- And if the principal promise doesn’t settle before the specified
$timeit will be cancelled and the wrapper promise is being rejected with the
Knowing all of this we can try to cancel the promise from the previous example:
Let’s add some debug messages to figure out what is happening with our promise when it is cancelled:
Run this script and see what happens:
The cancel handler has been triggered in 2 seconds as we expected, but it looks like the loop continues to run and stops only after 5 seconds. Seems like the timer from the resolve handler is still working, although we have cancelled the promise. Let’s add a debug message to the timer and run the script to check this:
The guess turned out to be right. But we have cancelled the promise, why is the timer still working? Here things come a bit tricky.
The cancellation of the promise means that the
cancel() method is being called on the promise. Dot.
timeout() function has no idea what is happening inside your promise, so it is your job to handle the cancellation of the promise. You should manually close opened resources like sockets or files, terminate processes and cancel timers. In our case, it means that we should manually
cancel() the timer in the cancel handler. To use the timer in different handlers we can
use statement it in these handlers and pass the timer object by reference:
Now the timer is cancelled and so the promise is truly cancelled. The rule of thumb is:
The promise itself when being cancelled is responsible for cleaning up any resources like open network sockets or file handles or terminating external processes or timers. Otherwise, this promise can still be pending and continue consuming resources.
As already mentioned, the wrapper timeout promise handles the principal promise events. If the principal promise resolves in specified
$time seconds the wrapper promise also will be resolved. If not - it fails:
For example, if the principal promise resolves in 5 seconds it will be cancelled by timeout in 2 seconds and then the timeout promise will be rejected:
But at the same time if the principal promise rejects the timeout promise also will be rejected. How to figure out what is the rejection reason? Was it a timeout or the principal promise has failed? The answer is: by attaching several reject handlers and type-hinting
React\Promise\Timer\TimeoutException in one of them. The
TimeoutException extends PHP’s built-in
Now, when running the script it is much more clear that the promise was cancelled due to a timeout:
Promise timeouts provide more control over the long-running promises. There is no need to wait until they resolve or fail. If we can’t or don’t want to wait we can simply setup a timeout. With timeouts, these promises will be cancelled. But remember that it is our job to handle the cancellation, which means that we should close all opened resources like sockets or files, terminate working processes and cancel running timers.
You can find examples from this article on GitHub.
This article is a part of the ReactPHP Series.