From 5e75436170001f9c734c5c939c3186ddc8897bcc Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:22:19 +0000 Subject: [PATCH 1/5] CI: run the SDK direction profiles in three concurrent lanes In each database job of Shared Server fixtures, the nine published-SDK direction profiles ran one after another (two pairs already overlapped), about 22 of the job's 64-72 minutes (train #729, PostgreSQL 17 job 120866907013: 12:19:50 to 12:41:36). Each profile already has its own containers, PHP and embedded databases, evidence and state directory, and its own workflow-ID prefix on the shared native server. sdk-direction-lanes.sh now runs them in three lanes balanced by those durations, in order within a lane. Every profile runs even after another fails; the step fails if any did, after printing each profile's log in its own group. The job's existing cleanup steps still stop and remove every profile's containers. Expected saving: about 14 minutes per database job. --- .github/workflows/server-parity.yml | 129 +----------------- .../server-parity/sdk-direction-lanes.sh | 49 +++++++ .../server-parity/sdk-directions/README.md | 2 + 3 files changed, 57 insertions(+), 123 deletions(-) create mode 100755 scripts/conformance/server-parity/sdk-direction-lanes.sh diff --git a/.github/workflows/server-parity.yml b/.github/workflows/server-parity.yml index 81878ea9f..ca03000ff 100644 --- a/.github/workflows/server-parity.yml +++ b/.github/workflows/server-parity.yml @@ -610,47 +610,8 @@ jobs: bash scripts/conformance/server-parity/legacy-marker-profile.sh mysql http://parity-php:8080 http://parity-native-a:8080 bash scripts/conformance/server-parity/range-repair-profile.sh mysql http://parity-native-a:8080 bash scripts/conformance/server-parity/range-repair-profile.sh mysql http://parity-native-a:8080 pending-native - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 - docker stop --time 30 "$RESOURCE_SCOPE-sdk-directions-server" - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 child - docker stop --time 30 "$RESOURCE_SCOPE-sdk-child-directions-server" - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 timer - docker stop --time 30 "$RESOURCE_SCOPE-sdk-timers-server" - # Separate queues/databases/evidence; preserve statuses even after children exit. - ( - signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 signal > parity-evidence/sdk-signals-profile.log 2>&1 || signal_status=$? - printf '%s\n' "$signal_status" > parity-evidence/sdk-signals-profile.status - ) & - ( - query_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 query > parity-evidence/sdk-queries-profile.log 2>&1 || query_status=$? - printf '%s\n' "$query_status" > parity-evidence/sdk-queries-profile.status - ) & - update_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 update || update_status=$? - docker stop --time 30 "$RESOURCE_SCOPE-sdk-updates-server" || true - failure_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 update-failure || failure_status=$? - wait - signal_status=$(cat parity-evidence/sdk-signals-profile.status) - query_status=$(cat parity-evidence/sdk-queries-profile.status) - cat parity-evidence/sdk-signals-profile.log parity-evidence/sdk-queries-profile.log - for completed_profile in sdk-signals sdk-queries sdk-update-failures; do - docker stop --time 30 "$RESOURCE_SCOPE-$completed_profile-server" || true - done - namespace_signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 namespace-signal || namespace_signal_status=$? - test "$signal_status" -eq 0 - test "$query_status" -eq 0 - test "$update_status" -eq 0 - test "$failure_status" -eq 0 - test "$namespace_signal_status" -eq 0 - docker stop --time 30 "$RESOURCE_SCOPE-sdk-namespace-signals-server" || true - selected_run_signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 selected-run-signal || selected_run_signal_status=$? - docker stop --time 30 "$RESOURCE_SCOPE-sdk-selected-run-signals-server" || true - test "$selected_run_signal_status" -eq 0 + # Every SDK direction profile, in three concurrent lanes. + bash scripts/conformance/server-parity/sdk-direction-lanes.sh mysql http://parity-native-a:8080 docker run --rm --user=1000:1000 --network "$RESOURCE_SCOPE" --entrypoint sh \ -v "$PWD:/source:ro" -v "$PWD/parity-evidence:/evidence" -w /source \ -e DW_PARITY_TOKEN=parity-test-token "$image" -ec ' @@ -1123,47 +1084,8 @@ jobs: bash scripts/conformance/server-parity/legacy-marker-profile.sh pgsql http://parity-php:8080 http://parity-native-a:8080 bash scripts/conformance/server-parity/range-repair-profile.sh pgsql http://parity-native-a:8080 bash scripts/conformance/server-parity/range-repair-profile.sh pgsql http://parity-native-a:8080 pending-native - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 - docker stop --time 30 "$RESOURCE_SCOPE-sdk-directions-server" - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 child - docker stop --time 30 "$RESOURCE_SCOPE-sdk-child-directions-server" - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 timer - docker stop --time 30 "$RESOURCE_SCOPE-sdk-timers-server" - # Separate queues/databases/evidence; preserve statuses even after children exit. - ( - signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 signal > parity-evidence/sdk-signals-profile.log 2>&1 || signal_status=$? - printf '%s\n' "$signal_status" > parity-evidence/sdk-signals-profile.status - ) & - ( - query_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 query > parity-evidence/sdk-queries-profile.log 2>&1 || query_status=$? - printf '%s\n' "$query_status" > parity-evidence/sdk-queries-profile.status - ) & - update_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 update || update_status=$? - docker stop --time 30 "$RESOURCE_SCOPE-sdk-updates-server" || true - failure_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 update-failure || failure_status=$? - wait - signal_status=$(cat parity-evidence/sdk-signals-profile.status) - query_status=$(cat parity-evidence/sdk-queries-profile.status) - cat parity-evidence/sdk-signals-profile.log parity-evidence/sdk-queries-profile.log - for completed_profile in sdk-signals sdk-queries sdk-update-failures; do - docker stop --time 30 "$RESOURCE_SCOPE-$completed_profile-server" || true - done - namespace_signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 namespace-signal || namespace_signal_status=$? - test "$signal_status" -eq 0 - test "$query_status" -eq 0 - test "$update_status" -eq 0 - test "$failure_status" -eq 0 - test "$namespace_signal_status" -eq 0 - docker stop --time 30 "$RESOURCE_SCOPE-sdk-namespace-signals-server" || true - selected_run_signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 selected-run-signal || selected_run_signal_status=$? - docker stop --time 30 "$RESOURCE_SCOPE-sdk-selected-run-signals-server" || true - test "$selected_run_signal_status" -eq 0 + # Every SDK direction profile, in three concurrent lanes. + bash scripts/conformance/server-parity/sdk-direction-lanes.sh pgsql http://parity-native-a:8080 docker run --rm --user=1000:1000 --network "$RESOURCE_SCOPE" --entrypoint sh \ -v "$PWD:/source:ro" -v "$PWD/parity-evidence:/evidence" -w /source \ -e DW_PARITY_TOKEN=parity-test-token "$image" -ec ' @@ -1628,47 +1550,8 @@ jobs: bash scripts/conformance/server-parity/legacy-marker-profile.sh sqlite http://parity-server:8080 http://parity-rust:8080 bash scripts/conformance/server-parity/range-repair-profile.sh sqlite http://parity-rust:8080 bash scripts/conformance/server-parity/range-repair-profile.sh sqlite http://parity-rust:8080 pending-native - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 - docker stop --time 30 "$RESOURCE_SCOPE-sdk-directions-server" - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 child - docker stop --time 30 "$RESOURCE_SCOPE-sdk-child-directions-server" - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 timer - docker stop --time 30 "$RESOURCE_SCOPE-sdk-timers-server" - # Separate queues/databases/evidence; preserve statuses even after children exit. - ( - signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 signal > parity-evidence/sdk-signals-profile.log 2>&1 || signal_status=$? - printf '%s\n' "$signal_status" > parity-evidence/sdk-signals-profile.status - ) & - ( - query_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 query > parity-evidence/sdk-queries-profile.log 2>&1 || query_status=$? - printf '%s\n' "$query_status" > parity-evidence/sdk-queries-profile.status - ) & - update_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 update || update_status=$? - docker stop --time 30 "$RESOURCE_SCOPE-sdk-updates-server" || true - failure_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 update-failure || failure_status=$? - wait - signal_status=$(cat parity-evidence/sdk-signals-profile.status) - query_status=$(cat parity-evidence/sdk-queries-profile.status) - cat parity-evidence/sdk-signals-profile.log parity-evidence/sdk-queries-profile.log - for completed_profile in sdk-signals sdk-queries sdk-update-failures; do - docker stop --time 30 "$RESOURCE_SCOPE-$completed_profile-server" || true - done - namespace_signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 namespace-signal || namespace_signal_status=$? - test "$signal_status" -eq 0 - test "$query_status" -eq 0 - test "$update_status" -eq 0 - test "$failure_status" -eq 0 - test "$namespace_signal_status" -eq 0 - docker stop --time 30 "$RESOURCE_SCOPE-sdk-namespace-signals-server" || true - selected_run_signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 selected-run-signal || selected_run_signal_status=$? - docker stop --time 30 "$RESOURCE_SCOPE-sdk-selected-run-signals-server" || true - test "$selected_run_signal_status" -eq 0 + # Every SDK direction profile, in three concurrent lanes. + bash scripts/conformance/server-parity/sdk-direction-lanes.sh sqlite http://parity-rust:8080 docker run --rm --user=1000:1000 --entrypoint php --network "$RESOURCE_SCOPE" \ -v "$PWD:/source:ro" -v "$PWD/parity-evidence:/evidence" -w /source \ -e DW_PARITY_TOKEN=parity-test-token "$image" scripts/conformance/server-parity/restart.php \ diff --git a/scripts/conformance/server-parity/sdk-direction-lanes.sh b/scripts/conformance/server-parity/sdk-direction-lanes.sh new file mode 100755 index 000000000..87f05cd88 --- /dev/null +++ b/scripts/conformance/server-parity/sdk-direction-lanes.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Run every published-SDK direction profile against one native server, in +# three concurrent lanes. Each profile already has its own containers, PHP and +# embedded databases, evidence and state directory, and its own workflow-ID +# prefix on the shared native server, so profiles in different lanes cannot +# see each other. Within a lane they run in order. Every profile runs even +# after another fails; the script fails if any did, after printing each log. +# The job's sdk-direction-cleanup steps stop and remove every profile's +# containers afterwards, as before. +set -euo pipefail +backend="${1:?Choose mysql, pgsql or sqlite}" +native_url="${2:?Set the independently isolated native URL}" +test -n "${RESOURCE_SCOPE:?Set the owning isolated network/resource scope}" +mkdir -p parity-evidence/sdk-direction-lanes +# Profile variant and its tag (evidence, log and container names). The lanes are +# balanced by the durations recorded in CI (about seven to eight minutes each). +lanes=( + "update:sdk-updates update-failure:sdk-update-failures child:sdk-child-directions" + "namespace-signal:sdk-namespace-signals timer:sdk-timers activity:sdk-directions" + "selected-run-signal:sdk-selected-run-signals signal:sdk-signals query:sdk-queries" +) +run_lane() { + local entry variant tag status + for entry in $1; do + variant=${entry%%:*} tag=${entry#*:} status=0 + bash scripts/conformance/server-parity/sdk-direction-profile.sh "$backend" "$native_url" "$variant" \ + > "parity-evidence/sdk-direction-lanes/$tag.log" 2>&1 || status=$? + printf '%s\n' "$status" > "parity-evidence/sdk-direction-lanes/$tag.status" + done +} +for lane in "${lanes[@]}"; do + run_lane "$lane" & +done +wait +failed=0 +for lane in "${lanes[@]}"; do + for entry in $lane; do + tag=${entry#*:} + status=$(cat "parity-evidence/sdk-direction-lanes/$tag.status" 2>/dev/null || echo missing) + echo "::group::$tag (exit $status)" + cat "parity-evidence/sdk-direction-lanes/$tag.log" + echo "::endgroup::" + if [ "$status" != 0 ]; then + echo "::error::SDK direction profile $tag exited $status" + failed=1 + fi + done +done +exit "$failed" diff --git a/scripts/conformance/server-parity/sdk-directions/README.md b/scripts/conformance/server-parity/sdk-directions/README.md index f5fb86a88..5fea6d9ea 100644 --- a/scripts/conformance/server-parity/sdk-directions/README.md +++ b/scripts/conformance/server-parity/sdk-directions/README.md @@ -34,6 +34,8 @@ bash scripts/conformance/server-parity/sdk-direction-profile.sh BACKEND NATIVE_U bash scripts/conformance/server-parity/sdk-direction-cleanup.sh BACKEND ``` +CI runs every variant through `sdk-direction-lanes.sh BACKEND NATIVE_URL`, in three concurrent lanes against one native server; each variant keeps its own databases, containers and evidence. + Set `RESOURCE_SCOPE` to the owning network and supply `MYSQL_IMAGE` or `POSTGRES_IMAGE` for that backend, as the shared workflow does. The profile starts the exact PHP reference and embedded engine in independent databases; From bf73ac29f83baf2098e45ad5d6f4af4cbee6b93b Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:26:25 +0000 Subject: [PATCH 2/5] Compare activity lease renewals as instants, not RFC3339 text heartbeat_renews_only_progress_deadline_and_cannot_resurrect_elapsed_authority compared two lease_expires_at strings. Native times are whole milliseconds and serialize without a fraction on a whole second, so a claim lease of ...:26Z sorted after a renewal of ...:26.045Z and the assertion failed about one run in a thousand (train #729, PostgreSQL 17 job 120852521096). The test now parses both and reports the values. --- rust/tests/execution/activity_deadlines.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rust/tests/execution/activity_deadlines.rs b/rust/tests/execution/activity_deadlines.rs index ea2d4d3a9..c67b5f724 100644 --- a/rust/tests/execution/activity_deadlines.rs +++ b/rust/tests/execution/activity_deadlines.rs @@ -483,9 +483,14 @@ async fn heartbeat_renews_only_progress_deadline_and_cannot_resurrect_elapsed_au assert_eq!(after.0, initial.0, "Start-to-close deadline remains fixed"); assert!(after.1 > initial.1, "Heartbeat deadline advances"); assert_eq!(after.2, initial.2, "Lifecycle deadline remains fixed"); + // Compare instants, not text: a lease on a whole second is written + // without a fraction ("...:26Z"), which sorts after "...:26.045Z". + let instant = |value: &Value| chrono::DateTime::parse_from_rfc3339(value.as_str().unwrap()).unwrap(); assert!( - renewed.1["lease_expires_at"].as_str().unwrap() - > activity["lease_expires_at"].as_str().unwrap() + instant(&renewed.1["lease_expires_at"]) > instant(&activity["lease_expires_at"]), + "{} renews {}", + renewed.1["lease_expires_at"], + activity["lease_expires_at"] ); let prefix = run_history(&app, &started["workflow_id"], &started["run_id"]).await; database From 337fe8de1658888cc9ab1bf133d22b16eb27e30f Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:41:38 +0000 Subject: [PATCH 3/5] CI: build the deadline-health native binary in its own job Every database job waited for published-sdk-adapters (about 15 minutes), but 10 of those minutes built the optimized native binary only the codec-deadline-health cells use (train #729: "Build one bounded optimized native artifact for deadline health", 10.0 minutes). The build, its upload and its container cleanup now run in a codec-native-build job beside the adapters; codec-deadline-health needs both, and the database jobs still need only the adapters, so they start about 10 minutes sooner. The build script and its bounded resources are unchanged. --- .github/workflows/server-parity.yml | 47 ++++++++++++++++++----------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/.github/workflows/server-parity.yml b/.github/workflows/server-parity.yml index 3384b7a4f..7b3b443ca 100644 --- a/.github/workflows/server-parity.yml +++ b/.github/workflows/server-parity.yml @@ -195,23 +195,6 @@ jobs: path: corrected-marker-adapters retention-days: 1 if-no-files-found: error - - name: Build one bounded optimized native artifact for deadline health - run: bash scripts/conformance/server-parity/codec-operations-build.sh codec-native-build - - name: Share the optimized binary without compiler caches - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: codec-native-build - path: | - codec-native-build/server - codec-native-build/build.json - codec-native-build/SHA256SUMS - retention-days: 1 - if-no-files-found: error - - name: Retire the owned optimized build container - if: always() - run: | - build_container="$RESOURCE_SCOPE-build" - docker rm -f "$build_container" >/dev/null 2>&1 || true - name: Share thin published SDK direction adapters uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: @@ -254,8 +237,36 @@ jobs: retention-days: 1 if-no-files-found: ignore + # The optimized native build only the deadline-health cells use. It runs + # beside the SDK adapters, so the database jobs, which need only the + # adapters, no longer wait for it. + codec-native-build: + runs-on: ubuntu-latest + timeout-minutes: 25 + env: + RESOURCE_SCOPE: dw-codec-native-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - name: Build one bounded optimized native artifact for deadline health + run: bash scripts/conformance/server-parity/codec-operations-build.sh codec-native-build + - name: Share the optimized binary without compiler caches + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: codec-native-build + path: | + codec-native-build/server + codec-native-build/build.json + codec-native-build/SHA256SUMS + retention-days: 1 + if-no-files-found: error + - name: Retire the owned optimized build container + if: always() + run: | + build_container="$RESOURCE_SCOPE-build" + docker rm -f "$build_container" >/dev/null 2>&1 || true + codec-deadline-health: - needs: published-sdk-adapters + needs: [published-sdk-adapters, codec-native-build] runs-on: ubuntu-latest timeout-minutes: 10 strategy: From c834849c8b0e0de1f630f9090a0a842e95c0a8c5 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:01:57 +0000 Subject: [PATCH 4/5] Stop each SDK direction profile's PHP server after its attempt, and test the lane wrapper Each lane stops its profile's PHP server as soon as the profile ends, so at most three run at once, and a failed stop only warns. The profile's status is kept either way. A shell-level test runs the wrapper with stub profiles and docker: all nine profiles run, every server is stopped after its profile, a failed profile does not stop its lane, and the wrapper fails only after printing every status and log. Also resolves the wrapper's Sonar findings. --- .github/workflows/server-parity.yml | 4 +- .../server-parity/sdk-direction-lanes.sh | 37 +++-- .../ServerParitySdkDirectionLanesTest.mjs | 133 ++++++++++++++++++ 3 files changed, 159 insertions(+), 15 deletions(-) create mode 100644 tests/Unit/ServerParitySdkDirectionLanesTest.mjs diff --git a/.github/workflows/server-parity.yml b/.github/workflows/server-parity.yml index 7b3b443ca..0bcd5f5a7 100644 --- a/.github/workflows/server-parity.yml +++ b/.github/workflows/server-parity.yml @@ -10,6 +10,7 @@ on: - tests/Fixtures/ServerParityBaselines/** - tests/Fixtures/ServerParityNormalization/** - tests/Unit/ServerParityRunnerTest.mjs + - tests/Unit/ServerParitySdkDirectionLanesTest.mjs - tests/Unit/ServerParityArtifactSummaryTest.mjs - tests/Unit/ServerParityQueryObserverTest.mjs - tests/Unit/ServerParityCodecDeadlineLoadTest.mjs @@ -66,6 +67,7 @@ on: - tests/Fixtures/ServerParityBaselines/** - tests/Fixtures/ServerParityNormalization/** - tests/Unit/ServerParityRunnerTest.mjs + - tests/Unit/ServerParitySdkDirectionLanesTest.mjs - tests/Unit/ServerParityArtifactSummaryTest.mjs - tests/Unit/ServerParityQueryObserverTest.mjs - tests/Unit/ServerParityCodecDeadlineLoadTest.mjs @@ -1462,7 +1464,7 @@ jobs: -e COMPOSER_HOME=/tmp/composer -v "$PWD:/source" -w /source "$image" \ 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/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/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/ServerParityServiceCatalogContractTest.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 > parity-evidence/comparator-tests.txt + "$image" --test tests/Unit/ServerParityRunnerTest.mjs tests/Unit/ServerParitySdkDirectionLanesTest.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/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/ServerParityServiceCatalogContractTest.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 > 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 diff --git a/scripts/conformance/server-parity/sdk-direction-lanes.sh b/scripts/conformance/server-parity/sdk-direction-lanes.sh index 87f05cd88..59e3dfc80 100755 --- a/scripts/conformance/server-parity/sdk-direction-lanes.sh +++ b/scripts/conformance/server-parity/sdk-direction-lanes.sh @@ -3,15 +3,20 @@ # three concurrent lanes. Each profile already has its own containers, PHP and # embedded databases, evidence and state directory, and its own workflow-ID # prefix on the shared native server, so profiles in different lanes cannot -# see each other. Within a lane they run in order. Every profile runs even -# after another fails; the script fails if any did, after printing each log. -# The job's sdk-direction-cleanup steps stop and remove every profile's -# containers afterwards, as before. +# see each other. Within a lane they run in order, and each profile's PHP +# server is stopped as soon as its attempt ends, so at most three run at once. +# Every profile runs even after another fails; the script fails if any did, +# after printing each log. The job's sdk-direction-cleanup steps remove every +# profile's containers afterwards, as before. set -euo pipefail backend="${1:?Choose mysql, pgsql or sqlite}" native_url="${2:?Set the independently isolated native URL}" -test -n "${RESOURCE_SCOPE:?Set the owning isolated network/resource scope}" -mkdir -p parity-evidence/sdk-direction-lanes +if [[ -z "${RESOURCE_SCOPE:-}" ]]; then + echo 'Set the owning isolated network/resource scope' >&2 + exit 2 +fi +evidence=parity-evidence/sdk-direction-lanes +mkdir -p "$evidence" # Profile variant and its tag (evidence, log and container names). The lanes are # balanced by the durations recorded in CI (about seven to eight minutes each). lanes=( @@ -20,12 +25,16 @@ lanes=( "selected-run-signal:sdk-selected-run-signals signal:sdk-signals query:sdk-queries" ) run_lane() { - local entry variant tag status - for entry in $1; do + local lane=$1 entry variant tag status + for entry in $lane; do variant=${entry%%:*} tag=${entry#*:} status=0 bash scripts/conformance/server-parity/sdk-direction-profile.sh "$backend" "$native_url" "$variant" \ - > "parity-evidence/sdk-direction-lanes/$tag.log" 2>&1 || status=$? - printf '%s\n' "$status" > "parity-evidence/sdk-direction-lanes/$tag.status" + > "$evidence/$tag.log" 2>&1 || status=$? + printf '%s\n' "$status" > "$evidence/$tag.status" + # The profile's status is already kept; a failed stop leaves the server to + # the final cleanup. + docker stop --time 30 "$RESOURCE_SCOPE-$tag-server" >/dev/null 2>&1 \ + || echo "::warning::The $tag PHP server was not stopped; the final cleanup removes it" >&2 done } for lane in "${lanes[@]}"; do @@ -36,12 +45,12 @@ failed=0 for lane in "${lanes[@]}"; do for entry in $lane; do tag=${entry#*:} - status=$(cat "parity-evidence/sdk-direction-lanes/$tag.status" 2>/dev/null || echo missing) + status=$(cat "$evidence/$tag.status" 2>/dev/null || echo missing) echo "::group::$tag (exit $status)" - cat "parity-evidence/sdk-direction-lanes/$tag.log" + cat "$evidence/$tag.log" echo "::endgroup::" - if [ "$status" != 0 ]; then - echo "::error::SDK direction profile $tag exited $status" + if [[ "$status" != 0 ]]; then + echo "::error::SDK direction profile $tag exited $status" >&2 failed=1 fi done diff --git a/tests/Unit/ServerParitySdkDirectionLanesTest.mjs b/tests/Unit/ServerParitySdkDirectionLanesTest.mjs new file mode 100644 index 000000000..250603298 --- /dev/null +++ b/tests/Unit/ServerParitySdkDirectionLanesTest.mjs @@ -0,0 +1,133 @@ +import assert from 'node:assert/strict'; +import {spawnSync} from 'node:child_process'; +import {chmodSync, copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import test from 'node:test'; + +const LANES = new URL('../../scripts/conformance/server-parity/sdk-direction-lanes.sh', import.meta.url).pathname; +const PROFILES = { + update: 'sdk-updates', 'update-failure': 'sdk-update-failures', child: 'sdk-child-directions', + 'namespace-signal': 'sdk-namespace-signals', timer: 'sdk-timers', activity: 'sdk-directions', + 'selected-run-signal': 'sdk-selected-run-signals', signal: 'sdk-signals', query: 'sdk-queries', +}; + +// Runs the wrapper in a scratch tree whose profile script and `docker` are +// stubs that append to one event log: a profile starts its PHP server and +// exits FAIL_ (default 0); `docker stop` exits STOP_FAIL_. +function runLanes(environment = {}) { + const root = mkdtempSync(join(tmpdir(), 'dw-sdk-lanes-')); + try { + const scripts = join(root, 'scripts/conformance/server-parity'); + const bin = join(root, 'bin'); + mkdirSync(scripts, {recursive: true}); + mkdirSync(bin); + copyFileSync(LANES, join(scripts, 'sdk-direction-lanes.sh')); + const events = join(root, 'events.log'); + writeFileSync(join(scripts, 'sdk-direction-profile.sh'), `#!/usr/bin/env bash +set -euo pipefail +variant=$3 +echo "start $variant $1 $2" >> "$EVENTS" +echo "profile output for $variant" +sleep 0.2 +echo "end $variant" >> "$EVENTS" +fail="FAIL_\${variant//-/_}" +exit "\${!fail:-0}" +`); + writeFileSync(join(bin, 'docker'), `#!/usr/bin/env bash +echo "docker $*" >> "$EVENTS" +name=\${*: -1} +tag=\${name#"$RESOURCE_SCOPE"-} +tag=\${tag%-server} +fail="STOP_FAIL_\${tag//-/_}" +exit "\${!fail:-0}" +`); + chmodSync(join(bin, 'docker'), 0o755); + const child = spawnSync('bash', ['scripts/conformance/server-parity/sdk-direction-lanes.sh', 'sqlite', 'http://native:8080'], { + cwd: root, encoding: 'utf8', timeout: 30000, + env: {...process.env, ...environment, PATH: `${bin}:${process.env.PATH}`, EVENTS: events, RESOURCE_SCOPE: 'dw-scope-1'}, + }); + const statuses = Object.fromEntries(Object.values(PROFILES).map(tag => [tag, + readFileSync(join(root, 'parity-evidence/sdk-direction-lanes', `${tag}.status`), 'utf8').trim()])); + return {child, statuses, events: readFileSync(events, 'utf8').trim().split('\n')}; + } finally { + rmSync(root, {recursive: true, force: true}); + } +} + +const started = (events, variant) => events.findIndex(event => event.startsWith(`start ${variant} `)); + +// Each profile ran once against the given backend and URL, and its PHP server +// was stopped after the profile ended and before the lane's next profile. +function assertEveryServerRetired(events) { + const running = new Set(); + let peak = 0; + for (const event of events) { + const [kind, ...rest] = event.split(' '); + if (kind === 'start') { + assert.deepEqual(rest.slice(1), ['sqlite', 'http://native:8080'], event); + running.add(PROFILES[rest[0]]); + peak = Math.max(peak, running.size); + } else if (kind === 'docker') { + assert.deepEqual(rest.slice(0, 3), ['stop', '--time', '30'], event); + const tag = rest[3].replace(/^dw-scope-1-/, '').replace(/-server$/, ''); + assert.ok(running.delete(tag), `${tag} stopped once, after it started`); + const end = events.indexOf(`end ${Object.keys(PROFILES).find(variant => PROFILES[variant] === tag)}`); + assert.ok(end >= 0 && end < events.indexOf(event), `${tag} stopped after its profile ended`); + } + } + assert.equal(running.size, 0, `servers left running: ${[...running]}`); + assert.ok(peak <= 3, `at most three PHP servers run at once, not ${peak}`); + const started = events.filter(event => event.startsWith('start ')).map(event => event.split(' ')[1]).sort(); + assert.deepEqual(started, Object.keys(PROFILES).sort(), 'all nine profiles ran once'); + assert.deepEqual(events.filter(event => event.startsWith('docker ')).map(event => event.split(' ').at(-1)).sort(), + Object.values(PROFILES).map(tag => `dw-scope-1-${tag}-server`).sort(), 'every profile server stopped once'); +} + +test('all nine profiles pass and each PHP server is stopped after its profile', () => { + const {child, statuses, events} = runLanes(); + assert.equal(child.status, 0, child.stderr); + assert.deepEqual(new Set(Object.values(statuses)), new Set(['0'])); + assertEveryServerRetired(events); + for (const [variant, tag] of Object.entries(PROFILES)) { + assert.match(child.stdout, new RegExp(`::group::${tag} \\(exit 0\\)\\nprofile output for ${variant}\\n::endgroup::`)); + } + assert.doesNotMatch(child.stderr, /::error::|::warning::/); +}); + +test('a failed profile still runs its lane and fails the wrapper only after every status and log', () => { + // The timer profile is second in its lane; the activity profile follows it. + const {child, statuses, events} = runLanes({FAIL_timer: '7'}); + assert.equal(child.status, 1); + assert.deepEqual(statuses, {...Object.fromEntries(Object.values(PROFILES).map(tag => [tag, '0'])), 'sdk-timers': '7'}); + assertEveryServerRetired(events); + assert.ok(started(events, 'activity') > events.indexOf('end timer'), 'the lane continues after a failure'); + assert.match(child.stderr, /^::error::SDK direction profile sdk-timers exited 7$/m); + assert.equal(child.stderr.match(/::error::/g).length, 1); + assert.match(child.stdout, /::group::sdk-timers \(exit 7\)\nprofile output for timer\n::endgroup::/); + assert.match(child.stdout, /::group::sdk-queries \(exit 0\)/, 'logs after the failure are still printed'); +}); + +test('a failed stop warns without changing any profile status', () => { + const {child, statuses, events} = runLanes({STOP_FAIL_sdk_updates: '1'}); + assert.equal(child.status, 0, child.stderr); + assert.equal(statuses['sdk-updates'], '0'); + assert.match(child.stderr, /^::warning::The sdk-updates PHP server was not stopped; the final cleanup removes it$/m); + assert.ok(started(events, 'update-failure') > events.indexOf('docker stop --time 30 dw-scope-1-sdk-updates-server'), + 'the lane continues after a failed stop'); +}); + +test('a failed profile and a failed stop of its server keep the profile status', () => { + const {child, statuses} = runLanes({FAIL_query: '3', STOP_FAIL_sdk_queries: '1'}); + assert.equal(child.status, 1); + assert.equal(statuses['sdk-queries'], '3'); + assert.match(child.stderr, /^::error::SDK direction profile sdk-queries exited 3$/m); +}); + +test('the wrapper refuses to run without an owning resource scope', () => { + const child = spawnSync('bash', [LANES, 'sqlite', 'http://native:8080'], { + encoding: 'utf8', env: {...process.env, RESOURCE_SCOPE: ''}, cwd: tmpdir(), + }); + assert.equal(child.status, 2); + assert.match(child.stderr, /Set the owning isolated network\/resource scope/); +}); From e5d2186e64d6a563e35b4c15772daf36a917e481 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:43:13 +0000 Subject: [PATCH 5/5] Run the three signal profiles in one lane: they share a task queue on the native server The namespace and selected-run signal profiles ran beside the plain signal profile, and on PostgreSQL 17 one's worker showed in another's routing diagnostics (server-parity-sdk-signals). The old serial workflow never overlapped them. A test now reads each profile's queues from its fixtures and refuses any two profiles that share one in different lanes. --- .../server-parity/sdk-direction-lanes.sh | 12 ++++---- .../ServerParitySdkDirectionLanesTest.mjs | 28 ++++++++++++++++++- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/scripts/conformance/server-parity/sdk-direction-lanes.sh b/scripts/conformance/server-parity/sdk-direction-lanes.sh index 59e3dfc80..713a18301 100755 --- a/scripts/conformance/server-parity/sdk-direction-lanes.sh +++ b/scripts/conformance/server-parity/sdk-direction-lanes.sh @@ -3,7 +3,7 @@ # three concurrent lanes. Each profile already has its own containers, PHP and # embedded databases, evidence and state directory, and its own workflow-ID # prefix on the shared native server, so profiles in different lanes cannot -# see each other. Within a lane they run in order, and each profile's PHP +# see each other unless they share a task queue. Within a lane they run in order, and each profile's PHP # server is stopped as soon as its attempt ends, so at most three run at once. # Every profile runs even after another fails; the script fails if any did, # after printing each log. The job's sdk-direction-cleanup steps remove every @@ -17,12 +17,14 @@ if [[ -z "${RESOURCE_SCOPE:-}" ]]; then fi evidence=parity-evidence/sdk-direction-lanes mkdir -p "$evidence" -# Profile variant and its tag (evidence, log and container names). The lanes are -# balanced by the durations recorded in CI (about seven to eight minutes each). +# Profile variant and its tag (evidence, log and container names). Profiles +# that share a task queue on the native server must share a lane: the three +# signal profiles all use server-parity-sdk-signals, and one's workers would +# show in another's routing diagnostics. Every other profile has its own queues. lanes=( "update:sdk-updates update-failure:sdk-update-failures child:sdk-child-directions" - "namespace-signal:sdk-namespace-signals timer:sdk-timers activity:sdk-directions" - "selected-run-signal:sdk-selected-run-signals signal:sdk-signals query:sdk-queries" + "signal:sdk-signals namespace-signal:sdk-namespace-signals selected-run-signal:sdk-selected-run-signals" + "timer:sdk-timers activity:sdk-directions query:sdk-queries" ) run_lane() { local lane=$1 entry variant tag status diff --git a/tests/Unit/ServerParitySdkDirectionLanesTest.mjs b/tests/Unit/ServerParitySdkDirectionLanesTest.mjs index 250603298..76d034787 100644 --- a/tests/Unit/ServerParitySdkDirectionLanesTest.mjs +++ b/tests/Unit/ServerParitySdkDirectionLanesTest.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import {spawnSync} from 'node:child_process'; -import {chmodSync, copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs'; +import {chmodSync, copyFileSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync} from 'node:fs'; import {tmpdir} from 'node:os'; import {join} from 'node:path'; import test from 'node:test'; @@ -131,3 +131,29 @@ test('the wrapper refuses to run without an owning resource scope', () => { assert.equal(child.status, 2); assert.match(child.stderr, /Set the owning isolated network\/resource scope/); }); + +// The native server is shared by every lane. Two profiles whose fixtures name +// the same task queue would see each other's workers, so they share a lane. +test('profiles that share a task queue run in the same lane', () => { + const script = readFileSync(LANES, 'utf8'); + const lanes = [...script.slice(script.indexOf('lanes=(')).split(')')[0].matchAll(/^ {2}"([^"]+)"$/gm)] + .map(match => match[1].split(' ').map(entry => entry.split(':')[1])); + assert.deepEqual(lanes.flat().sort(), Object.values(PROFILES).sort(), 'every profile in exactly one lane'); + const queues = tag => { + const directory = new URL(`../Fixtures/ServerParityProfiles/published-${tag}/`, import.meta.url); + const text = readdirSync(directory).filter(name => name.endsWith('.json')) + .map(name => readFileSync(new URL(name, directory), 'utf8')).join('\n'); + return new Set(text.match(/server-parity-sdk-[a-z-]+/g) ?? []); + }; + for (const [index, lane] of lanes.entries()) { + for (const other of lanes.slice(index + 1)) { + for (const tag of lane) { + for (const peer of other) { + const shared = [...queues(tag)].filter(queue => queues(peer).has(queue)); + assert.deepEqual(shared, [], `${tag} and ${peer} share ${shared} but run in different lanes`); + } + } + } + } + assert.ok(queues('sdk-signals').has('server-parity-sdk-signals'), 'queues are read from the fixtures'); +});