Skip to content

Rust #325: worker sessions like Server 2.11.1 (#736) - #740

Merged
rmcdaniel merged 22 commits into
mainfrom
rust-325-worker-sessions
Oct 25, 2026
Merged

rmcdaniel merged 22 commits into
mainfrom
rust-325-worker-sessions

Conversation

@rmcdaniel

Copy link
Copy Markdown
Member

Closes #736. Stacked on the #734 PR (base rust-325-worker-poll-fence); it merges after it.

Native serves worker sessions as published 2.11.1 does:

  • the five session routes;
  • session-bound activities: completion guards, claim admission and the compact session on the task, heartbeat and renew-lease renewal, and status;
  • the worker_sessions capability object with its verbs;
  • operator metrics.

The fixtures are worker-sessions and worker-session-activities, each recorded from 2.11.1 in three identical runs; both pass against native locally. One published defect is bounded explicitly and reported as #737: a reacquired session keeps its old failure_reason. Native execution tests pass on SQLite, PostgreSQL and MySQL. Still open: lifting the takeover refusal for PHP data that holds session rows.

…shed 2.11.1

Fixture first for #736. In its own namespace, with three workers on two
queues, the probe records the five session routes:
- the protocol gates (no header, 1.7, 1.21, 1.8 reaching validation),
  an unknown namespace on both planes, and the control-plane header;
- validation of create, heartbeat and close;
- registration, queue, requirements and create_if_missing refusals;
- create with a padded ID, duplicate requirements and a string lease;
  a float lease; reuse resetting omitted options without extending the
  TTL deadline;
- another worker's create, heartbeat and close; heartbeat of a missing
  session and a lease renewed from now; the per-worker session limit;
- lease expiry applied on read; reacquire by another worker with no
  options; close by the old owner, by query worker_id, again, by
  another worker; heartbeat and create of a closed session, and
  requirements refused before the closed lookup;
- the stored and the requested reacquire refusals; TTL expiry refusing
  heartbeat and create while the owner can still close;
- a deregistered holder orphaned on read, refused on create, and the
  session reacquired by another worker;
- a missing session's visibility, and the namespace list and metrics.

Session times are whole seconds, so the probe records them as spans
and presence flags; the list is compared by session ID. Three published
recordings are identical, and published 2.11.1 and embedded pass.

One published defect is bounded: renewHeldSession writes
failure_reason => null through array_filter(... !== null), which drops
it, so a reacquired session keeps its old failure_reason until a
heartbeat or close. The contract accepts the published value or the
intended null at exactly those paths. The unit test runs the recording,
the corrected variant, and ten refused mutations, including native's 501.
Native answered 501 for POST /api/worker/sessions, its heartbeat and
DELETE routes, and GET /api/worker-sessions and /{sessionId}. They now
follow WorkerSessionController and WorkerSessionRegistry:
- protocol 1.8 or newer (409 worker_sessions_unavailable below it),
  Laravel's rules after TrimStrings and ConvertEmptyStringsToNull, 412
  for an unregistered worker and 409 for another session queue;
- requirements checked before the session lookup; create (201), reuse
  and reacquire (200) with omitted options reset to their defaults, the
  TTL deadline never extended, an omitted connection kept and the queue
  defaulting to the worker's own; a float lease read as the default;
- the per-worker session limit over live leases; the owner, closed,
  terminal and option reacquire refusals;
- heartbeat renewing the lease from now without changing the stored
  lease; close, already_closed, and close from any status by the owner;
- lease and TTL expiry and a lost holder (gone or silent past the stale
  window) applied whenever a session is read, including the list, which
  orders by last change then session ID and counts each status.
Session times are written in whole seconds, as PHP keeps them.

Native clears a reacquired session's failure reason, as PHP's code
intends; published PHP drops that write (Server #737), and the fixture
bounds the difference. Session-bound activities, capabilities and
metrics follow in the next step.

The worker-sessions fixture passes 3/3 against native on SQLite, and
native execution tests pass on SQLite, PostgreSQL and MySQL.
…hed 2.11.1

Fixture first for #736's activity half. In its own namespace, with an
authoring worker, a worker without requirements, two GPU workers and one
on another queue, the probe records:
- schedule_activity with worker_session refused at protocol 1.7 and 1.17,
  on a timer command, without a session_id, and from a worker without the
  worker_sessions capability; then accepted, creating no session yet;
- the first claim skipping a worker without the requirement and a 1.7
  poll, admitted for a capable worker with the compact session on the
  task, and a second capable worker kept out while the session is held;
- the activity heartbeat and renew-lease renewing the session by the
  command's lease (90 seconds) with the full snapshot, and status with
  the compact form;
- after the holder closes the session: the heartbeat refused 409
  session_not_active with worker_session_renewal and not recorded,
  status answering worker_session_unavailable, completion still 200;
- a session's activity limit of one and of two, and a command queue
  other than the session queue that nobody can claim;
- operator metrics counting the sessions, and the worker_sessions
  capability object with its verbs.
Three published recordings are identical; published 2.11.1 and embedded
pass. The unit test runs the recording and eight refused mutations,
including native's current answers.
Native refused worker_session on schedule_activity as an unknown field,
ignored sessions when claiming and renewing activities, advertised
worker_sessions as a bare false and counted no sessions in operator
metrics. It now follows published 2.11.1:
- completion: schedule_activity accepts connection and worker_session;
  with any worker_session, PHP's guards in order (409
  worker_sessions_unavailable below protocol 1.8, 409
  portable_worker_affinity_protocol_floor_unavailable below 1.18, 409
  worker_capability_not_supported without the flat capability and a
  supported manifest entry), then the normalizer's 422 checks; the
  activity inherits the session's queue and connection when it names
  none and stores WorkerSessionOptions' snapshot in its options;
- claim: a session-bound activity goes only to a worker on 1.8 or
  newer, on the session queue, with every requirement; the session then
  admits the claim (created lazily, reused within its activity limit,
  or reacquired) or the claim rolls back; the task carries the compact
  session;
- the activity heartbeat and renew-lease renew the session by the
  command's lease first and answer the full snapshot; a refused renewal
  answers its status with worker_session_renewal and records nothing;
  status shows the compact form, read without a refresh, and answers
  worker_session_unavailable once the session is not the holder's live
  lease;
- the session snapshot counts its running activities;
  server_capabilities.worker_sessions is the published contract object
  with worker_session_verbs; operator metrics and dashboards count the
  namespace's sessions after a refresh.

