Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 39 additions & 29 deletions .github/workflows/server-parity.yml

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions docs/rust-server-port-checkpoints/2026-10-27-reference-2114.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# October 27, the parity reference moves to published 2.11.4

- **Before.** The Shared Server fixtures compared native and embedded
against published Server 2.11.1 (Workflow 2.10.2).
- **Reference.** Published Server 2.11.4, Core 2.14.2, as General and
Security verified it:
- source `ad3ac5fda0a30a58084b2d68569f2d91e23fabf3`;
- linux/amd64 `sha256:bee30cde5b70a429561a49f25daf4ef71a1e405d43322f78ff1b8137029c80f3`;
- linux/arm64 `sha256:cc297aa7d1f81b8bfa1169de6980dabe58d9776dcf513f379dcba2a51b1f5cc7`.

`tests/Fixtures/ServerParityBaselines/php-2.11.4.json` records it. The SDK,
CLI, Waterline and sample-app pins are unchanged. The takeover cohorts are
not affected: each names its own source release and layout (`php2111`
still takes over published 2.11.1 as `php-2.11.1`). 2.11.4 has no schema
change from 2.11.3. Its API route table is the same as 2.11.1's, so the
route inventory still holds.
- **Expectation changes**, all Core 2.14 behavior:
- `repair-pass`: the report adds the dispatch-outbox counts
(`service_call_dispatch_candidates`, `service_call_dispatches_enqueued`,
`service_call_dispatch_failures`).
- `service-calls`:
- an execute request's `wait_timeout_seconds` is bounded at 60;
- handler metadata names `service_call_idempotency_key` (null for a call
dispatched when it is accepted).
- **Native.**
- The repair report adds the three counts as zeros: native runs a call's
handler when it accepts the call, so it has no outbox.
- The execute bound is 60.
- Handler metadata carries the null key.
- Task-queue views report published 2.11.3's `poll_transport` as
`unavailable`, since native records no early poll disconnects. Every poll
returns the ordinary JSON response, declining the opt-in progress
transport; SDKs must accept both.
- **Boundary: Workflow #931.** `service-calls` no longer repeats an
idempotency key from another caller namespace.
- Published 2.11.1 created a separate call for that caller.
- 2.11.3 and 2.11.4 replay the first call and rebind its handler identity.
- The prepared correction refuses the foreign caller.

