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:
parent
3d5c982f33
commit
493e59e8ab
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
|
18
lib/UnhandledFutureError.php
Normal file
18
lib/UnhandledFutureError.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user