The worker-session-activities, worker-sessions and worker-poll-fence
fixtures pass against native locally, and native execution tests cover
admission, the held session, the activity limit, heartbeat renewal and
the closed-session refusals on SQLite, PostgreSQL and MySQL.
… rust-325-worker-sessions

# Conflicts:
#	rust/src/runtime/activity_heartbeats.rs
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security R2 blocker on exact head 0c36243b9e8a55eee46ef2c5a8ad5829d5c022e7: a replacement process that registers with the same worker_id inherits the prior process's active worker session instead of being fenced and required to reacquire it.

The stack already treats each registration as a new authority: registration rotates a new token on every call and supersedes the previous incarnation (registration path). Worker sessions discard that identity. The session row and holder lookup retain only lease_owner = worker_id (queries); holder liveness accepts whichever current registration has that ID and a fresh heartbeat (refresh); and an active session whose logical ID matches is renewed with outcome reused (acquire).

Consequently this deterministic sequence violates the advertised contract:

  1. Process A registers worker_id=w and creates session s for process-local GPU/filesystem state.
  2. Process B registers the same worker_id=w; A's registration incarnation is superseded and B receives a new token.
  3. B creates/claims s. The fresh registration row prevents orphaning and the session returns reused, so B may run under affinity state that only existed in A.

The capability says replacement_worker_must_reacquire_session=true and process_local_state_must_be_rebuilt_after_reacquire=true, and recommends sessions for process-local state (contract). There is also no incarnation credential on create/heartbeat/close, so stale process A and current process B are indistinguishable on those mutations after token rotation. This is a session-integrity/reliability issue within a namespace, not a cross-namespace authorization bypass. The published PHP implementation appears to share the logical-ID behavior; parity does not make the stronger advertised safety true.

Please fence session ownership by registration incarnation (or an equivalent non-reusable process authority) across explicit create/reuse, session-bound activity admission and renewal, heartbeat, and close. Same-ID replacement must make the prior holding ineligible for reused; only the current incarnation may reacquire/mutate it, and a stale incarnation must be refused. Add native regressions for same-ID re-registration covering explicit and activity-bound paths on SQLite, PostgreSQL, and MySQL, plus the public-server/SDK contract change if an incarnation credential must be carried on the wire. I changed no source and started no CI.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security R2 availability blocker on exact head 0c36243b9e8a55eee46ef2c5a8ad5829d5c022e7: unbounded worker-session requirement arrays are normalized and matched quadratically while native's server-wide durable-transition lock is held.

The 2 MiB request limit is the only aggregate bound. Each requirement is limited to 255 characters, but the item count is not. Both direct options and stored command snapshots deduplicate by growing a Vec and calling contains for every item (normalization, stored snapshot). Routing then checks every requirement with another linear capabilities.contains, both in claimable_by and acquisition (membership, acquire).

This is on the global transition path, not just an isolated parser. Workflow completion opens Store::begin, guards the command, and later calls command_session while applying it (completion transaction, command application). Activity claim similarly parses the stored snapshot and performs the nested membership scan inside its claim transaction (activity claim). Store::begin takes BEGIN IMMEDIATE on SQLite, one fixed PostgreSQL advisory lock, or the single MySQL ownership row, so one namespace blocks unrelated durable transitions while doing this CPU work (backend locks, PostgreSQL, MySQL).

Measured locally in the pinned Rust 1.99.0 container with the exact Vec<String>/contains loop and distinct 248-character common-prefix strings: 2,000 entries took 21 ms, 4,000 took 78 ms, and 8,000 took 399 ms. Eight thousand entries plus JSON punctuation fit below the default 2 MiB body limit. This excludes JSON decoding, capability matching, SQL, and response work. Repeated authenticated requests can therefore monopolize the cross-namespace transition lock. Published PHP normalizes with array_unique and builds a capability lookup with array_flip, so the quadratic behavior is not required for parity.

Please preserve first-occurrence ordering with a hash/set-backed seen index, build a set for capability membership, and/or add an explicit safe item-count bound coordinated with the public contract. Keep all request-derived normalization outside the transition lock where possible, and ensure stored-snapshot parsing remaining inside it is linear. Add near-limit distinct/duplicate requirement regressions and a non-quadratic comparison-count or bounded-work assertion across explicit creation, workflow command application, activity claim, heartbeat, and status paths. I changed no source and started no hosted CI; the benchmark used a disposable network-none, read-only container and left no container/image.

@rmcdaniel

Copy link
Copy Markdown
Member Author

R2 blocker on exact head 0c36243b9e8a55eee46ef2c5a8ad5829d5c022e7: worker-session numeric inputs have no portable upper bound and can panic the native request path.

The create validators accept any positive i64 for lease_seconds, ttl_seconds, and max_concurrent_activities (worker_sessions.rs rules at 102-106, positive_int at 143-150, and command validation at 309-314). Create then calls chrono::Duration::seconds(options.lease_seconds) and the same function for TTL at 480-481; reacquire/heartbeat repeat unchecked duration construction at 550 and 737. This head locks Chrono 0.4.45. Its documented implementation says TimeDelta::seconds panics when seconds exceed i64::MAX / 1000: https://docs.rs/chrono/0.4.45/src/chrono/time_delta.rs.html#213-236. A valid JSON value of 9223372036854775807 therefore passes native validation and reaches a deterministic panic instead of a stable 422 response.

Smaller extreme values are still non-portable. The persisted fields are Laravel unsignedInteger columns and the lease/TTL values are also added to database timestamps. Their usable range differs among SQLite, PostgreSQL, and MySQL. The published PHP controller and workflow-command validation have the same integer|min:1 rules with no maximum, and WorkerSessionRegistry performs unchecked addSeconds, so this is a shared protocol gap rather than a Rust-only exception. max_concurrent_activities also reaches the backend-specific integer column without a portable cap.

An authenticated worker can repeatedly turn syntactically valid session create/reacquire/heartbeat input into handler panics or backend-dependent failures. That violates the explicit parity/error contract and creates avoidable availability and log-pressure surface under a namespace credential.

Please define documented, operationally meaningful portable maxima per field; enforce them in the HTTP create/heartbeat and workflow-command paths in both implementations before time arithmetic or persistence; use checked duration/date construction in native code as defense in depth; and add max/max+1 plus i64::MAX regressions for direct session and command paths across every supported database. Rejection should be deterministic and state-free. I changed no source and started no CI.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Rust Server: answers and plan for the three R2 blockers on this head (6385687160, 6385776970, 6385863890).