Recorded against 2.11.4, that step and the three that follow from it were
the only differences in the whole selection. The step returns, with the
corrected answer for native too, once a release carries the correction.
Until then native keeps 2.11.1's separate call.
- **Checked.**
- CI's fixture selection (the corpus without `update-validation`) recorded
on SQLite against published 2.11.4. Before the boundary, only
`service-calls` differed.
- After it, `repair-pass`, `service-calls` and `storage-test` pass against
both 2.11.4 and native from this branch, and the two records compare
equal.
- The service-calls contract test passes 22/22. CI runs the full selection
on all four databases.
30 changes: 16 additions & 14 deletions docs/rust-server-port.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,40 +30,42 @@ Updated October 27, 2026.
maintenance repair loop (opt-in, `DW_RUST_MAINTENANCE_LOOP=1`); takeover
keeps runtime credentials and update validation tasks; takeover of
published 2.11.3, fresh and upgraded from 2.9.x.
- **In the queue or next, in order.**
1. [#799](https://github.com/durable-workflow/server/pull/799): the fixture
adapters' Cargo cache, saved only by pushes to main.
2. Message-stream retention (PHP's `MessageStreamRetentionCleanup`), and
takeover that keeps message and workflow streams.
3. The native release-candidate image (`Dockerfile.native`), its release
workflow and the takeover runbook's image commands.
4. The parity reference moves to published 2.11.3. Native already matches it
on 62 of 65 fixtures. The move waits on two published 2.11.3 defects
General owns: the storage diagnostic (#789) and cross-caller idempotent
replay (Workflow #931).
- [#799](https://github.com/durable-workflow/server/pull/799): the fixture
adapters' Cargo cache, saved only by pushes to main.
- [#801](https://github.com/durable-workflow/server/pull/801):
message-stream retention (PHP's `MessageStreamRetentionCleanup`), and
takeover that keeps message and workflow streams.
- **In the queue or next.**
- [#803](https://github.com/durable-workflow/server/pull/803): the native
release-candidate image (`Dockerfile.native`), its release workflow and
the takeover runbook's image commands.
- The parity reference moves to published 2.11.4
([checkpoint](rust-server-port-checkpoints/2026-10-27-reference-2114.md)).
- **Still refused at takeover:** workflow-to-workflow messages
(`workflow_messages`, written only by embedded PHP workflows) and external
payload references. PHP keeps references in about 30 table and column
locations in their URI form, so taking them over is its own project; the
recommendation is that the release candidate keeps refusing them.
- **Shared Server fixtures.** Native, embedded and published PHP 2.11.1 are
- **Shared Server fixtures.** Native, embedded and published PHP 2.11.4 are
compared on SQLite, MySQL 8.0, MariaDB 10.11 and PostgreSQL 17. Where the
published reference itself misbehaves, an explicit boundary separates it
from native, and the PHP owner has an issue for each case. Recent ones:
- Workflow #931, an idempotency key repeated from another caller namespace
(left out of `service-calls` until a release carries the correction);
- #796, `service-calls` under full-corpus SQLite load;
- #794, workflow-bound Nexus handlers.

No expectation is edited to make native pass.
- **Parity surface.** Native answers 174 of the 175 API route methods in
published 2.11.1's route table. Eight of those are the prepared
published 2.11.1's route table, which 2.11.4 leaves unchanged. Eight of those are the prepared
local-activity routes, which it answers with PHP's refusal for a bridge
without the prepared role. Nexus workflow-bound handler kinds answer 501
until #794 settles their contract. An execution test sends each route
through the native router, and the ones still answering
`501 rust_capability_not_implemented` must match
[`unserved-php-routes.v2.11.1.txt`](../rust/tests/fixtures/unserved-php-routes.v2.11.1.txt).
- **Remaining for the release candidate:**
- the 2.11.3 reference move;
- landing the 2.11.4 reference move;
- the scope question with the operator: prepared local activities,
workflow-bound Nexus handlers and external payload takeover;
- publishing the native image (General runs the release);
Expand Down
5 changes: 5 additions & 0 deletions rust/src/runtime/repair_pass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ fn empty_report(request: &PassRequest, throttled: bool) -> Map<String, Value> {
"deadline_expired_failures": [], "activity_timeout_candidates": 0, "activity_timeouts_enforced": 0,
"activity_timeout_failures": [], "service_call_timeout_candidates": 0, "service_call_timeouts_enforced": 0,
"service_call_timeout_failures": [],
// Published 2.11.3 (Core 2.14) also reports its service-call dispatch
// outbox. Native runs a call's handler when it accepts the call, so it
// never has a dispatch to enqueue.
"service_call_dispatch_candidates": 0, "service_call_dispatches_enqueued": 0,
"service_call_dispatch_failures": [],
});
match value {
Value::Object(map) => map,
Expand Down
10 changes: 7 additions & 3 deletions rust/src/runtime/service_calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ const EXECUTE_RULES: &laravel_rules::Rules = &[
("payload_codec", &[Rule::Nullable, Rule::String, Rule::Max(191)]),
("mode_override", &[Rule::Nullable, Rule::String, Rule::In(MODES)]),
("wait_for", &[Rule::Nullable, Rule::String, Rule::In(&["accepted", "completed"])]),
("wait_timeout_seconds", &[Rule::Nullable, Rule::Integer, Rule::Min(0), Rule::Max(86400)]),
// Published 2.11.3 bounds an update-bound wait at 60 seconds (#775).
("wait_timeout_seconds", &[Rule::Nullable, Rule::Integer, Rule::Min(0), Rule::Max(60)]),
("idempotency_key", &[Rule::Nullable, Rule::String, Rule::Max(191)]),
("caller_namespace", &[Rule::Nullable, Rule::String, Rule::Max(255)]),
("caller_workflow_instance_id", &[Rule::Nullable, Rule::String, Rule::Max(191)]),
Expand Down Expand Up @@ -1732,7 +1733,10 @@ store_methods! {
return Self::handler_failure(connection, call, "handler_target_missing", "Activity binding has no activity class or type.", attempt, max).await;
};
let id = binding_string(&binding, &["activity_execution_id"]).unwrap_or_else(upper_ulid);
let mut metadata = json!({"activity_execution_id": id, "activity_class": class, "activity_type": kind});
// Core 2.14 names the dispatch attempt's idempotency key; a call
// dispatched when it is accepted has none.
let mut metadata = json!({"activity_execution_id": id, "activity_class": class, "activity_type": kind,
"service_call_idempotency_key": null});
for (key, value) in [("connection", &execution.connection), ("queue", &execution.queue)] {
if let Some(value) = binding_string(&binding, &[key]).or_else(|| value.clone()) {
metadata[key] = json!(value);
Expand All @@ -1755,7 +1759,7 @@ store_methods! {
let handler = binding_string(&binding, &["carrier_handler", "handler", "handler_name"]);
let carrier = binding_string(&binding, &["carrier", "carrier_name"]);
let id = binding_string(&binding, &["carrier_request_id"]).unwrap_or_else(upper_ulid);
let mut metadata = json!({"carrier_request_id": id, "carrier_endpoint": endpoint});
let mut metadata = json!({"carrier_request_id": id, "carrier_endpoint": endpoint, "service_call_idempotency_key": null});
if let Some(handler) = &handler {
metadata["carrier_handler"] = json!(handler);
}
Expand Down
12 changes: 10 additions & 2 deletions rust/src/runtime/task_queues.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,14 @@ where
"current_leases":self.current_leases(queue, at).await?,"repair":self.repair_stats(queue, at).await?}))
}

/// `WorkerPollDiagnostics::snapshot` as published 2.11.3 reports it when
/// its observation store is unavailable. Native keeps no record of worker
/// polls that ended early, so it never claims `no_recent_observation`.
/// PHP's retention for an observation is 15 minutes.
fn poll_transport() -> Value {
json!({"status":"unavailable","retention_seconds":900,"last_disconnect":null})
}

/// One queue's detail with recent flow, activity attempts and admission.
async fn queue_view(&self, queue: &str, config: &QueryTaskConfig, detail: bool) -> Result<Value> {
let at = now();
Expand All @@ -395,9 +403,9 @@ where
});
Ok(if detail {
json!({"name":queue,"pollers":pollers,"stats":stats,"current_leases":base["current_leases"],
"repair":repair,"admission":admission})
"repair":repair,"admission":admission,"poll_transport":Self::poll_transport()})
} else {
json!({"name":queue,"stats":stats,"repair":repair,"admission":admission})
json!({"name":queue,"stats":stats,"repair":repair,"admission":admission,"poll_transport":Self::poll_transport()})
})
}

Expand Down
14 changes: 9 additions & 5 deletions rust/tests/execution/command_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ async fn update_wait_timeout_is_bounded_before_admission() {
}

// Server #775: a service operation bound to a workflow update hands its wait
// to the update admission. Native dispatches no workflow-bound handler yet, so
// neither the execute route nor the workflow-task command can carry a longer
// wait here.
// to the update admission. Published 2.11.3 bounds an execute request's wait
// at 60 seconds before routing, so a longer one is a validation failure.
// Native dispatches no workflow-bound handler yet, so a wait within the bound
// reaches the unserved handler kind, and the workflow-task command is refused.
#[tokio::test]
async fn service_operations_cannot_carry_an_update_wait_past_the_ceiling() {
let database = TestDatabase::new().await;
Expand All @@ -103,7 +104,10 @@ async fn service_operations_cannot_carry_an_update_wait_past_the_ceiling() {
let (status, created) = request(&app, "POST", path, body).await;
assert_eq!(status, StatusCode::CREATED, "{created}");
}
let (status, unserved) = request(&app, "POST", "/api/service-endpoints/billing/services/approvals/operations/approve/execute",
json!({"wait_timeout_seconds":61})).await;
let execute = "/api/service-endpoints/billing/services/approvals/operations/approve/execute";
let (status, refused) = request(&app, "POST", execute, json!({"wait_timeout_seconds":61})).await;
assert_eq!((status, refused["reason"].clone()), (StatusCode::UNPROCESSABLE_ENTITY, json!("validation_failed")), "{refused}");
assert_eq!(refused["errors"]["wait_timeout_seconds"][0], "The wait timeout seconds field must not be greater than 60.", "{refused}");
let (status, unserved) = request(&app, "POST", execute, json!({"wait_timeout_seconds":60})).await;
assert_eq!((status, unserved["reason"].clone()), (StatusCode::NOT_IMPLEMENTED, json!("rust_capability_not_implemented")), "{unserved}");
}
2 changes: 1 addition & 1 deletion rust/tests/execution/service_calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ async fn execute_requests_are_checked_like_the_controller() {
(json!({"arguments": {"codec": "avro", "blob": 5}}), "arguments.blob", "The payload envelope blob must be a string."),
(json!({"search_attributes": {"1bad": "x"}}), "search_attributes", "Search attribute key [1bad] must start with a letter"),
(json!({"mode_override": "fast"}), "mode_override", "The selected mode override is invalid."),
(json!({"wait_timeout_seconds": 86401}), "wait_timeout_seconds", "The wait timeout seconds field must not be greater than 86400."),
(json!({"wait_timeout_seconds": 61}), "wait_timeout_seconds", "The wait timeout seconds field must not be greater than 60."),
] {
let (status, refused) = execute(&app, "charge", body.clone()).await;
assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY, "{body}: {refused}");
Expand Down
5 changes: 5 additions & 0 deletions rust/tests/execution/waterline_reads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ async fn task_queue_views_follow_registrations_and_tasks() {
assert_eq!(status, StatusCode::OK, "{queue}");
assert_eq!(queue["pollers"][0]["worker_id"], "queue-worker", "{queue}");
assert_eq!(queue["admission"]["query_tasks"]["status"], "accepting", "{queue}");
// Published 2.11.3's poll diagnostics, reported as unobserved: native keeps
// no record of worker polls that ended early.
let unobserved = json!({"status":"unavailable","retention_seconds":900,"last_disconnect":null});
assert_eq!(queue["poll_transport"], unobserved, "{queue}");
assert_eq!(queues[0]["poll_transport"], unobserved, "{list}");
let (status, other) = get(&app, "queues", "/api/task-queues").await;
assert_eq!(status, StatusCode::OK, "{other}");
assert_eq!(other["task_queues"], json!([]), "{other}");
Expand Down
4 changes: 2 additions & 2 deletions scripts/conformance/server-parity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Requires Node 20+ and PHP 8.3+ with the locked adapter installed. Each target
must have its own isolated, already bootstrapped database. Set DW_PARITY_TOKEN
for the HTTP target; it is never recorded. Use the same --prefix on all targets
and a fresh database or prefix for a new run. --artifacts selects the exact
consumer tuple; the default is tests/Fixtures/ServerParityBaselines/php-2.11.1.json.
consumer tuple; the default is tests/Fixtures/ServerParityBaselines/php-2.11.4.json.
Repeat --fixture PATH to record or compare an explicitly selected candidate
set. --database-resource-limits accepts inspected memory_bytes and
memory_swap_bytes as a JSON object; comparisons require equal effective
Expand Down Expand Up @@ -81,7 +81,7 @@ try {
}
const record = {
schema: 'durable-workflow.server-parity-record/v1', target: values.target, mode: values.mode,
runner_revision: values['runner-revision'], artifacts: json(values.artifacts ?? resolve(root, 'tests/Fixtures/ServerParityBaselines/php-2.11.1.json')),
runner_revision: values['runner-revision'], artifacts: json(values.artifacts ?? resolve(root, 'tests/Fixtures/ServerParityBaselines/php-2.11.4.json')),
started_at: new Date().toISOString(), outcome: 'runner-blocked', fixture_hashes: hashes, cases: [],
database_resource_limits: parseDatabaseResourceLimits(values['database-resource-limits']),
};
Expand Down
2 changes: 1 addition & 1 deletion scripts/conformance/server-parity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ Docker kill and verifies exit code 137 before restarting the native process.

For an adapter installed elsewhere, set `DW_PARITY_SDK_AUTOLOAD` to its locked
`vendor/autoload.php`. `--artifacts` selects a frozen tuple manifest. The default
is `tests/Fixtures/ServerParityBaselines/php-2.11.1.json`; its SDK version must match
is `tests/Fixtures/ServerParityBaselines/php-2.11.4.json`; its SDK version must match
the installed lock. A frozen tuple does not prove every listed consumer ran.
The original `tests/Fixtures/ServerParity/php-baseline.json` remains an immutable
2.5.13 / Workflow 2.5.4 reference, together with its known NUL reproducer outside
Expand Down
2 changes: 1 addition & 1 deletion scripts/conformance/server-parity/role-profile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ test -n "$RESOURCE_SCOPE"
scope_identity="$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$GITHUB_JOB"
case "$RESOURCE_SCOPE" in *"$scope_identity"*) ;; *) exit 2 ;; esac
backend="$1"
image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json)
image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json)
rust_image=mirror.gcr.io/library/rust@sha256:ba81bc3eaa4422af576c0262515d96b0111a628a6ccc2c86557cf55c9a4bbee0
fixture=tests/Fixtures/ServerParityProfiles/role-tokens/admission-role-tokens.json
credentials=(-e DW_AUTH_TOKEN=parity-role-legacy -e DW_WORKER_TOKEN=parity-role-worker
Expand Down
Loading
Loading