Skip to content

Commit 5b9eaae

Browse files
authored
Merge pull request #801 from durable-workflow/rust-325-stream-retention
Release message streams at retention, and keep PHP streams at takeover
2 parents ad3ac5f + f9610d4 commit 5b9eaae

15 files changed

Lines changed: 562 additions & 131 deletions

‎.github/workflows/server-parity.yml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -523,8 +523,8 @@ jobs:
523523
-e DB_USERNAME=root -e DB_PASSWORD=parity-disposable-password \
524524
-e CACHE_STORE=file -e QUEUE_CONNECTION=database -e DW_AUTH_TOKEN=parity-test-token \
525525
"$layout" artisan server:bootstrap --force > parity-evidence/published-bootstrap.txt
526-
# Compiling the library tests with two jobs peaks near 4.2 GB; a 4 GB limit OOM-killed rustc (exit 101).
527-
docker run --rm --user="$(id -u):$(id -g)" --network "$RESOURCE_SCOPE" --cpus=2 --memory=6g --memory-swap=6g \
526+
# Compiling the test targets with two jobs peaked at 6.8 GB cold (cgroup memory.peak, October 27); a 6 GB limit OOM-killed rustc (signal 9, PR #801).
527+
docker run --rm --user="$(id -u):$(id -g)" --network "$RESOURCE_SCOPE" --cpus=2 --memory=8g --memory-swap=8g \
528528
-e CARGO_HOME=/cargo -e CARGO_TARGET_DIR=/target -e CARGO_BUILD_JOBS=2 -e DW_PARITY_PART -e DW_EXECUTION_SHARDS \
529529
-e DW_TEST_MYSQL_URL=mysql://root:parity-disposable-password@parity-mysql:3306/php_ref \
530530
-e DW_EXECUTION_MYSQL_URL=mysql://root:parity-disposable-password@parity-mysql:3306/php_ref \
@@ -1183,8 +1183,8 @@ jobs:
11831183
-e DB_USERNAME=parity -e DB_PASSWORD=parity-disposable-password \
11841184
-e CACHE_STORE=file -e QUEUE_CONNECTION=database -e DW_AUTH_TOKEN=parity-test-token \
11851185
"$layout" artisan server:bootstrap --force > parity-evidence/published-bootstrap.txt
1186-
# Compiling the library tests with two jobs peaks near 4.2 GB; a 4 GB limit OOM-killed rustc (exit 101).
1187-
docker run --rm --user="$(id -u):$(id -g)" --network "$RESOURCE_SCOPE" --cpus=2 --memory=6g --memory-swap=6g \
1186+
# Compiling the test targets with two jobs peaked at 6.8 GB cold (cgroup memory.peak, October 27); a 6 GB limit OOM-killed rustc (signal 9, PR #801).
1187+
docker run --rm --user="$(id -u):$(id -g)" --network "$RESOURCE_SCOPE" --cpus=2 --memory=8g --memory-swap=8g \
11881188
-e CARGO_HOME=/cargo -e CARGO_TARGET_DIR=/target -e CARGO_BUILD_JOBS=2 -e DW_PARITY_PART \
11891189
-e DW_TEST_POSTGRES_URL=postgres://parity:parity-disposable-password@parity-pg:5432/php_ref \
11901190
-e DW_EXECUTION_POSTGRES_URL=postgres://parity:parity-disposable-password@parity-pg:5432/php_ref \
@@ -1862,8 +1862,8 @@ jobs:
18621862
}
18631863
trap rust_test_failures ERR
18641864
mkdir -p cargo-home rust-target parity-evidence
1865-
# Compiling the library tests with two jobs peaks near 4.2 GB; a 4 GB limit OOM-killed rustc (exit 101).
1866-
docker run --rm --user="$(id -u):$(id -g)" --cpus=2 --memory=6g --memory-swap=6g \
1865+
# Compiling the test targets with two jobs peaked at 6.8 GB cold (cgroup memory.peak, October 27); a 6 GB limit OOM-killed rustc (signal 9, PR #801).
1866+
docker run --rm --user="$(id -u):$(id -g)" --cpus=2 --memory=8g --memory-swap=8g \
18671867
-e CARGO_HOME=/cargo -e CARGO_TARGET_DIR=/target -e CARGO_BUILD_JOBS=2 \
18681868
-e DW_UPGRADE_TEST_WRITABLE=/evidence -e DW_UPGRADE_TEST_READONLY=/source/parity-evidence \
18691869
-v "$PWD:/source:ro" -v "$PWD/cargo-home:/cargo" -v "$PWD/rust-target:/target" \
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# October 27, message-stream retention
2+
3+
- **Before.** Native's retention pass held any run that had
4+
`workflow_messages` rows or whose instance had inbound message streams, with
5+
`message_stream_cleanup_unavailable`. Published PHP's
6+
`MessageStreamRetentionCleanup` was never ported. So a namespace that used
7+
message streams kept those runs' details forever, and takeover refused any
8+
PHP database with stream rows.
9+
- **Change.** `prune_run` plans and applies the stream cleanup in the run's
10+
own prune transaction, under the transition lock. The plan follows PHP's
11+
`planForRun`, read before the run's summary goes:
12+
- **No inbound streams on the instance:** nothing to do.
13+
- **The instance's current run is terminal and no other run summary
14+
remains:** every item of the instance's streams is deleted, then the
15+
streams. Reported as `message_stream_items_deleted` and
16+
`message_streams_deleted`.
17+
- **Otherwise:** on each stream whose `cursor_checkpoint_run_id` is a
18+
retained later run (another run summary of the instance, or its current
19+
run), the items this run consumed lose their payloads. That covers items
20+
at or before the stream's cursor that are not released yet: `payload_blob`
21+
becomes `''` and `payload_released_at` is set. The item rows stay as
22+
identity tombstones. Reported as `message_stream_items_compacted`.
23+
`cleanup_blocked_*` markers naming this run are cleared.
24+
- The run's `workflow_messages` rows go with the run's other details
25+
(`messages_deleted`), as in PHP. Before, such a run was held.
26+
- The current run counts as retained only when its run row exists, as in
27+
PHP.
28+
- A run stays held with `external_payload_cleanup_unavailable` when an
29+
inbound item on its instance holds an external reference. Nothing writes
30+
one today; the hold keeps a later change from stranding stored objects.
31+
- **Not ported.** PHP's `markBlocked`, which marks a stream when
32+
external-payload cleanup blocks a prune. Native never deletes external
33+
objects: a run holding an external reference stays held as
34+
`external_payload_cleanup_unavailable`, and native refuses external
35+
references as message input.
36+
- **Tests** on SQLite, PostgreSQL, MySQL and MariaDB:
37+
- `retention_releases_consumed_messages_and_removes_the_last_runs_streams`:
38+
pruning a continued run compacts the two messages it consumed and keeps the
39+
third. Pruning the last run deletes the stream and its three items.
40+
- `retention_keeps_consumed_messages_no_retained_checkpoint_covers`: without
41+
a retained checkpoint, nothing is compacted and the stream stays.
42+
43+
## Takeover keeps message and workflow streams
44+
45+
- **Before.** Takeover refused any source with rows in
46+
`workflow_messages`, either kind of stream, or their items.
47+
- **Change.** Stream rows carry over unchanged.
48+
- Each closed or errored durable stream gets its `dw_stream_reclaims`
49+
deadline: 2.11.3's `reclaim_at`, or `closed_at` plus `retention_seconds`
50+
(3600 unless set) for earlier layouts. Reported as
51+
`stream_reclaims_recorded`; MySQL and MariaDB do it in journal step 10.
52+
- Inspection reports `stream_reference_inspection`. A stream row holding an
53+
external payload reference refuses with
54+
`upgrade_source_external_payloads_unsupported`.
55+
- Only `workflow_messages` stays refused, now as
56+
`upgrade_source_workflow_messages_unsupported`.
57+
- **Tests.**
58+
- `takeover_keeps_php_streams_and_records_their_reclaim_deadlines` (SQLite).
59+
- The 2.11.3 takeover test now copies a recorded deadline.
60+
- The refusal list covers a durable stream item holding an external
61+
reference.
62+
- Takeover qualification passes on PostgreSQL, MySQL and MariaDB,
63+
including a MySQL takeover killed after step 9.
64+
65+
## Review fixes before the PR
66+
67+
An independent review found no serious defects. Fixed:
68+
- A resumed MySQL takeover decides step 10's deadline source by layout, not
69+
by whether `reclaim_at` still exists. A 2.11.3 takeover killed after that
70+
column's drop had already copied every deadline, so it adopts nothing more
71+
instead of adding `closed_at + retention` rows for streams PHP had emptied.
72+
- The current run's missing row and the inbound-item hold above.
73+
- The payload scan's comment no longer says stream item rows are refused.
74+
- The retention test now puts the cursor exactly on the pruned run's last
75+
consumed message (so `<=` matters) and points the instance at a missing
76+
current run.

