diff --git a/lib/CancellationTokenSource.php b/lib/CancellationTokenSource.php index 2432dd2..fcfa925 100644 --- a/lib/CancellationTokenSource.php +++ b/lib/CancellationTokenSource.php @@ -2,9 +2,6 @@ namespace Amp; -use Revolt\EventLoop\Loop; -use function Revolt\EventLoop\defer; - /** * A cancellation token source provides a mechanism to cancel operations. * @@ -40,72 +37,13 @@ use function Revolt\EventLoop\defer; */ final class CancellationTokenSource { + private Internal\CancellableToken $source; private CancellationToken $token; - /** @var callable|null */ - private $onCancel; - public function __construct() { - $onCancel = &$this->onCancel; - - $this->token = new class($onCancel) implements CancellationToken { - private string $nextId = "a"; - - /** @var callable[] */ - private array $callbacks = []; - - /** @var \Throwable|null */ - private ?\Throwable $exception = null; - - /** - * @param callable|null $onCancel - * @param-out callable $onCancel - */ - public function __construct(?callable &$onCancel) - { - $onCancel = function (\Throwable $exception): void { - $this->exception = $exception; - - $callbacks = $this->callbacks; - $this->callbacks = []; - - foreach ($callbacks as $callback) { - Loop::queue($callback, $this->exception); - } - }; - } - - public function subscribe(callable $callback): string - { - $id = $this->nextId++; - - if ($this->exception) { - Loop::queue($callback, $this->exception); - } else { - $this->callbacks[$id] = $callback; - } - - return $id; - } - - public function unsubscribe(string $id): void - { - unset($this->callbacks[$id]); - } - - public function isRequested(): bool - { - return isset($this->exception); - } - - public function throwIfRequested(): void - { - if (isset($this->exception)) { - throw $this->exception; - } - } - }; + $this->source = new Internal\CancellableToken; + $this->token = new Internal\WrappedCancellationToken($this->source); } public function getToken(): CancellationToken @@ -116,14 +54,8 @@ final class CancellationTokenSource /** * @param \Throwable|null $previous Exception to be used as the previous exception to CancelledException. */ - public function cancel(\Throwable $previous = null): void + public function cancel(?\Throwable $previous = null): void { - if ($this->onCancel === null) { - return; - } - - $onCancel = $this->onCancel; - $this->onCancel = null; - $onCancel(new CancelledException($previous)); + $this->source->cancel($previous); } } diff --git a/lib/CancelledException.php b/lib/CancelledException.php index 25f8c40..7cc1da4 100644 --- a/lib/CancelledException.php +++ b/lib/CancelledException.php @@ -10,7 +10,7 @@ namespace Amp; */ class CancelledException extends \Exception { - public function __construct(\Throwable $previous = null) + public function __construct(?\Throwable $previous = null) { parent::__construct("The operation was cancelled", 0, $previous); } diff --git a/lib/Internal/CancellableToken.php b/lib/Internal/CancellableToken.php new file mode 100644 index 0000000..79a080a --- /dev/null +++ b/lib/Internal/CancellableToken.php @@ -0,0 +1,69 @@ +exception)) { + return; + } + + $this->exception = new CancelledException($previous); + + $callbacks = $this->callbacks; + $this->callbacks = []; + + foreach ($callbacks as $callback) { + Loop::queue($callback, $this->exception); + } + } + + public function subscribe(callable $callback): string + { + $id = $this->nextId++; + + if ($this->exception) { + Loop::queue($callback, $this->exception); + } else { + $this->callbacks[$id] = $callback; + } + + return $id; + } + + public function unsubscribe(string $id): void + { + unset($this->callbacks[$id]); + } + + public function isRequested(): bool + { + return isset($this->exception); + } + + public function throwIfRequested(): void + { + if (isset($this->exception)) { + throw $this->exception; + } + } +} diff --git a/lib/Internal/WrappedCancellationToken.php b/lib/Internal/WrappedCancellationToken.php new file mode 100644 index 0000000..a7aa8a2 --- /dev/null +++ b/lib/Internal/WrappedCancellationToken.php @@ -0,0 +1,36 @@ +token->subscribe($callback); + } + + public function unsubscribe(string $id): void + { + $this->token->unsubscribe($id); + } + + public function isRequested(): bool + { + return $this->token->isRequested(); + } + + public function throwIfRequested(): void + { + $this->token->throwIfRequested(); + } +} diff --git a/lib/TimeoutCancellationToken.php b/lib/TimeoutCancellationToken.php index 53b8956..e94b425 100644 --- a/lib/TimeoutCancellationToken.php +++ b/lib/TimeoutCancellationToken.php @@ -19,13 +19,19 @@ final class TimeoutCancellationToken implements CancellationToken */ public function __construct(float $timeout, string $message = "Operation timed out") { - $source = new CancellationTokenSource; - $this->token = $source->getToken(); + $this->token = $source = new Internal\CancellableToken; + + $trace = null; // Defined in case assertions are disabled. + \assert($trace = \debug_backtrace()); - $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); $this->watcher = Loop::delay($timeout, static function () use ($source, $message, $trace): void { - $trace = Internal\formatStacktrace($trace); - $source->cancel(new TimeoutException("$message\r\nTimeoutCancellationToken was created here:\r\n$trace")); + if ($trace) { + $message .= \sprintf("\r\n%s was created here: %s", self::class, Internal\formatStacktrace($trace)); + } else { + $message .= \sprintf(" (Enable assertions for a backtrace of the %s creation)", self::class); + } + + $source->cancel(new TimeoutException($message)); }); Loop::unreference($this->watcher); diff --git a/test/TimeoutCancellationTokenTest.php b/test/TimeoutCancellationTokenTest.php index c2f8432..9df14a4 100644 --- a/test/TimeoutCancellationTokenTest.php +++ b/test/TimeoutCancellationTokenTest.php @@ -26,8 +26,11 @@ class TimeoutCancellationTokenTest extends AsyncTestCase self::assertInstanceOf(TimeoutException::class, $exception->getPrevious()); $message = $exception->getPrevious()->getMessage(); - self::assertStringContainsString('TimeoutCancellationToken was created here', $message); - self::assertStringContainsString('TimeoutCancellationTokenTest.php:' . $line, $message); + + if ((int)ini_get('zend.assertions') > 0) { + self::assertStringContainsString('TimeoutCancellationToken was created here', $message); + self::assertStringContainsString('TimeoutCancellationTokenTest.php:' . $line, $message); + } } }