From 3e0194a9c5337ce45e4c7daff8b19e85b0565058 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 13:09:22 +0000 Subject: [PATCH 1/5] Provide explicit maintenance bootstrap for fixture observers --- tests/Integration/CooperativeCancellationTest.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/Integration/CooperativeCancellationTest.php b/tests/Integration/CooperativeCancellationTest.php index 962bbde5..1d8cf298 100644 --- a/tests/Integration/CooperativeCancellationTest.php +++ b/tests/Integration/CooperativeCancellationTest.php @@ -2008,8 +2008,21 @@ public function testBoundedAbandonKeepsRemoteCallbackIndependentAfterParentCance private function client(?Transport $transport = null, string $namespace = 'default'): Client { - return new Client($this->runtimeUrl, namespace: $namespace, token: $this->token, + $client = new Client($this->runtimeUrl, namespace: $namespace, token: $this->token, transport: $transport); + if ($transport !== null && method_exists($client, 'withActivityLeaseBootstrap')) { + // These owner-only observers wrap the default HTTP transport. The + // independent helper recreates that same URI/auth/TLS configuration, + // without inheriting the owner's fault injection or callback pipes. + $bootstrap = $this->directory.'/activity-lease-'.hash('sha256', $namespace).'.php'; + file_put_contents($bootstrap, 'runtimeUrl, true).', namespace: '.var_export($namespace, true).', ' + .'token: getenv("DURABLE_WORKFLOW_AUTH_TOKEN") ?: "test-token");'); + chmod($bootstrap, 0600); + $client = $client->withActivityLeaseBootstrap($bootstrap); + } + + return $client; } private function queue(string $kind): string From 2898d1bb38283703ce6edd0c2a53ba1c1b4d8927 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 13:49:48 +0000 Subject: [PATCH 2/5] Observe independent lease-helper presence in cooperative fixtures --- .../ActivityLeaseObservationTransport.php | 41 +++++++++++++++++++ .../CooperativeCancellationTest.php | 33 +++++++++++---- 2 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 tests/Integration/ActivityLeaseObservationTransport.php diff --git a/tests/Integration/ActivityLeaseObservationTransport.php b/tests/Integration/ActivityLeaseObservationTransport.php new file mode 100644 index 00000000..b2225aa9 --- /dev/null +++ b/tests/Integration/ActivityLeaseObservationTransport.php @@ -0,0 +1,41 @@ +inner = new Psr18Transport(); + } + + public function supportsBoundedRequests(): bool { return $this->inner->supportsBoundedRequests(); } + + public function send(string $method, string $uri, array $headers, ?array $body = null): ?array + { + return $this->inner->send($method, $uri, $headers, $body); + } + + public function sendBounded(string $method, string $uri, array $headers, ?array $body, int $timeoutSeconds): ?array + { + $reply = $this->inner->sendBounded($method, $uri, $headers, $body, $timeoutSeconds); + if (str_ends_with($uri, '/worker/heartbeat') && ($reply['acknowledged'] ?? false) === true) { + $temporary = $this->path.'.tmp'; + file_put_contents($temporary, json_encode([ + 'worker_id' => $reply['worker_id'] ?? null, 'acknowledged' => true, + ], JSON_THROW_ON_ERROR), LOCK_EX); + chmod($temporary, 0600); + rename($temporary, $this->path); + } + + return $reply; + } +} diff --git a/tests/Integration/CooperativeCancellationTest.php b/tests/Integration/CooperativeCancellationTest.php index 1d8cf298..5c12b87d 100644 --- a/tests/Integration/CooperativeCancellationTest.php +++ b/tests/Integration/CooperativeCancellationTest.php @@ -481,7 +481,7 @@ public function testScopedRemoteCallbackStopsWithoutApplicationHeartbeatsAndPare $handle = $client->startWorkflow('tests.php-cooperative', $queue, $queue, [$grouped ? 'scoped-remote-group-only' : 'scoped-remote-only']); $this->awaitMessage($ownerMessages, 'remote-entered'); - $this->awaitMessage($ownerMessages, 'owner-heartbeat'); + $this->awaitOwnerHeartbeat($ownerMessages, $ownerPid, $queue.'-'.$ownerPid); $pids = json_decode((string) file_get_contents($this->directory.'/processes'), true, flags: JSON_THROW_ON_ERROR); foreach ($pids as $pid) { self::assertTrue(posix_kill($pid, 0)); } $fence = json_decode((string) file_get_contents($this->directory.'/remote-fence'), true, flags: JSON_THROW_ON_ERROR); @@ -1546,7 +1546,7 @@ public function testRemoteRequestStopsBlockedOwnerAfterLiveOrColdWorkflowDeliver $handle = $client->startWorkflow('tests.php-cooperative', $queue, $queue, ['remote']); self::assertNotNull($handle->selectedRunId); $this->awaitMessage($ownerMessages, 'remote-entered'); - $this->awaitMessage($ownerMessages, 'owner-heartbeat'); + $this->awaitOwnerHeartbeat($ownerMessages, $ownerPid, $queue.'-'.$ownerPid); $pids = json_decode((string) file_get_contents($this->directory.'/processes'), true, flags: JSON_THROW_ON_ERROR); foreach ($pids as $pid) { self::assertTrue(posix_kill($pid, 0), 'The remote callback must still be active.'); } $remainingWorkflowLease = 0.0; @@ -1949,7 +1949,7 @@ public function testBoundedAbandonKeepsRemoteCallbackIndependentAfterParentCance $this->awaitMessage($ownerMessages, 'registered'); $handle = $client->startWorkflow('tests.php-cooperative', $queue, $queue, ['remote']); $this->awaitMessage($ownerMessages, 'remote-entered'); - $this->awaitMessage($ownerMessages, 'owner-heartbeat'); + $this->awaitOwnerHeartbeat($ownerMessages, $ownerPid, $queue.'-'.$ownerPid); $pids = json_decode((string) file_get_contents($this->directory.'/processes'), true, flags: JSON_THROW_ON_ERROR); $fence = json_decode((string) file_get_contents($this->directory.'/remote-fence'), true, flags: JSON_THROW_ON_ERROR); $accepted = $handle->requestSelectedRunCancellation(cleanupTimeoutSeconds: 30); @@ -2012,12 +2012,16 @@ private function client(?Transport $transport = null, string $namespace = 'defau transport: $transport); if ($transport !== null && method_exists($client, 'withActivityLeaseBootstrap')) { // These owner-only observers wrap the default HTTP transport. The - // independent helper recreates that same URI/auth/TLS configuration, - // without inheriting the owner's fault injection or callback pipes. - $bootstrap = $this->directory.'/activity-lease-'.hash('sha256', $namespace).'.php'; - file_put_contents($bootstrap, 'directory.'/activity-lease-'.getmypid().'-'.hash('sha256', $namespace).'.php'; + file_put_contents($bootstrap, 'runtimeUrl, true).', namespace: '.var_export($namespace, true).', ' - .'token: getenv("DURABLE_WORKFLOW_AUTH_TOKEN") ?: "test-token");'); + .'token: getenv("DURABLE_WORKFLOW_AUTH_TOKEN") ?: "test-token", ' + .'transport: new \\DurableWorkflow\\Tests\\Integration\\ActivityLeaseObservationTransport(' + .var_export($this->directory.'/activity-lease-heartbeat-'.getmypid().'.json', true).'));'); chmod($bootstrap, 0600); $client = $client->withActivityLeaseBootstrap($bootstrap); } @@ -2332,6 +2336,19 @@ function (ActivityContext $context, string $requestId, ?array $cancellationConte return [$pid, $parent]; } + /** @param resource $messages */ + private function awaitOwnerHeartbeat($messages, int $pid, string $workerId): void + { + if (!method_exists(Client::class, 'withActivityLeaseBootstrap')) { + $this->awaitMessage($messages, 'owner-heartbeat'); + return; + } + $name = 'activity-lease-heartbeat-'.$pid.'.json'; + $this->awaitObservationFile($name); + $observation = json_decode((string) file_get_contents($this->directory.'/'.$name), true, flags: JSON_THROW_ON_ERROR); + self::assertSame(['worker_id' => $workerId, 'acknowledged' => true], $observation); + } + /** @param resource $messages */ private function awaitMessage($messages, string $expected, int $timeoutSeconds = 15): void { From 6dda1378b8977b14864e8ca9213fdf6ac131a0a7 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:22:36 +0000 Subject: [PATCH 3/5] Give lease-reclaim fixtures an explicit retry budget --- tests/Integration/CooperativeCancellationTest.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/Integration/CooperativeCancellationTest.php b/tests/Integration/CooperativeCancellationTest.php index 5c12b87d..95ddbefa 100644 --- a/tests/Integration/CooperativeCancellationTest.php +++ b/tests/Integration/CooperativeCancellationTest.php @@ -1743,7 +1743,7 @@ public function testRemoteOwnerShutdownOrSigkillReapsCallbackBeforeCleanup(bool try { $this->awaitMessage($workflowMessages, 'registered'); $this->awaitMessage($ownerMessages, 'registered'); - $handle = $client->startWorkflow('tests.php-cooperative', $queue, $queue, ['remote']); + $handle = $client->startWorkflow('tests.php-cooperative', $queue, $queue, ['remote-retry']); $this->awaitMessage($ownerMessages, 'remote-entered'); $pids = json_decode((string) file_get_contents($this->directory.'/processes'), true, flags: JSON_THROW_ON_ERROR); $this->stopWorker($ownerPid, $kill); @@ -1799,7 +1799,7 @@ public function testKilledActivityOwnerIsReclaimedBeforeCooperativeCleanup(): vo try { $this->awaitMessage($workflowMessages, 'registered'); $this->awaitMessage($ownerMessages, 'registered'); - $handle = $client->startWorkflow('tests.php-cooperative', $queue, $queue, ['remote']); + $handle = $client->startWorkflow('tests.php-cooperative', $queue, $queue, ['remote-retry']); $this->awaitMessage($ownerMessages, 'remote-entered'); $original = json_decode((string) file_get_contents($this->directory.'/remote-fence'), true, flags: JSON_THROW_ON_ERROR); $processes = json_decode((string) file_get_contents($this->directory.'/processes'), true, flags: JSON_THROW_ON_ERROR); @@ -2194,11 +2194,15 @@ private function spawnWorker(string $queue, bool $loseReply = false, bool $userH static fn () => $context->localActivity('tests.php-cooperative-group-work-0'), static fn () => $context->localActivity('tests.php-cooperative-group-work-1'), ]); - } elseif ($kind === 'remote') { - $context->activity('tests.php-cooperative-remote', [], $remotePolicy === null ? [] : [ + } elseif (in_array($kind, ['remote', 'remote-retry'], true)) { + $options = $remotePolicy === null ? [] : [ 'cancellation_policy' => $remotePolicy, 'schedule_to_close_timeout' => 60, - ]); + ]; + // Lease loss settles through the captured retry policy. + // Only owner-death/reclaim fixtures need a second attempt. + if ($kind === 'remote-retry') { $options['retry_policy'] = ['max_attempts' => 2]; } + $context->activity('tests.php-cooperative-remote', [], $options); } else { $context->sleep(300); } From 823602a55e033046d1834c844a2b47f66110fa7f Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:23:02 +0000 Subject: [PATCH 4/5] Preserve cooperative profile migration after the main refresh --- .github/workflows/ci.yml | 38 ++++++++++++++----- .github/workflows/published-cancellation.yml | 36 +++++++++++++----- .../service-mode-published-smoke.yml | 38 ++++++++++++++----- 3 files changed, 83 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 082f6299..c2a7b442 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -776,7 +776,7 @@ jobs: cooperative-integration: name: Connected cooperative cancellation if: ${{ github.event_name == 'workflow_dispatch' && inputs.cooperative_qualification }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 25 env: COMPOSE_PROJECT_NAME: php-cooperative-${{ github.run_id }}-${{ github.run_attempt }} @@ -788,22 +788,40 @@ jobs: with: path: sdk-php persist-credentials: false - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 - with: - python-version: '3.12' + - name: Select Ubuntu's Python 3.12 + run: | + /usr/bin/python3 -I -S -c 'import sys; assert sys.version_info[:2] == (3, 12)' + echo /usr/bin >> "$GITHUB_PATH" + - name: Create isolated Composer state + run: | + state="$(mktemp -d "$RUNNER_TEMP/php-check.XXXXXX")" + mkdir -m 700 "$state/home" "$state/cache" "$state/tmp" + { + echo "COMPOSER_HOME=$state/home" + echo "COMPOSER_CACHE_DIR=$state/cache" + echo "TMPDIR=$state/tmp" + } >> "$GITHUB_ENV" - name: Select authenticated dependency trust id: trust - uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@242c2a7fb23459db2e1ada5f84dbee6d61d9b1e5 # clean-trust with: clients: pip,python,composer,curl,git,cargo - name: Disable APT download retries (CVE-2026-109518) run: echo 'Acquire::Retries "0";' | sudo tee /etc/apt/apt.conf.d/99-no-retries >/dev/null - - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + - uses: durable-workflow/.github/.github/actions/setup-php-linux@411a14ac07d32073eed6ab4977f2825eba4739a7 # bound-linux-php with: php-version: '8.3' - extensions: pcntl, posix - tools: composer:2.10.3 + ini-file: production + tools: composer:2.10.3,phpunit:11.5.57 coverage: none + - name: Require the stock cooperative worker capabilities + run: | + php -r 'foreach (["pcntl", "posix"] as $extension) { + if (!extension_loaded($extension)) { throw new RuntimeException("Missing ".$extension); } + } + foreach (["pcntl_fork", "pcntl_waitpid", "pcntl_signal", "pcntl_async_signals", "posix_kill"] as $function) { + if (!function_exists($function)) { throw new RuntimeException("Missing ".$function); } + }' - name: Check out the exact public Server source env: SERVER_COMMIT: ${{ inputs.server_commit }} @@ -840,7 +858,7 @@ jobs: '{qualification:"source",sdk_commit:$sdk,server_commit:$server,native_source_overlay:($native | if length > 0 then . else null end),python_sdk_commit:($python | if length > 0 then . else null end),rust_sdk_commit:($rust | if length > 0 then . else null end),cli_source_commit:($cli | if length > 0 then . else null end)}' \ > sdk-php/cooperative-source-provenance.json - name: Verify Composer trust before requests - uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + uses: durable-workflow/.github/.github/actions/verify-composer-trust@242c2a7fb23459db2e1ada5f84dbee6d61d9b1e5 # clean-trust - name: Bind exact CLI inspection source if: ${{ inputs.cli_commit != '' }} env: @@ -901,7 +919,7 @@ jobs: printf 'DURABLE_WORKFLOW_POLYGLOT_RUST_BINARY=%s\n' "$binary" >> "$GITHUB_ENV" printf 'DURABLE_WORKFLOW_POLYGLOT_ARTIFACT_DIRECTORY=%s/sdk-php/polyglot-evidence\n' "$GITHUB_WORKSPACE" >> "$GITHUB_ENV" - name: Verify Composer trust before requests - uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + uses: durable-workflow/.github/.github/actions/verify-composer-trust@242c2a7fb23459db2e1ada5f84dbee6d61d9b1e5 # clean-trust with: working-directory: sdk-php - run: composer install --no-interaction --prefer-dist --no-progress diff --git a/.github/workflows/published-cancellation.yml b/.github/workflows/published-cancellation.yml index a2151a78..d2bd4492 100644 --- a/.github/workflows/published-cancellation.yml +++ b/.github/workflows/published-cancellation.yml @@ -33,7 +33,7 @@ permissions: jobs: mixed-cascade: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 20 env: COMPOSE_PROJECT_NAME: published-cancellation-${{ github.run_id }}-${{ github.run_attempt }} @@ -60,26 +60,44 @@ jobs: for version in "$PHP_SDK_VERSION" "$PYTHON_SDK_VERSION" "$RUST_SDK_VERSION" "$CLI_VERSION"; do [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.[1-9][0-9]*)?$ ]] done - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 - with: - python-version: '3.12' + - name: Select Ubuntu's Python 3.12 + run: | + /usr/bin/python3 -I -S -c 'import sys; assert sys.version_info[:2] == (3, 12)' + echo /usr/bin >> "$GITHUB_PATH" + - name: Create isolated Composer state + run: | + state="$(mktemp -d "$RUNNER_TEMP/php-check.XXXXXX")" + mkdir -m 700 "$state/home" "$state/cache" "$state/tmp" + { + echo "COMPOSER_HOME=$state/home" + echo "COMPOSER_CACHE_DIR=$state/cache" + echo "TMPDIR=$state/tmp" + } >> "$GITHUB_ENV" - name: Bootstrap published-consumer dependency trust - uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@242c2a7fb23459db2e1ada5f84dbee6d61d9b1e5 # clean-trust with: clients: pip,python,composer,curl,git,cargo - name: Disable APT download retries (CVE-2026-109518) run: echo 'Acquire::Retries "0";' | sudo tee /etc/apt/apt.conf.d/99-no-retries >/dev/null - - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + - uses: durable-workflow/.github/.github/actions/setup-php-linux@411a14ac07d32073eed6ab4977f2825eba4739a7 # bound-linux-php with: php-version: '8.3' - extensions: pcntl, posix - tools: composer:2.10.3 + ini-file: production + tools: composer:2.10.3,phpunit:11.5.57 coverage: none + - name: Require the stock cooperative worker capabilities + run: | + php -r 'foreach (["pcntl", "posix"] as $extension) { + if (!extension_loaded($extension)) { throw new RuntimeException("Missing ".$extension); } + } + foreach (["pcntl_fork", "pcntl_waitpid", "pcntl_signal", "pcntl_async_signals", "posix_kill"] as $function) { + if (!function_exists($function)) { throw new RuntimeException("Missing ".$function); } + }' - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable with: toolchain: 1.86.0 - name: Verify Composer trust before requests - uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + uses: durable-workflow/.github/.github/actions/verify-composer-trust@242c2a7fb23459db2e1ada5f84dbee6d61d9b1e5 # clean-trust - name: Install the published SDKs and CLI run: | set -euo pipefail diff --git a/.github/workflows/service-mode-published-smoke.yml b/.github/workflows/service-mode-published-smoke.yml index 10462562..f2c6c945 100644 --- a/.github/workflows/service-mode-published-smoke.yml +++ b/.github/workflows/service-mode-published-smoke.yml @@ -28,7 +28,7 @@ permissions: jobs: source-free-service-mode: if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 env: DURABLE_WORKFLOW_SERVER_URL: http://127.0.0.1:8080 @@ -54,24 +54,42 @@ jobs: exit 1 fi - - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 - with: - python-version: '3.12' + - name: Select Ubuntu's Python 3.12 + run: | + /usr/bin/python3 -I -S -c 'import sys; assert sys.version_info[:2] == (3, 12)' + echo /usr/bin >> "$GITHUB_PATH" + - name: Create isolated Composer state + run: | + state="$(mktemp -d "$RUNNER_TEMP/php-check.XXXXXX")" + mkdir -m 700 "$state/home" "$state/cache" "$state/tmp" + { + echo "COMPOSER_HOME=$state/home" + echo "COMPOSER_CACHE_DIR=$state/cache" + echo "TMPDIR=$state/tmp" + } >> "$GITHUB_ENV" - name: Select authenticated dependency trust - uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@242c2a7fb23459db2e1ada5f84dbee6d61d9b1e5 # clean-trust with: clients: pip,python,composer,curl,git - name: Disable APT download retries (CVE-2026-109518) run: echo 'Acquire::Retries "0";' | sudo tee /etc/apt/apt.conf.d/99-no-retries >/dev/null - - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + - uses: durable-workflow/.github/.github/actions/setup-php-linux@411a14ac07d32073eed6ab4977f2825eba4739a7 # bound-linux-php with: php-version: '8.3' - extensions: pcntl, posix - tools: composer:2.10.3 + ini-file: production + tools: composer:2.10.3,phpunit:11.5.57 coverage: none + - name: Require the stock cooperative worker capabilities + run: | + php -r 'foreach (["pcntl", "posix"] as $extension) { + if (!extension_loaded($extension)) { throw new RuntimeException("Missing ".$extension); } + } + foreach (["pcntl_fork", "pcntl_waitpid", "pcntl_signal", "pcntl_async_signals", "posix_kill"] as $function) { + if (!function_exists($function)) { throw new RuntimeException("Missing ".$function); } + }' - name: Verify Composer trust before requests - uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + uses: durable-workflow/.github/.github/actions/verify-composer-trust@242c2a7fb23459db2e1ada5f84dbee6d61d9b1e5 # clean-trust - name: Resolve the immutable published SDK release env: SDK_VERSION: ${{ inputs.sdk_version }} @@ -110,7 +128,7 @@ jobs: PHP - name: Verify Composer trust before requests - uses: durable-workflow/.github/.github/actions/verify-composer-trust@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust + uses: durable-workflow/.github/.github/actions/verify-composer-trust@242c2a7fb23459db2e1ada5f84dbee6d61d9b1e5 # clean-trust - name: Install only the published package env: SDK_VERSION: ${{ inputs.sdk_version }} From 21691383232e7bd6e58fee392e8076a140b4a811 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 14:42:15 +0000 Subject: [PATCH 5/5] Wait for advertised stop receipts and verify retry task lineage --- tests/Integration/CooperativeCancellationTest.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/Integration/CooperativeCancellationTest.php b/tests/Integration/CooperativeCancellationTest.php index 95ddbefa..1aedcd76 100644 --- a/tests/Integration/CooperativeCancellationTest.php +++ b/tests/Integration/CooperativeCancellationTest.php @@ -1645,7 +1645,7 @@ public function testRemoteRequestStopsBlockedOwnerAfterLiveOrColdWorkflowDeliver && ($event['payload']['activity_type'] ?? null) === 'tests.php-cooperative-remote'); self::assertSame($userHeartbeat, count($heartbeats) > 0, 'Owner checks must not manufacture user progress.'); $fence = json_decode((string) file_get_contents($this->directory.'/remote-fence'), true, flags: JSON_THROW_ON_ERROR); - if (getenv('DURABLE_WORKFLOW_CHILD_POLICY_QUALIFICATION') === '1') { + if (($client->clusterInfo()->raw['worker_protocol']['server_capabilities']['activity_cancellation_acknowledgement'] ?? false) === true) { $until = microtime(true) + 5; do { $status = $client->activityTaskStatus($fence['task_id'], $fence['activity_attempt_id'], $fence['lease_owner']); @@ -1822,7 +1822,16 @@ public function testKilledActivityOwnerIsReclaimedBeforeCooperativeCleanup(): vo // pass. No clock, database row or production lease is changed. $this->awaitReclaimedActivity($ownerMessages); $replacement = json_decode((string) file_get_contents($this->directory.'/remote-fence'), true, flags: JSON_THROW_ON_ERROR); - self::assertSame($original['task_id'], $replacement['task_id']); + self::assertNotSame($original['task_id'], $replacement['task_id']); + $retries = array_values(array_filter($this->history($client, $handle), static fn (array $event): bool => + $event['event_type'] === 'ActivityRetryScheduled' + && ($event['payload']['retry_of_task_id'] ?? null) === $original['task_id'])); + self::assertCount(1, $retries); + self::assertSame($replacement['task_id'], $retries[0]['payload']['retry_task_id']); + self::assertSame($original['activity_attempt_id'], $retries[0]['payload']['retry_after_attempt_id']); + self::assertSame(1, $retries[0]['payload']['retry_after_attempt']); + self::assertSame(2, $retries[0]['payload']['max_attempts']); + self::assertSame('lease_expired', $retries[0]['payload']['retry_reason']); self::assertNotSame($original['activity_attempt_id'], $replacement['activity_attempt_id']); self::assertNotSame($original['lease_owner'], $replacement['lease_owner']); self::assertSame(2, $replacement['attempt_number']);