Landing order. I'm taking #740 out of the A+B unit. #739, which contains #712, will land as its own top of stack once its R2 answers are accepted. #740 follows with the fixes below, so the worker-session contract work doesn't hold the poll-fence and update-validation fixes.

1. Requirement lists are linear (6385776970). Implemented locally, no contract change. One first-occurrence helper backed by a hash set replaces both Vec::contains dedupes (request options and stored snapshot). Capability membership uses a set built once per check, for claimable_by and acquisition alike. Request options are still normalized before Store::begin. Snapshot parsing inside the claim and completion transactions is now linear. A unit test counts equality comparisons on 8,000 near-limit strings (248-character shared prefix, each doubled) and fails above 64,000, against about 32 million for the old scan. Request-level regressions at that size follow on the create, command, claim, heartbeat and status paths.

2. Portable numeric maxima (6385863890). Implemented in both servers:

  • lease_seconds at most 86,400. A heartbeat renews the lease, so a longer one only delays holder-loss detection.
  • ttl_seconds at most 2,592,000 (30 days). This keeps ttl_expires_at inside a MySQL TIMESTAMP for years, and every value fits PostgreSQL's integer.
  • max_concurrent_activities at most 1,000.

They are ordinary Laravel max rules on the create and heartbeat routes and on commands.*.worker_session.*, so values above them are refused with 422 before any arithmetic or write. Native also uses checked chrono construction for every session time (try_seconds plus checked_add_signed), so even a stored out-of-range value returns an error instead of panicking. Max, max+1 and i64::MAX / PHP_INT_MAX regressions cover create, heartbeat and the command path in both servers, and assert that refusals write nothing. The PHP suite passes locally. The worker-sessions-runtime.schema.json options get the matching maximum. Narrowing input is breaking in principle, but values above these maxima already panic native or fail per backend in PHP, so no working client depends on them. I'll note it in the schema and the server reference.

3. Session holders are fenced by registration incarnation (6385687160). Design:

  • Each session records a SHA-256 digest of the holder registration's token (the same digest the cooperative claim already stores). Native keeps it in a native-owned dw_worker_session_holders table (excluded from the layout comparison like the other dw_ tables). PHP adds a nullable lease_owner_incarnation column.
  • On every read that applies holder loss, a session whose holder incarnation is no longer the worker's current registration becomes orphaned with failure_reason: worker_registration_replaced, exactly like a stale heartbeat today. So after a same-ID replacement:
    • the replacement's create or session-bound claim goes through the existing reacquire path and answers reacquired, never reused;
    • explicit heartbeat, close and activity-heartbeat renewal on the inherited session get the existing session_not_active refusal until it is reacquired;
    • the old process's later heartbeat is refused the same way.
      Rows written before the change have no digest and orphan once, which costs one reacquire.
  • On the wire: create, heartbeat and close accept an optional registration_token. When sent, it must be the worker's current token, otherwise 409 stale_worker_registration with no state change. Without it, the server can't tell a stale process from its replacement once the replacement has reacquired, so SDKs should send it. The field is additive, and I'll file the SDK follow-ups with the schema change.

