diff --git a/lib/Future.php b/lib/Future.php index f2d91d5..1008daa 100644 --- a/lib/Future.php +++ b/lib/Future.php @@ -114,7 +114,9 @@ final class Future $cancellationId = $token?->subscribe(function (\Throwable $reason) use (&$callbackId, $suspension): void { $this->state->unsubscribe($callbackId); - $suspension->throw($reason); + if (!$this->state->isComplete()) { // Resume has already been scheduled if complete. + $suspension->throw($reason); + } }); $callbackId = $this->state->subscribe(static function (?\Throwable $error, mixed $value) use ( diff --git a/test/Future/FutureTest.php b/test/Future/FutureTest.php index e65bec0..d52cfea 100644 --- a/test/Future/FutureTest.php +++ b/test/Future/FutureTest.php @@ -2,6 +2,7 @@ namespace Amp\Future; +use Amp\CancellationTokenSource; use Amp\CancelledException; use Amp\Deferred; use Amp\Future; @@ -9,6 +10,7 @@ use Amp\TimeoutCancellationToken; use PHPUnit\Framework\TestCase; use Revolt\EventLoop\Loop; use function Amp\Future\spawn; +use function Revolt\EventLoop\defer; use function Revolt\EventLoop\delay; class FutureTest extends TestCase @@ -140,6 +142,21 @@ class FutureTest extends TestCase self::assertTrue($future->join($token)); } + public function testCompleteThenCancelJoin(): void + { + $deferred = new Deferred; + $source = new CancellationTokenSource; + $future = $deferred->getFuture(); + + defer(function () use ($future, $source): void { + self::assertSame(1, $future->join($source->getToken())); + }); + + $deferred->complete(1); + $source->cancel(); + + delay(0.01); // Tick the event loop to enter defer callback. + } /** * @template T