From f2504601bd1af281343f22337e200e4c624ff70b Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 21 Oct 2026 11:04:34 +0000 Subject: [PATCH 1/9] Reproduce select member cancellation lost at the HTTP boundary --- .../Feature/SelectionCancellationHttpTest.php | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 tests/Feature/SelectionCancellationHttpTest.php diff --git a/tests/Feature/SelectionCancellationHttpTest.php b/tests/Feature/SelectionCancellationHttpTest.php new file mode 100644 index 000000000..345123f51 --- /dev/null +++ b/tests/Feature/SelectionCancellationHttpTest.php @@ -0,0 +1,126 @@ +createNamespace('default'); + $this->createNamespace('other'); + $this->registerWorker('select-worker', 'select-http', supportedWorkflowTypes: ['select-http']); + $this->registerWorker('activity-worker', 'later', supportedActivityTypes: ['lose']); + } + + public function test_losing_queued_activity_is_cancelled_through_http_and_cannot_be_claimed(): void + { + [$runId, $task, $cancel] = $this->stageTimerWinner(); + $this->complete($task, [$cancel])->assertOk()->assertJsonPath('recorded', true); + + $this->assertSame('cancelled', ActivityExecution::query()->sole()->status->value); + $this->assertSame(1, WorkflowHistoryEvent::query()->where('workflow_run_id', $runId) + ->where('event_type', HistoryEventType::SelectionOperationCancelled->value)->count()); + $this->postJson('/api/worker/activity-tasks/poll', [ + 'worker_id' => 'activity-worker', 'task_queue' => 'later', + ], $this->workerHeaders())->assertOk()->assertJsonPath('task', null); + + $resume = $this->pollWorkflow(); + $this->complete($resume, [[ + 'type' => 'complete_workflow', 'result' => Serializer::serialize(['timer-won', 9007199254740993, null, 'λ']), + ]])->assertOk()->assertJsonPath('recorded', true); + $this->assertSame(RunStatus::Completed, WorkflowRun::query()->findOrFail($runId)->status); + $this->assertSame(0, WorkflowTask::query()->where('workflow_run_id', $runId) + ->where('task_type', TaskType::Activity->value)->where('status', TaskStatus::Leased->value)->count()); + } + + #[DataProvider('invalidMembers')] + public function test_invalid_or_foreign_member_cannot_cancel_the_original_activity(array $changes, string $namespace, int $status): void + { + [$runId, $task, $cancel] = $this->stageTimerWinner(); + $before = WorkflowHistoryEvent::query()->where('workflow_run_id', $runId)->count(); + $this->complete($task, [array_replace($cancel, $changes)], $namespace)->assertStatus($status); + $this->assertSame('scheduled', ActivityExecution::query()->sole()->status->value); + $this->assertSame($before, WorkflowHistoryEvent::query()->where('workflow_run_id', $runId)->count()); + $this->assertSame(TaskStatus::Leased, WorkflowTask::query()->findOrFail($task['task_id'])->status); + } + + public static function invalidMembers(): array + { + return [ + 'wrong operation' => [['operation_identity' => 'foreign-activity'], 'default', 409], + 'wrong member' => [['member_key' => 'deadline'], 'default', 409], + 'outside group' => [['member_base_sequence' => 3], 'default', 422], + 'wrong namespace' => [[], 'other', 404], + ]; + } + + private function stageTimerWinner(): array + { + $start = $this->postJson('/api/workflows', [ + 'workflow_id' => 'select-http', 'workflow_type' => 'select-http', 'task_queue' => 'select-http', + ], $this->apiHeaders())->assertCreated(); + $task = $this->pollWorkflow(); + $commands = []; + foreach ([['schedule_activity', 'act', 'activity'], ['start_timer', 'deadline', 'timer']] as $index => [$type, $key, $kind]) { + $entry = [ + 'parallel_group_id' => 'select-calls:1:2', 'parallel_group_kind' => 'mixed', + 'parallel_group_mode' => 'select', 'parallel_group_base_sequence' => 1, + 'parallel_group_size' => 2, 'parallel_group_index' => $index, + 'selection_member_key' => $key, 'selection_member_index' => $index, + 'selection_member_base_sequence' => 1 + $index, 'selection_member_size' => 1, + 'selection_member_kind' => $kind, + ]; + $commands[] = array_merge([ + 'type' => $type, ...$entry, 'parallel_group_path' => [$entry], + ], $index === 0 ? ['activity_type' => 'lose', 'queue' => 'later'] : ['delay_seconds' => 0]); + } + $this->complete($task, $commands)->assertOk()->assertJsonPath('recorded', true); + $timerTask = WorkflowTask::query()->where('task_type', TaskType::Timer->value)->sole(); + $this->app->call([new RunTimerTask($timerTask->id), 'handle']); + $cancel = [ + 'type' => 'cancel_selection_operation', 'selection_group_id' => 'select-calls:1:2', + 'member_key' => 'act', 'member_index' => 0, 'member_base_sequence' => 1, 'member_size' => 1, + 'operation_kind' => 'activity', 'operation_identity' => ActivityExecution::query()->sole()->id, + ]; + + return [$start->json('run_id'), $this->pollWorkflow(), $cancel]; + } + + private function pollWorkflow(): array + { + return $this->postJson('/api/worker/workflow-tasks/poll', [ + 'worker_id' => 'select-worker', 'task_queue' => 'select-http', + ], $this->workerHeaders())->assertOk()->json('task'); + } + + private function complete(array $task, array $commands, string $namespace = 'default'): \Illuminate\Testing\TestResponse + { + return $this->postJson('/api/worker/workflow-tasks/'.$task['task_id'].'/complete', [ + 'lease_owner' => 'select-worker', 'workflow_task_attempt' => $task['workflow_task_attempt'], + 'commands' => $commands, + ], $this->workerHeaders($namespace)); + } +} From 885114bcc04c8cd6dd463ad5cde61a8efc5d4aa3 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 21 Oct 2026 11:08:15 +0000 Subject: [PATCH 2/9] Retain select cancellation identity through worker HTTP validation --- app/Http/Controllers/Api/WorkerController.php | 141 +-------------- .../WorkflowTaskCompletionValidation.php | 161 ++++++++++++++++++ docs/select-member-cancellation-recovery.md | 37 ++++ .../Feature/SelectionCancellationHttpTest.php | 62 ++++++- .../WorkflowTaskCommandFieldRetentionTest.php | 100 +++++++++++ 5 files changed, 356 insertions(+), 145 deletions(-) create mode 100644 app/Support/WorkflowTaskCompletionValidation.php create mode 100644 docs/select-member-cancellation-recovery.md create mode 100644 tests/Feature/WorkflowTaskCommandFieldRetentionTest.php diff --git a/app/Http/Controllers/Api/WorkerController.php b/app/Http/Controllers/Api/WorkerController.php index 9f63b4ac5..6c230b67a 100644 --- a/app/Http/Controllers/Api/WorkerController.php +++ b/app/Http/Controllers/Api/WorkerController.php @@ -51,7 +51,7 @@ use App\Support\WorkflowQueryTaskBroker; use App\Support\WorkflowReplayEligibility; use App\Support\WorkflowStreamCommandProcessor; -use App\Support\WorkflowStreamService; +use App\Support\WorkflowTaskCompletionValidation; use App\Support\WorkflowTaskLeaseRecovery; use App\Support\WorkflowTaskPoller; use App\Support\WorkflowUpdateValidationTaskBroker; @@ -1659,144 +1659,7 @@ private function mutateWorkflowTaskCommands(Request $request, string $taskId, bo 'message_stream_waits.*.after_position' => ['required', 'integer', 'min:0'], ]); - $completionRules = [ - 'lease_owner' => ['required', 'string'], - 'workflow_task_attempt' => ['required', 'integer', 'min:1'], - 'commands' => ['required', 'array', 'min:1'], - 'commands.*.type' => ['required', 'string'], - 'commands.*.cancellation_scope_id' => ['sometimes', 'string', 'min:1', 'max:255'], - 'commands.*.cancellation_cleanup' => ['sometimes', 'required', 'array:scope_id,request_id,delivery_history_event_id'], - 'commands.*.cancellation_cleanup.scope_id' => ['sometimes', 'required', 'string', 'max:255'], - 'commands.*.cancellation_cleanup.request_id' => ['required_with:commands.*.cancellation_cleanup', 'string', 'max:255'], - 'commands.*.cancellation_cleanup.delivery_history_event_id' => ['required_with:commands.*.cancellation_cleanup', 'string', 'max:255'], - 'commands.*.result' => ['nullable'], - 'commands.*.activity_type' => ['nullable', 'string'], - 'commands.*.arguments' => ['nullable'], - 'commands.*.connection' => ['nullable', 'string'], - 'commands.*.queue' => ['nullable', 'string'], - 'commands.*.retry_policy' => ['nullable', 'array'], - 'commands.*.retry_policy.max_attempts' => ['nullable', 'integer', 'min:1'], - 'commands.*.retry_policy.backoff_seconds' => ['nullable', 'array'], - 'commands.*.retry_policy.backoff_seconds.*' => ['integer', 'min:0'], - 'commands.*.retry_policy.non_retryable_error_types' => ['nullable', 'array'], - 'commands.*.retry_policy.non_retryable_error_types.*' => ['string'], - 'commands.*.retry_policy.*' => ['nullable'], - 'commands.*.start_to_close_timeout' => ['nullable', 'integer', 'min:1'], - 'commands.*.schedule_to_start_timeout' => ['nullable', 'integer', 'min:1'], - 'commands.*.schedule_to_close_timeout' => ['nullable', 'integer', 'min:1'], - 'commands.*.heartbeat_timeout' => ['nullable', 'integer', 'min:1'], - 'commands.*.execution_mode' => ['nullable', 'string', 'in:local'], - 'commands.*.outcome' => ['nullable', 'string', 'in:completed,failed,timed_out,cancelled'], - 'commands.*.attempts' => ['nullable', 'array', 'max:100'], - 'commands.*.attempts.*.attempt_id' => ['nullable', 'string', 'max:255'], - 'commands.*.attempts.*.attempt_number' => ['required', 'integer', 'min:1'], - 'commands.*.attempts.*.outcome' => ['required', 'string', 'in:completed,failed,timed_out,cancelled'], - 'commands.*.attempts.*.duration_ms' => ['nullable', 'integer', 'min:0'], - 'commands.*.attempts.*.message' => ['nullable', 'string'], - 'commands.*.attempts.*.exception_type' => ['nullable', 'string', 'max:255'], - 'commands.*.attempts.*.non_retryable' => ['nullable', 'boolean'], - 'commands.*.attempts.*.timeout_kind' => ['nullable', 'string', 'in:start_to_close,schedule_to_close,heartbeat'], - 'commands.*.attempts.*.retry_reason' => ['nullable', 'string', 'in:failure,timeout,cold_replay'], - 'commands.*.attempts.*.backoff_seconds' => ['nullable', 'integer', 'min:0'], - 'commands.*.attempts.*.heartbeats' => ['nullable', 'array', 'max:1000'], - 'commands.*.attempts.*.heartbeats.*.details' => ['nullable', 'array'], - 'commands.*.attempts.*.heartbeats.*.elapsed_ms' => ['nullable', 'integer', 'min:0'], - 'commands.*.attempts.*.heartbeats.*.*' => ['nullable'], - 'commands.*.attempts.*.*' => ['nullable'], - 'commands.*.worker_session' => ['nullable', 'array'], - 'commands.*.worker_session.session_id' => ['nullable', 'string', 'max:255'], - 'commands.*.worker_session.connection' => ['nullable', 'string', 'max:255'], - 'commands.*.worker_session.queue' => ['nullable', 'string', 'max:255'], - 'commands.*.worker_session.requirements' => ['nullable', 'array'], - 'commands.*.worker_session.requirements.*' => ['string', 'max:255'], - 'commands.*.worker_session.lease_seconds' => ['nullable', 'integer', 'min:1'], - 'commands.*.worker_session.ttl_seconds' => ['nullable', 'integer', 'min:1'], - 'commands.*.worker_session.max_concurrent_activities' => ['nullable', 'integer', 'min:1'], - 'commands.*.worker_session.create_if_missing' => ['nullable', 'boolean'], - 'commands.*.worker_session.allow_reacquire_after_failure' => ['nullable', 'boolean'], - 'commands.*.execution_timeout_seconds' => ['nullable', 'integer', 'min:1'], - 'commands.*.run_timeout_seconds' => ['nullable', 'integer', 'min:1'], - 'commands.*.workflow_type' => ['nullable', 'string'], - 'commands.*.delay_seconds' => ['nullable', 'integer', 'min:0'], - 'commands.*.message' => ['nullable', 'string'], - 'commands.*.timeout_kind' => ['nullable', 'string', 'in:start_to_close,schedule_to_close,heartbeat'], - 'commands.*.payload_codec' => ['nullable', 'string'], - 'commands.*.update_id' => ['nullable', 'string'], - 'commands.*.exception_class' => ['nullable', 'string'], - 'commands.*.exception_type' => ['nullable', 'string'], - 'commands.*.exception' => ['nullable', 'array'], - 'commands.*.failed_step_sequence' => ['nullable', 'integer', 'min:1'], - 'commands.*.failed_activity_execution_id' => ['nullable', 'string', 'max:255'], - 'commands.*.change_id' => ['nullable', 'string'], - 'commands.*.version' => ['nullable', 'integer'], - 'commands.*.min_supported' => ['nullable', 'integer'], - 'commands.*.max_supported' => ['nullable', 'integer'], - 'commands.*.attributes' => ['nullable', 'array'], - 'commands.*.attribute_types' => ['nullable', 'array'], - 'commands.*.attribute_types.*' => ['string'], - 'commands.*.entries' => ['nullable', 'array'], - 'commands.*.non_retryable' => ['nullable', 'boolean'], - 'commands.*.parent_close_policy' => ['nullable', 'string'], - 'commands.*.cancellation_policy' => ['nullable', 'string', 'in:try_cancel,wait_cancellation_completed,abandon'], - 'commands.*.condition_key' => ['nullable', 'string'], - 'commands.*.condition_definition_fingerprint' => ['nullable', 'string'], - 'commands.*.condition_wait_occurrence_id' => ['nullable', 'string'], - 'commands.*.signal_name' => ['nullable', 'string'], - 'commands.*.timeout_seconds' => ['nullable', 'integer', 'min:0'], - 'commands.*.endpoint_name' => ['nullable', 'string', 'max:191'], - 'commands.*.service_name' => ['nullable', 'string', 'max:191'], - 'commands.*.operation_name' => ['nullable', 'string', 'max:191'], - 'commands.*.request_payload' => ['nullable'], - 'commands.*.namespace' => ['nullable', 'string', 'max:191'], - 'commands.*.caller_namespace' => ['nullable', 'string', 'max:191'], - 'commands.*.service_call_id' => ['nullable', 'string', 'max:191'], - 'commands.*.idempotency_key' => ['nullable', 'string', 'max:191'], - 'commands.*.mode_override' => ['nullable', 'string', 'in:sync,async'], - 'commands.*.wait_for' => ['nullable', 'string', 'in:accepted,completed'], - 'commands.*.wait_timeout_seconds' => ['nullable', 'integer', 'min:0'], - 'commands.*.target_workflow_instance_id' => ['nullable', 'string', 'max:191'], - 'commands.*.target_workflow_run_id' => ['nullable', 'string', 'max:191'], - 'commands.*.business_key' => ['nullable', 'string', 'max:191'], - 'commands.*.labels' => ['nullable', 'array'], - 'commands.*.memo' => ['nullable', 'array'], - 'commands.*.search_attributes' => ['nullable', 'array'], - 'commands.*.duplicate_start_policy' => ['nullable', 'string', 'in:reject_duplicate,return_existing_active'], - 'commands.*.metadata' => ['nullable', 'array'], - 'commands.*.request_payload_reference' => ['nullable', 'string', 'max:191'], - 'commands.*.principal_subject' => ['prohibited'], - 'commands.*.principal_method' => ['prohibited'], - 'commands.*.principal_roles' => ['prohibited'], - 'commands.*.principal_tenant' => ['prohibited'], - 'commands.*.principal_claims' => ['prohibited'], - ...WorkflowCommandNormalizer::parallelMetadataValidationRules(), - 'commands.*.workflow_stream' => ['nullable', 'array'], - 'commands.*.workflow_stream.operation' => ['required_with:commands.*.workflow_stream', 'string', 'in:append,close,error'], - 'commands.*.workflow_stream.stream_name' => ['required_with:commands.*.workflow_stream', 'string', 'max:191'], - 'commands.*.workflow_stream.command_identity' => ['required_with:commands.*.workflow_stream', 'string', 'max:191'], - 'commands.*.workflow_stream.command_ordinal' => ['required_with:commands.*.workflow_stream', 'integer', 'min:0'], - 'commands.*.workflow_stream.items' => ['nullable', 'array', 'max:'.WorkflowStreamService::DEFAULT_MAX_ITEMS_PER_APPEND], - 'commands.*.workflow_stream.items.*' => ['array'], - 'commands.*.workflow_stream.items.*.payload' => ['nullable'], - 'commands.*.workflow_stream.items.*.payload_reference' => ['nullable', 'string', 'max:191'], - 'commands.*.workflow_stream.items.*.payload_codec' => ['nullable', 'string', 'max:64'], - 'commands.*.workflow_stream.items.*.idempotency_key' => ['nullable', 'string', 'max:191'], - 'commands.*.workflow_stream.items.*.item_type' => ['nullable', 'string', 'max:64'], - 'commands.*.workflow_stream.items.*.content_type' => ['nullable', 'string', 'max:191'], - 'commands.*.workflow_stream.max_pending_items' => ['nullable', 'integer', 'min:1'], - 'commands.*.workflow_stream.error_reason' => ['nullable', 'string', 'max:191'], - 'commands.*.workflow_stream.retention_seconds' => ['nullable', 'integer', 'min:1'], - 'sticky_cache' => ['nullable', 'array'], - 'sticky_cache.worker_id' => ['required_with:sticky_cache', 'string', 'max:255'], - 'sticky_cache.workflow_id' => ['required_with:sticky_cache', 'string', 'max:255'], - 'sticky_cache.run_id' => ['required_with:sticky_cache', 'string', 'max:255'], - 'sticky_cache.build_id' => ['required_with:sticky_cache', 'string', 'max:255'], - 'sticky_cache.ttl_seconds' => ['required_with:sticky_cache', 'integer', 'min:1', 'max:3600'], - 'sticky_cache.metrics' => ['nullable', 'array'], - 'sticky_cache.metrics.hit' => ['nullable', 'integer', 'min:0'], - 'sticky_cache.metrics.miss' => ['nullable', 'integer', 'min:0'], - 'sticky_cache.metrics.eviction' => ['nullable', 'integer', 'min:0'], - 'sticky_cache.metrics.forced_cold_replay' => ['nullable', 'integer', 'min:0'], - ]; + $completionRules = WorkflowTaskCompletionValidation::rules(); $topLevelRules = []; if ($group) { diff --git a/app/Support/WorkflowTaskCompletionValidation.php b/app/Support/WorkflowTaskCompletionValidation.php new file mode 100644 index 000000000..2da7ae8f7 --- /dev/null +++ b/app/Support/WorkflowTaskCompletionValidation.php @@ -0,0 +1,161 @@ +> */ + public static function rules(): array + { + return [ + 'lease_owner' => ['required', 'string'], + 'workflow_task_attempt' => ['required', 'integer', 'min:1'], + 'commands' => ['required', 'array', 'min:1'], + 'commands.*.type' => ['required', 'string'], + 'commands.*.selection_group_id' => ['sometimes', 'string'], + 'commands.*.member_key' => ['sometimes', 'string'], + 'commands.*.member_index' => ['sometimes', 'integer', 'min:0'], + 'commands.*.member_base_sequence' => ['sometimes', 'integer', 'min:1'], + 'commands.*.member_size' => ['sometimes', 'integer', 'min:1'], + 'commands.*.operation_kind' => ['sometimes', 'string'], + 'commands.*.operation_identity' => ['sometimes', 'string'], + 'commands.*.cancellation_scope_id' => ['sometimes', 'string', 'min:1', 'max:255'], + 'commands.*.cancellation_cleanup' => ['sometimes', 'required', 'array:scope_id,request_id,delivery_history_event_id'], + 'commands.*.cancellation_cleanup.scope_id' => ['sometimes', 'required', 'string', 'max:255'], + 'commands.*.cancellation_cleanup.request_id' => ['required_with:commands.*.cancellation_cleanup', 'string', 'max:255'], + 'commands.*.cancellation_cleanup.delivery_history_event_id' => ['required_with:commands.*.cancellation_cleanup', 'string', 'max:255'], + 'commands.*.result' => ['nullable'], + 'commands.*.activity_type' => ['nullable', 'string'], + 'commands.*.arguments' => ['nullable'], + 'commands.*.connection' => ['nullable', 'string'], + 'commands.*.queue' => ['nullable', 'string'], + 'commands.*.retry_policy' => ['nullable', 'array'], + 'commands.*.retry_policy.max_attempts' => ['nullable', 'integer', 'min:1'], + 'commands.*.retry_policy.backoff_seconds' => ['nullable', 'array'], + 'commands.*.retry_policy.backoff_seconds.*' => ['integer', 'min:0'], + 'commands.*.retry_policy.non_retryable_error_types' => ['nullable', 'array'], + 'commands.*.retry_policy.non_retryable_error_types.*' => ['string'], + 'commands.*.retry_policy.*' => ['nullable'], + 'commands.*.start_to_close_timeout' => ['nullable', 'integer', 'min:1'], + 'commands.*.schedule_to_start_timeout' => ['nullable', 'integer', 'min:1'], + 'commands.*.schedule_to_close_timeout' => ['nullable', 'integer', 'min:1'], + 'commands.*.heartbeat_timeout' => ['nullable', 'integer', 'min:1'], + 'commands.*.execution_mode' => ['nullable', 'string', 'in:local'], + 'commands.*.outcome' => ['nullable', 'string', 'in:completed,failed,timed_out,cancelled'], + 'commands.*.attempts' => ['nullable', 'array', 'max:100'], + 'commands.*.attempts.*.attempt_id' => ['nullable', 'string', 'max:255'], + 'commands.*.attempts.*.attempt_number' => ['required', 'integer', 'min:1'], + 'commands.*.attempts.*.outcome' => ['required', 'string', 'in:completed,failed,timed_out,cancelled'], + 'commands.*.attempts.*.duration_ms' => ['nullable', 'integer', 'min:0'], + 'commands.*.attempts.*.message' => ['nullable', 'string'], + 'commands.*.attempts.*.exception_type' => ['nullable', 'string', 'max:255'], + 'commands.*.attempts.*.non_retryable' => ['nullable', 'boolean'], + 'commands.*.attempts.*.timeout_kind' => ['nullable', 'string', 'in:start_to_close,schedule_to_close,heartbeat'], + 'commands.*.attempts.*.retry_reason' => ['nullable', 'string', 'in:failure,timeout,cold_replay'], + 'commands.*.attempts.*.backoff_seconds' => ['nullable', 'integer', 'min:0'], + 'commands.*.attempts.*.heartbeats' => ['nullable', 'array', 'max:1000'], + 'commands.*.attempts.*.heartbeats.*.details' => ['nullable', 'array'], + 'commands.*.attempts.*.heartbeats.*.elapsed_ms' => ['nullable', 'integer', 'min:0'], + 'commands.*.attempts.*.heartbeats.*.*' => ['nullable'], + 'commands.*.attempts.*.*' => ['nullable'], + 'commands.*.worker_session' => ['nullable', 'array'], + 'commands.*.worker_session.session_id' => ['nullable', 'string', 'max:255'], + 'commands.*.worker_session.connection' => ['nullable', 'string', 'max:255'], + 'commands.*.worker_session.queue' => ['nullable', 'string', 'max:255'], + 'commands.*.worker_session.requirements' => ['nullable', 'array'], + 'commands.*.worker_session.requirements.*' => ['string', 'max:255'], + 'commands.*.worker_session.lease_seconds' => ['nullable', 'integer', 'min:1'], + 'commands.*.worker_session.ttl_seconds' => ['nullable', 'integer', 'min:1'], + 'commands.*.worker_session.max_concurrent_activities' => ['nullable', 'integer', 'min:1'], + 'commands.*.worker_session.create_if_missing' => ['nullable', 'boolean'], + 'commands.*.worker_session.allow_reacquire_after_failure' => ['nullable', 'boolean'], + 'commands.*.execution_timeout_seconds' => ['nullable', 'integer', 'min:1'], + 'commands.*.run_timeout_seconds' => ['nullable', 'integer', 'min:1'], + 'commands.*.workflow_type' => ['nullable', 'string'], + 'commands.*.delay_seconds' => ['nullable', 'integer', 'min:0'], + 'commands.*.message' => ['nullable', 'string'], + 'commands.*.timeout_kind' => ['nullable', 'string', 'in:start_to_close,schedule_to_close,heartbeat'], + 'commands.*.payload_codec' => ['nullable', 'string'], + 'commands.*.update_id' => ['nullable', 'string'], + 'commands.*.exception_class' => ['nullable', 'string'], + 'commands.*.exception_type' => ['nullable', 'string'], + 'commands.*.exception' => ['nullable', 'array'], + 'commands.*.failed_step_sequence' => ['nullable', 'integer', 'min:1'], + 'commands.*.failed_activity_execution_id' => ['nullable', 'string', 'max:255'], + 'commands.*.change_id' => ['nullable', 'string'], + 'commands.*.version' => ['nullable', 'integer'], + 'commands.*.min_supported' => ['nullable', 'integer'], + 'commands.*.max_supported' => ['nullable', 'integer'], + 'commands.*.attributes' => ['nullable', 'array'], + 'commands.*.attribute_types' => ['nullable', 'array'], + 'commands.*.attribute_types.*' => ['string'], + 'commands.*.entries' => ['nullable', 'array'], + 'commands.*.non_retryable' => ['nullable', 'boolean'], + 'commands.*.parent_close_policy' => ['nullable', 'string'], + 'commands.*.cancellation_policy' => ['nullable', 'string', 'in:try_cancel,wait_cancellation_completed,abandon'], + 'commands.*.condition_key' => ['nullable', 'string'], + 'commands.*.condition_definition_fingerprint' => ['nullable', 'string'], + 'commands.*.condition_wait_occurrence_id' => ['nullable', 'string'], + 'commands.*.signal_name' => ['nullable', 'string'], + 'commands.*.timeout_seconds' => ['nullable', 'integer', 'min:0'], + 'commands.*.endpoint_name' => ['nullable', 'string', 'max:191'], + 'commands.*.service_name' => ['nullable', 'string', 'max:191'], + 'commands.*.operation_name' => ['nullable', 'string', 'max:191'], + 'commands.*.request_payload' => ['nullable'], + 'commands.*.namespace' => ['nullable', 'string', 'max:191'], + 'commands.*.caller_namespace' => ['nullable', 'string', 'max:191'], + 'commands.*.service_call_id' => ['nullable', 'string', 'max:191'], + 'commands.*.idempotency_key' => ['nullable', 'string', 'max:191'], + 'commands.*.mode_override' => ['nullable', 'string', 'in:sync,async'], + 'commands.*.wait_for' => ['nullable', 'string', 'in:accepted,completed'], + 'commands.*.wait_timeout_seconds' => ['nullable', 'integer', 'min:0'], + 'commands.*.target_workflow_instance_id' => ['nullable', 'string', 'max:191'], + 'commands.*.target_workflow_run_id' => ['nullable', 'string', 'max:191'], + 'commands.*.business_key' => ['nullable', 'string', 'max:191'], + 'commands.*.labels' => ['nullable', 'array'], + 'commands.*.memo' => ['nullable', 'array'], + 'commands.*.search_attributes' => ['nullable', 'array'], + 'commands.*.duplicate_start_policy' => ['nullable', 'string', 'in:reject_duplicate,return_existing_active'], + 'commands.*.metadata' => ['nullable', 'array'], + 'commands.*.request_payload_reference' => ['nullable', 'string', 'max:191'], + 'commands.*.principal_subject' => ['prohibited'], + 'commands.*.principal_method' => ['prohibited'], + 'commands.*.principal_roles' => ['prohibited'], + 'commands.*.principal_tenant' => ['prohibited'], + 'commands.*.principal_claims' => ['prohibited'], + ...WorkflowCommandNormalizer::parallelMetadataValidationRules(), + 'commands.*.workflow_stream' => ['nullable', 'array'], + 'commands.*.workflow_stream.operation' => ['required_with:commands.*.workflow_stream', 'string', 'in:append,close,error'], + 'commands.*.workflow_stream.stream_name' => ['required_with:commands.*.workflow_stream', 'string', 'max:191'], + 'commands.*.workflow_stream.command_identity' => ['required_with:commands.*.workflow_stream', 'string', 'max:191'], + 'commands.*.workflow_stream.command_ordinal' => ['required_with:commands.*.workflow_stream', 'integer', 'min:0'], + 'commands.*.workflow_stream.items' => ['nullable', 'array', 'max:'.WorkflowStreamService::DEFAULT_MAX_ITEMS_PER_APPEND], + 'commands.*.workflow_stream.items.*' => ['array'], + 'commands.*.workflow_stream.items.*.payload' => ['nullable'], + 'commands.*.workflow_stream.items.*.payload_reference' => ['nullable', 'string', 'max:191'], + 'commands.*.workflow_stream.items.*.payload_codec' => ['nullable', 'string', 'max:64'], + 'commands.*.workflow_stream.items.*.idempotency_key' => ['nullable', 'string', 'max:191'], + 'commands.*.workflow_stream.items.*.item_type' => ['nullable', 'string', 'max:64'], + 'commands.*.workflow_stream.items.*.content_type' => ['nullable', 'string', 'max:191'], + 'commands.*.workflow_stream.max_pending_items' => ['nullable', 'integer', 'min:1'], + 'commands.*.workflow_stream.error_reason' => ['nullable', 'string', 'max:191'], + 'commands.*.workflow_stream.retention_seconds' => ['nullable', 'integer', 'min:1'], + 'sticky_cache' => ['nullable', 'array'], + 'sticky_cache.worker_id' => ['required_with:sticky_cache', 'string', 'max:255'], + 'sticky_cache.workflow_id' => ['required_with:sticky_cache', 'string', 'max:255'], + 'sticky_cache.run_id' => ['required_with:sticky_cache', 'string', 'max:255'], + 'sticky_cache.build_id' => ['required_with:sticky_cache', 'string', 'max:255'], + 'sticky_cache.ttl_seconds' => ['required_with:sticky_cache', 'integer', 'min:1', 'max:3600'], + 'sticky_cache.metrics' => ['nullable', 'array'], + 'sticky_cache.metrics.hit' => ['nullable', 'integer', 'min:0'], + 'sticky_cache.metrics.miss' => ['nullable', 'integer', 'min:0'], + 'sticky_cache.metrics.eviction' => ['nullable', 'integer', 'min:0'], + 'sticky_cache.metrics.forced_cold_replay' => ['nullable', 'integer', 'min:0'], + ]; + } +} diff --git a/docs/select-member-cancellation-recovery.md b/docs/select-member-cancellation-recovery.md new file mode 100644 index 000000000..5b9db73cb --- /dev/null +++ b/docs/select-member-cancellation-recovery.md @@ -0,0 +1,37 @@ +# Recover a workflow blocked while cancelling a select member + +PHP Server 2.10.0 discards select-member cancellation identity fields +when validating a workflow-task completion. The runtime then refuses the +`cancel_selection_operation` command. A worker can report that refusal as a +workflow replay error, leaving the run waiting with `workflow_replay_blocked`. +The activity the workflow tried to cancel can still be queued or running. + +Upgrade to the corrected PHP Server release before repairing affected runs. +The correction preserves the existing command format and does not require an +SDK or Workflow dependency upgrade. It does not automatically retry blocked +runs or undo an activity that has already executed. + +## Inspect and repair the original run + +Check the selected run's history and activity state. Confirm that the failure +names selection cancellation validation rather than another replay error. +Keep the original workflow and run IDs. Use the existing selected-run repair +endpoint with an operator-authorized token and the original namespace: + +```sh +curl --fail-with-body --request POST \ + "$DW_SERVER_URL/api/workflows/$WORKFLOW_ID/runs/$RUN_ID/repair" \ + --header "Authorization: Bearer $DW_API_TOKEN" \ + --header "X-Namespace: $DW_NAMESPACE" \ + --header 'X-Durable-Workflow-Control-Plane-Version: 2' \ + --header 'Content-Type: application/json' \ + --data '{"request_id":"repair-select-member-cancellation"}' +``` + +Use URL-encoded identifiers in the path. Keep a compatible workflow worker +running on the original queue. Repair makes the original run available for +replay; the workflow can then submit its original cancellation and complete. +Verify the new workflow-task outcome and the member's recorded outcome in +history. A still-queued losing activity records `SelectionOperationCancelled` +and cannot be claimed after cancellation. Already completed operations retain +their recorded results. diff --git a/tests/Feature/SelectionCancellationHttpTest.php b/tests/Feature/SelectionCancellationHttpTest.php index 345123f51..6576c9067 100644 --- a/tests/Feature/SelectionCancellationHttpTest.php +++ b/tests/Feature/SelectionCancellationHttpTest.php @@ -38,7 +38,9 @@ protected function setUp(): void public function test_losing_queued_activity_is_cancelled_through_http_and_cannot_be_claimed(): void { [$runId, $task, $cancel] = $this->stageTimerWinner(); - $this->complete($task, [$cancel])->assertOk()->assertJsonPath('recorded', true); + $this->complete($task, [$cancel, [ + 'type' => 'complete_workflow', 'result' => Serializer::serialize(['timer-won', 9007199254740993, null, 'λ']), + ]])->assertOk()->assertJsonPath('recorded', true); $this->assertSame('cancelled', ActivityExecution::query()->sole()->status->value); $this->assertSame(1, WorkflowHistoryEvent::query()->where('workflow_run_id', $runId) @@ -47,10 +49,6 @@ public function test_losing_queued_activity_is_cancelled_through_http_and_cannot 'worker_id' => 'activity-worker', 'task_queue' => 'later', ], $this->workerHeaders())->assertOk()->assertJsonPath('task', null); - $resume = $this->pollWorkflow(); - $this->complete($resume, [[ - 'type' => 'complete_workflow', 'result' => Serializer::serialize(['timer-won', 9007199254740993, null, 'λ']), - ]])->assertOk()->assertJsonPath('recorded', true); $this->assertSame(RunStatus::Completed, WorkflowRun::query()->findOrFail($runId)->status); $this->assertSame(0, WorkflowTask::query()->where('workflow_run_id', $runId) ->where('task_type', TaskType::Activity->value)->where('status', TaskStatus::Leased->value)->count()); @@ -62,7 +60,7 @@ public function test_invalid_or_foreign_member_cannot_cancel_the_original_activi [$runId, $task, $cancel] = $this->stageTimerWinner(); $before = WorkflowHistoryEvent::query()->where('workflow_run_id', $runId)->count(); $this->complete($task, [array_replace($cancel, $changes)], $namespace)->assertStatus($status); - $this->assertSame('scheduled', ActivityExecution::query()->sole()->status->value); + $this->assertSame('pending', ActivityExecution::query()->sole()->status->value); $this->assertSame($before, WorkflowHistoryEvent::query()->where('workflow_run_id', $runId)->count()); $this->assertSame(TaskStatus::Leased, WorkflowTask::query()->findOrFail($task['task_id'])->status); } @@ -77,6 +75,58 @@ public static function invalidMembers(): array ]; } + public function test_previously_blocked_run_can_be_repaired_and_cancel_its_original_member(): void + { + [$runId, $task, $cancel] = $this->stageTimerWinner(); + $this->postJson('/api/worker/workflow-tasks/'.$task['task_id'].'/fail', [ + 'lease_owner' => 'select-worker', 'workflow_task_attempt' => $task['workflow_task_attempt'], + 'failure' => [ + 'type' => 'ServerError', + 'message' => 'Workflow task completion failed after commands were produced: Selection cancellation must identify one authored member within a durable selection group.', + ], + ], $this->workerHeaders())->assertOk()->assertJsonPath('recorded', true); + $this->assertSame(0, WorkflowTask::query()->where('workflow_run_id', $runId) + ->where('task_type', TaskType::Workflow->value)->where('status', TaskStatus::Ready->value)->count()); + + $this->postJson('/api/workflows/select-http/runs/'.$runId.'/repair', [ + 'request_id' => 'repair-select-validation', + ], $this->apiHeaders())->assertOk(); + $this->complete($this->pollWorkflow(), [$cancel, [ + 'type' => 'complete_workflow', 'result' => Serializer::serialize('recovered'), + ]])->assertOk()->assertJsonPath('recorded', true); + $this->assertSame(RunStatus::Completed, WorkflowRun::query()->findOrFail($runId)->status); + $this->assertSame('cancelled', ActivityExecution::query()->sole()->status->value); + } + + public function test_expired_original_claim_cannot_cancel_a_member(): void + { + [$runId, $task, $cancel] = $this->stageTimerWinner(); + $before = WorkflowHistoryEvent::query()->where('workflow_run_id', $runId)->count(); + WorkflowTask::query()->findOrFail($task['task_id'])->forceFill(['lease_expires_at' => now()->subSecond()])->save(); + $this->complete($task, [$cancel])->assertStatus(409)->assertJsonPath('recorded', false); + $this->assertSame('pending', ActivityExecution::query()->sole()->status->value); + $this->assertSame($before, WorkflowHistoryEvent::query()->where('workflow_run_id', $runId)->count()); + } + + #[DataProvider('requiredMemberFields')] + public function test_missing_identity_field_is_refused_without_mutation(string $field): void + { + [$runId, $task, $cancel] = $this->stageTimerWinner(); + $before = WorkflowHistoryEvent::query()->where('workflow_run_id', $runId)->count(); + unset($cancel[$field]); + $this->complete($task, [$cancel])->assertUnprocessable(); + $this->assertSame('pending', ActivityExecution::query()->sole()->status->value); + $this->assertSame($before, WorkflowHistoryEvent::query()->where('workflow_run_id', $runId)->count()); + } + + public static function requiredMemberFields(): array + { + return array_map(static fn (string $field): array => [$field], [ + 'selection_group_id', 'member_key', 'member_index', 'member_base_sequence', + 'member_size', 'operation_kind', 'operation_identity', + ]); + } + private function stageTimerWinner(): array { $start = $this->postJson('/api/workflows', [ diff --git a/tests/Feature/WorkflowTaskCommandFieldRetentionTest.php b/tests/Feature/WorkflowTaskCommandFieldRetentionTest.php new file mode 100644 index 000000000..fd683ff28 --- /dev/null +++ b/tests/Feature/WorkflowTaskCommandFieldRetentionTest.php @@ -0,0 +1,100 @@ +commands(); + $types = array_merge(WorkerProtocolVersion::terminalCommandTypes(), WorkerProtocolVersion::nonTerminalCommandTypes()); + $covered = array_column($commands, 'type'); + sort($types); + sort($covered); + $this->assertSame($types, $covered, 'Every package command needs a field-retention example.'); + + foreach ($commands as $command) { + $normalized = WorkflowCommandNormalizer::normalize([$command]); + $validated = Validator::make([ + 'lease_owner' => 'original-worker', 'workflow_task_attempt' => 1, + 'commands' => [$command], + ], WorkflowTaskCompletionValidation::rules())->validate(); + $this->assertSame($normalized, WorkflowCommandNormalizer::normalize($validated['commands']), $command['type']); + } + } + + public function test_unknown_fields_stay_out_and_server_principal_fields_are_prohibited(): void + { + $input = [ + 'lease_owner' => 'original-worker', 'workflow_task_attempt' => 1, + 'commands' => [['type' => 'complete_workflow', 'result' => null, 'unknown' => 'discard']], + ]; + $validated = Validator::make($input, WorkflowTaskCompletionValidation::rules())->validate(); + $this->assertArrayNotHasKey('unknown', $validated['commands'][0]); + foreach (['principal_subject', 'principal_method', 'principal_roles', 'principal_tenant', 'principal_claims'] as $field) { + $forged = $input; + $forged['commands'][0][$field] = 'forged'; + $this->assertTrue(Validator::make($forged, WorkflowTaskCompletionValidation::rules())->fails(), $field); + } + } + + private function commands(): array + { + $payload = Serializer::serialize([9007199254740993, null, 'λ']); + $retry = ['max_attempts' => 2, 'backoff_seconds' => [1], 'non_retryable_error_types' => ['InvalidInput']]; + + return [ + ['type' => 'complete_workflow', 'result' => $payload], + ['type' => 'fail_workflow', 'message' => 'original failure', 'exception_class' => 'OriginalFailure', + 'non_retryable' => true, 'failed_step_sequence' => 1, 'failed_activity_execution_id' => 'original-activity', + 'exception' => ['type' => 'OriginalFailure', 'message' => 'original failure']], + ['type' => 'continue_as_new', 'workflow_type' => 'next-type', 'queue' => 'next-queue', 'arguments' => $payload], + ['type' => 'cancel_selection_operation', 'selection_group_id' => 'select-calls:1:2', + 'member_key' => '0', 'member_index' => 0, 'member_base_sequence' => 1, 'member_size' => 1, + 'operation_kind' => 'activity', 'operation_identity' => 'original-activity'], + ['type' => 'schedule_activity', 'activity_type' => 'activity', 'arguments' => $payload, + 'connection' => 'database', 'queue' => 'activities', 'retry_policy' => $retry, + 'start_to_close_timeout' => 10, 'schedule_to_start_timeout' => 5, + 'schedule_to_close_timeout' => 30, 'heartbeat_timeout' => 2], + ['type' => 'start_timer', 'delay_seconds' => 0], + ['type' => 'start_child_workflow', 'workflow_type' => 'child', 'arguments' => $payload, + 'connection' => 'database', 'queue' => 'children', 'parent_close_policy' => 'abandon', + 'cancellation_policy' => 'abandon', 'execution_timeout_seconds' => 30, + 'run_timeout_seconds' => 10, 'retry_policy' => $retry], + ['type' => 'start_service_operation', 'endpoint_name' => 'endpoint', 'service_name' => 'service', + 'operation_name' => 'operation', 'request_payload' => $payload, 'namespace' => 'target', + 'caller_namespace' => 'source', 'service_call_id' => 'call-id', 'idempotency_key' => 'key', + 'mode_override' => 'async', 'wait_for' => 'completed', 'wait_timeout_seconds' => 0, + 'target_workflow_instance_id' => 'target-workflow', 'target_workflow_run_id' => 'target-run', + 'connection' => 'database', 'queue' => 'target-queue', 'business_key' => 'business-key', + 'labels' => ['label'], 'memo' => ['key' => 'value'], 'search_attributes' => ['State' => 'active'], + 'duplicate_start_policy' => 'return_existing_active', 'metadata' => ['key' => 'value'], + 'request_payload_reference' => 'reference-id'], + ['type' => 'complete_update', 'update_id' => 'update-id', 'result' => $payload], + ['type' => 'fail_update', 'update_id' => 'update-id', 'message' => 'failure', + 'exception_type' => 'UpdateFailure', 'non_retryable' => true], + ['type' => 'record_side_effect', 'result' => $payload], + ['type' => 'record_local_activity', 'activity_type' => 'local', 'outcome' => 'completed', + 'arguments' => $payload, 'result' => $payload, 'retry_policy' => $retry, + 'start_to_close_timeout' => 10, 'schedule_to_close_timeout' => 30, 'heartbeat_timeout' => 2, + 'attempts' => [['attempt_id' => 'local-attempt', 'attempt_number' => 1, 'outcome' => 'completed', + 'duration_ms' => 3, 'heartbeats' => [['elapsed_ms' => 1, 'details' => ['step' => 'started']]]]]], + ['type' => 'record_version_marker', 'change_id' => 'change', 'version' => 0, 'min_supported' => -1, 'max_supported' => 1], + ['type' => 'upsert_memo', 'entries' => MemoPayload::mapEnvelope(['key' => 'λ', 'delete' => null])], + ['type' => 'upsert_search_attributes', 'attributes' => ['State' => 'active'], 'attribute_types' => ['State' => 'keyword']], + ['type' => 'open_condition_wait', 'condition_key' => 'condition', 'timeout_seconds' => 0, + 'condition_definition_fingerprint' => 'fingerprint', 'condition_wait_occurrence_id' => 'occurrence'], + ['type' => 'open_signal_wait', 'signal_name' => 'signal', 'timeout_seconds' => 0], + ]; + } +} From a19ff2ec59d091bfa34f47b3bb9158b8729c6201 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 21 Oct 2026 11:10:46 +0000 Subject: [PATCH 3/9] Use the actual expired-claim response contract in the HTTP regression --- tests/Feature/SelectionCancellationHttpTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Feature/SelectionCancellationHttpTest.php b/tests/Feature/SelectionCancellationHttpTest.php index 6576c9067..4ac52cf07 100644 --- a/tests/Feature/SelectionCancellationHttpTest.php +++ b/tests/Feature/SelectionCancellationHttpTest.php @@ -6,6 +6,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Queue; +use Illuminate\Testing\TestResponse; use PHPUnit\Framework\Attributes\DataProvider; use Tests\Feature\Concerns\ServerTestHelpers; use Tests\TestCase; @@ -103,7 +104,7 @@ public function test_expired_original_claim_cannot_cancel_a_member(): void [$runId, $task, $cancel] = $this->stageTimerWinner(); $before = WorkflowHistoryEvent::query()->where('workflow_run_id', $runId)->count(); WorkflowTask::query()->findOrFail($task['task_id'])->forceFill(['lease_expires_at' => now()->subSecond()])->save(); - $this->complete($task, [$cancel])->assertStatus(409)->assertJsonPath('recorded', false); + $this->complete($task, [$cancel])->assertStatus(409); $this->assertSame('pending', ActivityExecution::query()->sole()->status->value); $this->assertSame($before, WorkflowHistoryEvent::query()->where('workflow_run_id', $runId)->count()); } @@ -166,7 +167,7 @@ private function pollWorkflow(): array ], $this->workerHeaders())->assertOk()->json('task'); } - private function complete(array $task, array $commands, string $namespace = 'default'): \Illuminate\Testing\TestResponse + private function complete(array $task, array $commands, string $namespace = 'default'): TestResponse { return $this->postJson('/api/worker/workflow-tasks/'.$task['task_id'].'/complete', [ 'lease_owner' => 'select-worker', 'workflow_task_attempt' => $task['workflow_task_attempt'], From d055dc55dcd15c59a64dfc6bc728d154a3197368 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 21 Oct 2026 11:12:20 +0000 Subject: [PATCH 4/9] Prepare Server 2.10.1 and Helm 0.2.3 for select cancellation recovery --- composer.json | 2 +- composer.lock | 2 +- docker-compose.dedicated-matching.yml | 4 ++-- docker-compose.memo-rolling.yml | 4 ++-- docker-compose.published.yml | 4 ++-- docker-compose.small-cluster.yml | 2 +- docker-compose.yml | 8 ++++---- docs/select-member-cancellation-recovery.md | 2 +- k8s/README.md | 10 +++++----- k8s/helm/durable-workflow/Chart.yaml | 6 +++--- k8s/helm/durable-workflow/README.md | 2 +- .../durable-workflow/ci/existing-secrets-values.yaml | 2 +- .../durable-workflow/ci/ingress-and-hpa-values.yaml | 2 +- .../durable-workflow/ci/inline-secrets-values.yaml | 2 +- k8s/helm/durable-workflow/templates/_helpers.tpl | 2 +- k8s/helm/durable-workflow/values.yaml | 2 +- k8s/helm/examples/values-dev.yaml | 2 +- .../examples/values-external-secrets-operator.yaml | 2 +- .../examples/values-production-existing-secrets.yaml | 2 +- k8s/migration-job.yaml | 2 +- k8s/scheduler-cronjob.yaml | 2 +- k8s/secret.yaml | 2 +- k8s/server-deployment.yaml | 2 +- k8s/worker-deployment.yaml | 2 +- resources/release/source-release.json | 4 ++-- scripts/k8s-kind-smoke.sh | 2 +- tests/Feature/SelectionCancellationHttpTest.php | 6 +++--- 27 files changed, 42 insertions(+), 42 deletions(-) diff --git a/composer.json b/composer.json index e40bcd2d7..ec9ad5514 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,7 @@ }, "extra": { "durable-workflow": { - "product-train": "2.10.0" + "product-train": "2.10.1" }, "laravel": { "dont-discover": [] diff --git a/composer.lock b/composer.lock index a76e2a3f7..8e9e72613 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6efd01e2e4b94293e21ace88e7088f04", + "content-hash": "050062197e287c4c7ddc1ccd2605d0e5", "packages": [ { "name": "apache/avro", diff --git a/docker-compose.dedicated-matching.yml b/docker-compose.dedicated-matching.yml index 77b4b2b41..1ec3d1349 100644 --- a/docker-compose.dedicated-matching.yml +++ b/docker-compose.dedicated-matching.yml @@ -32,13 +32,13 @@ name: durable-workflow-server # daemon reports `shape: dedicated`. # Generated by scripts/ci/sync-source-release.mjs. Do not edit the fallback. -x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.10.0}} +x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.10.1}} x-server-environment: &server-environment APP_NAME: "Durable Workflow Server" APP_ENV: ${APP_ENV:-local} DW_SERVER_KEY: ${DW_SERVER_KEY:-} - APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.10.0}} + APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.10.1}} APP_DEBUG: ${APP_DEBUG:-false} DB_CONNECTION: mysql DB_HOST: mysql diff --git a/docker-compose.memo-rolling.yml b/docker-compose.memo-rolling.yml index 1e43c93b7..83147fc2b 100644 --- a/docker-compose.memo-rolling.yml +++ b/docker-compose.memo-rolling.yml @@ -49,14 +49,14 @@ services: command: ["server-bootstrap"] environment: <<: *runtime-environment - APP_VERSION: ${APP_VERSION:-2.10.0} + APP_VERSION: ${APP_VERSION:-2.10.1} successor: image: ${DW_MEMO_SUCCESSOR_IMAGE:-durable-workflow/server-memo-rolling:local} ports: !override [] environment: <<: *runtime-environment - APP_VERSION: ${APP_VERSION:-2.10.0} + APP_VERSION: ${APP_VERSION:-2.10.1} DW_SERVER_ID: memo-successor DW_SERVER_TOPOLOGY_SHAPE: standalone_server DW_SERVER_PROCESS_CLASS: server_http_node diff --git a/docker-compose.published.yml b/docker-compose.published.yml index 7514a273a..cf3bed2b7 100644 --- a/docker-compose.published.yml +++ b/docker-compose.published.yml @@ -1,13 +1,13 @@ name: durable-workflow-server # Generated by scripts/ci/sync-source-release.mjs. Do not edit the fallback. -x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.10.0}} +x-server-image: &server-image ${DW_SERVER_IMAGE:-durableworkflow/server:${DW_SERVER_TAG:-2.10.1}} x-server-environment: &server-environment APP_NAME: "Durable Workflow Server" APP_ENV: ${APP_ENV:-local} DW_SERVER_KEY: ${DW_SERVER_KEY:-} - APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.10.0}} + APP_VERSION: ${APP_VERSION:-${DW_SERVER_TAG:-2.10.1}} APP_DEBUG: ${APP_DEBUG:-false} LOG_CHANNEL: ${LOG_CHANNEL:-stderr} LOG_LEVEL: ${LOG_LEVEL:-info} diff --git a/docker-compose.small-cluster.yml b/docker-compose.small-cluster.yml index 5750b765b..b416bcf96 100644 --- a/docker-compose.small-cluster.yml +++ b/docker-compose.small-cluster.yml @@ -12,7 +12,7 @@ x-server-build: &server-build x-server-environment: &server-environment APP_NAME: "Durable Workflow Server" APP_ENV: testing - APP_VERSION: ${APP_VERSION:-2.10.0} + APP_VERSION: ${APP_VERSION:-2.10.1} APP_DEBUG: "false" DW_SERVER_KEY: ${DW_SERVER_KEY:-base64:5Zt4nUhlCm3DD0nLXZJQdHiwPfb56yGo9gNV/g3jYbY=} DB_CONNECTION: ${DW_SMALL_CLUSTER_DB:-mysql} diff --git a/docker-compose.yml b/docker-compose.yml index ff6b2bb1e..c7d5d7b5a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: DW_SERVER_KEY: "${DW_SERVER_KEY:-}" DW_SERVER_TOPOLOGY_SHAPE: standalone_server DW_SERVER_PROCESS_CLASS: server_http_node - APP_VERSION: "${APP_VERSION:-2.10.0}" + APP_VERSION: "${APP_VERSION:-2.10.1}" APP_DEBUG: "false" DB_CONNECTION: mysql DB_HOST: mysql @@ -62,7 +62,7 @@ services: APP_NAME: "Durable Workflow Server" APP_ENV: local DW_SERVER_KEY: "${DW_SERVER_KEY:-}" - APP_VERSION: "${APP_VERSION:-2.10.0}" + APP_VERSION: "${APP_VERSION:-2.10.1}" APP_DEBUG: "false" DB_CONNECTION: mysql DB_HOST: mysql @@ -124,7 +124,7 @@ services: DW_SERVER_KEY: "${DW_SERVER_KEY:-}" DW_SERVER_TOPOLOGY_SHAPE: standalone_server DW_SERVER_PROCESS_CLASS: worker_node - APP_VERSION: "${APP_VERSION:-2.10.0}" + APP_VERSION: "${APP_VERSION:-2.10.1}" DB_CONNECTION: mysql DB_HOST: mysql DB_PORT: 3306 @@ -178,7 +178,7 @@ services: DW_SERVER_KEY: "${DW_SERVER_KEY:-}" DW_SERVER_TOPOLOGY_SHAPE: standalone_server DW_SERVER_PROCESS_CLASS: scheduler_node - APP_VERSION: "${APP_VERSION:-2.10.0}" + APP_VERSION: "${APP_VERSION:-2.10.1}" DB_CONNECTION: mysql DB_HOST: mysql DB_PORT: 3306 diff --git a/docs/select-member-cancellation-recovery.md b/docs/select-member-cancellation-recovery.md index 5b9db73cb..1f8393344 100644 --- a/docs/select-member-cancellation-recovery.md +++ b/docs/select-member-cancellation-recovery.md @@ -6,7 +6,7 @@ when validating a workflow-task completion. The runtime then refuses the workflow replay error, leaving the run waiting with `workflow_replay_blocked`. The activity the workflow tried to cancel can still be queued or running. -Upgrade to the corrected PHP Server release before repairing affected runs. +Upgrade to PHP Server 2.10.1 or later before repairing affected runs. The correction preserves the existing command format and does not require an SDK or Workflow dependency upgrade. It does not automatically retry blocked runs or undo an activity that has already executed. diff --git a/k8s/README.md b/k8s/README.md index b58d07ed8..1f64202c7 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -13,7 +13,7 @@ The checked-in manifests are synchronized with the repository's stable source release and pin its Docker Hub tag: ```text -durableworkflow/server:2.10.0 +durableworkflow/server:2.10.1 ``` Before production use, patch every workload image to the exact published tag or @@ -21,15 +21,15 @@ digest you intend to run: ```bash kubectl set image -n durable-workflow deploy/durable-workflow-server \ - server=durableworkflow/server:2.10.0 + server=durableworkflow/server:2.10.1 kubectl set image -n durable-workflow deploy/durable-workflow-worker \ - worker=durableworkflow/server:2.10.0 + worker=durableworkflow/server:2.10.1 kubectl set image -n durable-workflow cronjob/durable-workflow-scheduler \ - scheduler=durableworkflow/server:2.10.0 + scheduler=durableworkflow/server:2.10.1 ``` GitHub Container Registry publishes the same release line at -`ghcr.io/durable-workflow/server:2.10.0`. Digest pinning is preferred for strict +`ghcr.io/durable-workflow/server:2.10.1`. Digest pinning is preferred for strict change control. The manifests expect you to provide: diff --git a/k8s/helm/durable-workflow/Chart.yaml b/k8s/helm/durable-workflow/Chart.yaml index 9f0ea4438..ee733b24b 100644 --- a/k8s/helm/durable-workflow/Chart.yaml +++ b/k8s/helm/durable-workflow/Chart.yaml @@ -5,11 +5,11 @@ type: application # The chart's own semver version. Bumped on every chart release; treated as # independent of the server image version (appVersion). Breaking-change rules # for this version live in docs/UPGRADING.md alongside the chart. -version: 0.2.2 +version: 0.2.3 # The immutable Durable Workflow Server identity this chart release packages. # The onboarding default in values.yaml and appVersion are generated from the # checked-in source release record. -appVersion: "2.10.0" +appVersion: "2.10.1" kubeVersion: ">=1.27.0-0" home: https://durable-workflow.github.io/docs/2.0/deployment sources: @@ -30,7 +30,7 @@ annotations: # exact commit that most recently changed the packaged chart. org.opencontainers.image.source: https://github.com/durable-workflow/server dev.durable-workflow.source-revision: "unreleased" - dev.durable-workflow.image-reference: "docker.io/durableworkflow/server:2.10.0" + dev.durable-workflow.image-reference: "docker.io/durableworkflow/server:2.10.1" artifacthub.io/license: MIT artifacthub.io/category: integration-delivery # Free-form changelog for the current chart release shown by Artifact Hub. diff --git a/k8s/helm/durable-workflow/README.md b/k8s/helm/durable-workflow/README.md index 61b0d7a20..4883a86fa 100644 --- a/k8s/helm/durable-workflow/README.md +++ b/k8s/helm/durable-workflow/README.md @@ -63,7 +63,7 @@ helm install durable-workflow ./k8s/helm/durable-workflow \ ```yaml image: - tag: "2.10.0" + tag: "2.10.1" # Pin a digest in production: # digest: "sha256:abc123..." # memoPayloadStorage: "raw-json-v1" # Required for a digest or custom image. diff --git a/k8s/helm/durable-workflow/ci/existing-secrets-values.yaml b/k8s/helm/durable-workflow/ci/existing-secrets-values.yaml index 9072e9a18..3d1e2d812 100644 --- a/k8s/helm/durable-workflow/ci/existing-secrets-values.yaml +++ b/k8s/helm/durable-workflow/ci/existing-secrets-values.yaml @@ -1,7 +1,7 @@ # CI fixture: GitOps / externally-managed-secret path. The chart consumes # existing Secrets and renders no Secret resources of its own. image: - tag: "2.10.0" + tag: "2.10.1" externalDatabase: connection: pgsql diff --git a/k8s/helm/durable-workflow/ci/ingress-and-hpa-values.yaml b/k8s/helm/durable-workflow/ci/ingress-and-hpa-values.yaml index b1d930ed5..ab05a5d9f 100644 --- a/k8s/helm/durable-workflow/ci/ingress-and-hpa-values.yaml +++ b/k8s/helm/durable-workflow/ci/ingress-and-hpa-values.yaml @@ -1,6 +1,6 @@ # CI fixture: ingress + autoscaling enabled. Exercises optional templates. image: - tag: "2.10.0" + tag: "2.10.1" externalDatabase: connection: mysql diff --git a/k8s/helm/durable-workflow/ci/inline-secrets-values.yaml b/k8s/helm/durable-workflow/ci/inline-secrets-values.yaml index 9031db341..5a32320fd 100644 --- a/k8s/helm/durable-workflow/ci/inline-secrets-values.yaml +++ b/k8s/helm/durable-workflow/ci/inline-secrets-values.yaml @@ -2,7 +2,7 @@ # chart's render path is exercised end-to-end. Real deployments should use # existingSecret instead. image: - tag: "2.10.0" + tag: "2.10.1" externalDatabase: connection: mysql diff --git a/k8s/helm/durable-workflow/templates/_helpers.tpl b/k8s/helm/durable-workflow/templates/_helpers.tpl index e0177287c..df6f4a5b7 100644 --- a/k8s/helm/durable-workflow/templates/_helpers.tpl +++ b/k8s/helm/durable-workflow/templates/_helpers.tpl @@ -88,7 +88,7 @@ resolved by an explicit capability declaration or an existing workload marker. {{- define "durable-workflow.memoPayloadStorageForImage" -}} {{- $image := toString . -}} {{- $normalized := regexReplaceAll "^index\\.docker\\.io/" $image "docker.io/" -}} -{{- if eq $normalized "docker.io/durableworkflow/server:2.10.0" -}} +{{- if eq $normalized "docker.io/durableworkflow/server:2.10.1" -}} dual-v1 {{- else if regexMatch "^docker\\.io/durableworkflow/server:2\\.0\\.0-rc\\.[0-9]+$" $normalized -}} {{- $releaseCandidate := atoi (regexFind "[0-9]+$" $normalized) -}} diff --git a/k8s/helm/durable-workflow/values.yaml b/k8s/helm/durable-workflow/values.yaml index b0f960047..d521dd292 100644 --- a/k8s/helm/durable-workflow/values.yaml +++ b/k8s/helm/durable-workflow/values.yaml @@ -21,7 +21,7 @@ image: registry: docker.io repository: durableworkflow/server # Generated by scripts/ci/sync-source-release.mjs. Do not edit this default. - tag: "2.10.0" + tag: "2.10.1" # Optional digest pin. When set, takes precedence over tag for change control. # Example: "sha256:abc123..." digest: "" diff --git a/k8s/helm/examples/values-dev.yaml b/k8s/helm/examples/values-dev.yaml index 68ccfe92e..9ba84bb69 100644 --- a/k8s/helm/examples/values-dev.yaml +++ b/k8s/helm/examples/values-dev.yaml @@ -3,7 +3,7 @@ # shape in production. image: - tag: "2.10.0" + tag: "2.10.1" externalDatabase: connection: mysql diff --git a/k8s/helm/examples/values-external-secrets-operator.yaml b/k8s/helm/examples/values-external-secrets-operator.yaml index 83b970697..bc6d93e1b 100644 --- a/k8s/helm/examples/values-external-secrets-operator.yaml +++ b/k8s/helm/examples/values-external-secrets-operator.yaml @@ -5,7 +5,7 @@ # concern. image: - tag: "2.10.0" + tag: "2.10.1" externalDatabase: connection: pgsql diff --git a/k8s/helm/examples/values-production-existing-secrets.yaml b/k8s/helm/examples/values-production-existing-secrets.yaml index b427f40d5..1bcccace7 100644 --- a/k8s/helm/examples/values-production-existing-secrets.yaml +++ b/k8s/helm/examples/values-production-existing-secrets.yaml @@ -10,7 +10,7 @@ image: repository: durable-workflow/server # Pin a digest in production for change-control auditability. digest: "" # e.g. "sha256:abc123..." - tag: "2.10.0" + tag: "2.10.1" externalDatabase: connection: pgsql diff --git a/k8s/migration-job.yaml b/k8s/migration-job.yaml index 8eb2da137..f1d87ebdf 100644 --- a/k8s/migration-job.yaml +++ b/k8s/migration-job.yaml @@ -13,7 +13,7 @@ spec: restartPolicy: OnFailure containers: - name: migrate - image: durableworkflow/server:2.10.0 + image: durableworkflow/server:2.10.1 command: ["server-entrypoint"] args: ["server-bootstrap"] envFrom: diff --git a/k8s/scheduler-cronjob.yaml b/k8s/scheduler-cronjob.yaml index f80da825c..fed3121ca 100644 --- a/k8s/scheduler-cronjob.yaml +++ b/k8s/scheduler-cronjob.yaml @@ -24,7 +24,7 @@ spec: restartPolicy: Never containers: - name: scheduler - image: durableworkflow/server:2.10.0 + image: durableworkflow/server:2.10.1 command: ["server-entrypoint"] args: - sh diff --git a/k8s/secret.yaml b/k8s/secret.yaml index 5084f55ac..5236c860a 100644 --- a/k8s/secret.yaml +++ b/k8s/secret.yaml @@ -12,7 +12,7 @@ metadata: app.kubernetes.io/name: durable-workflow data: APP_NAME: "Durable Workflow Server" - APP_VERSION: "2.10.0" + APP_VERSION: "2.10.1" APP_ENV: production APP_DEBUG: "false" DB_CONNECTION: mysql diff --git a/k8s/server-deployment.yaml b/k8s/server-deployment.yaml index 214da09e1..38ca279b0 100644 --- a/k8s/server-deployment.yaml +++ b/k8s/server-deployment.yaml @@ -23,7 +23,7 @@ spec: spec: containers: - name: server - image: durableworkflow/server:2.10.0 + image: durableworkflow/server:2.10.1 ports: - containerPort: 8080 name: http diff --git a/k8s/worker-deployment.yaml b/k8s/worker-deployment.yaml index 84a6df475..39e90f133 100644 --- a/k8s/worker-deployment.yaml +++ b/k8s/worker-deployment.yaml @@ -19,7 +19,7 @@ spec: spec: containers: - name: worker - image: durableworkflow/server:2.10.0 + image: durableworkflow/server:2.10.1 command: ["server-entrypoint"] args: ["php", "artisan", "queue:work", "--sleep=1", "--tries=3", "--max-time=3600"] envFrom: diff --git a/resources/release/source-release.json b/resources/release/source-release.json index 271e80038..6d20c9916 100644 --- a/resources/release/source-release.json +++ b/resources/release/source-release.json @@ -1,9 +1,9 @@ { "schema": "durable-workflow.server.source-release/v1", "server": { - "version": "2.10.0" + "version": "2.10.1" }, "helm_chart": { - "version": "0.2.2" + "version": "0.2.3" } } diff --git a/scripts/k8s-kind-smoke.sh b/scripts/k8s-kind-smoke.sh index 3d5efad4e..5e4e2ab07 100755 --- a/scripts/k8s-kind-smoke.sh +++ b/scripts/k8s-kind-smoke.sh @@ -7,7 +7,7 @@ cluster="${K8S_SMOKE_CLUSTER:-durable-workflow-server-smoke}" image="${K8S_SMOKE_IMAGE:-durableworkflow/server:k8s-smoke}" # Generated by scripts/ci/sync-source-release.mjs so the smoke replaces the # same default shipped by the public manifests. -manifest_image="durableworkflow/server:2.10.0" +manifest_image="durableworkflow/server:2.10.1" kind_node_image="${K8S_SMOKE_KIND_NODE_IMAGE:-kindest/node:v1.29.4}" artifact_dir="${K8S_SMOKE_ARTIFACT_DIR:-/tmp/durable-workflow-k8s-kind-smoke-artifacts}" rendered_dir="${artifact_dir}/rendered-manifests" diff --git a/tests/Feature/SelectionCancellationHttpTest.php b/tests/Feature/SelectionCancellationHttpTest.php index 4ac52cf07..f9f10e199 100644 --- a/tests/Feature/SelectionCancellationHttpTest.php +++ b/tests/Feature/SelectionCancellationHttpTest.php @@ -102,11 +102,11 @@ public function test_previously_blocked_run_can_be_repaired_and_cancel_its_origi public function test_expired_original_claim_cannot_cancel_a_member(): void { [$runId, $task, $cancel] = $this->stageTimerWinner(); - $before = WorkflowHistoryEvent::query()->where('workflow_run_id', $runId)->count(); WorkflowTask::query()->findOrFail($task['task_id'])->forceFill(['lease_expires_at' => now()->subSecond()])->save(); - $this->complete($task, [$cancel])->assertStatus(409); + $this->complete($task, [$cancel])->assertStatus(409)->assertJsonPath('reason', 'lease_expired'); $this->assertSame('pending', ActivityExecution::query()->sole()->status->value); - $this->assertSame($before, WorkflowHistoryEvent::query()->where('workflow_run_id', $runId)->count()); + $this->assertSame(0, WorkflowHistoryEvent::query()->where('workflow_run_id', $runId) + ->where('event_type', HistoryEventType::SelectionOperationCancelled->value)->count()); } #[DataProvider('requiredMemberFields')] From 171d25391523966c636dbe21ac0384d241e9b5c5 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 21 Oct 2026 11:55:16 +0000 Subject: [PATCH 5/9] Reuse the workflow stream validation condition --- app/Support/WorkflowTaskCompletionValidation.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/Support/WorkflowTaskCompletionValidation.php b/app/Support/WorkflowTaskCompletionValidation.php index 2da7ae8f7..591defb2c 100644 --- a/app/Support/WorkflowTaskCompletionValidation.php +++ b/app/Support/WorkflowTaskCompletionValidation.php @@ -9,6 +9,8 @@ /** Transport rules retain the package command grammar before semantic normalization. */ final class WorkflowTaskCompletionValidation { + private const REQUIRED_WORKFLOW_STREAM = 'required_with:commands.*.workflow_stream'; + /** @return array> */ public static function rules(): array { @@ -130,10 +132,10 @@ public static function rules(): array 'commands.*.principal_claims' => ['prohibited'], ...WorkflowCommandNormalizer::parallelMetadataValidationRules(), 'commands.*.workflow_stream' => ['nullable', 'array'], - 'commands.*.workflow_stream.operation' => ['required_with:commands.*.workflow_stream', 'string', 'in:append,close,error'], - 'commands.*.workflow_stream.stream_name' => ['required_with:commands.*.workflow_stream', 'string', 'max:191'], - 'commands.*.workflow_stream.command_identity' => ['required_with:commands.*.workflow_stream', 'string', 'max:191'], - 'commands.*.workflow_stream.command_ordinal' => ['required_with:commands.*.workflow_stream', 'integer', 'min:0'], + 'commands.*.workflow_stream.operation' => [self::REQUIRED_WORKFLOW_STREAM, 'string', 'in:append,close,error'], + 'commands.*.workflow_stream.stream_name' => [self::REQUIRED_WORKFLOW_STREAM, 'string', 'max:191'], + 'commands.*.workflow_stream.command_identity' => [self::REQUIRED_WORKFLOW_STREAM, 'string', 'max:191'], + 'commands.*.workflow_stream.command_ordinal' => [self::REQUIRED_WORKFLOW_STREAM, 'integer', 'min:0'], 'commands.*.workflow_stream.items' => ['nullable', 'array', 'max:'.WorkflowStreamService::DEFAULT_MAX_ITEMS_PER_APPEND], 'commands.*.workflow_stream.items.*' => ['array'], 'commands.*.workflow_stream.items.*.payload' => ['nullable'], From f3999954dd3c50a0060962c02d5715e118db54a7 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 21 Oct 2026 14:28:52 +0000 Subject: [PATCH 6/9] security: require patched PHP and reduce request startup exposure --- Dockerfile | 5 ++- README.md | 4 +++ docker/php-custom.ini | 6 +++- docs/security/php-runtime-2026-10-21.md | 42 +++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 docs/security/php-runtime-2026-10-21.md diff --git a/Dockerfile b/Dockerfile index a816cf61a..ca2125655 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,10 @@ RUN git clone --depth 1 --branch "${PHPREDIS_VERSION}" https://github.com/phpred exit 1; \ fi -FROM php:8.3-apache AS base +FROM php:8.3.36-apache AS base + +# Both web and CLI roles require the fixed PHP runtime, including cached builds. +RUN php -r 'if (PHP_MAJOR_VERSION !== 8 || PHP_MINOR_VERSION !== 3 || PHP_VERSION_ID < 80336) { fwrite(STDERR, "Patched PHP 8.3.36 or later on the 8.3 line is required; found ".PHP_VERSION.PHP_EOL); exit(1); }' COPY --from=phpredis-source /phpredis /usr/src/php/ext/redis diff --git a/README.md b/README.md index 94e8dfe8a..e2823d8fb 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ Laravel applications can also run the engine directly in ## Run Server +For the 21 October 2026 critical PHP update, follow the +[PHP runtime security notice](docs/security/php-runtime-2026-10-21.md) and verify +the patched runtime in every deployed role. + The published Compose stack starts Server with MySQL, Redis, a queue worker, and the scheduler. It bootstraps the database and the default namespace before accepting traffic. diff --git a/docker/php-custom.ini b/docker/php-custom.ini index acd2190a6..c4846b8d8 100644 --- a/docker/php-custom.ini +++ b/docker/php-custom.ini @@ -4,7 +4,11 @@ expose_php = Off ; The API enforces separate bounded JSON and binary transport limits. PHP must ; not consume/discard POST bodies or emit warnings before that boundary runs. enable_post_data_reading = Off -post_max_size = 0 +; Keep the default 64 MiB binary transport usable. The API applies its stricter +; JSON and per-route limits; it does not accept PHP multipart file uploads. +post_max_size = 64M +upload_max_filesize = 64M +session.upload_progress.enabled = Off display_errors = Off display_startup_errors = Off log_errors = On diff --git a/docs/security/php-runtime-2026-10-21.md b/docs/security/php-runtime-2026-10-21.md new file mode 100644 index 000000000..6af5b16e7 --- /dev/null +++ b/docs/security/php-runtime-2026-10-21.md @@ -0,0 +1,42 @@ +# Critical PHP runtime update, 21 October 2026 + +The PHP project disclosed two critical vulnerabilities affecting web runtimes, +including Apache with mod_php, PHP-FPM and FrankenPHP: + +- CVE-2026-109618, out-of-memory request-state corruption. +- CVE-2026-109619, stale request-owned global state. + +Read the [upstream advisory](https://www.php.net/archive/2026.php#2026-10-21-1). +The fixed PHP versions are 8.2.35, 8.3.36, 8.4.27 and 8.5.12. Older branches +require a vendor backport or an upgrade to a supported patched release. + +## Update an installed Server + +Use a [Server release](https://github.com/durable-workflow/server/releases) whose +notes record the patched PHP version and exact published image digest. Pull that +image and replace every API, worker, scheduler and bootstrap role. An unchanged +running container does not adopt a newly pulled image. + +Verify the actual `PHP_VERSION` in each replacement role and the PHP version used +by the HTTP handler. Do not infer it from a floating base tag, environment +variable, application version, cached build or startup banner. Check readiness, +authentication, worker polling/completion and your external-payload transport +after the rollout. + +Published version tags remain immutable. Historical images, including frozen +conformance references, keep their original bytes and do not become patched. +Rollback to a vulnerable generation restores service only temporarily and must +be followed by a forward update. + +## Configuration reductions are incomplete + +The Server image disables automatic POST parsing and session upload-progress +tracking before request handling. Finite POST/upload settings preserve the +default 64 MiB binary transport; the API separately enforces its JSON and +authenticated binary limits. Large payloads use the runtime's binary transport, +not PHP multipart uploads. + +These settings reduce exposure but do not fix either vulnerability. Follow the +upstream guidance for an installation that cannot update immediately, including +its memory-limit and container-boundary tradeoffs. Only a patched PHP runtime +completes the correction. Do not treat a configuration-only image as patched. From 5d8735c547070464647f2c87801d45b6d5aaab6f Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 21 Oct 2026 15:10:51 +0000 Subject: [PATCH 7/9] Build patched Apache PHP from verified upstream release --- Dockerfile | 78 ++- docker/php-keyring.gpg | 804 ++++++++++++++++++++++++ docs/security/php-runtime-2026-10-21.md | 16 + 3 files changed, 897 insertions(+), 1 deletion(-) create mode 100644 docker/php-keyring.gpg diff --git a/Dockerfile b/Dockerfile index ca2125655..e54d9aba3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,83 @@ RUN git clone --depth 1 --branch "${PHPREDIS_VERSION}" https://github.com/phpred exit 1; \ fi -FROM php:8.3.36-apache AS base +# Use the signed upstream fix while Docker Official Image publication catches up. +# Configure options match docker-library/php at f6048b5f6e3c2202ee082665a4ccc0d90de291a3. +FROM php:8.3.35-apache@sha256:c59c808172ee3672cd334f8f2a5d7b1480a9d7273a368f412b8ee9b4ef418184 AS patched-php + +LABEL dev.durable-workflow.php.source.version="8.3.36" \ + dev.durable-workflow.php.source.sha256="dd54693aa334c5c4d72e9aee96b082c2473750a18a6c1d105789c93a3b1692fd" + +ARG PHP_BUILD_JOBS=2 +ENV PHP_VERSION="8.3.36" \ + PHP_URL="https://www.php.net/distributions/php-8.3.36.tar.xz" \ + PHP_ASC_URL="https://www.php.net/distributions/php-8.3.36.tar.xz.asc" \ + PHP_SHA256="dd54693aa334c5c4d72e9aee96b082c2473750a18a6c1d105789c93a3b1692fd" + +COPY docker/php-keyring.gpg /tmp/php-keyring.gpg + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + $PHPIZE_DEPS apache2-dev ca-certificates curl gnupg \ + libargon2-dev libcurl4-openssl-dev libonig-dev libreadline-dev \ + libsodium-dev libsqlite3-dev libssl-dev libxml2-dev xz-utils zlib1g-dev; \ + printf '%s %s\n' \ + '43fcb3eaf65536cb0833c229b492c70cfad8562138164dd13673e08925d6c56c' \ + /tmp/php-keyring.gpg | sha256sum --check -; \ + curl --fail --location --show-error --silent --output /usr/src/php.tar.xz "${PHP_URL}"; \ + printf '%s %s\n' "${PHP_SHA256}" /usr/src/php.tar.xz | sha256sum --check -; \ + curl --fail --location --show-error --silent --output /tmp/php.tar.xz.asc "${PHP_ASC_URL}"; \ + export GNUPGHOME="$(mktemp -d)"; \ + gpg --batch --import /tmp/php-keyring.gpg; \ + gpg --batch --verify /tmp/php.tar.xz.asc /usr/src/php.tar.xz; \ + gpgconf --kill all; \ + rm -rf "${GNUPGHOME}" /usr/src/php; \ + mkdir -p /usr/src/php; \ + tar --extract --file /usr/src/php.tar.xz --xz --strip-components=1 --directory /usr/src/php; \ + cd /usr/src/php; \ + gnu_arch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \ + deb_multiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \ + export CFLAGS="${PHP_CFLAGS}" CPPFLAGS="${PHP_CPPFLAGS}" LDFLAGS="${PHP_LDFLAGS}" \ + PHP_BUILD_PROVIDER='https://github.com/docker-library/php' PHP_UNAME='Linux - Docker'; \ + ./configure \ + --build="${gnu_arch}" \ + --sysconfdir="${PHP_INI_DIR%/php}" \ + --with-config-file-path="${PHP_INI_DIR}" \ + --with-config-file-scan-dir="${PHP_INI_DIR}/conf.d" \ + --enable-option-checking=fatal --with-mhash --with-pic \ + --enable-mbstring --enable-mysqlnd --with-password-argon2 --with-sodium=shared \ + --with-pdo-sqlite=/usr --with-sqlite3=/usr --with-curl --with-iconv \ + --with-openssl --with-readline --with-zlib --disable-phpdbg --with-pear \ + --with-libdir="lib/${deb_multiarch}" --disable-cgi --with-apxs2; \ + make -j "${PHP_BUILD_JOBS}"; \ + make install; \ + cp php.ini-development php.ini-production "${PHP_INI_DIR}/"; \ + strip --strip-all /usr/local/bin/php /usr/lib/apache2/modules/libphp.so; \ + test "$(php -r 'echo PHP_VERSION;')" = "${PHP_VERSION}"; \ + rm -rf /var/lib/apt/lists/* /tmp/php.tar.xz.asc /tmp/php-keyring.gpg + +FROM php:8.3.35-apache@sha256:c59c808172ee3672cd334f8f2a5d7b1480a9d7273a368f412b8ee9b4ef418184 AS base + +# Replace every PHP runtime/build artifact, including the Apache module and +# extension sources used below. A changed image environment is not a PHP update. +COPY --from=patched-php /usr/local/bin/php /usr/local/bin/php +COPY --from=patched-php /usr/local/bin/php-config /usr/local/bin/php-config +COPY --from=patched-php /usr/local/bin/phpize /usr/local/bin/phpize +COPY --from=patched-php /usr/local/lib/php/ /usr/local/lib/php/ +COPY --from=patched-php /usr/local/include/php/ /usr/local/include/php/ +COPY --from=patched-php /usr/lib/apache2/modules/libphp.so /usr/lib/apache2/modules/libphp.so +COPY --from=patched-php /usr/src/php.tar.xz /usr/src/php.tar.xz +COPY --from=patched-php /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini-development +COPY --from=patched-php /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini-production + +ENV PHP_VERSION="8.3.36" \ + PHP_URL="https://www.php.net/distributions/php-8.3.36.tar.xz" \ + PHP_ASC_URL="https://www.php.net/distributions/php-8.3.36.tar.xz.asc" \ + PHP_SHA256="dd54693aa334c5c4d72e9aee96b082c2473750a18a6c1d105789c93a3b1692fd" + +LABEL dev.durable-workflow.php.source.version="8.3.36" \ + dev.durable-workflow.php.source.sha256="dd54693aa334c5c4d72e9aee96b082c2473750a18a6c1d105789c93a3b1692fd" # Both web and CLI roles require the fixed PHP runtime, including cached builds. RUN php -r 'if (PHP_MAJOR_VERSION !== 8 || PHP_MINOR_VERSION !== 3 || PHP_VERSION_ID < 80336) { fwrite(STDERR, "Patched PHP 8.3.36 or later on the 8.3 line is required; found ".PHP_VERSION.PHP_EOL); exit(1); }' diff --git a/docker/php-keyring.gpg b/docker/php-keyring.gpg new file mode 100644 index 000000000..0c6cf79f8 --- /dev/null +++ b/docker/php-keyring.gpg @@ -0,0 +1,804 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=SWGM +-----END PGP PUBLIC KEY BLOCK----- diff --git a/docs/security/php-runtime-2026-10-21.md b/docs/security/php-runtime-2026-10-21.md index 6af5b16e7..cfdf22aba 100644 --- a/docs/security/php-runtime-2026-10-21.md +++ b/docs/security/php-runtime-2026-10-21.md @@ -28,6 +28,22 @@ conformance references, keep their original bytes and do not become patched. Rollback to a vulnerable generation restores service only temporarily and must be followed by a forward update. +## Image build provenance + +While the official Apache image is being published, this image builds PHP +8.3.36 from the upstream release tarball on an immutable Apache foundation. +The build verifies the tarball's fixed SHA-256 and detached signature against +the published PHP release keyring, whose SHA-256 is also fixed. Configure +options follow docker-library/php revision +`f6048b5f6e3c2202ee082665a4ccc0d90de291a3`. + +The resulting CLI, Apache module, runtime libraries, headers and extension +source replace the older foundation's PHP artifacts. The build checks the +actual CLI version before installing the application. Release qualification +must additionally check the real Apache handler and normal API, worker and +binary transport behavior. Source checksums or an image label alone do not +establish a working patched image. + ## Configuration reductions are incomplete The Server image disables automatic POST parsing and session upload-progress From 404ac64dd44fd02342812f04d03c93a454a25ea6 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 21 Oct 2026 15:38:56 +0000 Subject: [PATCH 8/9] Enforce HTTPS source redirects and clarify immutable PHP build inputs --- Dockerfile | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index e54d9aba3..df1d38cdd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN git clone --depth 1 --branch "${PHPREDIS_VERSION}" https://github.com/phpred # Use the signed upstream fix while Docker Official Image publication catches up. # Configure options match docker-library/php at f6048b5f6e3c2202ee082665a4ccc0d90de291a3. -FROM php:8.3.35-apache@sha256:c59c808172ee3672cd334f8f2a5d7b1480a9d7273a368f412b8ee9b4ef418184 AS patched-php +FROM php@sha256:c59c808172ee3672cd334f8f2a5d7b1480a9d7273a368f412b8ee9b4ef418184 AS patched-php LABEL dev.durable-workflow.php.source.version="8.3.36" \ dev.durable-workflow.php.source.sha256="dd54693aa334c5c4d72e9aee96b082c2473750a18a6c1d105789c93a3b1692fd" @@ -29,15 +29,18 @@ COPY docker/php-keyring.gpg /tmp/php-keyring.gpg RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ - $PHPIZE_DEPS apache2-dev ca-certificates curl gnupg \ + autoconf dpkg-dev file g++ gcc libc-dev make pkg-config re2c \ + apache2-dev ca-certificates curl gnupg \ libargon2-dev libcurl4-openssl-dev libonig-dev libreadline-dev \ libsodium-dev libsqlite3-dev libssl-dev libxml2-dev xz-utils zlib1g-dev; \ printf '%s %s\n' \ '43fcb3eaf65536cb0833c229b492c70cfad8562138164dd13673e08925d6c56c' \ /tmp/php-keyring.gpg | sha256sum --check -; \ - curl --fail --location --show-error --silent --output /usr/src/php.tar.xz "${PHP_URL}"; \ + curl --proto '=https' --proto-redir '=https' --fail --location --show-error --silent \ + --output /usr/src/php.tar.xz "${PHP_URL}"; \ printf '%s %s\n' "${PHP_SHA256}" /usr/src/php.tar.xz | sha256sum --check -; \ - curl --fail --location --show-error --silent --output /tmp/php.tar.xz.asc "${PHP_ASC_URL}"; \ + curl --proto '=https' --proto-redir '=https' --fail --location --show-error --silent \ + --output /tmp/php.tar.xz.asc "${PHP_ASC_URL}"; \ export GNUPGHOME="$(mktemp -d)"; \ gpg --batch --import /tmp/php-keyring.gpg; \ gpg --batch --verify /tmp/php.tar.xz.asc /usr/src/php.tar.xz; \ @@ -65,9 +68,9 @@ RUN set -eux; \ cp php.ini-development php.ini-production "${PHP_INI_DIR}/"; \ strip --strip-all /usr/local/bin/php /usr/lib/apache2/modules/libphp.so; \ test "$(php -r 'echo PHP_VERSION;')" = "${PHP_VERSION}"; \ - rm -rf /var/lib/apt/lists/* /tmp/php.tar.xz.asc /tmp/php-keyring.gpg + rm -rf /usr/src/php /var/lib/apt/lists/* /tmp/php.tar.xz.asc /tmp/php-keyring.gpg -FROM php:8.3.35-apache@sha256:c59c808172ee3672cd334f8f2a5d7b1480a9d7273a368f412b8ee9b4ef418184 AS base +FROM php@sha256:c59c808172ee3672cd334f8f2a5d7b1480a9d7273a368f412b8ee9b4ef418184 AS base # Replace every PHP runtime/build artifact, including the Apache module and # extension sources used below. A changed image environment is not a PHP update. @@ -90,7 +93,9 @@ LABEL dev.durable-workflow.php.source.version="8.3.36" \ dev.durable-workflow.php.source.sha256="dd54693aa334c5c4d72e9aee96b082c2473750a18a6c1d105789c93a3b1692fd" # Both web and CLI roles require the fixed PHP runtime, including cached builds. -RUN php -r 'if (PHP_MAJOR_VERSION !== 8 || PHP_MINOR_VERSION !== 3 || PHP_VERSION_ID < 80336) { fwrite(STDERR, "Patched PHP 8.3.36 or later on the 8.3 line is required; found ".PHP_VERSION.PHP_EOL); exit(1); }' +RUN php -r 'if (PHP_MAJOR_VERSION !== 8 || PHP_MINOR_VERSION !== 3 || PHP_VERSION_ID < 80336) { \ + fwrite(STDERR, "Patched PHP 8.3.36 or later on the 8.3 line is required; found ".PHP_VERSION.PHP_EOL); \ + exit(1); }' COPY --from=phpredis-source /phpredis /usr/src/php/ext/redis From 725751833e9dbf9f97c3c866a61f4ed0841b2c83 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 21 Oct 2026 15:51:48 +0000 Subject: [PATCH 9/9] Sort the unchanged PHP build dependency list --- Dockerfile | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index df1d38cdd..cb76dd1f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,10 +29,29 @@ COPY docker/php-keyring.gpg /tmp/php-keyring.gpg RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ - autoconf dpkg-dev file g++ gcc libc-dev make pkg-config re2c \ - apache2-dev ca-certificates curl gnupg \ - libargon2-dev libcurl4-openssl-dev libonig-dev libreadline-dev \ - libsodium-dev libsqlite3-dev libssl-dev libxml2-dev xz-utils zlib1g-dev; \ + apache2-dev \ + autoconf \ + ca-certificates \ + curl \ + dpkg-dev \ + file \ + g++ \ + gcc \ + gnupg \ + libargon2-dev \ + libc-dev \ + libcurl4-openssl-dev \ + libonig-dev \ + libreadline-dev \ + libsodium-dev \ + libsqlite3-dev \ + libssl-dev \ + libxml2-dev \ + make \ + pkg-config \ + re2c \ + xz-utils \ + zlib1g-dev; \ printf '%s %s\n' \ '43fcb3eaf65536cb0833c229b492c70cfad8562138164dd13673e08925d6c56c' \ /tmp/php-keyring.gpg | sha256sum --check -; \