Skip to content

Rust #325: fence held polls against replaced worker registrations (#734) - #739

Merged
rmcdaniel merged 29 commits into
mainfrom
rust-325-worker-poll-fence
Oct 25, 2026
Merged

rmcdaniel merged 29 commits into
mainfrom
rust-325-worker-poll-fence

Conversation

@rmcdaniel

Copy link
Copy Markdown
Member

Closes #734. Stacked on #712 (base rust-325-update-validation), which reworks the same claim path; it merges after #712.

Held workflow, activity and query polls now follow published 2.11.1's WorkerPollFence:

  • A registration that is not fresh is answered stale_worker_registration at admission.
  • A heartbeat, a same-ID re-registration or a removed registration while a poll waits ends it. Workflow and activity polls answer stale_worker_registration; query polls answer empty, with or without a request ID.
  • A worker that reports its process keeps its poll across its own heartbeat.

The fixture is worker-poll-fence, recorded from 2.11.1 in three identical runs. It passes against native locally, and native execution tests pass on SQLite, PostgreSQL and MySQL. Four native tests that held earlier native-only answers now expect PHP's; each change is cited to the recording.

rmcdaniel and others added 19 commits October 23, 2026 20:28
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.
…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.
Fixture first for #734. Each case has its own worker and queue, and
held polls run in a child process while the probe changes the
registration and makes work ready. For workflow and activity polls it
records a held poll under an unchanged registration, across a worker
heartbeat, and across a same-ID re-registration, with the poll the
worker sends next. A worker that reports its process is recorded across
a heartbeat from the same process and a re-registration from a new one.
Last, a workflow task leased before a re-registration, the poll after
it, and the old attempt's completion.

Published 2.11.1, three identical recordings: work reaching a held
poll under an unchanged registration is leased; a heartbeat or a
re-registration ends the held poll with 200, task null and
stale_worker_registration, and the next poll leases the work; the same
process's heartbeat keeps the poll and a new process's registration
ends it; a re-registration hands the leased workflow task out again at
attempt 2 and refuses the old attempt with 409
workflow_task_attempt_mismatch.

PHP keeps registration times in whole seconds, so each registration
change comes more than a second after the previous one. The contract
unit test runs the recording and eight mutations, including a fence
that is missing, ignores a heartbeat, over-fences the same process, or
keeps the old lease.
…gistrations

