Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
d2a5d27
Server parity: Waterline v1 projection import fixture, recorded from …
rmcdaniel Oct 25, 2026
7f3793d
Rust #325: project Waterline v1 workflows like Server 2.11.1
rmcdaniel Oct 25, 2026
385da0c
Merge branch 'rust-325-workflow-import' into rust-325-waterline-import
rmcdaniel Oct 25, 2026
2530ed1
Merge branch 'rust-325-workflow-import' into rust-325-waterline-import
rmcdaniel Oct 25, 2026
2a112b2
Merge branch 'rust-325-workflow-import' into rust-325-waterline-import
rmcdaniel Oct 25, 2026
50d6555
Merge branch 'rust-325-workflow-import' into rust-325-waterline-import
rmcdaniel Oct 25, 2026
880cb1d
Merge branch 'rust-325-workflow-import' (main through #740) into rust…
rmcdaniel Oct 25, 2026
71c4a61
Rust #325: no run operation routes remain missing
rmcdaniel Oct 25, 2026
fcf20b5
Merge branch 'rust-325-workflow-import' (main through #748) into rust…
rmcdaniel Oct 25, 2026
93215b0
Name the workflow import defects' issue
rmcdaniel Oct 25, 2026
a197d6e
Merge remote-tracking branch 'origin/main' (#764) into rust-325-water…
rmcdaniel Oct 25, 2026
6d8eec0
Rust #325: keep Waterline v1 projections read-only and bounded
rmcdaniel Oct 25, 2026
6676720
Rust #325: prove appends racing a projection import never write
rmcdaniel Oct 25, 2026
f9fdc66
Rust #325: build Waterline v1 events within the byte limit
rmcdaniel Oct 25, 2026
3e62112
Rust #325: observe the projection install waiting on the transition lock
rmcdaniel Oct 26, 2026
6838f47
Rust #325: observe the SQLite install reaching the transition lock
rmcdaniel Oct 26, 2026
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
3 changes: 2 additions & 1 deletion .github/workflows/server-parity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ on:
- tests/Unit/ServerParityActivityCancellationAckContractTest.mjs
- tests/Unit/ServerParityServiceCatalogContractTest.mjs
- tests/Unit/ServerParityWorkflowImportContractTest.mjs
- tests/Unit/ServerParityWaterlineImportContractTest.mjs
- tests/Unit/ServerParitySystemMetricsContractTest.mjs
- tests/Unit/ServerParityOperatorDashboardTypesContractTest.mjs
- tests/Unit/ServerParityPrometheusMetricsContractTest.mjs
Expand Down Expand Up @@ -1801,7 +1802,7 @@ jobs:
bash scripts/ci/run-composer.sh --rm -v "$PWD:/source" -w /source -- \
install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress
docker run --rm --user=1000:1000 --entrypoint node -v "$PWD:/source:ro" -w /source \
"$image" --test tests/Unit/ServerParityRunnerTest.mjs tests/Unit/ServerParitySdkDirectionLanesTest.mjs tests/Unit/ServerParityCodecDeadlineCellsTest.mjs tests/Unit/ServerParityCooperativeContractTest.mjs tests/Unit/ServerParityChildCancellationContractTest.mjs tests/Unit/ServerParityScheduleContractTest.mjs tests/Unit/ServerParityVisibilityContractTest.mjs tests/Unit/ServerParityAdmissionContractTest.mjs tests/Unit/ServerParityRoleTokenContractTest.mjs tests/Unit/ServerParityWorkerDeregistrationContractTest.mjs tests/Unit/ServerParitySearchAttributeContractTest.mjs tests/Unit/ServerParityWorkerListingContractTest.mjs tests/Unit/ServerParityWorkerReplacementContractTest.mjs tests/Unit/ServerParityPatchDeploymentContractTest.mjs tests/Unit/ServerParityWaitingHistoryContractTest.mjs tests/Unit/ServerParityTaskRetryContractTest.mjs tests/Unit/ServerParityPublishedPatchContractTest.mjs tests/Unit/ServerParityPublishedPatchTransportTest.mjs tests/Unit/ServerParityRecordFileTest.mjs tests/Unit/ServerParityFailureLocationTest.mjs tests/Unit/ServerParityFailureExcerptTest.mjs tests/Unit/ServerParityLateWorkflowCompletionContractTest.mjs tests/Unit/ServerParityStorageTestContractTest.mjs tests/Unit/ServerParitySchedulePoliciesContractTest.mjs tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs tests/Unit/ServerParityScheduleCatalogContractTest.mjs tests/Unit/ServerParityOperatorMetricsContractTest.mjs tests/Unit/ServerParityActivityLeaseRenewalContractTest.mjs tests/Unit/ServerParityActivityCancellationAckContractTest.mjs tests/Unit/ServerParityServiceCatalogContractTest.mjs tests/Unit/ServerParityWorkflowImportContractTest.mjs tests/Unit/ServerParitySystemMetricsContractTest.mjs tests/Unit/ServerParityOperatorDashboardTypesContractTest.mjs tests/Unit/ServerParityPrometheusMetricsContractTest.mjs tests/Unit/ServerParityPayloadCleanupContractTest.mjs tests/Unit/ServerParityRepairPassContractTest.mjs tests/Unit/ServerParityClusterInfoContractTest.mjs tests/Unit/ServerParityNamespaceDeleteContractTest.mjs tests/Unit/ServerParityMessageStreamsContractTest.mjs tests/Unit/ServerParityWorkflowPollRequestContractTest.mjs tests/Unit/ServerParityUpdateValidationContractTest.mjs tests/Unit/ServerParityFixtureSelectionTest.mjs tests/Unit/ServerParityUpdateValidationRetryTest.mjs tests/Unit/ServerParityKilledAuthorPollTest.mjs tests/Unit/ServerParityImagePullTest.mjs tests/Unit/ServerParityWorkerSessionsContractTest.mjs tests/Unit/ServerParityWorkerSessionActivitiesContractTest.mjs > parity-evidence/comparator-tests.txt
"$image" --test tests/Unit/ServerParityRunnerTest.mjs tests/Unit/ServerParitySdkDirectionLanesTest.mjs tests/Unit/ServerParityCodecDeadlineCellsTest.mjs tests/Unit/ServerParityCooperativeContractTest.mjs tests/Unit/ServerParityChildCancellationContractTest.mjs tests/Unit/ServerParityScheduleContractTest.mjs tests/Unit/ServerParityVisibilityContractTest.mjs tests/Unit/ServerParityAdmissionContractTest.mjs tests/Unit/ServerParityRoleTokenContractTest.mjs tests/Unit/ServerParityWorkerDeregistrationContractTest.mjs tests/Unit/ServerParitySearchAttributeContractTest.mjs tests/Unit/ServerParityWorkerListingContractTest.mjs tests/Unit/ServerParityWorkerReplacementContractTest.mjs tests/Unit/ServerParityPatchDeploymentContractTest.mjs tests/Unit/ServerParityWaitingHistoryContractTest.mjs tests/Unit/ServerParityTaskRetryContractTest.mjs tests/Unit/ServerParityPublishedPatchContractTest.mjs tests/Unit/ServerParityPublishedPatchTransportTest.mjs tests/Unit/ServerParityRecordFileTest.mjs tests/Unit/ServerParityFailureLocationTest.mjs tests/Unit/ServerParityFailureExcerptTest.mjs tests/Unit/ServerParityLateWorkflowCompletionContractTest.mjs tests/Unit/ServerParityStorageTestContractTest.mjs tests/Unit/ServerParitySchedulePoliciesContractTest.mjs tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs tests/Unit/ServerParityScheduleCatalogContractTest.mjs tests/Unit/ServerParityOperatorMetricsContractTest.mjs tests/Unit/ServerParityActivityLeaseRenewalContractTest.mjs tests/Unit/ServerParityActivityCancellationAckContractTest.mjs tests/Unit/ServerParityServiceCatalogContractTest.mjs tests/Unit/ServerParityWorkflowImportContractTest.mjs tests/Unit/ServerParityWaterlineImportContractTest.mjs tests/Unit/ServerParitySystemMetricsContractTest.mjs tests/Unit/ServerParityOperatorDashboardTypesContractTest.mjs tests/Unit/ServerParityPrometheusMetricsContractTest.mjs tests/Unit/ServerParityPayloadCleanupContractTest.mjs tests/Unit/ServerParityRepairPassContractTest.mjs tests/Unit/ServerParityClusterInfoContractTest.mjs tests/Unit/ServerParityNamespaceDeleteContractTest.mjs tests/Unit/ServerParityMessageStreamsContractTest.mjs tests/Unit/ServerParityWorkflowPollRequestContractTest.mjs tests/Unit/ServerParityUpdateValidationContractTest.mjs tests/Unit/ServerParityFixtureSelectionTest.mjs tests/Unit/ServerParityUpdateValidationRetryTest.mjs tests/Unit/ServerParityKilledAuthorPollTest.mjs tests/Unit/ServerParityImagePullTest.mjs tests/Unit/ServerParityWorkerSessionsContractTest.mjs tests/Unit/ServerParityWorkerSessionActivitiesContractTest.mjs > parity-evidence/comparator-tests.txt
docker run --rm --user=1000:1000 --cpus=1 --memory=256m --memory-swap=256m \
--network=none --entrypoint node -v "$PWD:/source:ro" -w /source \
"$image" --test tests/Unit/ServerParityCodecDeadlineLoadTest.mjs > parity-evidence/codec-load-tests.txt
Expand Down
45 changes: 31 additions & 14 deletions docs/rust-server-port.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,17 @@ Updated October 24, 2026.
([#737](https://github.com/durable-workflow/server/issues/737)).

No expectation is edited to make native pass.
- **Parity surface.** Native serves 139 of the 175 API route methods in
- **Parity surface.** Native serves 140 of the 175 API route methods in
published 2.11.1's route table, with message streams, update validation,
activity cancellation acknowledgement, worker sessions and embedded v2
workflow import. An execution test sends each one through the native
router, and the ones still answering `501 rust_capability_not_implemented`
must match
activity cancellation acknowledgement, worker sessions, and embedded v2
and Waterline v1 workflow import. An execution test sends each one 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).
Unlike published PHP, native's embedded v2 import refuses a bundle whose
rows reference rows outside it, links only runs of one namespace, and reads
the signing key only from the request body
([#765](https://github.com/durable-workflow/server/issues/765)).
- **Next action.** Land the items above in order, keep porting the unserved
routes fixture first, and package the native binary into the server image.

Expand Down Expand Up @@ -149,12 +153,6 @@ any other unknown path or method answers PHP's own 404 or 405
`update-validation` fixture. Native does not replay a validation task by
`poll_request_id` or bind a request id to its task kinds; see
`docs/rust-server-port-checkpoints/2026-10-23-update-validation.md`.)
- **Run operations.**
- Waterline v1 import. (Embedded v2 import is served:
`POST /api/workflows/import/embedded-v2`. Unlike published PHP, native
refuses a bundle whose rows reference rows outside it, links only runs of
one namespace, and reads the signing key only from the request body
([#765](https://github.com/durable-workflow/server/issues/765)).)
- **Other families.**
- Message streams are served
([checkpoint](rust-server-port-checkpoints/2026-10-23-message-streams.md)),
Expand Down Expand Up @@ -234,9 +232,28 @@ contract fixture before its correction.
batch size, and still caps parallel groups at 100 members and a run at 64
updates over its lifetime. #612 tracks the rest
([checkpoint](rust-server-port-checkpoints/2026-10-23-structural-limits.md)).
- **Environment settings.** Of PHP's 166 contract settings, native honors
43 and ignores 57 that change nothing it serves. It refuses to start on a
non-default value of the other 66
- **Waterline v1 projections, ahead of published PHP.**
- Native refuses a message stream append to a projection with
`409 v1_projection_read_only`, at the route before the body is read and
again under the store's lock. Published 2.11.1 accepts it and creates a
signal and a workflow task
([#769](https://github.com/durable-workflow/server/issues/769)).
- Native refuses a projection that would write more history events or
bytes than one transaction may (`DW_V2_LIMIT_HISTORY_TRANSACTION_SIZE`,
`_BYTES`) with `409 v1_projection_too_large`, before reading or writing
any row. The count is checked from the source's row counts before any
event is built; then each event is built and measured in turn, and the
first event past the byte limit stops the build. Published 2.11.1 has no limit
([#770](https://github.com/durable-workflow/server/issues/770)).
- Operator metrics, the health snapshot and the backend capabilities
publish the active `history_transaction_size` and
`history_transaction_bytes` (0 when disabled), as Workflow #934's
`StructuralLimits::snapshot()` does.
- **Environment settings.** Of PHP's 164 contract settings, native honors
49 and ignores 51 that change nothing it serves. It also honors
`DW_V2_LIMIT_HISTORY_TRANSACTION_BYTES`, which the Workflow package reads
and PHP's contract does not list yet. It refuses to start on a
non-default value of the other 64
([checkpoint](rust-server-port-checkpoints/2026-10-22-environment-contract.md)).
Several of those are cheap to honor: repair timing and the multi-node
declaration.
Expand Down
41 changes: 38 additions & 3 deletions rust/src/config_audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ pub fn audit(environment: &BTreeMap<String, String>) -> Report {
.keys()
.map(String::as_str)
.filter(|name| name.starts_with("DW_"))
.filter(|name| !SETTINGS.iter().any(|setting| setting.name == *name))
.filter(|name| !SETTINGS.iter().chain(NATIVE_HONORED).any(|setting| setting.name == *name))
.filter(|name| !native_only(name))
.collect();
if !unknown.is_empty() {
Expand All @@ -484,7 +484,7 @@ pub fn audit(environment: &BTreeMap<String, String>) -> Report {
));
}
for name in environment.keys() {
if let Some(setting) = SETTINGS.iter().find(|setting| setting.legacy == Some(name.as_str())) {
if let Some(setting) = SETTINGS.iter().chain(NATIVE_HONORED).find(|setting| setting.legacy == Some(name.as_str())) {
report.warnings.push(format!(
"Legacy env var {name} is still honored but deprecated — rename to {}.",
setting.name
Expand Down Expand Up @@ -542,6 +542,14 @@ const UNLISTED: &[Setting] = &[
Setting { name: "DW_V2_LEGACY_SCHEDULE_STORAGE_TIMEZONE", legacy: Some("WORKFLOW_V2_LEGACY_SCHEDULE_STORAGE_TIMEZONE"), disposition: Refused(Empty, LEGACY_SCHEDULE_TIMEZONE) },
];

/// Settings native honors that PHP's contract does not list yet: the
/// Workflow package reads them (Workflow #930), and PHP's contract gains them
/// when the PHP Server takes that release. They are not unknown names, and
/// their legacy names warn as the contract's do.
pub const NATIVE_HONORED: &[Setting] = &[
Setting { name: "DW_V2_LIMIT_HISTORY_TRANSACTION_BYTES", legacy: Some("WORKFLOW_V2_LIMIT_HISTORY_TRANSACTION_BYTES"), disposition: Honored },
];

/// Every `vars` entry of `config/dw-contract.php`, with its contract legacy name.
pub const SETTINGS: &[Setting] = &[
Setting { name: "DW_ACTIVITY_TASK_MAX_ACTIVE_LEASES_PER_NAMESPACE", legacy: Some("WORKFLOW_SERVER_ACTIVITY_TASK_MAX_ACTIVE_LEASES_PER_NAMESPACE"), disposition: Refused(NoPositiveInt, TASK_ADMISSION) },
Expand Down Expand Up @@ -664,7 +672,7 @@ pub const SETTINGS: &[Setting] = &[
Setting { name: "DW_V2_HISTORY_EXPORT_SIGNING_KEY", legacy: Some("WORKFLOW_V2_HISTORY_EXPORT_SIGNING_KEY"), disposition: Honored },
Setting { name: "DW_V2_HISTORY_EXPORT_SIGNING_KEY_ID", legacy: Some("WORKFLOW_V2_HISTORY_EXPORT_SIGNING_KEY_ID"), disposition: Honored },
Setting { name: "DW_V2_LIMIT_COMMAND_BATCH_SIZE", legacy: Some("WORKFLOW_V2_LIMIT_COMMAND_BATCH_SIZE"), disposition: Refused(Int(1000), STRUCTURAL) },
Setting { name: "DW_V2_LIMIT_HISTORY_TRANSACTION_SIZE", legacy: Some("WORKFLOW_V2_LIMIT_HISTORY_TRANSACTION_SIZE"), disposition: Ignored },
Setting { name: "DW_V2_LIMIT_HISTORY_TRANSACTION_SIZE", legacy: Some("WORKFLOW_V2_LIMIT_HISTORY_TRANSACTION_SIZE"), disposition: Honored },
Setting { name: "DW_V2_LIMIT_MEMO_SIZE_BYTES", legacy: Some("WORKFLOW_V2_LIMIT_MEMO_SIZE_BYTES"), disposition: Honored },
Setting { name: "DW_V2_LIMIT_PAYLOAD_SIZE_BYTES", legacy: Some("WORKFLOW_V2_LIMIT_PAYLOAD_SIZE_BYTES"), disposition: Honored },
Setting { name: "DW_V2_LIMIT_PENDING_ACTIVITIES", legacy: Some("WORKFLOW_V2_LIMIT_PENDING_ACTIVITIES"), disposition: Refused(Int(2000), STRUCTURAL) },
Expand Down Expand Up @@ -1074,6 +1082,33 @@ mod tests {
assert!(report.refusal().is_some_and(|refusal| !refusal.contains(secret)));
}

#[test]
fn the_history_transaction_limits_are_honored_settings() {
let honored = |name: &str| SETTINGS.iter().chain(NATIVE_HONORED).find(|setting| setting.name == name)
.is_some_and(|setting| matches!(setting.disposition, Honored));
for name in ["DW_V2_LIMIT_HISTORY_TRANSACTION_SIZE", "DW_V2_LIMIT_HISTORY_TRANSACTION_BYTES"] {
assert!(honored(name), "{name}");
}
// NATIVE_HONORED names are outside PHP's contract and listed once.
for setting in NATIVE_HONORED {
assert!(!contract().iter().any(|(name, ..)| name == setting.name), "{} is now in the contract", setting.name);
assert!(!SETTINGS.iter().any(|listed| listed.name == setting.name));
}
// Strict mode starts with each primary name set.
let mut primary = environment(&[("DW_V2_LIMIT_HISTORY_TRANSACTION_SIZE", "123"), ("DW_V2_LIMIT_HISTORY_TRANSACTION_BYTES", "4567")]);
primary.insert("DW_ENV_AUDIT_STRICT".into(), "1".into());
let report = audit(&primary);
assert_eq!((report.warnings.as_slice(), report.refusal()), (&[] as &[String], None));
// A legacy name warns as deprecated, naming its primary, and nothing else.
let report = audit(&environment(&[("WORKFLOW_V2_LIMIT_HISTORY_TRANSACTION_SIZE", "123"),
("WORKFLOW_V2_LIMIT_HISTORY_TRANSACTION_BYTES", "4567")]));
assert_eq!(report.warnings, [
"Legacy env var WORKFLOW_V2_LIMIT_HISTORY_TRANSACTION_BYTES is still honored but deprecated — rename to DW_V2_LIMIT_HISTORY_TRANSACTION_BYTES.",
"Legacy env var WORKFLOW_V2_LIMIT_HISTORY_TRANSACTION_SIZE is still honored but deprecated — rename to DW_V2_LIMIT_HISTORY_TRANSACTION_SIZE.",
]);
assert_eq!(report.refusal(), None);
}

#[test]
fn unknown_and_legacy_names_warn_and_strict_mode_refuses_them() {
let values = [
Expand Down
11 changes: 8 additions & 3 deletions rust/src/runtime/continuations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ where
/// `formatRunListEntry` renders it. The current run is the highest
/// numbered one.
pub(super) async fn list_runs(&self, workflow: &str) -> Result<Value> {
let rows=Self::query("SELECT r.id,r.workflow_type,r.business_key,r.status,r.run_number,r.queue,r.compatibility,r.started_at,r.closed_at FROM workflow_runs r JOIN workflow_instances i ON i.id=r.workflow_instance_id WHERE i.id=$1 AND i.namespace=$2 AND r.namespace=$3 ORDER BY r.run_number LIMIT 1025")
let rows=Self::query("SELECT r.id,r.workflow_type,r.business_key,r.status,r.run_number,r.queue,r.compatibility,r.started_at,r.closed_at,r.import_source FROM workflow_runs r JOIN workflow_instances i ON i.id=r.workflow_instance_id WHERE i.id=$1 AND i.namespace=$2 AND r.namespace=$3 ORDER BY r.run_number LIMIT 1025")
.bind(workflow).bind(self.namespace.as_ref()).bind(self.namespace.as_ref()).fetch_all(&self.pool).await?;
if rows.is_empty() {
return Err(refuse(StatusCode::NOT_FOUND, "instance_not_found"));
Expand All @@ -67,15 +67,20 @@ where
let required = compatibility.as_deref().map(str::trim).filter(|value| !value.is_empty());
let (supported, reason) = super::run_diagnostics::fleet_supports(&fleet, required, queue.as_deref());
let memo = self.run_memo(&run).await?;
runs.push(json!({"run_id":run,"run_number":DB::number(&row,"run_number")?,
let mut entry = json!({"run_id":run,"run_number":DB::number(&row,"run_number")?,
"workflow_type":DB::string(&row,"workflow_type")?,"business_key":DB::optional_string(&row,"business_key")?,
"status":status,"status_bucket":super::visibility::status_bucket(&status)?,
"is_terminal":matches!(status.as_str(), "completed" | "failed" | "cancelled" | "terminated"),
"is_current_run":run == current,"task_queue":queue,"compatibility":compatibility,
"compatibility_status":if supported { "compatible" } else { "no_compatible_worker" },
"compatibility_supported_in_fleet":supported,"compatibility_fleet_reason":reason,
"started_at":super::task_queues::json_time(DB::optional_instant(&row,"started_at")?),
"closed_at":super::task_queues::json_time(DB::optional_instant(&row,"closed_at")?),"memo":memo}));
"closed_at":super::task_queues::json_time(DB::optional_instant(&row,"closed_at")?),"memo":memo});
// `formatRunListEntry` leaves a Waterline v1 projection's memo out.
if DB::optional_string(&row, "import_source")?.as_deref() == Some(super::waterline_import::IMPORT_SOURCE) {
entry.as_object_mut().expect("run entry").remove("memo");
}
runs.push(entry);
}
Ok(json!({"workflow_id":workflow,"run_count":runs.len(),"runs":runs}))
}
Expand Down
Loading
Loading