Repository navigation
Conversation
An update whose durable contract declares a validator now waits for a validator-capable worker to approve it, as published PHP does: admission refusals in PHP's order, validation tasks keyed by run, update and request id, the dedicated and multiplexed worker polls, fenced approve and reject routes, lease expiry and re-claim, and 504 timeouts with their reasons. An accepted update's receipt no longer carries accepted: true, as PHP's result mapper drops it. The update-validation fixture records 49 steps against published PHP; native matches all of them.
|
End-to-end check with the published Python SDK (
Before this branch, the same worker could not register against native, because native advertised no update validation contract. |
|
Security review of exact
This is source acceptance only. The exact-head database/parity jobs are still running, and #712 does not qualify or replace #708/current-main integration, protected publication, multi-architecture, or credential gates. |
|
Security terminal hosted-evidence review at exact This is not yet a merge acceptance because the branch is eight commits behind current |
|
Current-main integration at exact head |
…blished 2.11.1 Fixture first for the poll_request_id contract of workflow-task polls. One worker records an empty poll and its replay after work arrived, a fresh id's lease and its replay, the replay after a workflow-task heartbeat renewed the lease, a padded copy of the id, the same id after its task completed while another was ready, and after nothing was left. Published 2.11.1: the empty id replays empty; the lease replays with the renewed lease; the padded id is the same id; a completed delivery polls afresh and leases the next task; with nothing ready the id answers empty. Native currently leases the empty id's retry, gives the fresh id nothing, and refuses the drained id with 409 poll_cached_task_expired, which PHP never sends. The unit test runs the contract on the recorded observation and on six mutations, including those native answers. Published 2.11.1 passes 3/3 locally with its maintenance loop running.
Native's workflow-poll poll_request_id differed from PHP's WorkflowTaskPollRequestStore: an empty answer was not kept, so a retry leased work that arrived meanwhile; a lease replayed its stale lease time; the key ignored queue and build; and an ID whose task had closed was refused with 409 poll_cached_task_expired, which PHP never sends. A workflow poll with a trimmed, non-empty request ID now: - binds the ID to its task kinds and keeps its answer in dw_query_cache, keyed by namespace, queue, registered build, worker and ID, as PHP's cache keys are; - replays that answer while it is deliverable: an empty or queue-changed answer for the pending window (poll timeout + 5 s, capped at a minute), a lease while its task stays leased to the worker at the same attempt, with the task's current lease; - polls afresh when the lease is no longer deliverable, so it leases the next ready task or answers empty; - writes the lease's answer in the claim transaction, so a concurrent duplicate replays it instead of leasing another task, and records _server_poll_request_id in the task payload, as PHP's PollRequestLeaseBinding does. Activity polls keep their receipts. The workflow poll, its heartbeat and the activity status route now report times with microseconds, as PHP does. The workflow-poll-request-replay fixture passes 3/3 against native locally, and a native execution test pins the same rules. Full native SQLite suite: 253 library and 454 execution tests pass.
… rust-325-update-validation-poll # Conflicts: # .github/workflows/server-parity.yml
The probe waits through the 10-second validation timeout, a 6-second lease expiry, a 2-second completion wait and several short polls before PHP does any work. On the published PHP SQLite runtime it exceeded the recorder's generic 45-second probe limit while making its last calls (train #723, job 120776618912: spawnSync php ETIMEDOUT). Expected results are unchanged.
…perseded PHPUnit runs cancel The update-validation probe's waiting child requests now retry PHP's documented backend_lock_pressure refusal (nothing committed) with the same rule as every other probe request. seconds stays the answered attempt's own wait; retries are reported on stderr. PHPUnit feature suite and Feature source qualification used a job-level always(), which GitHub never cancels, so #724's superseded-run cancellation left the old run holding the concurrency group. !cancelled() still runs both jobs when a needed job failed, so the required checks stay fail-closed.
…or its PHP leg The SQLite job's main corpus now excludes update-validation.json. A separate script records that fixture for php, embedded and rust and compares the three. Its published PHP record runs with PHP's maintenance loop stopped: under that loop's SQLite writer contention, PHP 2.11.1 loses its own validation-timeout and lease-reclaim writes to "database is locked" and its validation poll answers without the typed lock-pressure refusal (10 of 15 local runs failed with the loop running, 5 of 5 and 3 of 3 passed with it stopped). Expected results are unchanged. MySQL and PostgreSQL keep the fixture in their main corpus with their loops running. The runner gains --exclude-fixture, which must name a selected fixture and cannot be combined with --fixture. The maintenance window helper is #722's, moved to a shared path. Tests cover the selection rules, the workflow wiring and every restore path of the helper. PHPUnit Feature returns to main's job conditions and drops its concurrency group. The central action policy requires always() on the PHPUnit feature suite gate, and GitHub never cancels an always() job, so cancel-in-progress would only queue a newer run behind it. Shared Server fixtures keeps #724's cancellation.
Sonar reported 11 style findings in the two new scripts. Conditions use [[ ]], the state reader ends with an explicit return, and positional parameters are read into named locals. Behavior is unchanged: a missing COMMAND still fails under set -u, and each assertion still fails the caller under set -e. PhpMaintenanceWindowTest passes 5/5, and one local end-to-end record stopped and restored the real maintenance container.
Fixture first for the poll_request_id contract #712 advertises (normalized_task_kind_set binding, poll_request_task_kinds_conflict). On its own queue the probe now records: a multiplexed poll with an ID; its retry with the kinds reordered; the same ID with the workflow kind alone and with the validation kind alone; and a default (workflow-only) ID's lease, its retry with both kinds, and its plain replay. Published 2.11.1: the reordered retry replays the same validation task at attempt 1; both other sets answer 409 poll_request_task_kinds_conflict with requested_task_kinds, bound_task_kinds and the ID; the default ID refuses the multiplexed retry (bound ["workflow"]) and replays its own lease. The 49 existing expected steps are unchanged; only their identifier labels renumber around the 8 new steps. The fixture's int64 input was kept byte-exact by splicing the expected block as text. The recorded observation vector is the new 2.11.1 recording; its unit test passes 22/22, and published 2.11.1 passes 2/2 locally.
…lidation deliveries Native's update-validation poll advertised PHP's poll_request_id contract (normalized_task_kind_set binding, poll_request_task_kinds_conflict) without keeping it. A multiplexed retry re-polled: the cursor had already turned, so it could lease a second task, and a retry with another kind set was never refused. Native's cursor was read outside any transaction and turned in a separate one, so concurrent probes could both read the same first kind. A trimmed, non-empty poll_request_id now binds to its normalized kind set (either order of the two kinds is one set), as WorkflowTaskPollRequestStore does: - a live binding to another set answers 409 poll_request_task_kinds_conflict with requested_task_kinds, bound_task_kinds and the ID, before anything is replayed or claimed, and the existing lease stays held; - a validation delivery replays while its task stays leased to the worker at the delivered attempt; once it is not deliverable, the ID polls afresh, so an expired lease is claimed again at the next attempt; - the binding and answer expire as PHP's resultTtl sets them: five seconds after a live lease ends, capped at an hour, or the pending window. Each multiplexed probe is now one transaction, WorkflowTaskKindSelector's: replay a concurrent duplicate's answer, create the queue's cursor at update_validation if missing and lock it, claim the kind it names first and then the other, turn the cursor after a lease, and record the answer under the request ID. The workflow claim and the validation claim gain transaction-taking variants for this. Native execution tests on SQLite, PostgreSQL and MySQL cover the reordered retry, both conflict sets, the default ID retried with both kinds, the expired validation lease re-claimed at attempt 2 under its ID, a completed validation delivery polling afresh, concurrent duplicates sharing one delivery and one cursor turn, and concurrent probes leasing one task of each kind. Reading the cursor before the probe's transaction, or dropping the in-transaction replay, fails those concurrency tests on PostgreSQL. The update-validation and workflow-poll-request-replay fixtures pass 2/2 against native.
|
|
Security R2 blocker on current exact head Claims correctly serialize on the binding row and replay a result found inside the claim transaction (replay/lock). The timeout path has a gap after its last probe, though. Both the workflow loop (single-kind) and multiplexed loop (multiplexed) can:
That method locks the binding but never rechecks the now-deliverable result; it unconditionally updates the result row to The concurrent regression starts with both task kinds already ready, so every duplicate encounters the cached lease during its probe and does not exercise the probe-to-terminal-write window (current test). Please make the terminal no-task write conditional in the same transaction: after locking/binding, re-read and return any deliverable answer committed by a duplicate; write |



Part of #325.
Problem
The Python SDK lets a workflow declare update validators. A worker with validators registers only when the server advertises
update_validation_tasksand itssynchronous_update_validationcontract. Native advertised neither, and refused a validated update with 422update_validation_not_implemented. So a Python worker with validators could not run against native.Change
Native now serves synchronous update validation as published Server 2.11.1 does (
ValidatedExternalWorkflowUpdateAdmission,WorkflowUpdateValidationTaskBroker):Admission. For a live run with a complete durable contract and a declared update, native checks, in PHP's order: a recorded receipt for the
request_id; whether the contract states validators at all (409update_validator_contract_missingif not); the arguments; and arequest_id. None of these refusals is recorded.Waiting for the validator. The caller then waits for a validator without holding a transaction, up to
DW_UPDATE_VALIDATION_TIMEOUT(10 s).request_id, with a hash of the arguments.UpdateRejectedand answers 422.Worker routes.
POST /api/worker/update-validation-tasks/poll;POST /api/worker/update-validation-tasks/{task}/approve;POST /api/worker/update-validation-tasks/{task}/reject.They use Laravel's request rules, and completions are fenced like PHP's (owner, attempt, lease, worker freshness, terminal duplicates). A claim lasts
DW_UPDATE_VALIDATION_LEASE_TIMEOUT(5 s). An expired lease can be claimed again with the next attempt. The task carries the run's replay history and its unsigned history export.Multiplexed workflow poll.
task_kindsis validated. With validation tasks requested, the poll refuses a worker without the capability, answersno_workflow_capabilityfor a worker without workflow types, and alternates kinds throughworkflow_task_poll_cursors. Every workflow task namestask_kind: "workflow". Workflow poll refusals now carry the worker protocol envelope, as PHP's do.Poll request IDs (the contract Security asked for on Rust Server train: MySQL statement closes (#714), update validation, policy-map JSON, worker history presentation #723). A trimmed, non-empty
poll_request_idon a workflow-task poll keeps published PHP'sWorkflowTaskPollRequestStorecontract:poll_request_task_kinds_conflictwithrequested_task_kinds,bound_task_kindsand the ID. Nothing is replayed or claimed, and the existing lease stays held.poll_cached_task_expiredis gone.resultTtlschedule.workflow_task_poll_cursorsrow atupdate_validationif missing, and locks it.Advertisement. The capability, its contract and both timeouts are advertised, and the two
DW_UPDATE_VALIDATION_*settings are now honored.Found on the way.
accepted: true; PHP's result mapper sendsacceptedonly when it is false.update_validatorsnow makes remote updates refuse, as on PHP. Current SDKs all send the field.The decision record is
docs/rust-server-port-checkpoints/2026-10-23-update-validation.md.Fixture
update-validationrecords 49 steps against published PHP. They cover:Waiting update requests run in child PHP processes. The contract normalizes identifiers (as labels, so reuse stays checked), timestamps and the probe's workflow prefix. It also checks that each timeout really waited.
Checks (poll request IDs)
workflow-poll-request-replayrecords empty replay, lease replay after a heartbeat, a padded ID, and polling afresh after completion and after drain.update-validationadds 8 steps: a multiplexed ID, its reordered retry, both other sets, and a default ID retried with both kinds.Checks
ServerParityUpdateValidationContractTest: 22 tests, including 19 refused changes.update_validation, 3 tests) pass on SQLite, PostgreSQL and MySQL, with the other update test modules (28 tests per database).PhpImageContextTest) needs Docker inside the Node container.Not done yet
WorkerPollFencefor workflow, activity and query polls is Rust #325: fence held workflow and activity polls against replaced worker registrations #734. It is stacked on this PR because it touches the same claim path.workflow_update_runtime_contractcluster manifest is not published yet; that belongs to the cluster info work.