A native workflow or activity long poll kept leasing work after its
worker re-registered or heartbeated, where published PHP's
WorkerPollFence ends it with stale_worker_registration so a poll left
behind by a replaced process cannot take work meant for its
replacement (#734).

A poll now snapshots its registration when admitted:
- the routing revision (queue, build ID, workflow and activity types,
  definition fingerprints, command contracts and capabilities, compared
  with PHP's normalizations);
- the process identity a worker reports (host, process ID, process
  start);
- otherwise the update and heartbeat times, in the whole seconds PHP
  keeps them.

A registration that is not active or has not heartbeated within the
stale window answers 200 stale_worker_registration at once, uncached,
before any replay. Before every probe, and again inside the claim
transaction, a registration that no longer matches the snapshot ends
the poll with that answer and leases nothing; a request ID keeps it for
the pending window, as PHP's result cache does. Multiplexed polls are
fenced the same way.

The worker-poll-fence, update-validation and
workflow-poll-request-replay fixtures pass 3/3 against native. Native
execution tests cover the unchanged control, a heartbeat and a
re-registration ending held workflow and activity polls, a reported
process kept across its own heartbeat and fenced by a new one, and a
silent registration answered stale at admission without being cached.
…ences

The worker-poll-fence fixture now also records, from published 2.11.1
(three identical recordings):
- a held workflow poll whose registration is removed while it waits
  ends with 200 stale_worker_registration, not a refusal;
- held query polls across a heartbeat or a same-ID re-registration,
  with and without a request ID, end empty, and the next query poll
  leases the query, which is answered.
The probe declares a 150-second budget, which the recorder honors as it
does for the other long fixtures.

Native query polls fenced only by the registration incarnation: a
heartbeat kept a held query poll, and a replacement answered stale.
They now take the same WorkerPollFence snapshot as workflow and
activity polls, and a registration replaced or silenced during the wait
ends the poll empty, as PHP's performPoll does; a stale registration at
admission, or at the start of a coordinated round, still answers stale.

Tests that held native's earlier answers now expect PHP's: a held poll
whose registration another node removes ends 200 stale; a held query
poll fenced off by a replacement ends empty. Two tests whose runs outlast
the 30-second stale window now heartbeat before polling, as a PHP worker
must. A native execution test covers a held query poll ending empty on a
heartbeat. The fixture passes 2/2 against native.
…lacement in the next second stays fenced to the same worker
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security R2 blocker on exact head 6632827769f5ad4892d49c56008ab7487e1f4e8c: the held-poll fence does not actually identify the registration incarnation, so an old poll can survive a same-ID re-registration and lease work intended for the replacement.

FENCE_ROW omits the active registration token, and WorkerPollFence snapshots only routing, optional process metadata, and updated_at / last_heartbeat_at truncated to whole Unix seconds (implementation, snapshot). holds treats equal process metadata as authoritative regardless of registration, or falls back to those second-resolution timestamps (comparison).

But every registration already generates and atomically rotates a unique incarnation token, including a same-ID registration with unchanged process metadata (registration). Therefore either of these leaves the old fence looking current:

  • no process metadata, with replacement registration occurring in the same stored second; or
  • identical reported process metadata, at any time (for example a same-process re-registration/retry).

The first race is masked by the native test helper's deliberate 1.1-second delay before every change, and the process case only tests a changed process_started_at (test setup, cases). This defeats the PR's security boundary: the pre-replacement HTTP poll can pass the in-transaction fence and claim a task after the replacement owns the logical worker ID.

Please make the unique registration incarnation the fence authority. A heartbeat must leave the token unchanged; every successful registration must rotate it; every pre-probe and in-transaction claim check must require the snapshotted token to remain current. Add rapid same-second and identical-process-metadata re-registration regressions for workflow, activity, and query polls on SQLite, PostgreSQL, and MySQL, while retaining the same-process heartbeat case. The published PHP WorkerPollFence appears to share the heuristic, so update/bound that public path as part of the linked defect rather than treating parity as the safety property. I changed no source and started no CI.

rmcdaniel and others added 6 commits October 24, 2026 21:47
…of overwriting it

Security's R2 finding on #712: a held poll with a poll_request_id could
finish its last empty probe, lose the transition lock while a duplicate
with the same ID leased a workflow task or update validation, and then
record empty (or task_queue_changed) over that live lease. The timing
duplicate returned empty, later retries replayed empty, and a claimant
whose response was lost could not recover its lease by retrying.

The no-task write now binds the request ID, rereads any deliverable lease
a duplicate committed, and returns it for the caller to replay. It records
empty or task_queue_changed only when there is none. The single-kind loop
(both terminal answers) and the multiplexed workflow/update-validation
loop all replay that lease. The claim path shares the same helper.

Debug builds gain a test-only pause point per request ID, so regressions
commit the duplicate's lease exactly between the final probe and the
terminal write, without sleeping. They cover a workflow lease before
empty, a workflow lease before task_queue_changed, and both kinds before
the multiplexed empty, each with a lost-response retry. Release builds
compile neither the pause points nor their call sites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security's R2 finding on #739: the fence snapshotted routing, optional
process metadata and whole-second registration times, never the
registration itself. A same-ID re-registration that repeated its process
metadata, or landed in the same stored second without any, left an old
poll current, so it could lease work meant for the replacement.

Every native registration already rotates a unique incarnation token and
a heartbeat keeps it. The fence row now reads the active token, and a
snapshot whose token is no longer active is never current, on top of
PHP's checks. The pre-probe check and every in-transaction claim check
(workflow, activity, query and update validation) read the same row. A
registration without a token, such as one kept from a PHP takeover, keeps
that absence, so only the heuristic applies to it.

Regressions re-register with the same process metadata and in the same
stored second without metadata, for held workflow, activity and query
polls, and keep the existing same-process heartbeat case. The PHP fence
gets the same correction in Server #749.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts were in two workflows. The fixtures workflow drops its old
pull_request path list, as main does; the push path list it now
classifies pull requests with already carries every path this stack
added. Its comparator step runs main's list plus this stack's three
tests. The update-validation status check stays at the end of the
fixtures step, before main's new step that starts the cohorts' shared
native node. The PHPUnit workflow keeps the stack's maintenance-window
step beside main's merge queue and fixtures result steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve lease

Merging #712's terminal-write fix into the poll fence left two callers of
the old signature: the stale_worker_registration answer a held poll
writes when its registration is replaced. That answer now takes the same
path as empty and task_queue_changed. Under the binding lock it replays a
lease a duplicate with the same request ID committed and still holds, and
records stale only when there is none. A lease the re-registration
released is not deliverable, so it is never replayed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The combined B unit ran the whole execution suite for the first time with
the incarnation fence and #712's replay together:

- cross_kind_poll's replacement scenario expected the old heuristic's
  empty answer. Its same-process re-registration now rotates the
  incarnation, so the held poll answers stale_worker_registration. It
  still never wakes with a task, which is what the scenario guards.
- The new re-registration test's activity case completed a workflow task
  leased to the worker it re-registers. Re-registration releases the
  replaced registration's leases, so that completion was refused. A
  separate author worker on the same queue now takes the task.
- The query re-registration test no longer drops each case's database
  while the runtime pool still holds connections, which PostgreSQL
  refuses. The file's other tests do not drop theirs either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel
rmcdaniel changed the base branch from rust-325-update-validation to main October 24, 2026 22:38
@rmcdaniel
rmcdaniel marked this pull request as ready for review October 24, 2026 22:39
@rmcdaniel

rmcdaniel commented Oct 24, 2026 •

Copy link
Copy Markdown
Member Author

Rust Server: fixed in 74aeeab31 (Security R2 finding 6385707510). This PR is now the B unit: it targets main and contains #712, its fix 90965ac85, and a merge of current main (#746, #733). #740 is out of this unit until its own R2 fixes land (plan: #740 (comment)).

Fix. The registration incarnation is now the fence authority:

  • The fence row reads the worker's active incarnation token. A snapshot whose token is no longer the active one is never current, whatever its process metadata and timing. PHP's checks still apply on top.
  • Every native registration rotates the token, and a heartbeat keeps it.
  • The pre-probe check (worker_poll_fence_holds) and every in-transaction claim check (worker_poll_fence_holds_in: workflow, activity, query and update validation) read the same row.
  • A registration without a token (one kept from a PHP takeover) keeps that absence, so only the heuristic applies to it.

Regressions. Re-registration that repeats the same process metadata, and re-registration in the same stored second without metadata. Both run for held workflow, activity and query polls. The existing same-process heartbeat cases are kept and still hold the poll. The activity case now uses a separate author worker for the run's first workflow task, because a re-registration releases the replaced registration's leases.

Two follow-ups from combining the stack.

  • c576d6af8: Serve synchronous update validation like Server 2.11.1 #712 changed the no-task write's signature, and this PR's fence answer (stale_worker_registration) was a third caller. It now takes the same replaying path, so a live lease a duplicate committed under the same request ID replays instead of being overwritten by stale. A lease the re-registration released is not deliverable and is never replayed.
  • cross_kind_poll's replacement scenario expected the old heuristic's empty. A same-ID re-registration now fences the held poll, so it answers stale_worker_registration. It still never wakes with a task, which is what that scenario guards.

PHP. The published fence shares the heuristic, so it gets the same correction: #749 / branch fix/749-poll-fence-registration-token (2dcc0e7b3). The PHP snapshot carries a SHA-256 digest of registration_token as registration_incarnation, never the credential itself (the cooperative claim already persisted that digest under the same key, so stored claims are unchanged). isCurrent and isCurrentForUpdate require it. New PHP tests cover same-process and same-second re-registration plus the digest-only snapshot. All three fail on the published fence, and the seven PHP test files that touch the fence pass (313 tests). I'll open that PR once this unit's CI cycle settles.

Matrix (local). SQLite: full --test execution on the combined unit, 469 passed; the 2 failures were the test expectations fixed in 0625d9f6e, and those two modules then passed 11/11, plus the library, upgrade_inspect and binary targets (263 unit tests). PostgreSQL 16 and MySQL 8.0: the touched modules (worker_poll_fence, workflow_poll_requests, update_validation, update_wait, query_*, poll_waits, worker_incarnations, cross_kind_poll), 38/38 each after the follow-up. Hosted CI runs the full matrix on the B unit head 0625d9f6e.

…r and lease

The upgrade-source cohort resumes PHP's originals under native as the
worker PHP registered, then waits for the original timer and the expired
PHP lease to come due. Both come due after the 30-second stale window.
With the poll fence (#739), native now refuses polls from a registration
whose last heartbeat is older than that window, as PHP's
WorkerPollFence::isFresh does. The harness never heartbeated, so every
late poll answered stale_worker_registration and the timer's task was
never claimed: "The original timer did not fire under native", in every
cohort.

The resume loop now heartbeats before each poll, as an SDK worker does.
The poll and its checks are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Rust Server: the first hosted cycle on 0625d9f6e (fixtures run 40139501116) failed only the six cohorts parts, plus the aggregate result. All failed in upgrade-source-profile.sh with The original timer did not fire under native. Every native-tests and corpus part passed, and so did PHPUnit and SonarQube.

Cause. The takeover-resume harness resumes PHP's originals under native as the worker PHP registered, then waits for the original timer and the expired PHP lease. Both come due after the 30-second stale window. With this PR's poll fence, native refuses polls from a registration whose last heartbeat is older than that window, which is PHP's WorkerPollFence::isFresh rule. The harness never heartbeated, so every late poll answered stale_worker_registration and the timer's task was never claimed. The harness only passed before because native had no fence.

Fix. 1b518e766: the resume loop heartbeats before each poll, as an SDK worker does. The poll and all of its checks are unchanged.

Local check. The cohort's own script, SQLite, with this head's debug binary:

  • 0625d9f6e: exit 255 with the same error.
  • 1b518e766: the default PHP 2.8.0 run passes (71 s) and the PHP 2.9.7 run passes (81 s).

A new hosted cycle is running on 1b518e766.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Rust Server: hosted CI is terminal and green on the B unit's exact head 1b518e766f5d60054d04d7ef9649a2730d59d9c3, which targets main and contains #712.

  • Shared Server fixtures run 40142372545: success, 23:21:03 to 00:00:27 UTC (39m24s). Every cohorts part passed with the resume-harness heartbeat (1b518e766).
  • PHPUnit Feature 40142372371: success. SonarQube 40142372364: success.
  • On the head, 42 check runs succeeded and 1 was path-skipped. None failed.

Ready for Security's R2 recheck of #712 (finding 6385727724) and #739 (finding 6385707510) on this head. It merges after #745. #740 will follow as its own unit, stacked on this one.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Second-round Security review accepts the fixes for the #712 terminal-answer race and the #739 registration-incarnation fence at exact current head 1b518e766f5d60054d04d7ef9649a2730d59d9c3.

For #712, fix commit 90965ac85 directly descends from the reviewed defective head. Every terminal no-task answer now acquires the request binding lock, re-reads the cached result in the same transaction, returns a still-deliverable workflow or update-validation lease, and writes empty, task_queue_changed, or the follow-up stale-registration result only when no such lease exists. Claim paths use the same ordered binding lock before replay or lease. The debug-only deterministic barrier tests cover a duplicate committing between the final empty probe and terminal write, including single-kind empty, single-kind queue change, both multiplexed task kinds, and recovery of a lost response by retrying the same ID. The hosted native-test matrices exercise these tests on SQLite, PostgreSQL, MySQL, MariaDB, and the existing-PHP layouts.

For #739, fix commit 74aeeab31 directly descends from its reviewed defective head. The fence snapshot now includes the active registration's unique incarnation token; both pre-probe and in-transaction checks require that exact token to remain current before workflow, activity, multiplexed, or query claims. Registration rotation remains atomic with the registration upsert, while heartbeat updates do not rotate the token. Regressions cover rapid same-second registration without process metadata and identical process metadata for workflow, activity, and query polls; the existing same-process-heartbeat case remains. The intentional native safety difference from published PHP is explicitly bounded in open defect #749 rather than being mistaken for parity.

I also reviewed follow-ups c576d6af8, 0625d9f6e, and 1b518e766, confirmed both fixes remain in the current head, and ran git diff --check over the reviewed range. The reported exact-head hosted cycle is terminal with 42 successful checks, one path-skipped check, and no failures, including the full database/cohort/native matrices, CodeQL, Sonar, public-boundary, and repository checks. I did not run Rust builds on the lab host.

This accepts these two cited fixes at the exact head above, not an unreviewed future merge result. Merge current main, preserve the reviewed behavior through conflict resolution, and run the required full exact-head cycle before final merge.

…to rust-325-worker-poll-fence-native

# Conflicts:
#	.github/workflows/phpunit-feature.yml
#	.github/workflows/server-parity.yml
#	rust/src/runtime/http.rs
#	rust/src/runtime/store.rs
#	rust/tests/execution.rs
#	scripts/conformance/server-parity/contract.mjs
#	scripts/conformance/server-parity/probe.php
@sonarqubecloud

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

Rust Server: exact-head evidence for Security's re-acknowledgement of 17d061a6323dade83dbc113f666339a6a56a2328. Security last accepted 1b518e766 (6392596801). #745 merged as 38a96f0b8, and this head adds #745 and current main, including #755 (1c6558f45).

Hosted CI on this head: terminal success on the first attempt. Shared Server fixtures run 40162397220: all 34 jobs succeeded. PHPUnit Feature 40162397173, SonarQube 40162397179, Public Boundary 40162397172, 40162395805 and 40162393853, and PR #739 40162394377 all succeeded. The PR is CLEAN.

Earlier head 9b085b699 (the same tree minus #755). Run 40159712308: every job passed except execution-and-codec (corpus) (job 121069436918). That job failed the pre-native legacy-marker check same original poll recovers in the published PHP 2.11.1 record on SQLite; no native record ran. It's the same signature as #754's first entry (comment 6393328578). Its rerun was cancelled when #755 moved main.

Merge structure

Every file traces to an accepted side or a named resolution (classified at 9b085b699; 17d061a63 then replaces only main's 19 files with main's blobs)

  • All 39 files only Rust #325: fence held polls against replaced worker registrations (#734) #739 changed are blob-identical to 1b518e766.
  • 62 of the 63 files only Rust #325: activity cancellation acknowledgement like Server 2.11.1 #745 changed are blob-identical to 6df4e7340. The exception is rust/tests/fixtures/unserved-php-routes.v2.11.1.txt: it drops the 3 update-validation routes, because this branch serves them (that's what route_coverage checks).
  • 13 files changed on both sides:
    • 5 are byte-identical to git's own automatic merge: activity_heartbeats.rs, backend.rs, execution.rs, runtime/mod.rs, external_payloads/commands.rs.
    • 7 had conflicts and were resolved by hand: phpunit-feature.yml, server-parity.yml, http.rs, store.rs, rust/tests/execution.rs, contract.mjs, probe.php. Every line either side added relative to the merge base is present in the head. The one exception is the comparator test-list line in server-parity.yml, which both sides rewrote. The head's line names exactly the union of both sides' test files (36 and 35, 38 distinct), with nothing added or dropped.
    • docs/rust-server-port.md combines both sides' entries and updates the route count to the combined state.
  • Resolution choices: store.rs keeps Rust #325: fence held polls against replaced worker registrations (#734) #739's _server_poll_request_id binding, then Rust #325: activity cancellation acknowledgement like Server 2.11.1 #745's workflow/activity claim binding. http.rs registers both route sets. The capabilities advertise both message_streams and update_validation.

Local qualification: the SQLite all-targets suite on 9b085b699 (tree c9ec348d; 17d061a63 adds no Rust): 271 unit, 483 execution, 2 upgrade-inspect and 1 binary test pass; 35 unit tests and 1 execution test are ignored.

Holding the merge for Security's re-acknowledgement of this head.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security recheck accepts exact head 17d061a6323dade83dbc113f666339a6a56a2328 and releases the hold for merge. I independently verified the consolidated receipt in comment 6393701161 against GitHub: the PR is clean and mergeable; its rollup is terminal with 48 successes, one intended path skip, and no failure/pending/cancelled check. Merge parents and trees match the receipt: 890e280ba combines the previously accepted #739 head with accepted #745; 9b085b699 has the same tree as 890e280ba; and current head 17d061a63 adds the merged #755 main delta through parent 1c6558f45.

The focused security paths from accepted head 1b518e766 remain byte-identical at the current head: the registration-incarnation fence, poll pause/readiness, workflow/query/cross-kind poll implementations, and their focused regressions. Although store.rs also contains accepted-main work, the exact no_task_answer and replayed_poll function hashes are unchanged, and the accepted-to-current patch does not alter the registration-token/fence symbols. This preserves the fixes accepted in comment 6392596801 while the complete exact-head database/cohort/corpus/SDK-direction, PHPUnit, CodeQL, Sonar, public-boundary, and repository checks are green.

Merge only this exact head under the Rust owner’s #325 authority. Any head change requires revalidation. This review does not alter the separate #518 credential-rotation hold or #519 temporary-tunnel preservation gate.

@rmcdaniel
rmcdaniel merged commit 871e4ad into main Oct 25, 2026
49 checks passed
@rmcdaniel
rmcdaniel deleted the rust-325-worker-poll-fence branch October 25, 2026 05:04
rmcdaniel added a commit that referenced this pull request Oct 25, 2026
…-worker-sessions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rmcdaniel added a commit that referenced this pull request Oct 25, 2026
…st-325-ci-codec-one-job

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rmcdaniel added a commit that referenced this pull request Oct 25, 2026
…nto rust-325-workflow-import

Native now serves 139 route methods: main's 138 plus embedded v2 import.
The runtime keeps both new modules, and the comparator node test list
keeps both sides' tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rmcdaniel added a commit that referenced this pull request Oct 25, 2026
…nto rust-325-record-local-activity

The worker capability manifest keeps this branch's local activity entries
and main's worker session and update validation entries. The comparator,
probe and node test lists keep both sides' fixtures and tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rmcdaniel added a commit that referenced this pull request Oct 25, 2026
…nto rust-325-cancellation-scopes

Workflow-task completion admits cancellation scopes before it guards
worker-session commands, in published 2.11.1's order. A scheduled
activity's options keep main's worker session and add its cancellation
scope, as PHP's bridge appends it. Native serves 141 route methods;
cancellation scope checkpoints stay missing. The probe timeout chain and
the comparator node test list keep both sides' entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rmcdaniel added a commit that referenced this pull request Oct 25, 2026
) into rust-325-nexus-operations

Native now serves 140 route methods: main's 138 plus the two
caller-indexed Nexus operation reads. The comparator node test list keeps
both sides' tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rmcdaniel added a commit that referenced this pull request Oct 25, 2026
) into rust-325-workflow-stream-writes

Native now serves 140 route methods: main's 138 plus durable workflow
stream appends and closes. The comparator node test list keeps both
sides' tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rust #325: fence held workflow and activity polls against replaced worker registrations

1 participant