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
3 changes: 2 additions & 1 deletion .github/workflows/server-parity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ on:
- tests/Unit/ServerParityLargeCommandBatchesContractTest.mjs
- tests/Unit/ServerParityManyUpdatesContractTest.mjs
- tests/Unit/ServerParityWorkerErrorBodiesContractTest.mjs
- tests/Unit/ServerParityChildDescriptionsContractTest.mjs
- tests/Unit/ServerParityStartSearchAttributesContractTest.mjs
- tests/Unit/ServerParityQueryArgumentsContractTest.mjs
- tests/Unit/ServerParityQueryContractArgumentsContractTest.mjs
Expand Down Expand Up @@ -1990,7 +1991,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/ServerParityRunnerBlockedExcerptTest.mjs tests/Unit/ServerParityProbeNotesTest.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/ServerParityConcurrentWaitsContractTest.mjs tests/Unit/ServerParityContinueTransfersContractTest.mjs tests/Unit/ServerParityWorkerErrorBodiesContractTest.mjs tests/Unit/ServerParityLargeCommandBatchesContractTest.mjs tests/Unit/ServerParityManyUpdatesContractTest.mjs tests/Unit/ServerParityStartSearchAttributesContractTest.mjs tests/Unit/ServerParityQueryArgumentsContractTest.mjs tests/Unit/ServerParityQueryContractArgumentsContractTest.mjs tests/Unit/ServerParityParentCloseCancellationContractTest.mjs tests/Unit/ServerParityRecordLocalActivityContractTest.mjs tests/Unit/ServerParityActivityCancellationAckContractTest.mjs tests/Unit/ServerParityServiceCatalogContractTest.mjs tests/Unit/ServerParityServiceEndpointsContractTest.mjs tests/Unit/ServerParityServiceCallsContractTest.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/ServerParityCancellationScopesContractTest.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/ServerParityAdapterCacheTest.mjs tests/Unit/ServerParityWorkerSessionsContractTest.mjs tests/Unit/ServerParityWorkerSessionActivitiesContractTest.mjs tests/Unit/ServerParityNexusOperationsContractTest.mjs tests/Unit/ServerParityBridgeAdaptersContractTest.mjs tests/Unit/ServerParityWorkflowStreamWritesContractTest.mjs > parity-evidence/comparator-tests.txt
"$image" --test tests/Unit/ServerParityRunnerTest.mjs tests/Unit/ServerParityRunnerBlockedExcerptTest.mjs tests/Unit/ServerParityProbeNotesTest.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/ServerParityConcurrentWaitsContractTest.mjs tests/Unit/ServerParityContinueTransfersContractTest.mjs tests/Unit/ServerParityChildDescriptionsContractTest.mjs tests/Unit/ServerParityWorkerErrorBodiesContractTest.mjs tests/Unit/ServerParityLargeCommandBatchesContractTest.mjs tests/Unit/ServerParityManyUpdatesContractTest.mjs tests/Unit/ServerParityStartSearchAttributesContractTest.mjs tests/Unit/ServerParityQueryArgumentsContractTest.mjs tests/Unit/ServerParityQueryContractArgumentsContractTest.mjs tests/Unit/ServerParityParentCloseCancellationContractTest.mjs tests/Unit/ServerParityRecordLocalActivityContractTest.mjs tests/Unit/ServerParityActivityCancellationAckContractTest.mjs tests/Unit/ServerParityServiceCatalogContractTest.mjs tests/Unit/ServerParityServiceEndpointsContractTest.mjs tests/Unit/ServerParityServiceCallsContractTest.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/ServerParityCancellationScopesContractTest.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/ServerParityAdapterCacheTest.mjs tests/Unit/ServerParityWorkerSessionsContractTest.mjs tests/Unit/ServerParityWorkerSessionActivitiesContractTest.mjs tests/Unit/ServerParityNexusOperationsContractTest.mjs tests/Unit/ServerParityBridgeAdaptersContractTest.mjs tests/Unit/ServerParityWorkflowStreamWritesContractTest.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
30 changes: 30 additions & 0 deletions docs/rust-server-port-checkpoints/2026-10-29-child-descriptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# October 29, descriptions with several open children

- **Before.** Native's child summary (`child_summary.rs`), which describes a
parent waiting on children, gave up when:
- a second child was open while another was still waiting;
- a child's `parent_close_policy` was not `abandon`;
- more than 100 children had been started over the run's life;
- a timer wait was also open.

