From 5287ee95b0dde0adbfd96c15a437a441606619ee Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:45:00 +0000 Subject: [PATCH] Run the published-SDK direction lanes as their own fixture part The corpus part was the Shared Server fixtures' long pole: 25 to 31 minutes per database, about 36 minutes per cycle. On run 40136659746 (postgres-17), its comparison step took 26.8 minutes, and the three SDK direction lanes took 12.7 of them, after the records, profiles and range repair and before the restart cases. The lanes depend on nothing the corpus records leave behind: each profile brings its own published PHP and embedded databases and its own workflow-ID prefix, and needs only a native node. They now run in a fourth matrix part, sdk-directions, in the MySQL, PostgreSQL and SQLite jobs, against a native node that part starts on the same bootstrapped database. Every profile and lane still runs once per database; the corpus part keeps its records, profiles, range repair and restart cases. Each lane's group header now records the profile's duration, so lane balance can be measured from the job log. On run 40136659746 the lane of the three signal profiles, which share a task queue, took 12.7 minutes; the others took 10.3 and 6.7. The parts test now requires four parts and the lanes in exactly one step, in the sdk-directions part. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/server-parity.yml | 140 +++++++++++++++--- .../server-parity/sdk-direction-lanes.sh | 9 +- tests/Unit/ServerParityPartsTest.mjs | 13 +- .../ServerParitySdkDirectionLanesTest.mjs | 6 +- 4 files changed, 139 insertions(+), 29 deletions(-) diff --git a/.github/workflows/server-parity.yml b/.github/workflows/server-parity.yml index 03ae6484f..5bac4b4dd 100644 --- a/.github/workflows/server-parity.yml +++ b/.github/workflows/server-parity.yml @@ -318,13 +318,14 @@ jobs: timeout-minutes: 60 strategy: fail-fast: false - # Each database runs in three parts on separate runners: the native + # Each database runs in four parts on separate runners: the native # database suites and TLS checks, the shared corpus with its profiles and - # restart cases, and the separate cohorts. Every part bootstraps the - # same database and native build; no part depends on another. + # restart cases, the published-SDK direction profiles, and the separate + # cohorts. Every part bootstraps the same database and native build; no + # part depends on another. matrix: label: [mysql-8.0, existing-php-mysql-matrix, mariadb-10.11] - part: [native-tests, corpus, cohorts] + part: [native-tests, corpus, cohorts, sdk-directions] include: - label: mysql-8.0 image: mirror.gcr.io/library/mysql@sha256:62fb722c78b24245ddff1796a0fcee4a49cc5b87e0aaaf20c92d1da9e0a2497b @@ -607,8 +608,6 @@ 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 - # 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 ' @@ -678,6 +677,39 @@ jobs: # check has run, so one difference cannot hide the rest. test "$native_record_status" -eq 0 test "$compare_status" -eq 0 + - name: Compare every published-SDK direction profile in three concurrent lanes + if: ${{ matrix.part == 'sdk-directions' }} + run: | + set -euo pipefail + # Bounded container log tails make a failure in this step diagnosable from its own log. + container_log_tails() { + bash scripts/conformance/server-parity/container-log-tails.sh + } + trap container_log_tails ERR + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + docker run --rm --user="$(id -u):$(id -g)" --entrypoint composer \ + -e COMPOSER_HOME=/tmp/composer -v "$PWD:/source" -w /source "$image" \ + install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress + # Each profile brings its own published PHP and embedded databases and + # shares this part's native node, as the profiles shared the corpus's. + docker run -d --name "$RESOURCE_SCOPE-native-a" --network "$RESOURCE_SCOPE" --network-alias=parity-native-a \ + --user="$(id -u):$(id -g)" --cpus=1 --memory=256m --memory-swap=256m \ + -e DW_RUST_EXPERIMENTAL=1 -e DW_AUTH_TOKEN=parity-test-token -e DW_BIND_ADDRESS=0.0.0.0:8080 \ + -e DB_CONNECTION=mysql -e DB_HOST=parity-mysql -e DB_DATABASE=native_ref \ + -e DB_USERNAME=root -e DB_PASSWORD=parity-disposable-password \ + -v "$PWD/rust-target:/target:ro" --tmpfs /state --entrypoint /target/debug/durable-workflow-server \ + mirror.gcr.io/library/rust@sha256:ba81bc3eaa4422af576c0262515d96b0111a628a6ccc2c86557cf55c9a4bbee0 + ready=0 + for attempt in $(seq 1 30); do + if docker run --rm --user=1000:1000 --network "$RESOURCE_SCOPE" --entrypoint curl "$image" \ + -fsS http://parity-native-a:8080/api/ready > parity-evidence/sdk-native-readiness.json; then + ready=1 + break + fi + sleep 1 + done + test "$ready" = 1 + bash scripts/conformance/server-parity/sdk-direction-lanes.sh mysql http://parity-native-a:8080 - name: Start the shared native node the cohorts use id: prepare_cohorts if: ${{ matrix.part == 'cohorts' }} @@ -862,13 +894,14 @@ jobs: timeout-minutes: 60 strategy: fail-fast: false - # Each database runs in three parts on separate runners: the native + # Each database runs in four parts on separate runners: the native # database suites and TLS checks, the shared corpus with its profiles and - # restart cases, and the separate cohorts. Every part bootstraps the - # same database and native build; no part depends on another. + # restart cases, the published-SDK direction profiles, and the separate + # cohorts. Every part bootstraps the same database and native build; no + # part depends on another. matrix: label: [existing-php-matrix, postgres-17] - part: [native-tests, corpus, cohorts] + part: [native-tests, corpus, cohorts, sdk-directions] include: - label: existing-php-matrix image: mirror.gcr.io/library/postgres@sha256:f1c3376c26f2609ab9f29f71f824103fe2fcd8ee0346485cb6122a4f93df6f94 @@ -1118,8 +1151,6 @@ 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 - # 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 ' @@ -1189,6 +1220,39 @@ jobs: # check has run, so one difference cannot hide the rest. test "$native_record_status" -eq 0 test "$compare_status" -eq 0 + - name: Compare every published-SDK direction profile in three concurrent lanes + if: ${{ matrix.part == 'sdk-directions' }} + run: | + set -euo pipefail + # Bounded container log tails make a failure in this step diagnosable from its own log. + container_log_tails() { + bash scripts/conformance/server-parity/container-log-tails.sh + } + trap container_log_tails ERR + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + docker run --rm --user="$(id -u):$(id -g)" --entrypoint composer \ + -e COMPOSER_HOME=/tmp/composer -v "$PWD:/source" -w /source "$image" \ + install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress + # Each profile brings its own published PHP and embedded databases and + # shares this part's native node, as the profiles shared the corpus's. + docker run -d --name "$RESOURCE_SCOPE-native-a" --network "$RESOURCE_SCOPE" --network-alias=parity-native-a \ + --user="$(id -u):$(id -g)" --cpus=1 --memory=256m --memory-swap=256m \ + -e DW_RUST_EXPERIMENTAL=1 -e DW_AUTH_TOKEN=parity-test-token -e DW_BIND_ADDRESS=0.0.0.0:8080 \ + -e DB_CONNECTION=pgsql -e DB_HOST=parity-pg -e DB_DATABASE=native_ref \ + -e DB_USERNAME=parity -e DB_PASSWORD=parity-disposable-password \ + -v "$PWD/rust-target:/target:ro" --tmpfs /state --entrypoint /target/debug/durable-workflow-server \ + mirror.gcr.io/library/rust@sha256:ba81bc3eaa4422af576c0262515d96b0111a628a6ccc2c86557cf55c9a4bbee0 + ready=0 + for attempt in $(seq 1 30); do + if docker run --rm --user=1000:1000 --network "$RESOURCE_SCOPE" --entrypoint curl "$image" \ + -fsS http://parity-native-a:8080/api/ready > parity-evidence/sdk-native-readiness.json; then + ready=1 + break + fi + sleep 1 + done + test "$ready" = 1 + bash scripts/conformance/server-parity/sdk-direction-lanes.sh pgsql http://parity-native-a:8080 - name: Start the shared native node the cohorts use id: prepare_cohorts if: ${{ matrix.part == 'cohorts' }} @@ -1417,12 +1481,12 @@ jobs: timeout-minutes: 60 strategy: fail-fast: false - # Three parts on separate runners: the native suites and codec, the - # shared corpus with its profiles and restart cases, and the separate - # cohorts. Every part builds the same native server; no part depends on - # another. + # Four parts on separate runners: the native suites and codec, the + # shared corpus with its profiles and restart cases, the published-SDK + # direction profiles, and the separate cohorts. Every part builds the + # same native server; no part depends on another. matrix: - part: [native-tests, corpus, cohorts] + part: [native-tests, corpus, cohorts, sdk-directions] env: DW_PARITY_PART: ${{ matrix.part }} steps: @@ -1635,8 +1699,6 @@ 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 - # 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 \ @@ -1705,6 +1767,46 @@ jobs: # check has run, so one difference cannot hide the rest. test "$native_record_status" -eq 0 test "$compare_status" -eq 0 + - name: Compare every published-SDK direction profile in three concurrent lanes + if: ${{ matrix.part == 'sdk-directions' }} + env: + RESOURCE_SCOPE: dw-parity-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} + run: | + set -euo pipefail + # Bounded container log tails make a failure in this step diagnosable from its own log. + container_log_tails() { + bash scripts/conformance/server-parity/container-log-tails.sh + } + trap container_log_tails ERR + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + docker run --rm --user="$(id -u):$(id -g)" --entrypoint composer \ + -e COMPOSER_HOME=/tmp/composer -v "$PWD:/source" -w /source "$image" \ + install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress + mkdir -p parity-state parity-evidence + # The profiles' published PHP containers run as 1000:1000; grant that + # user alone, not everyone. + setfacl -m u:1000:rwx,d:u:1000:rwx parity-state parity-evidence + docker network create "$RESOURCE_SCOPE" + # Each profile brings its own published PHP and embedded databases and + # shares this part's native node, as the profiles shared the corpus's. + docker run -d --name "$RESOURCE_SCOPE-rust" --network "$RESOURCE_SCOPE" --network-alias=parity-rust \ + --user="$(id -u):$(id -g)" --cpus=1 --memory=256m --memory-swap=256m \ + -e DW_RUST_EXPERIMENTAL=1 -e DW_AUTH_TOKEN=parity-test-token \ + -e DB_DATABASE=/state/rust.sqlite -e DW_BIND_ADDRESS=0.0.0.0:8080 \ + -v "$PWD/rust-target:/target:ro" -v "$PWD/parity-state:/state" \ + --entrypoint /target/debug/durable-workflow-server \ + mirror.gcr.io/library/rust@sha256:ba81bc3eaa4422af576c0262515d96b0111a628a6ccc2c86557cf55c9a4bbee0 + ready=0 + for attempt in $(seq 1 30); do + if docker run --rm --user=1000:1000 --network "$RESOURCE_SCOPE" --entrypoint curl "$image" \ + -fsS http://parity-rust:8080/api/ready > parity-evidence/sdk-native-readiness.json; then + ready=1 + break + fi + sleep 1 + done + test "$ready" = 1 + bash scripts/conformance/server-parity/sdk-direction-lanes.sh sqlite http://parity-rust:8080 - name: Start the shared native node the cohorts use id: prepare_cohorts if: ${{ matrix.part == 'cohorts' }} diff --git a/scripts/conformance/server-parity/sdk-direction-lanes.sh b/scripts/conformance/server-parity/sdk-direction-lanes.sh index 713a18301..d083f81a5 100755 --- a/scripts/conformance/server-parity/sdk-direction-lanes.sh +++ b/scripts/conformance/server-parity/sdk-direction-lanes.sh @@ -27,12 +27,14 @@ lanes=( "timer:sdk-timers activity:sdk-directions query:sdk-queries" ) run_lane() { - local lane=$1 entry variant tag status + local lane=$1 entry variant tag status started for entry in $lane; do - variant=${entry%%:*} tag=${entry#*:} status=0 + variant=${entry%%:*} tag=${entry#*:} status=0 started=$SECONDS bash scripts/conformance/server-parity/sdk-direction-profile.sh "$backend" "$native_url" "$variant" \ > "$evidence/$tag.log" 2>&1 || status=$? printf '%s\n' "$status" > "$evidence/$tag.status" + # Each profile's duration, so lane balance can be measured from the log. + printf '%s\n' "$((SECONDS - started))" > "$evidence/$tag.seconds" # 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 \ @@ -48,7 +50,8 @@ for lane in "${lanes[@]}"; do for entry in $lane; do tag=${entry#*:} status=$(cat "$evidence/$tag.status" 2>/dev/null || echo missing) - echo "::group::$tag (exit $status)" + seconds=$(cat "$evidence/$tag.seconds" 2>/dev/null || echo unknown) + echo "::group::$tag (exit $status, $seconds s)" cat "$evidence/$tag.log" echo "::endgroup::" if [[ "$status" != 0 ]]; then diff --git a/tests/Unit/ServerParityPartsTest.mjs b/tests/Unit/ServerParityPartsTest.mjs index 2e57c6de2..5a8a81b82 100644 --- a/tests/Unit/ServerParityPartsTest.mjs +++ b/tests/Unit/ServerParityPartsTest.mjs @@ -1,4 +1,4 @@ -// Each Shared Server fixtures database job runs in three parts on separate +// Each Shared Server fixtures database job runs in four parts on separate // runners. Every step either prepares the part (and so runs in every part) or // belongs to exactly one part, so no check runs twice or not at all. import test from 'node:test'; @@ -6,7 +6,7 @@ import assert from 'node:assert/strict'; import {readFileSync} from 'node:fs'; const workflow = readFileSync('.github/workflows/server-parity.yml', 'utf8'); -const PARTS = ['native-tests', 'corpus', 'cohorts']; +const PARTS = ['native-tests', 'corpus', 'cohorts', 'sdk-directions']; const EVERY_PART = [/^actions\/checkout@/, /^Fetch /, /^Create TLS fixture certificates$/, /^Bootstrap independent /, /^Collect /, /^Summarize /, /^Retain /]; @@ -39,9 +39,9 @@ function partsOf(step) { for (const [name, key] of [['mysql-execution', 'Compare three runtimes'], ['postgres-storage', 'Compare three runtimes'], ['execution-and-codec', 'Execute and compare Rust']]) { - test(`${name} runs every check in exactly one of three parts`, () => { + test(`${name} runs every check in exactly one of four parts`, () => { const block = job(name); - assert.match(block, /^ {8}part: \[native-tests, corpus, cohorts\]$/m); + assert.match(block, /^ {8}part: \[native-tests, corpus, cohorts, sdk-directions\]$/m); assert.match(block, /^ {6}DW_PARITY_PART: \$\{\{ matrix\.part \}\}$/m); assert.match(block, /^ {4}timeout-minutes: 60$/m); const all = steps(block); @@ -55,6 +55,11 @@ for (const [name, key] of [['mysql-execution', 'Compare three runtimes'], ['post } const inPart = part => all.filter(step => partsOf(step).length === 1 && partsOf(step)[0] === part).map(step => step.name); assert.ok(inPart('corpus').some(step => step.startsWith(key)), `${name}: the corpus comparison is in the corpus part`); + // The published-SDK direction lanes run once, in their own part, so the + // corpus part no longer waits for them. + assert.deepEqual(inPart('sdk-directions'), ['Compare every published-SDK direction profile in three concurrent lanes'], name); + const lanes = all.filter(step => step.text.includes('sdk-direction-lanes.sh')).map(step => step.name); + assert.deepEqual(lanes, ['Compare every published-SDK direction profile in three concurrent lanes'], `${name}: lanes run in one step`); const cohorts = inPart('cohorts'); for (const cohort of ['Start the shared native node the cohorts use', 'Compare separate configured role-token cohort', 'Back up, take over and resume a separate published PHP upgrade source', diff --git a/tests/Unit/ServerParitySdkDirectionLanesTest.mjs b/tests/Unit/ServerParitySdkDirectionLanesTest.mjs index 76d034787..4970acc80 100644 --- a/tests/Unit/ServerParitySdkDirectionLanesTest.mjs +++ b/tests/Unit/ServerParitySdkDirectionLanesTest.mjs @@ -90,7 +90,7 @@ test('all nine profiles pass and each PHP server is stopped after its profile', 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.match(child.stdout, new RegExp(`::group::${tag} \\(exit 0, \\d+ s\\)\\nprofile output for ${variant}\\n::endgroup::`)); } assert.doesNotMatch(child.stderr, /::error::|::warning::/); }); @@ -104,8 +104,8 @@ test('a failed profile still runs its lane and fails the wrapper only after ever 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'); + assert.match(child.stdout, /::group::sdk-timers \(exit 7, \d+ s\)\nprofile output for timer\n::endgroup::/); + assert.match(child.stdout, /::group::sdk-queries \(exit 0, \d+ s\)/, 'logs after the failure are still printed'); }); test('a failed stop warns without changing any profile status', () => {