Repository navigation
Rust #325: serve durable workflow stream writes (items, close) - #773
Conversation
…d 2.11.1
The producer routes for durable workflow streams, POST
runs/{run}/streams/{name}/items and /close, are the last stream routes
native does not serve, and native's reads only ever see an empty run.
This fixture records what published 2.11.1 answers over raw HTTP:
- an append opens a stream at offset 0, a repeated idempotency key
returns the existing offset, and only new items move the counters;
- the pending-items cap commits the items before it and then answers 429
stream_full;
- a read window that stops short of the head keeps pending_items, one
that reaches the head clears it, and a bounded wait on an open stream
returns empty after waiting;
- close closes once and keeps a terminal stream's first close, error and
retention (3600 seconds by default); closed and errored streams refuse
appends with 409 and their tails read as terminal;
- every validation, stream-name, external-reference and unknown-run
refusal, including the different 400 reasons append and close give.
The contract checks exact bodies, whole-second UTC times that never run
backwards, and that no refusal wrote anything. A unit test runs it on the
recorded 2.11.1 observation and on 18 mutations it must refuse. A fresh
published PHP recording passes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er 2.11.1
Native now serves POST .../streams/{name}/items and .../close. The run's
stream list, description and item reads now return real rows.
- Appends open the stream on first use, assign offsets from 0, and return
the existing offset for a repeated idempotency key.
- The pending-items cap keeps the items before it, then answers 429.
- Closing is final. Reads that reach the head clear the pending count, and
a bounded wait polls every 250 ms, as PHP's readWithWait does.
- Times are stored as whole seconds, as Eloquent writes them.
- Laravel's trimming applies to payloads, and an empty object reads back
as a list, as in PHP.
- `offset` is reserved, so its statements are per-dialect constants.
Continue-as-new no longer refuses an instance with durable streams: they
stay with their run, as in PHP.
Three differences are documented in the module: a batch with no item
fields gets 400 instead of PHP's 500; a read at the head does not clear a
later append's count; a reference object in payload_reference gets 501.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…low-stream-writes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…low-stream-writes The port guide counts 145 served route methods: stream writes add two and cancellation scopes three to the base 140. The workflow test list carries both contract tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…low-stream-writes Kept both sides. Stream items stay read-write and close stays write, next to the read-only Nexus operation reads, and the unserved inventory drops both pairs of routes. The port guide counts 147 served route methods: the base 140 plus stream writes, cancellation scopes and Nexus reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security review — HOLD on exact head 7245239, tree 51f73ed0097fc1326a380d6c258ece2ac6b93f1d. The namespace binding, static SQL, body and page bounds, read-only projection check, and external-reference refusal look sound. Four resource and retry invariants remain blocking before these write routes are enabled:
Required: enforce the same effective namespace quota policy in the same transaction and under the namespace row lock, with fail-closed typed behavior when configured policy cannot be evaluated; or leave both routes refused. Cover new-stream admission, multi-item batch admission at the remaining boundary, dedupe not spending quota, rollback/no partial mutation on quota refusal, shared inbound plus outbound usage, and concurrent/mixed-writer safety on the supported databases.
Also add direct regressions for projected-v1 append and close refusal. Current green checks do not exercise these conditions. Please send a new SHA/tree, complete delta, counterfactuals, and fresh exact-head CI after correction. No live, deployment, or migration clearance is implied. |
An append and a clean or errored close against a projected v1 run answer 409 v1_projection_read_only, and no stream or item row is written. With the read-only checks removed from append and close, the append is accepted and the test fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Held on Security's review (comment 6409052533) and converted to draft. Native will not serve the two stream write routes until the shared contract is fixed.
The next Rust activation proceeds without this PR: bridge adapters. |
|
Security correction to blocker 1 in my earlier hold: native does not silently ignore a configured namespace durable-state quota. I independently verified exact head 7245239 config_audit.rs and main.rs. DW_NAMESPACE_DURABLE_LIMITS, DW_NAMESPACE_DURABLE_HARD_LIMITS, and DW_NAMESPACE_DURABLE_OVERRIDES accept only unset or an empty object; any non-empty or invalid policy is a startup refusal, and main exits on the audit refusal before authentication/storage/server startup. The published cutover contract also explicitly requires all PHP write-capable roles to stop before sequential native takeover and forbids a mixed PHP/native database fleet. Therefore I withdraw the claimed configured-quota bypass and mixed-writer locking blocker. Preserve those startup refusals and the no-mixed-fleet invariant; no native implementation of PHP namespace quotas is required for #773 while those constraints remain exact and tested. The other blockers remain. With empty quota policy, caller-unbounded max_pending_items and retention_seconds, ambiguous partial-prefix 429 commits, and promised-but-unenforced closed-stream retention still allow unbounded or surprising durable growth in both published PHP and native. Server #774 is the correct shared PHP/native remediation and must land before #773 serves these routes. The new projected-v1 append/clean-close/error-close refusal regression described at d25d98f is responsive and should stay in the successor. #773 remains held/draft until #774 lands, the routes remain refused meanwhile, and a successor SHA/tree plus complete delta and fresh exact-head CI are reviewed. This correction narrows the hold; it does not clear the current head. |
…rkflow-stream-writes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rkflow-stream-writes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The native producer routes adopt the shared durable stream contract fixed in #774: - max_pending_items is at most 10000 and retention_seconds at most 2592000, refused with 422 by the append and close rules. - An append resolves stored and repeated idempotency keys first, then refuses the whole batch when its fresh items would pass the cap. The transaction is dropped, so nothing is written and a stream the append opened is not created. Appends run under the transition lock, so concurrent first appends open one stream and never pass the cap. - A closed or errored stream past closed_at plus its retention reads as not found: list omits it, and describe, subscribe and close answer 404. - prune_expired_streams reclaims expired streams in bounded, resumable steps (at most N streams per pass, oldest close first, and M items in transactions of at most 500), from the heartbeat inline retention pass under its own per-namespace throttle and from a general operator retention pass. The workflow-stream-writes contract accepts the whole-batch refusal and bounds published 2.11.1's partial commit as a published defect. New execution tests cover the whole-batch refusal, both bounds, concurrent near-cap and first-open appends through two pools, expiry and the bounded pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rkflow-stream-writes # Conflicts: # .github/workflows/server-parity.yml # docs/rust-server-port.md # rust/src/runtime/backend.rs
…rkflow-stream-writes
…rkflow-stream-writes
…efusal (#774) Native follows #785's stream lifecycle: - A closed stream past its retention reads as not found, append included, and keeps its row until its run is pruned. - Its deadline lives in the native-owned dw_stream_reclaims relation (schema 6 on all four backends; catalogs, upgrade lists and parity scripts updated), indexed (namespace, reclaim_at, stream_id). - The retention pass reads due streams from that index in deadline order, deletes items in bounded batches under the transition lock, and stops blocked when items hold external references, which native does not delete yet. - Run retention deletes a pruned run's streams unless they hold external references, and an append to a run whose details are pruned answers 404 stream_not_found instead of opening a stream. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…low-stream-writes
…n refusal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
Merged through the merge queue at 20:35Z as
Native durable workflow stream writes are on main, with the #774 producer contract: bounds, whole-batch capacity, expiry, reclamation through 🤖 Generated with Claude Code |



Part of #325. Native durable workflow stream writes, matching published Server 2.11.1 and the #774 producer contract (#785), fixture first.
Review tier: advisory (pre-RC native). It changes only the native server and a native-owned schema relation. It touches no published PHP surface, credentials, production data or CI trust. Policy: #325 comment 6423232503.
Fixture first.
workflow-stream-writeswas recorded from published 2.11.1 against its SQLite reference. Theparity.v1.workflow_streamsrun is started and left waiting; no worker is needed. The fixture covers:stream_full. Published 2.11.1 committed the items before the cap first, which the contract bounds as a published defect;pending_itemsat the head but not on a partial window, plus a bounded wait;The contract (
workflow-stream-writes-contract.mjs) refuses 18 mutations, including native's previous 501 answer.Native writes.
rust/src/runtime/workflow_streams.rsnow servesPOST .../streams/{name}/itemsandPOST .../streams/{name}/close. List, describe and item reads now return real rows.Y-m-d H:i:sstores them.[], both as in PHP.offsetis a reserved word, so its statements are per-dialectBackendconstants.unserved-php-routes.v2.11.1.txt.The #774 contract, as #785 defines it for PHP:
max_pending_itemsis at most 10000 andretention_secondsat most 2592000, refused with 422.closed_at + retention_secondsa stream reads as not found for list, describe, subscribe, close and append. Its row stays until its run is pruned, so the name is never opened again at offset 0.dw_stream_reclaims, indexed(namespace, reclaim_at, stream_id). It is schema version 6 on SQLite, PostgreSQL, MySQL and MariaDB, with catalogs, upgrade lists and the parity schema scripts updated. The retention pass reads due streams from that index in deadline order and deletes items in bounded batches under the transition lock. Native deletes no external payload objects yet, so a stream whose items hold an external reference stops the pass, blocked, with its items kept.stream_not_foundinstead of opening a stream, as PHP'slockStreamrefuses it.Continue-as-new. Native used to refuse continue-as-new for any instance with durable streams. HTTP producers can now create streams on a native run, so that refusal would have become reachable. PHP leaves a run's streams with the run, so native now does the same (
continue_as_new_keeps_durable_streams_with_the_predecessor).Three documented differences, also listed in the module doc and the port log:
items, and PHP fails with 500. Native answers the service's 400append requires at least one item. A batch with only some such items drops them, as PHP does.pending_itemsonly while the stream is still at that head. PHP's unlocked save can also clear the count of an append committed after the read.payload_referencegets 501rust_capability_not_implemented; PHP resolves and claims it. Any other non-null value gets PHP's 422external_payload_unsupported.Not in this PR:
workflow_streamcommand;workflow_streams_contract;Qualification
Head
0b39fd74, parent29d09a86, with maina3e0bab0merged in, which includes #785. The native source was qualified at511bd5b2; later commits merged main (adding only #785's PHP files) and updated the port log.workflow_stream_bounds,history_retention), including the pruned-run append refusal.workflow-stream-writesandmessage-streamspass.app/changes beyond Bound workflow stream producers, refuse whole batches at capacity, and expire closed streams (#774) #785's, which are already on main.🤖 Generated with Claude Code