The parent was then described with no wait (`wait_kind` null).
- **Contract** (published 2.11.5; `WorkflowController::show`, Workflow
2.15.1 `RunWaitView`): a parent is described by its open waits. With
several open children it waits for the first. An open timer is described
ahead of open children, whichever was scheduled first. The parent-close
policy does not change the description.
- **Fixture.** `child-descriptions`, raw worker HTTP on protocol 1.20 in a
fresh namespace. Each case completes the parent's first workflow task with
its commands and records the answer and the description: one child, two
children, a child then a timer, a timer then a child, and a child under
`parent_close_policy: terminate`.
- **Native.**
- The summary keeps any number of open children, with no lifetime cap.
- Any parent-close policy is allowed.
- When a timer wait is already chosen and every child is still open, the
timer stays the run's wait.
- **Not covered here.** A child with a cancellation policy other than
`abandon` or with a retry or timeout policy, and a child beside an open
activity, are still described without their wait, pending evidence.
- **Tests.** `child_descriptions.rs`. On the previous source, two children
and a child beside a timer were described with a null wait.
14 changes: 12 additions & 2 deletions docs/rust-server-port.md
Original file line number Diff line number Diff line change
Expand Up @@ -586,8 +586,18 @@ contract fixture before its correction.
`many-updates` fixture records a run waiting for a signal through 65
completed updates, then a 66th left accepted, against published 2.11.5
([checkpoint](rust-server-port-checkpoints/2026-10-29-many-updates.md)).
The child summary still stops at 100 children over a run's life; that
needs its own fixture.
- **Descriptions with several open children** (October 29, closed).
Published PHP describes a parent by its open waits however many children
it holds: the first open child, or an open timer ahead of open children,
under any parent-close policy. Native's child summary allowed one waiting
child at a time, only the `abandon` parent-close policy, at most 100
children over a run's life, and no timer beside a child. Such a parent
was described with no wait. The `child-descriptions` fixture records five
cases against published 2.11.5
([checkpoint](rust-server-port-checkpoints/2026-10-29-child-descriptions.md)).
A child with a cancellation policy other than `abandon`, a retry or
timeout policy, or beside an open activity is still described without
its wait, pending its own evidence.
- **Trace context (Core 2.10, published in Server 2.11.1).** Core adds
nullable `observability_context` columns on runs, commands and tasks.
Native has no carrier: it accepts and ignores a top-level carrier on
Expand Down
29 changes: 18 additions & 11 deletions rust/src/runtime/child_summary.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Sequential child liveness requires original typed identity and retained waits.
//! Child liveness requires original typed identity and retained waits. A run
//! may hold any number of open children, each under any parent-close policy.
use super::{
Result,
active_summary::{ActiveSummary, SummaryWait},
Expand Down Expand Up @@ -33,13 +34,10 @@ impl ChildSummaryHistory {
return false;
};
if event == "ChildWorkflowScheduled" {
if self.calls.len() >= 100
|| self.calls.contains_key(&sequence)
|| self
.calls
.values()
.any(|child| child.status == "waiting" || child.id == identity.id)
|| payload["parent_close_policy"] != "abandon"
// Any number of children may be open at once, and the parent-close
// policy only decides what happens when the parent closes.
if self.calls.contains_key(&sequence)
|| self.calls.values().any(|child| child.id == identity.id)
|| payload["cancellation_policy"] != "abandon"
|| !payload["retry_policy"].is_null()
|| !payload["timeout_policy"].is_null()
Expand Down Expand Up @@ -152,9 +150,9 @@ where
history: &ChildSummaryHistory,
summary: &mut ActiveSummary,
) -> Result<bool> {
let rows = Self::query("SELECT w.*,c.status AS call_status,c.resolved_child_run_id,c.resolved_child_instance_id,c.child_workflow_type,c.metadata AS call_metadata,l.id AS link_id,l.child_workflow_run_id AS link_run,l.child_workflow_instance_id AS link_instance FROM workflow_run_waits w LEFT JOIN workflow_child_calls c ON c.parent_workflow_run_id=w.workflow_run_id AND c.sequence=w.sequence LEFT JOIN workflow_links l ON l.parent_workflow_run_id=w.workflow_run_id AND l.sequence=w.sequence AND l.link_type='child_workflow' WHERE w.workflow_run_id=$1 AND w.kind='child' ORDER BY w.sequence LIMIT 101")
let rows = Self::query("SELECT w.*,c.status AS call_status,c.resolved_child_run_id,c.resolved_child_instance_id,c.child_workflow_type,c.metadata AS call_metadata,l.id AS link_id,l.child_workflow_run_id AS link_run,l.child_workflow_instance_id AS link_instance FROM workflow_run_waits w LEFT JOIN workflow_child_calls c ON c.parent_workflow_run_id=w.workflow_run_id AND c.sequence=w.sequence LEFT JOIN workflow_links l ON l.parent_workflow_run_id=w.workflow_run_id AND l.sequence=w.sequence AND l.link_type='child_workflow' WHERE w.workflow_run_id=$1 AND w.kind='child' ORDER BY w.sequence")
.bind(run_id).fetch_all(&mut **tx).await?;
if rows.len() != history.calls.len() || rows.len() > 100 {
if rows.len() != history.calls.len() {
return Ok(false);
}
for row in &rows {
Expand All @@ -171,7 +169,16 @@ where
if rows.is_empty() {
return Ok(true);
}
// Other wait families retain their separately qualified precedence.
// An open timer wait describes the run ahead of open children, as
// PHP's run description does. Other wait families, and a resolved
// child beside a timer, retain their separately qualified precedence.
let all_open = rows.iter().all(|row| DB::string(row, "status").is_ok_and(|status| status == "open"));
if summary.wait.as_ref().is_some_and(|wait| wait.kind == "timer")
&& all_open
&& summary.task_type.as_deref().is_none_or(|kind| kind == "timer")
{
return Ok(true);
}
if summary
.wait
.as_ref()
Expand Down
2 changes: 2 additions & 0 deletions rust/tests/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,8 @@ mod close_inventory;
mod start_search_attributes;
#[path = "execution/worker_error_bodies.rs"]
mod worker_error_bodies;
#[path = "execution/child_descriptions.rs"]
mod child_descriptions;

enum TestDatabase {
Sqlite(tempfile::TempDir),
Expand Down
42 changes: 42 additions & 0 deletions rust/tests/execution/child_descriptions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use super::*;

// Published Server 2.11.5 describes a parent by its open waits however many
// children it holds: the first open child, or an open timer ahead of open
// children, under any parent-close policy. The child-descriptions fixture
// records the same cases.

fn child(policy: Option<&str>) -> Value {
let mut command = json!({"type":"start_child_workflow","workflow_type":"echo","arguments":envelope(Payload::Array(vec![]))});
if let Some(policy) = policy {
command["parent_close_policy"] = json!(policy);
}
command
}

#[tokio::test]
async fn a_parent_is_described_by_its_waits_with_several_children() {
let timer = json!({"type":"start_timer","delay_seconds":3600});
let child_wait = (json!("child"), json!("Waiting for child workflow echo"));
let timer_wait = (json!("timer"), json!("Waiting for timer"));
for (id, commands, expected) in [
("one-child", json!([child(None)]), child_wait.clone()),
("two-children", json!([child(None), child(None)]), child_wait.clone()),
("child-then-timer", json!([child(None), timer.clone()]), timer_wait.clone()),
("timer-then-child", json!([timer.clone(), child(None)]), timer_wait.clone()),
("terminating-child", json!([child(Some("terminate"))]), child_wait.clone()),
] {
let database = TestDatabase::new().await;
let runtime = database.open().await.unwrap();
let app = router(runtime.clone());
register(&app, "worker", json!(["echo"]), json!([])).await;
start(&app, id).await;
let task = poll(&app, "worker", "workflow").await;
let (status, body) = finish_task(&app, &task, commands).await;
assert_eq!(status, StatusCode::OK, "{id}: {body}");
let (_, described) = request(&app, "GET", &format!("/api/workflows/{id}"), Value::Null).await;
assert_eq!(described["status"], "waiting", "{id}: {described}");
assert_eq!((described["wait_kind"].clone(), described["wait_reason"].clone()), expected, "{id}: {described}");
runtime.close().await;
database.remove().await;
}
}
23 changes: 23 additions & 0 deletions scripts/conformance/server-parity/child-descriptions-contract.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import assert from 'node:assert/strict';

// Reviewed parent descriptions while children are open
// (child-descriptions-probe.php), recorded against published Server 2.11.5.
// The fixture's `expected` holds the whole observation.
export function checkChildDescriptions(fixture, observation) {
const definition = fixture.child_descriptions;
const state = observation.child_descriptions;
const projection = {scope: 'child_descriptions', cases: Object.keys(definition.cases).length};
assert.ok(state && typeof state === 'object', 'actual child descriptions observation');
if (observation.mode === 'embedded') {
assert.deepStrictEqual(state, {applicable: false, reason: 'embedded_has_no_worker_protocol'},
'embedded child descriptions explicitly inapplicable');
return projection;
}
assert.equal(state.applicable, true, 'HTTP targets record child descriptions');
for (const [run, expected] of Object.entries(definition.expected)) {
assert.deepStrictEqual(state[run], expected, `child descriptions ${run}`);
}
assert.deepStrictEqual(Object.keys(state).sort(), ['applicable', ...Object.keys(definition.expected)].sort(),
'child descriptions cases');
return projection;
}
Loading
Loading