1
0
mirror of https://github.com/danog/amp.git synced 2025-01-22 13:21:16 +01:00

Throw unhandled Future failures to the event loop

This commit is contained in:
Aaron Piotrowski 2021-09-19 11:54:02 -05:00
parent 3d5c982f33
commit 493e59e8ab
No known key found for this signature in database
GPG Key ID: ADD1EF783EDE9EEB
5 changed files with 102 additions and 5 deletions

View File

@ -107,6 +107,14 @@ final class Future
return $this->state->isComplete();
}
/**
* Do not forward unhandled errors to the event loop handler.
*/
public function ignore(): void
{
$this->state->ignore();
}
/**
* Awaits the operation to complete.
*

View File

@ -2,6 +2,7 @@
namespace Amp\Internal;
use Amp\UnhandledFutureError;
use Revolt\EventLoop\Loop;
use Amp\Future;
@ -17,8 +18,10 @@ final class FutureState
private bool $complete = false;
private bool $handled = false;
/**
* @var array<string, (callable(?\Throwable, ?T, string): void)>
* @var array<string, callable(?\Throwable, ?T, string): void>
*/
private array $callbacks = [];
@ -29,12 +32,21 @@ final class FutureState
private ?\Throwable $throwable = null;
public function __destruct()
{
if ($this->throwable && !$this->handled) {
$throwable = new UnhandledFutureError($this->throwable);
Loop::queue(static fn () => throw $throwable);
}
}
/**
* Registers a callback to be notified once the operation is complete or errored.
*
* The callback is invoked directly from the event loop context, so suspension within the callback is not possible.
*
* @param (callable(?\Throwable, ?T, string): void) $callback Callback invoked on error / successful completion of the future.
* @param callable(?\Throwable, ?T, string): void $callback Callback invoked on error / successful completion of
* the future.
*
* @return string Identifier that can be used to cancel interest for this future.
*/
@ -42,6 +54,8 @@ final class FutureState
{
$id = self::$nextId++;
$this->handled = true; // Even if unsubscribed later, consider the future handled.
if ($this->complete) {
Loop::queue($callback, $this->throwable, $this->result, $id);
} else {
@ -105,6 +119,14 @@ final class FutureState
return $this->complete;
}
/**
* Suppress the exception thrown to the loop error handler if and operation error is not handled by a callback.
*/
public function ignore(): void
{
$this->handled = true;
}
private function invokeCallbacks(): void
{
$this->complete = true;

View File

@ -0,0 +1,18 @@
<?php
namespace Amp;
/**
* Will be thrown to the event loop error handler in case a future exception is not handled.
*/
final class UnhandledFutureError extends \Error
{
public function __construct(?\Throwable $previous = null)
{
$message = 'Unhandled future error: "' . $previous->getMessage()
. '"; Await the Future with Future::await() before the future is destroyed or use '
. 'Future::ignore() to suppress this exception';
parent::__construct($message, 0, $previous);
}
}

View File

@ -8,6 +8,7 @@ use Amp\Future;
use Amp\TimeoutCancellationToken;
use PHPUnit\Framework\TestCase;
use Revolt\EventLoop\Loop;
use function Amp\delay;
use function Amp\Future\all;
class AllTest extends TestCase
@ -39,6 +40,17 @@ class AllTest extends TestCase
all([Future::error(new \Exception('foo')), Future::complete(2)]);
}
public function testTwoThrowingWithOneLater(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('foo');
$deferred = new Deferred;
Loop::delay(0.1, static fn () => $deferred->error(new \Exception('bar')));
all([Future::error(new \Exception('foo')), $deferred->getFuture()]);
}
public function testTwoGeneratorThrows(): void
{
$this->expectException(\Exception::class);

View File

@ -6,14 +6,16 @@ use Amp\CancellationTokenSource;
use Amp\CancelledException;
use Amp\Deferred;
use Amp\Future;
use Amp\PHPUnit\AsyncTestCase;
use Amp\PHPUnit\LoopCaughtException;
use Amp\PHPUnit\TestException;
use Amp\TimeoutCancellationToken;
use PHPUnit\Framework\TestCase;
use Revolt\EventLoop\Loop;
use function Amp\coroutine;
use function Amp\delay;
use function Revolt\EventLoop\queue;
class FutureTest extends TestCase
class FutureTest extends AsyncTestCase
{
public function testIterate(): void
{
@ -154,8 +156,43 @@ class FutureTest extends TestCase
$deferred->complete(1);
$source->cancel();
}
delay(0.01); // Tick the event loop to enter defer callback.
public function testUnhandledError(): void
{
$deferred = new Deferred;
$deferred->error(new TestException);
unset($deferred);
$this->expectException(LoopCaughtException::class);
}
public function testUnhandledErrorFromFutureError(): void
{
$future = Future::error(new TestException);
unset($future);
$this->expectException(LoopCaughtException::class);
}
public function testIgnoringUnhandledErrors(): void
{
$deferred = new Deferred;
$deferred->getFuture()->ignore();
$deferred->error(new TestException);
unset($deferred);
Loop::setErrorHandler($this->createCallback(0));
}
public function testIgnoreUnhandledErrorFromFutureError(): void
{
$future = Future::error(new TestException);
$future->ignore();
unset($future);
Loop::setErrorHandler($this->createCallback(0));
}
/**