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', () => {