‎docs/rust-server-port.md‎

Lines changed: 55 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -14,92 +14,60 @@ qualify a later tree.
1414

1515
## Current state
1616

17-
Updated October 24, 2026.
18-
19-
- **Merged October 24.** Train
20-
[#729](https://github.com/durable-workflow/server/pull/729) (merge
21-
`61219609b`):
22-
- MySQL statement closes are sent with the next command
23-
([#717](https://github.com/durable-workflow/server/pull/717)), ending the
24-
MySQL-family stalls of [#714](https://github.com/durable-workflow/server/issues/714);
25-
- policy maps that are not JSON objects are refused, like PHP #705, and a
26-
namespace quota map that is not UTF-8 is refused under either name
27-
([#719](https://github.com/durable-workflow/server/pull/719));
28-
- worker history shows consumed signal sequences, hydrating only each page's
29-
own signals, plus full task snapshots
30-
([#710](https://github.com/durable-workflow/server/pull/710));
31-
- a newer push cancels a superseded Shared Server fixtures run
32-
([#724](https://github.com/durable-workflow/server/pull/724)).
33-
- **Merged October 20-23.** These are summarized in the checkpoint history:
34-
- backup-first takeover on every database;
35-
- multi-node kill and restart;
36-
- ownership guards, search attributes, namespace update, history export,
37-
retention and activity timeouts;
38-
- the October 21-23 trains;
39-
- the move to the published 2.11.1 reference;
40-
- the GHSA-6q48-6c7w-9gw2 fixes.
41-
42-
Each train's PR holds its evidence.
43-
- **In review or qualifying, in merge order.**
44-
1. CI speed and the merge queue:
45-
- [#741](https://github.com/durable-workflow/server/pull/741) runs the
46-
SDK direction profiles in parallel lanes, about 14 minutes off every
47-
database job, and fixes a timestamp-comparison test flake;
48-
- [#742](https://github.com/durable-workflow/server/pull/742) adds the
49-
merge-queue prerequisites (`merge_group` triggers, one fixtures
50-
result, and fixture cells skipped when a change cannot affect them).
51-
2. This train: operator metrics read across PHP's redispatch steps
52-
([#708](https://github.com/durable-workflow/server/pull/708)),
53-
query-authority clients that wait for the acknowledgement
54-
([#720](https://github.com/durable-workflow/server/pull/720)), and the
55-
machine-checked route list below.
56-
3. Condition waits without a key, task snapshots, the SQLite patch cohort and
57-
message streams (#713, #718, #722, #709).
58-
4. Update validation with PHP's poll request ID contract
59-
([#712](https://github.com/durable-workflow/server/pull/712)), then the
60-
held-poll registration fence
61-
([#739](https://github.com/durable-workflow/server/pull/739), #734) and
62-
worker sessions
63-
([#740](https://github.com/durable-workflow/server/pull/740), #736),
64-
stacked in that order.
65-
5. Activity cancellation acknowledgement, and pip, Composer and Cargo trust
66-
in CI ([#673](https://github.com/durable-workflow/server/pull/673)).
17+
Updated October 27, 2026.
18+
19+
- **Merged October 26-27, through the merge queue.**
20+
- [#790](https://github.com/durable-workflow/server/pull/790): prepared
21+
local-activity refusals.
22+
- [#793](https://github.com/durable-workflow/server/pull/793): Nexus catalog
23+
administration, service-call execute, show and cancel, shared admission
24+
limits, and catalog call reads.
25+
- [#795](https://github.com/durable-workflow/server/pull/795): takeover of
26+
published 2.11.1 databases (fresh and upgraded from 2.9.x), keeping search
27+
attribute definitions, worker sessions and service calls, plus the repair
28+
pass's service-call deadline sweep.
29+
- [#797](https://github.com/durable-workflow/server/pull/797): native's own
30+
maintenance repair loop (opt-in, `DW_RUST_MAINTENANCE_LOOP=1`); takeover
31+
keeps runtime credentials and update validation tasks; takeover of
32+
published 2.11.3, fresh and upgraded from 2.9.x.
33+
- **In the queue or next, in order.**
34+
1. [#799](https://github.com/durable-workflow/server/pull/799): the fixture
35+
adapters' Cargo cache, saved only by pushes to main.
36+
2. Message-stream retention (PHP's `MessageStreamRetentionCleanup`), and
37+
takeover that keeps message and workflow streams.
38+
3. The native release-candidate image (`Dockerfile.native`), its release
39+
workflow and the takeover runbook's image commands.
40+
4. The parity reference moves to published 2.11.3. Native already matches it
41+
on 62 of 65 fixtures. The move waits on two published 2.11.3 defects
42+
General owns: the storage diagnostic (#789) and cross-caller idempotent
43+
replay (Workflow #931).
44+
- **Still refused at takeover:** workflow-to-workflow messages
45+
(`workflow_messages`, written only by embedded PHP workflows) and external
46+
payload references. PHP keeps references in about 30 table and column
47+
locations in their URI form, so taking them over is its own project; the
48+
recommendation is that the release candidate keeps refusing them.
6749
- **Shared Server fixtures.** Native, embedded and published PHP 2.11.1 are
6850
compared on SQLite, MySQL 8.0, MariaDB 10.11 and PostgreSQL 17. Where the
6951
published reference itself misbehaves, an explicit boundary separates it
70-
from native, and the PHP owner has an issue for each case:
71-
- `operator-metrics` across PHP's redispatch window (#708);
72-
- the published-SDK patch cohort's SQLite poll refusal
73-
([#721](https://github.com/durable-workflow/server/issues/721), #722);
74-
- update validation under SQLite write locks
75-
([#726](https://github.com/durable-workflow/server/issues/726), fixed in
76-
PHP by #730);
77-
- `worker-sdk-persistent-pressure`'s shutdown window
78-
([#728](https://github.com/durable-workflow/server/issues/728));
79-
- a retried activity's cooperative cancellation
80-
([Workflow #923](https://github.com/durable-workflow/workflow/issues/923));
81-
- a reacquired worker session keeping its old failure reason
82-
([#737](https://github.com/durable-workflow/server/issues/737)).
52+
from native, and the PHP owner has an issue for each case. Recent ones:
53+
- #796, `service-calls` under full-corpus SQLite load;
54+
- #794, workflow-bound Nexus handlers.
8355

8456
No expectation is edited to make native pass.
8557
- **Parity surface.** Native answers 174 of the 175 API route methods in
86-
published 2.11.1's route table, with message streams, durable workflow
87-
stream writes, update validation, activity cancellation acknowledgement,
88-
worker sessions, workflow-task cancellation scopes, service catalog
89-
administration, Nexus service-call execution, show and cancel for the
90-
handler kinds that stay accepted, caller-indexed Nexus operation reads,
91-
bridge adapter webhooks, and embedded v2 and Waterline v1 workflow import.
92-
Eight of those are the prepared local-activity routes, which it answers
93-
with PHP's refusal for a bridge without the prepared role. An execution
94-
test sends each one through the native router, and the ones still
95-
answering `501 rust_capability_not_implemented` must match
58+
published 2.11.1's route table. Eight of those are the prepared
59+
local-activity routes, which it answers with PHP's refusal for a bridge
60+
without the prepared role. Nexus workflow-bound handler kinds answer 501
61+
until #794 settles their contract. An execution test sends each route
62+
through the native router, and the ones still answering
63+
`501 rust_capability_not_implemented` must match
9664
[`unserved-php-routes.v2.11.1.txt`](../rust/tests/fixtures/unserved-php-routes.v2.11.1.txt).
97-
Unlike published PHP, native's embedded v2 import refuses a bundle whose
98-
rows reference rows outside it, links only runs of one namespace, and reads
99-
the signing key only from the request body
100-
([#765](https://github.com/durable-workflow/server/issues/765)).
101-
- **Next action.** Land the items above in order, keep porting the unserved
102-
routes fixture first, and package the native binary into the server image.
65+
- **Remaining for the release candidate:**
66+
- the 2.11.3 reference move;
67+
- the scope question with the operator: prepared local activities,
68+
workflow-bound Nexus handlers and external payload takeover;
69+
- publishing the native image (General runs the release);
70+
- differential and performance qualification of the published image.
10371

10472
## API surface still missing
10573

@@ -412,14 +380,14 @@ contract fixture before its correction.
412380
workflow task guard does not check registration freshness yet, and the
413381
local activity and cancellation scope checkpoints still use the older fence.
414382
Add a fixture for each before changing them.
415-
- **Retention of runs with external payloads or message streams.**
416-
Published PHP deletes a pruned run's external payload objects and releases
417-
its message stream items. It also compacts its instance's consumed inbound
418-
stream items, and removes the instance's streams with its last run. Native
419-
does not clean any of these up yet. It skips such a run, including any run
420-
whose instance has inbound streams, with
421-
`external_payload_cleanup_unavailable` or
422-
`message_stream_cleanup_unavailable`, and keeps its details.
383+
- **Retention of runs with external payloads.** Published PHP deletes a
384+
pruned run's external payload objects. Native does not delete them yet: it
385+
skips such a run with `external_payload_cleanup_unavailable` and keeps its
386+
details. A run whose instance has inbound message streams is pruned as PHP's
387+
`MessageStreamRetentionCleanup` prunes it (October 27): the messages it
388+
consumed lose their payloads once a retained later run's cursor checkpoint
389+
covers them, and the instance's last retained run, once terminal, takes the
390+
instance's streams with it.
423391
- **Deeply nested JSON bodies.** Published PHP decodes JSON up to 512 levels
424392
deep. Native checks every body as PHP does before any route, but its
425393
handlers parse with serde_json's 128-level limit. A body nested 129 to 512

‎docs/rust-server-upgrade-takeover.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,20 @@ inspection is `unqualified` and takeover refuses with
7777
`upgrade_source_external_payload_storage_unsupported` before any change. A
7878
disabled policy does not block takeover.
7979

80+
Inbound message streams and durable workflow streams carry over unchanged:
81+
native serves them from PHP's own tables. Each closed or errored durable
82+
stream gets the reclaim deadline native's retention pass reads,
83+
`dw_stream_reclaims`: PHP 2.11.3's recorded `reclaim_at`, or for earlier
84+
releases its `closed_at` plus its retention (an hour unless set), as 2.11.3's
85+
migration backfills it. Every inspection reports
86+
`stream_reference_inspection`: the number of stream items, durable streams
87+
and inbound messages holding an external payload reference. Native does not
88+
resolve those, so any such row makes the inspection `unqualified` and takeover
89+
refuses with `upgrade_source_external_payloads_unsupported`.
90+
Workflow-to-workflow messages (`workflow_messages`) are not delivered by
91+
native yet, so a source holding any refuses with
92+
`upgrade_source_workflow_messages_unsupported`.
93+
8094
PHP 2.11.3 (Core 2.14) schedules a service call's handler in the
8195
`workflow_service_call_dispatches` outbox and dispatches it in a later pass.
8296
Native runs the handler when it accepts the call, so it cannot resume a

‎rust/src/config_audit.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,10 @@ pub fn audit(environment: &BTreeMap<String, String>) -> Report {
502502

503503
/// Names native reads that PHP's contract does not list.
504504
fn native_only(name: &str) -> bool {
505-
matches!(name, "DW_BIND_ADDRESS" | "DW_ENV" | "DW_RUST_EXPERIMENTAL" | "DW_UPGRADE_BACKUP_FINGERPRINT")
505+
matches!(
506+
name,
507+
"DW_BIND_ADDRESS" | "DW_ENV" | "DW_RUST_EXPERIMENTAL" | "DW_RUST_MAINTENANCE_LOOP" | "DW_UPGRADE_BACKUP_FINGERPRINT"
508+
)
506509
|| ["DW_RUST_UPGRADE_QUEUE_", "DW_SCHEMA_TEST_", "DW_TAKEOVER_TEST_", "DW_TEST_"]
507510
.iter()
508511
.any(|prefix| name.starts_with(prefix))
@@ -820,6 +823,7 @@ mod tests {
820823
("DW_WORKFLOW_MEMO_MIGRATION_RECOVERY", ""),
821824
("DW_ENV_AUDIT_STRICT", "1"),
822825
("DW_RUST_EXPERIMENTAL", "1"),
826+
("DW_RUST_MAINTENANCE_LOOP", "1"),
823827
("DW_BIND_ADDRESS", "0.0.0.0:8080"),
824828
];
825829
let report = audit(&environment(&shipped));

0 commit comments

Comments
 (0)