Status: 1 and 2 are written and pass locally (PHP 13/13 in WorkerSessionProtocolTest; native builds after the current #745 run finishes). 3 is next. I'll answer here with exact heads, matrices and counterfactuals before asking for R2 again.

rmcdaniel and others added 3 commits October 24, 2026 22:27
…rs, keep requirements linear

Security's three R2 findings on #740, in both servers.

Session holders are fenced by registration incarnation. Each create and
renewal records a SHA-256 digest of the holder registration's token:
PHP in a new lease_owner_incarnation column, native in a native-owned
dw_worker_session_holders table (schema version 5). Wherever holder loss
is applied, a session whose holder incarnation is no longer the worker's
current registration becomes orphaned with worker_registration_replaced.
A same-ID replacement then reacquires (never reuses) the session through
create or a session-bound claim, and heartbeats are refused with
session_not_active until it does. A worker heartbeat keeps the
registration, so the original holder still reuses. Create, heartbeat and
close accept an optional registration_token. One that is not the worker's
current token is refused with 409 stale_worker_registration and changes
nothing. Upgrading keeps running holders: the PHP migration backfills
active sessions from their holder's current token, and native takeover
seeds the holder table the same way. The compiled native catalogs gain
only the new table's rows.

Session numbers have portable maxima: lease_seconds 86,400, ttl_seconds
2,592,000 and max_concurrent_activities 1,000. They are enforced as
Laravel max rules on the create and heartbeat routes and in
schedule_activity commands, before arithmetic or writes. Native also
builds every session time with checked chrono, so an out-of-range stored
value answers an error instead of panicking. The runtime schema's request
fields carry the same maximum and document the token and new reasons.

Requirement lists are linear. One first-occurrence helper backed by a
hash set replaces both Vec::contains dedupes, and capability membership
uses a set built once per check. A comparison-counting unit test fails on
a quadratic scan of 8,000 near-limit entries.

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

The worker-sessions and session-activities contracts are checked while
recording, but their own unit tests (the recorded observation plus each
mutation the contract must refuse) were never run. They now run in the
comparator step and trigger the fixtures on change, like the others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rmcdaniel and others added 5 commits October 24, 2026 22:50
…ersion from its constant

Native takeover still refuses any PHP source with worker-session rows
(upgrade_source_worker_sessions_unsupported), so seeding session holders
during takeover could never run. It is removed rather than kept as
untested code. When that refusal is lifted, the takeover must seed
dw_worker_session_holders from each active session's current
registration, as the PHP migration backfills lease_owner_incarnation.

The PHP 2.9.7 takeover test asserted marker version 4. It now reads the
schema version constant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ust-325-worker-sessions

Conflicts resolved as unions. PHP guards worker-session commands before
validating message streams, so native does too. Capabilities advertise
both worker sessions and message streams. The activity heartbeat view
keeps the session and adds the cancellation receipt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unserved-route list arrived with #745 and still named the five
worker-session routes this branch serves, so route_coverage failed after
the merge. Native now serves 138 of the 175 published route methods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel
rmcdaniel changed the base branch from rust-325-worker-poll-fence to main October 25, 2026 05:04
rmcdaniel and others added 2 commits October 25, 2026 06:00
…-worker-sessions

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

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

Copy link
Copy Markdown
Member Author

Rust Server: fixes for Security's three R2 blockers (6385687160, 6385776970, 6385863890), at exact head 5f75f10835b63502a68734b7ba088e07c87ee9a3. The plan was in 6389880091. #739 (which contains #712) has merged, so this PR now targets main.

1. Session holders are fenced by registration incarnation (6385687160).

  • Every create and renewal records a SHA-256 digest of the holder registration's token. PHP stores it in a new lease_owner_incarnation column; native stores it in a native-owned dw_worker_session_holders table (schema version 5, excluded from the layout comparison like the other dw_ tables).
  • Wherever holder loss is applied, a session whose holder incarnation is no longer the worker's current registration becomes orphaned with failure_reason: worker_registration_replaced.
  • After a same-ID replacement:
    • The replacement's create or session-bound claim reacquires (never reuses) the session.
    • The old process's heartbeat is refused with session_not_active.
    • A worker heartbeat keeps the registration, so the original holder still reuses.
  • Create, heartbeat and close accept an optional registration_token. A token that isn't the worker's current one gets 409 stale_worker_registration and changes nothing.
  • Upgrade: the PHP migration backfills active sessions from their holder's current token. Native takeover still refuses a PHP source that has worker-session rows (upgrade_source_worker_sessions_unsupported), so there is nothing to seed there yet. When that refusal is lifted, takeover must seed the holder table the same way (recorded in 1ef0c644e).

2. Portable numeric maxima (6385863890).

  • Limits: lease_seconds ≤ 86,400; ttl_seconds ≤ 2,592,000; max_concurrent_activities ≤ 1,000.
  • They are Laravel max rules on the create and heartbeat routes and on commands.*.worker_session.*, so values above them get 422 before any arithmetic or write.
  • Native builds every session time with checked chrono (try_seconds and checked_add_signed), so a stored out-of-range value returns an error instead of panicking.
  • The runtime schema's request fields carry the same maximum.

3. Requirement lists are linear (6385776970). One first-occurrence helper backed by a hash set replaces both Vec::contains dedupes. Capability membership uses a set built once per check.

Counterfactuals. These ran today against the head you reviewed, 0c36243b9, with only the new tests applied:

Test Reviewed head This head
session_numbers_are_bounded_on_every_path 201 created for lease_seconds above the maximum 422, must not be greater than 86400
a_same_id_replacement_reacquires_the_session_and_fences_the_old_process old process heartbeat 200, session active 409 session_not_active, orphaned, worker_registration_replaced
a_same_id_replacement_reacquires_a_session_through_an_activity_claim session stays active orphaned, worker_registration_replaced
requirement_lists_stay_linear_at_the_request_limit (old dedupe put back into this head) 64,012,621 comparisons, fails the 64,000 bound passes

Qualification on this head:

  • cargo test --locked --all-targets, run locally in the pinned CI Rust image on this exact head: 275 unit tests and 489 execution tests passed, plus the upgrade-inspection tests. One execution test is ignored, as on main.
  • Node: the workflow's comparator test list, which includes both worker-session contract tests, plus ServerParityPartsTest.mjs. 2,226 of 2,226 passed.
  • actionlint 1.7.7 reports the same 20 shellcheck warnings it reports on main (SC2034/SC2155 in existing scripts), and nothing else. The Docker isolation policy and the public boundary check over origin/main..HEAD pass.
  • rust/ is unchanged since 4b746ec05. I didn't rerun the native fixtures locally on this head. CI runs every fixture and PHPUnit on it, and I will add those results here.

Merge evidence.

Asking for R2 on this exact head.

…ads it

SonarCloud rule rust:S7424 refused a derived Hash beside a hand-written
PartialEq on the test-only Counted helper. Both read only the inner string,
so they agreed; implementing Hash by hand makes that explicit.

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

Copy link
Copy Markdown
Member Author

New exact head: 8b606429001e446c7b2fae98bda13a56be7b902d. It is one commit on top of 5f75f1083, so the answer above (6394802255) still covers everything else.

What changed. SonarQube failed the quality gate on 5f75f1083 with one new bug, rust:S7424 at rust/src/runtime/worker_sessions.rs:1077. The test-only Counted helper used to count comparisons in requirement_lists_stay_linear_at_the_request_limit had a derived Hash beside a hand-written PartialEq. Both read only the inner string, so they never disagreed. The commit now writes Hash by hand over that same string. It touches no production code: one file, 8 lines added and 1 removed, all inside #[cfg(test)].

Checks.

  • Locally, the four worker_sessions unit tests pass on this head, including the linear-requirements test.
  • On 5f75f1083, PHPUnit Feature and Public Boundary passed.
  • CI is rerunning everything on the new head.

Sonar code smells left open. php:S1142 (more than 3 returns) at app/Support/WorkerSessionRegistry.php:110 and :203. These are guard-clause early returns in the incarnation checks. Splitting them would make the fencing order harder to read, so I'm leaving them; they don't fail the gate.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security R2 still blocks exact head 8b606429001e446c7b2fae98bda13a56be7b902d: same-ID replacement fencing is bypassed whenever an explicit session mutation omits registration_token.

Both implementations intentionally make the new credential nullable. Native's validator declares it Nullable and stale_registration immediately accepts None (validation, fence). PHP does the same; its own comment accurately says omission makes a stale process and the replacement indistinguishable after reacquisition (controller rule, fence). The schema exposes the field but does not require it on create, heartbeat, or close (schema).

The deterministic failure is:

  1. Process A registers worker ID w and creates session s.
  2. Process B registers the same ID, then reacquires s with B's token.
  3. Stale A sends create/reuse, heartbeat, or close with worker_id=w and no token, as every pre-change client does.
  4. The fence accepts omission. Because the session now records the current incarnation and the logical owner is still w, A can change the session options/lease or close B's active session.

The added regression actually codifies part of the bypass: after B reacquires, an omitted-token heartbeat is expected to succeed (PHP, native). There is no omitted-token close/reuse counterfactual. This leaves the original requirement, “only the current incarnation may reacquire/mutate it, and a stale incarnation must be refused,” unsatisfied and creates the exact rolling-replacement cleanup race the incarnation was meant to prevent.

Please require the current registration token on all explicit create/reuse, heartbeat, and close mutations whenever the registration has an incarnation. If compatibility needs a protocol/capability transition, gate the stronger contract on that transition rather than advertising unconditional replacement fencing while accepting an unauthenticated incarnation. Add post-reacquire omitted-token tests for all three verbs and prove they make no state change; keep activity-attempt behavior separately explicit because in-flight attempts retain at-least-once semantics.

The portable maxima and linear requirement-list changes resolve my other two prior findings in source review. I did not run or alter the Rust workspace and I am not treating the still-running/failing CI set as passing evidence.

… they send it

Security's R2 found that an explicit worker session create, heartbeat or
close without a registration token was accepted even after a same-ID
replacement had reacquired the session, so a stale process could reuse,
renew or close its replacement's session. The published PHP SDK sends no
token, so requiring it from every worker would break published clients.

A worker now opts in with the worker_session_registration_token
capability at registration. When the worker ID's current registration
declares it, every explicit create, heartbeat and close must carry the
current token. A missing token or a non-current one gets 409
stale_worker_registration and changes nothing. Without the capability, a
request without a token acts for the current registration, as published.
PHP and native apply the same fence first in each request's transaction,
and the runtime schema states the conditional contract.

Tests in both: after reacquisition the stale process, with its old token
or none, is refused on all three verbs and the session is unchanged; the
published tokenless path stays explicit. With the old fence restored,
PHP's test sees 200 for 409 and native's stale create reuses the session.

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

Copy link
Copy Markdown
Member Author

Rust Server: fix for Security's R2 blocker (6394992551), at exact head ad7ad3aea7189f2b300b3a2783683dc7e64a9d30.

The blocker was valid. After a same-ID replacement had reacquired a session, the stale process could send a create/reuse, heartbeat or close without a token and still change the replacement's session. The previous test asserted that a tokenless heartbeat succeeded.

Why the token can't simply be required. The published PHP SDK 2.2.6 uses worker sessions but never sends registration_token. That includes the version the shared fixtures run (scripts/conformance/server-parity/vendor/durable-workflow/sdk, which has no registration_token anywhere). Requiring the token from every worker would break published clients. So, as you suggested, the stronger contract is gated on a capability transition.

The fix, in PHP and native:

  • A worker can declare the capability worker_session_registration_token at registration (WorkerSessionRegistry::REGISTRATION_TOKEN_CAPABILITY; native REGISTRATION_TOKEN_CAPABILITY).
  • When the worker's current registration declares it, every explicit create/reuse, heartbeat and close must carry the current registration token.
  • A missing token or a non-current token gets 409 stale_worker_registration and changes nothing. The fence is the first statement inside each request's transaction, before any session row is read or written.
  • The fence follows the current registration. If the replacement is token-aware, a stale process can't act on its sessions, with or without a token, even if the stale process is an older SDK.
  • Without the capability, the published contract is unchanged: a request without a token acts for the worker ID's current registration. A token that is sent must still be current.
  • worker-sessions-runtime.schema.json now states this conditional contract instead of unconditional replacement fencing. It also states that session-bound activity claims and their heartbeats keep their own attempt leases, which are unchanged.

Tests, the same cases in PHP (WorkerSessionProtocolTest) and native (rust/tests/execution/worker_sessions.rs):

  • a_same_id_replacement_reacquires_the_session_and_fences_the_old_process: the worker declares the capability. After the replacement reacquires, the stale process sends create/reuse (with different lease_seconds and max_concurrent_activities), heartbeat (with lease_seconds: 900) and close, first with its old token, then with none. Each gets 409 stale_worker_registration. The session is compared before and after: the whole PHP row (getAttributes()), and native's GET /api/worker-sessions/{id} answer. Both are unchanged.
  • without_the_token_capability_a_tokenless_request_acts_for_the_current_registration: states the published path explicitly. Tokenless reacquire, heartbeat and close work, and a stale token is still refused.

Counterfactuals, run with the old fence (a missing token accepted) and the new tests:

  • PHP: fences_the_old_process fails with Failed asserting that 200 is identical to 409; the published-path test passes.
  • Native: fences_the_old_process fails at the first stale request: the tokenless create answers 200 with outcome reused, so the stale process takes over the replacement's session, where the test expects 409 stale_worker_registration. The published-path test passes.

Qualification on this head:

  • Native cargo test --locked --all-targets, run locally in the pinned CI Rust image on this exact head's source: 275 unit and 490 execution tests passed, plus upgrade inspection.
  • PHP tests/Feature/WorkerSessionProtocolTest.php: 16 tests and 293 assertions passed.
  • Pint passes on both changed PHP files. The schema parses, and the public boundary check passes.
  • CI is running on this head; I'll add its results here.
  • On the previous head 8b6064290, every check was green, including Shared Server fixtures on the first attempt (run 40173583253).

Asking for R2 on this exact head.

@rmcdaniel

Copy link
Copy Markdown
Member Author

CI on exact head ad7ad3aea7189f2b300b3a2783683dc7e64a9d30: every check passed on the first attempt.

  • Shared Server fixtures, run 40175622430: 08:11 to 08:36 UTC.
  • PHPUnit Feature, run 40175622445.
  • SonarQube, run 40175622459. The quality gate passes. The only open PR issues are the two accepted php:S1142 guard-clause code smells in WorkerSessionRegistry.php.
  • Public Boundary.

Waiting on Security's exact-head R2 before merging.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security R3 on exact head ad7ad3aea7189f2b300b3a2783683dc7e64a9d30: the capability transition and sequential old-token/omitted-token regressions resolve the previously demonstrated path, but one blocking PHP concurrency gap remains.

WorkerSessionRegistry::staleRegistration() is the first call in each transaction, but its WorkerRegistration query is a plain, non-locking read. WorkerController serializes same-ID re-registration by locking that same registration row, while explicit session operations do not participate in that lock. Create also resolves and passes a WorkerRegistration model before its transaction.

A valid interleaving on MySQL/PostgreSQL is therefore:

  1. Old request A resolves the old registration and staleRegistration() reads the old token/capability, so A passes.
  2. Replacement B locks the registration row, rotates the token, and commits.
  3. B reacquires the session with the new token and commits.
  4. A then obtains the session-row lock. It no longer rechecks or locks the registration, so its create/heartbeat/close can renew, rewrite, or close B's session. Create can also write the old model's incarnation back onto the session.

Thus “the current token is required” and “a stale process changes nothing” are not yet true under concurrency; the current tests prove only the sequential case. Native does not have this particular gap because each backend's begin() takes its cross-process write fence (BEGIN IMMEDIATE, PostgreSQL transaction advisory lock, or the InnoDB schema-row lock).

Please make PHP explicit session operations hold the same registration-row serialization boundary from token/capability validation through the session mutation, and make create derive the worker used for routing/capabilities/incarnation from that locked current row rather than the pre-transaction model. Account for SQLite, where lockForUpdate() alone is a no-op, using the repository's established writer-lock pattern. Add a deterministic two-connection regression that pauses A after its credential read, completes replacement registration/reacquisition in B, then proves A cannot commit a stale create/reuse, heartbeat, or close after B.

The published tokenless compatibility case can remain conditional as documented. This review does not broaden the requested change or require tokens from PHP SDK 2.2.6.

Security's R3 found a PHP check-then-write race. The registration fence
was a plain read, while same-ID re-registration locks that row before it
rotates the token. A session request could pass the fence, let a
replacement rotate and reacquire, and then still write. Create could even
record the replaced model's incarnation, since it used the registration
the controller read before its transaction.

Each explicit create, heartbeat and close transaction now first locks the
worker's current registration row (lockForUpdate). On SQLite, which
ignores FOR UPDATE, it first takes the writer lock with a no-op update,
as ExternalPayloadBackupHold does. The token fence checks that locked row,
and create admits against it for routing, capabilities and the recorded
holder. Native already serializes writes in begin(); its create now also
re-reads the registration inside that transaction.

WorkerSessionRegistrationRaceTest forks two real MySQL and PostgreSQL
connections. A session request paused after its registration lock makes
the re-registration wait; a create paused after its first read admits
against the replacement's registration. With the previous registry the
six lock cases and both published-worker create cases fail.

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

Copy link
Copy Markdown
Member Author

Rust Server: fix for Security's R3 concurrency blocker (6395498496), at exact head 50f87f6adb5a6803f641d3c88e122a04ebb61f94.

PHP: session requests now hold the registration row through the session write (app/Support/WorkerSessionRegistry.php).

  • A new lockedRegistration() runs first in each explicit create/reuse, heartbeat and close transaction. It reads the worker's current registration lockForUpdate(), the same row same-ID re-registration locks in WorkerController before rotating the token. A session request therefore either commits before the rotation or waits for it and sees it. It can't check one incarnation and write for the next.
  • On SQLite, lockForUpdate() is a no-op. lockedRegistration() first takes the writer lock with a no-op update of that row, before any read, the same pattern as ExternalPayloadBackupHold::locked(). The default DB_SQLITE_TRANSACTION_MODE=IMMEDIATE already serializes these transactions; the explicit lock keeps that true if the mode is changed.
  • staleRegistration() now checks the locked row it is given. It doesn't read again.
  • Create now admits against the locked current row, not the model the controller loaded before the transaction. Routing, capability checks and the recorded holder incarnation all come from that row. If the registration disappeared, create answers 412 worker_not_registered.
  • Lock order matches re-registration (registration row, then session row). No path locks a session row and then the registration.

Native. Your review found no gap here, because each backend's begin() serializes writes. For the same reason as in PHP, create now re-reads the registration inside that transaction and admits against it, instead of the copy read before begin().

Deterministic two-connection regression: tests/Feature/WorkerSessionRegistrationRaceTest.php. It uses the same fork-and-socket pattern as WorkflowTaskRepairRaceTest, on real MySQL and PostgreSQL (DW_TEST_COMPLETION_*, as PHPUnit Feature CI provides).

  • test_a_session_request_holds_the_registration_until_it_commits, 6 cases (MySQL and PostgreSQL × create/reuse, heartbeat, close):
    1. Actor A, a worker that declared worker_session_registration_token, sends the verb with its current token. A pauses right after its registration FOR UPDATE read.
    2. Actor B starts the same-ID re-registration. The test then asserts that B is waiting on a lock (performance_schema.data_lock_waits or pg_locks).
    3. A is released and commits for the incarnation that was current. B then completes with a new token.
    4. A's old token is refused (409 stale_worker_registration) on the same verb, and the session row is unchanged.
  • test_a_create_admits_against_the_registration_current_when_it_commits, 4 cases (MySQL and PostgreSQL × token worker, published tokenless worker). This is the interleaving you describe:
    1. A's create pauses after its first credential read, the controller's pre-transaction read.
    2. B re-registers and reacquires the session.
    3. A continues. The token worker's old-token create is refused and the session row is unchanged. The published worker's tokenless create reuses the replacement's session.
    4. In both cases the holder stays the replacement's incarnation.

Counterfactuals, run against this exact test file with ad7ad3aea's registry restored. Result: 10 tests, 6 errors, 2 failures.

  • All 6 lock cases fail at their first assertion: A never pauses on a registration lock, because no lock exists.
  • Both published-worker create cases fail with the session keeps the replacement as its holder: the old code writes the replaced registration's incarnation back onto the session.
  • The two token-worker create cases pass on the old code too, because a sequential old-token refusal already worked. They stay as positive checks.

Qualification on this head:

  • Native cargo test --locked --all-targets, run locally in the pinned CI Rust image: 275 unit and 490 execution tests passed.
  • PHP WorkerSessionRegistrationRaceTest: 10 of 10 passed against disposable MySQL 8.0 and PostgreSQL containers.
  • WorkerSessionProtocolTest: 16 of 16 passed on SQLite. The broader WorkerSession|ActivityWorkerProtocol filter: 47 tests passed, 8 skipped (database-gated).
  • Pint passes on the changed PHP files, and so does the public boundary check.
  • CI is running on this head; I'll add its results here.

Asking for R3 on this exact head.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security R4 on exact head 50f87f6adb5a6803f641d3c88e122a04ebb61f94: the R3 cross-transaction TOCTOU is fixed, but one lifecycle authorization gap remains.

heartbeat() and close() in PHP call staleRegistration($this->lockedRegistration(...), $registrationToken). When the current registration is absent and the request omits the optional token, staleRegistration(null, null) returns null, so the request proceeds to the old session row and may renew or close it. Native has the same fail-open: the omitted-token branch of stale_registration() interprets an absent REGISTRATION_CAPABILITIES row as “capability not declared” and returns no refusal. Both fenced and legacy deregistration delete the registration while leaving the session row for holder-loss handling, so this is reachable after deregistration. The session's former process can continue mutating it after its authority has been removed simply by omitting the token.

Required correction before acceptance:

  • Every explicit create, heartbeat, and close must establish that a current registration exists inside the same registration-fenced transaction before mutating a session. An absent current registration must fail closed (the existing 412 worker_not_registered contract is suitable) regardless of whether registration_token was omitted.
  • Preserve tokenless compatibility only while a current registration exists and does not declare worker_session_registration_token.
  • Add PHP and native regression coverage: create a session, deregister/delete its worker registration, then prove omitted-token heartbeat and close are refused and the session row is byte-for-byte unchanged. Cover both token-capable and published tokenless registration shapes. Add a deregistration/session-operation race where practical so the lock ordering proves either the session mutation commits before deregistration or the request observes no registration and is refused afterward.

The new same-ID replacement locking and two-connection tests otherwise address the R3 finding as designed.

…gistration

Security's R4: after deregistration deletes the worker's registration, an
explicit heartbeat or close without a token passed the fence (no
registration, so no declared token capability) and reached the old
session, which it could renew, mark or close.

Each explicit create, heartbeat and close now requires the worker's
current registration inside its fenced transaction, before the token
rules: without one it answers 412 worker_not_registered and changes
nothing, with or without a token. Tokenless compatibility applies only
while a current registration exists without the token capability. PHP
and native match, with the same messages.

Tests: PHP and native deregister a token worker (fenced route) and a
published worker (plain route), then send heartbeat and close with the
old token and without; each is refused and the session is unchanged.
With the R3 code PHP answers 409 instead of 412 and native reaches the
session (409 session_not_active). The two-connection race test adds a
deregistration that waits on a paused heartbeat or close's registration
lock, after which the same request is refused.

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

Copy link
Copy Markdown
Member Author

Rust Server: fix for Security's R4 blocker (6395829512), at exact head 7f79034a21b70cb567883ee37e78cef93fe3725c.

Fail closed without a current registration. Each explicit create/reuse, heartbeat and close now requires the worker's current registration inside its fenced transaction, before the token rules:

  • PHP lockedRegistration(); native SESSION_REGISTRATION inside begin().
  • Without a registration, the request answers 412 worker_not_registered and changes nothing, with or without registration_token. Messages: Worker must be registered before creating|renewing|closing a worker session. (create keeps the controller's existing message).
  • Tokenless compatibility now applies only while a current registration exists that doesn't declare worker_session_registration_token.
  • PHP staleRegistration() now takes a non-null registration. Native heartbeat and close share one session_request_fence, and native create checks existence before the token.

Regressions, PHP and native. a_deregistered_worker_can_neither_renew_nor_close_its_session:

  • It deregisters a token worker through the fenced route (POST /registrations/{id}/deregister with its token) and a published worker through the plain route (DELETE /registrations/{id}).
  • It then sends heartbeat (lease_seconds: 900) and close, without a token and with the old token. Each gets 412 worker_not_registered.
  • The session stays byte-identical. PHP compares the whole row from before deregistration. Native compares GET /api/worker-sessions/{id}, taken after deregistration, since that view reports the lost holder as orphaned on read, as PHP's does.

Race coverage (WorkerSessionRegistrationRaceTest::test_a_session_request_and_a_deregistration_serialize): MySQL and PostgreSQL × heartbeat and close, for a published tokenless worker.

  1. A pauses right after its registration FOR UPDATE.
  2. The deregistration (DELETE) is shown waiting on that lock (data_lock_waits / pg_locks).
  3. A commits while the registration exists, then the deregistration completes.
  4. The same request then gets 412 and the session row is unchanged.

Counterfactuals with the R3 code (50f87f6ad):

  • PHP's deregistration test fails with Expected response status code [412] but received 409.
  • Native's fails at the first heartbeat with (409, session_not_active) instead of (412, worker_not_registered): the request reached the session.

Qualification on this head:

  • Native cargo test --locked --all-targets, run locally in the pinned CI Rust image: 275 unit and 491 execution tests passed.
  • PHP, with in-memory SQLite: the WorkerSession|ActivityWorkerProtocol|WorkerDeregistration filter passed 84 tests, with 16 skipped because they need databases (WorkerSessionProtocolTest is 17 of 17). WorkerSessionRegistrationRaceTest passed 14 of 14 against disposable MySQL 8.0 and PostgreSQL.
  • Pint and the public boundary check pass.
  • CI is running on this head.

Previous head 50f87f6ad CI. PHPUnit Feature passed with all 10 race tests running (3,122 tests, the same 37 skipped as before). Sonar and Boundary passed. Shared Server fixtures failed in one job: the published PHP 2.11.1 record of the SQLite update-validation profile, whose worker_lost step answered in 0.066 s instead of waiting for the validation timeout. That reference-side flake doesn't involve this branch's code; I'll record it on #754.

Asking for R4 on this exact head.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security review remains blocking at exact head 7f79034a21b70cb567883ee37e78cef93fe3725c.

R4 correctly makes an absent registration fail before create, heartbeat, or close. A narrower PHP-only fence window remains:

  • WorkerRegistrationDeregistrar::deregister() locks the registration, changes its status to superseded, and commits that transaction before recovering workflow tasks and deleting the row.
  • WorkerSessionRegistry::lockedRegistration() treats any present row as current. It does not reject STATUS_SUPERSEDED.
  • For a published/tokenless worker, staleRegistration() then permits the request. During the recovery interval before deletion, the old process can create/reacquire, renew, or close a session even though deregistration already established the superseded fence.

That conflicts with the deregistrar's stated handoff boundary and with WorkerPollFence, which rejects a superseded row even if its heartbeat is fresh. Native ordinary deregistration keeps deletion in one transaction, so the observable window is PHP-specific.

Please reject STATUS_SUPERSEDED inside the same session transaction, before token checks or session mutation. Add PHP coverage for tokenless create, heartbeat, and close against a superseded registration with an unchanged session row; also cover the deregistration-first ordering if the race harness can expose the interval. Preserve the existing absent-row 412 worker_not_registered behavior and PHP/native contract intentionally for the superseded-row response.

@rmcdaniel

Copy link
Copy Markdown
Member Author

CI on exact head 7f79034a21b70cb567883ee37e78cef93fe3725c: every check passed on the first attempt.

  • Shared Server fixtures, run 40181095153.
  • PHPUnit Feature, run 40181095170.
  • SonarQube, run 40181095187. The gate passes; the only open issues are the two accepted php:S1142 code smells.
  • Public Boundary.

The PR is mergeable on current main 7ede756b1. Waiting on Security's exact-head R4 before merging.

Security's R5: plain deregistration commits the registration as
superseded, recovers its leases and only then deletes the row. A
published tokenless create, heartbeat or close in that window still
passed the fence and could reacquire, renew or close the session.

Each explicit create, heartbeat and close now refuses a locked
registration whose status is superseded, right after the absent-row
check and before the token rules, with the refusal the worker heartbeat
already gives: 409 worker_registration_superseded and its remediation.
Native, whose plain deregistration is atomic, answers identically.

Tests: PHP and native refuse create, heartbeat and close from a
superseded registration of a token worker and of a published worker,
with the session unchanged. The race test pauses a real deregistration
after its superseded commit, before the delete, and shows tokenless
create, heartbeat and close refused in that window, then 412 once the
row is gone. With the R4 code each of these is accepted (200).

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

Copy link
Copy Markdown
Member Author

Rust Server: fix for Security's R5 blocker (6396031786), at exact head cd8beca3dbc7b73c9bb2987043dca3bee54682d4.

Superseded registrations are refused inside the fenced transaction.

  • Each explicit create/reuse, heartbeat and close now refuses a locked current registration whose status is superseded. The check comes right after the absent-row check, which is still 412 worker_not_registered, and before the token rules and any session read or write.
  • The answer reuses the refusal the worker heartbeat already gives a superseded registration: 409 worker_registration_superseded, Worker registration was fenced after its leases were recovered., worker_id, and remediation: Restart and register the worker with a new worker_id.
  • PHP: supersededRegistration(). Native: registration_superseded() on the same row read, with status added to SESSION_REGISTRATION, and an identical body.
  • Native plain deregistration is atomic, as you note, so there the check is defensive and keeps the PHP/native contract identical.

Tests:

  • a_superseded_registration_can_neither_create_renew_nor_close_its_session, in PHP and native. For a token worker and a published worker, the registration is set to superseded. Create/reuse (with lease_seconds: 5), heartbeat (lease_seconds: 900) and close each get the 409 above, and the session stays unchanged. PHP compares the whole row; native compares the session view.
  • WorkerSessionRegistrationRaceTest::test_session_requests_inside_a_deregistrations_superseded_window_are_refused, on MySQL and PostgreSQL. The plain DELETE deregistration runs in a forked process and pauses on Laravel's TransactionCommitted event once its row reads superseded. That is after the fence transaction commits and before recovery deletes the row. In that window, a published tokenless create, heartbeat and close each get 409 worker_registration_superseded, with the session row unchanged. Once released, the deregistration deletes the row, and a heartbeat then gets 412 worker_not_registered.

Counterfactuals with the R4 code (7f79034a2):

  • PHP superseded test: Expected response status code [409] but received 200; the create reused the session.
  • Race window, on both databases: Expected response status code [409] but received 200.
  • Native: (200, Null, Null) instead of (409, worker_registration_superseded, …).

Qualification on this head:

  • Native cargo test --locked --all-targets, run locally in the pinned CI Rust image: 275 unit and 492 execution tests passed.
  • PHP WorkerSessionProtocolTest with in-memory SQLite: 18 tests, 353 assertions, all passed.
  • WorkerSessionRegistrationRaceTest: 16 of 16 passed against disposable MySQL 8.0 and PostgreSQL.
  • Pint and the public boundary check pass.
  • CI is running on this head.
  • The previous head 7f79034a2 was green on every check (comment 6396233810).

Asking for R5 on this exact head.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security R5 disposition: the prior superseded-registration race blocker is resolved at exact head cd8beca3dbc7b73c9bb2987043dca3bee54682d4.

I reviewed the exact delta from blocked head 7f79034a21b70cb567883ee37e78cef93fe3725c. PHP now checks the locked current registration for superseded immediately after the existing absent-row 412 fence and before registration-token evaluation or any session-row access for explicit create, heartbeat, and close. It returns the established 409 worker_registration_superseded contract, so both token-capable and published tokenless workers are fenced without session mutation during the previously exploitable mark/recover/delete gap.

Native now selects and evaluates the same registration status before token/session work for all three verbs and returns the same response contract. This is defensive parity because native deregistration is atomic.

Regression coverage includes token and published workers across create/heartbeat/close with an unchanged session assertion in both implementations. The PHP database race test additionally pauses real plain deregistration after the superseded transaction commits but before row deletion, proves all three tokenless requests return 409 without mutation on MySQL and PostgreSQL, then proves the post-delete 412 boundary. The exact delta is git diff --check clean.

No remaining R5 source-level security finding. This clears my earlier blocker for this exact head only; CI completion, merge, release, and installed acceptance remain with the owning workflow.

@sonarqubecloud

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

CI on exact head cd8beca3dbc7b73c9bb2987043dca3bee54682d4: every check passed on the first attempt.

  • Shared Server fixtures, run 40183862103.
  • PHPUnit Feature, run 40183862070, including the race tests against MySQL and PostgreSQL.
  • SonarQube, run 40183862067.
  • Public Boundary.

The PR is mergeable on current main 7ede756b1. Waiting on Security's exact-head R5.

@rmcdaniel
rmcdaniel marked this pull request as ready for review October 25, 2026 10:40
@rmcdaniel
rmcdaniel merged commit 1bca67a into main Oct 25, 2026
61 checks passed
@rmcdaniel
rmcdaniel deleted the rust-325-worker-sessions branch October 25, 2026 10:41
@rmcdaniel

rmcdaniel commented Oct 25, 2026 •

Copy link
Copy Markdown
Member Author

Merged as 1bca67a, pinned to the head Security passed at R5, cd8beca3dbc7b73c9bb2987043dca3bee54682d4 (6396385851). Main's tree fd93f1b2 is that head's tree.

Native now serves the five worker-session routes: 138 of the 175 published route methods. In PHP and native, the session fence:

  • requires the current registration and refuses a superseded one;
  • holds the registration row through each session write;
  • requires the current token from workers that declare worker_session_registration_token;
  • bounds session numbers portably;
  • keeps requirement lists linear.

The branches are deleted.

rmcdaniel added a commit that referenced this pull request Oct 25, 2026
…dec-one-job

The comparator node test list keeps main's worker session tests and this
branch's codec deadline cell test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rmcdaniel added a commit that referenced this pull request Oct 25, 2026
In #740's final fixtures run (40183862103) mysql-8.0 native-tests took
20.7 min against 21.0 for existing-php-mysql-matrix, almost all of it the
bootstrap step's native build and execution tests. Halving only the
existing matrix would leave mysql-8.0 as the run's longest part. Give
mysql-8.0 the same two shards and its own native-tests-b combination; the
parts test checks both labels' pairs.

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
…ment served

Both are served on main (worker sessions since #740), but the missing
route list still named them.

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

Native now serves 140 route methods: main's 138 plus both imports. 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>
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: serve worker sessions like Server 2.11.1

1 participant