diff --git a/CHANGELOG.md b/CHANGELOG.md index 6101faf3a..9443ccf81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +## 2.20.3 - 2026-11-04 + +- Rollback, embedded Workflow package: 2.20.3 changes no schema, history + event or public API. Rolling back restores the shared-cursor, empty + receive replay and fake task-drain precision bugs. A kept stream now + records receives at the current step. Earlier versions look them up at the opening step and can replay + those runs differently after rollback. - A message on one stream stays receivable until it is consumed or expires (#1011). The run's message cursor is shared by signals, updates and every stream, so a signal, an update or a receive on another stream used to move @@ -24,6 +31,18 @@ its replay matches too. Consumptions that earlier releases recorded at the opened step still replay. +- `WorkflowStub::runReadyTasks()` now drains fake-workflow tasks at their + exact due time, including fractional seconds (#1026). It compares the + sampled clock using the configured task model's persisted precision. + SQLite previously skipped tasks due at that instant because it compared + their fractional timestamps with a whole-second query value. Future + fractional deadlines remain pending until they are due. +- Known limitation (#1024): a query can return nothing for a message an + update handler received, because the query replays the handler at the + wrong step. The query changes no message rows. The workflow's own replay + and completed output still return what the handler received. This issue + predates 2.20.3 and remains open. + ## 2.20.2 - 2026-11-04 - Rollback, embedded Workflow package: 2.20.2 changes no schema, history diff --git a/composer.json b/composer.json index 0169cbe19..db3ff7f3c 100644 --- a/composer.json +++ b/composer.json @@ -87,7 +87,7 @@ "dev-main": "2.0.x-dev" }, "durable-workflow": { - "product-train": "2.20.2", + "product-train": "2.20.3", "laravel-embedded-upgrade-contract": "resources/laravel-embedded-upgrade-contract.json", "laravel-dependency-security-policy": "resources/laravel-dependency-security-policy.json" }, diff --git a/tests/Unit/V2/ExporterExecutionTracingTest.php b/tests/Unit/V2/ExporterExecutionTracingTest.php index efb8901e4..80101c662 100644 --- a/tests/Unit/V2/ExporterExecutionTracingTest.php +++ b/tests/Unit/V2/ExporterExecutionTracingTest.php @@ -42,12 +42,12 @@ public function testCollectorFailureDoesNotChangeColdWorkflowCompletion(string $ $server->stop(); } try { - $admitted = $this->worker($directory, 'admit', $address); + $admitted = $this->worker($directory, 'admit', $address, $mode === 'refused' ? 0 : 1); $workers = []; for ($i = 0; $i < 3; ++$i) { - $workers[] = $this->worker($directory, 'work', $address); + $workers[] = $this->worker($directory, 'work', $address, $mode === 'refused' ? 0 : $i + 2); } - $finished = $this->worker($directory, 'inspect', $address); + $finished = $this->worker($directory, 'inspect', $address, $mode === 'refused' ? 0 : 4); self::assertSame('completed', $finished['status']); self::assertSame([9007199254740993, null, '雪'], $finished['output']); self::assertSame(3, $finished['tasks']); @@ -230,7 +230,22 @@ private function requests(string $directory): array ) : []; } - private function worker(string $directory, string $mode, string $address): array + private function awaitRequests(string $directory, int $count): array + { + $deadline = hrtime(true) + 1_000_000_000; + + do { + $requests = $this->requests($directory); + if (count($requests) >= $count) { + return $requests; + } + usleep(1_000); + } while (hrtime(true) < $deadline); + + return $this->requests($directory); + } + + private function worker(string $directory, string $mode, string $address, int $expectedRequestCount): array { $process = new Process([ PHP_BINARY, __DIR__ . '/../../Fixtures/V2/tracing-process.php', $directory, $mode, 'once', @@ -249,6 +264,9 @@ private function worker(string $directory, string $mode, string $address): array if ($mode === 'work') { self::assertStringContainsString('Export failure', $process->getErrorOutput()); } + // Settle this worker's timed-out export before the next worker + // snapshots the shared journal to detect premature exports. + self::assertCount($expectedRequestCount, $this->awaitRequests($directory, $expectedRequestCount)); return $result; }