diff --git a/.github/workflows/server-parity.yml b/.github/workflows/server-parity.yml index 33ca9f07b..31dac1aa8 100644 --- a/.github/workflows/server-parity.yml +++ b/.github/workflows/server-parity.yml @@ -144,7 +144,7 @@ jobs: bash scripts/ci/pull-images.sh \ mirror.gcr.io/library/rust@sha256:300ec56abce8cc9448ddea2172747d048ed902a3090e6b57babb2bf19f754081 \ mirror.gcr.io/library/python@sha256:bf44cdfcb76cd3b41e879bc058fc37ec5872002ccfde7fcb765e218cde0cd79c \ - "$(baseline php-2.11.1)" + "$(baseline php-2.11.4)" # The adapters' pinned, --locked dependencies build the same way every # run. Restore their public Cargo registry data and build tree, keyed # by the toolchain image, the runner and every tracked adapter input. @@ -415,7 +415,7 @@ jobs: bash scripts/ci/pull-images.sh \ mirror.gcr.io/library/rust@sha256:ba81bc3eaa4422af576c0262515d96b0111a628a6ccc2c86557cf55c9a4bbee0 \ "$MYSQL_IMAGE" \ - "$(baseline php-2.11.1)" \ + "$(baseline php-2.11.4)" \ "$(baseline php-2.5.14)" - name: Fetch this run's published SDK adapters uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 @@ -450,7 +450,7 @@ jobs: - name: Create TLS fixture certificates run: | set -euo pipefail - image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) mkdir -p mysql-tls parity-evidence chmod 0777 parity-evidence docker run --rm --user="$(id -u):$(id -g)" --network=none \ @@ -514,7 +514,7 @@ jobs: docker run --rm --user="$(id -u):$(id -g)" --network "$RESOURCE_SCOPE" --entrypoint mysql \ "$MYSQL_IMAGE" -h parity-mysql -uroot -pparity-disposable-password \ -e 'CREATE DATABASE native_ref CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' - image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) # The physical layout stays the frozen published 2.5.14 one; behavior # runs on the current reference after migrating that database forward. layout=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.5.14.json) @@ -563,7 +563,7 @@ jobs: 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) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) # Composer runs confined in the pinned image, never in the published image under test. bash scripts/ci/run-composer.sh --rm -v "$PWD:/source" -w /source -- \ install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress @@ -640,7 +640,7 @@ jobs: 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) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) export DW_PARITY_RUNNER_REVISION=$(git rev-parse HEAD) mkdir -p parity-consumers curl --fail --location --retry 3 --output parity-consumers/dw.phar https://github.com/durable-workflow/cli/releases/download/2.2.0/dw.phar @@ -706,7 +706,7 @@ jobs: 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) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) bash scripts/conformance/server-parity/published-sdk-patch-profile.sh mysql http://parity-php:8080 http://parity-native-a:8080 bash scripts/conformance/server-parity/repeated-patch-profile.sh mysql http://parity-php:8080 http://parity-native-a:8080 bash scripts/conformance/server-parity/legacy-marker-profile.sh mysql http://parity-php:8080 http://parity-native-a:8080 @@ -786,7 +786,7 @@ jobs: 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) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) bash scripts/ci/run-composer.sh --rm -v "$PWD:/source" -w /source -- \ install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress # Each profile brings its own published PHP and embedded databases and @@ -834,7 +834,7 @@ jobs: if: ${{ matrix.part == 'cohorts' }} run: | set -euo pipefail - image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) 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 -d --name "$RESOURCE_SCOPE-native-b" --network "$RESOURCE_SCOPE" --network-alias=parity-native-b \ @@ -918,7 +918,7 @@ jobs: if: ${{ matrix.part == 'corrected-markers' }} run: | set -euo pipefail - image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) 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 -d --name "$RESOURCE_SCOPE-native-b" --network "$RESOURCE_SCOPE" --network-alias=parity-native-b \ @@ -1048,7 +1048,7 @@ jobs: run: | set -euo pipefail mkdir -p parity-evidence parity-summary - image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) docker run --rm --user="$(id -u):$(id -g)" --network=none --cpus=1 --memory=256m --memory-swap=256m \ --entrypoint node -e PARITY_JOB_OUTCOME -e GITHUB_SHA \ -v "$PWD:/source:ro" -v "$PWD/parity-evidence:/evidence:ro" -v "$PWD/parity-summary:/summary" \ @@ -1100,7 +1100,7 @@ jobs: bash scripts/ci/pull-images.sh \ mirror.gcr.io/library/rust@sha256:ba81bc3eaa4422af576c0262515d96b0111a628a6ccc2c86557cf55c9a4bbee0 \ "$POSTGRES_IMAGE" \ - "$(baseline php-2.11.1)" \ + "$(baseline php-2.11.4)" \ "$(baseline php-2.5.14)" - name: Fetch this run's published SDK adapters uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 @@ -1174,7 +1174,7 @@ jobs: "$POSTGRES_IMAGE" --version > parity-evidence/postgres-version.txt docker run --rm --user="$(id -u):$(id -g)" --network "$RESOURCE_SCOPE" \ -e PGPASSWORD=parity-disposable-password --entrypoint createdb "$POSTGRES_IMAGE" -h parity-pg -U parity native_ref - image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) # The physical layout stays the frozen published 2.5.14 one; behavior # runs on the current reference after migrating that database forward. layout=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.5.14.json) @@ -1225,7 +1225,7 @@ jobs: 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) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) # Composer runs confined in the pinned image, never in the published image under test. bash scripts/ci/run-composer.sh --rm -v "$PWD:/source" -w /source -- \ install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress @@ -1303,7 +1303,7 @@ jobs: 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) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) export DW_PARITY_RUNNER_REVISION=$(git rev-parse HEAD) mkdir -p parity-consumers curl --fail --location --retry 3 --output parity-consumers/dw.phar https://github.com/durable-workflow/cli/releases/download/2.2.0/dw.phar @@ -1371,7 +1371,7 @@ jobs: 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) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) bash scripts/conformance/server-parity/published-sdk-patch-profile.sh pgsql http://parity-php:8080 http://parity-native-a:8080 bash scripts/conformance/server-parity/repeated-patch-profile.sh pgsql http://parity-php:8080 http://parity-native-a:8080 bash scripts/conformance/server-parity/legacy-marker-profile.sh pgsql http://parity-php:8080 http://parity-native-a:8080 @@ -1451,7 +1451,7 @@ jobs: 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) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) bash scripts/ci/run-composer.sh --rm -v "$PWD:/source" -w /source -- \ install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress # Each profile brings its own published PHP and embedded databases and @@ -1498,7 +1498,7 @@ jobs: if: ${{ matrix.part == 'cohorts' }} run: | set -euo pipefail - image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) 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 -d --name "$RESOURCE_SCOPE-native-b" --network "$RESOURCE_SCOPE" --network-alias=parity-native-b \ @@ -1584,7 +1584,7 @@ jobs: if: ${{ matrix.part == 'corrected-markers' }} run: | set -euo pipefail - image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) 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 -d --name "$RESOURCE_SCOPE-native-b" --network "$RESOURCE_SCOPE" --network-alias=parity-native-b \ @@ -1756,7 +1756,7 @@ jobs: run: | set -euo pipefail mkdir -p parity-evidence parity-summary - image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) docker run --rm --user="$(id -u):$(id -g)" --network=none --cpus=1 --memory=256m --memory-swap=256m \ --entrypoint node -e PARITY_JOB_OUTCOME -e GITHUB_SHA \ -v "$PWD:/source:ro" -v "$PWD/parity-evidence:/evidence:ro" -v "$PWD/parity-summary:/summary" \ @@ -1799,7 +1799,7 @@ jobs: } bash scripts/ci/pull-images.sh \ mirror.gcr.io/library/rust@sha256:ba81bc3eaa4422af576c0262515d96b0111a628a6ccc2c86557cf55c9a4bbee0 \ - "$(baseline php-2.11.1)" \ + "$(baseline php-2.11.4)" \ "$(baseline php-2.5.14)" - name: Fetch this run's published SDK adapters uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 @@ -1870,7 +1870,7 @@ jobs: -v "$PWD/parity-evidence:/evidence" -w /source/rust --entrypoint bash \ mirror.gcr.io/library/rust@sha256:ba81bc3eaa4422af576c0262515d96b0111a628a6ccc2c86557cf55c9a4bbee0 -c \ 'cargo test --locked --offline --all-targets > /evidence/rust-tests.txt && cargo build --locked --offline && cargo run --locked --offline --example codec-fixtures -- ../tests/Fixtures/ServerParity/Codec/v1.json > /evidence/rust-codec.json' - image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) docker run --rm --user="$(id -u):$(id -g)" --network=none --entrypoint php \ -v "$PWD:/source:ro" -v "$PWD/parity-evidence:/evidence" -w /source "$image" \ scripts/conformance/server-parity/codec.php tests/Fixtures/ServerParity/Codec/v1.json \ @@ -1894,7 +1894,7 @@ jobs: 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) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) mkdir -p parity-state parity-evidence chmod 0777 parity-state parity-evidence touch parity-state/php.sqlite parity-state/embedded.sqlite parity-state/php-layout.sqlite @@ -1965,7 +1965,7 @@ jobs: 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) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) export DW_PARITY_RUNNER_REVISION=$(git rev-parse HEAD) mkdir -p parity-consumers curl --fail --location --retry 3 --output parity-consumers/dw.phar https://github.com/durable-workflow/cli/releases/download/2.2.0/dw.phar @@ -1976,6 +1976,7 @@ jobs: -e DW_PARITY_TOKEN=parity-test-token -e DW_PARITY_RUNNER_REVISION -e DW_PARITY_CLI_PHAR "$image" scripts/conformance/server-parity.mjs record \ --mode http --url http://parity-server:8080 --target php --output /evidence/php.json \ --exclude-fixture tests/Fixtures/ServerParity/update-validation.json \ + --exclude-fixture tests/Fixtures/ServerParity/service-calls.json \ --include-fixture tests/Fixtures/ServerParityPending/patch-insertion-activity-pending.json \ --include-fixture tests/Fixtures/ServerParityPending/patch-insertion-activity-completed.json \ --include-fixture tests/Fixtures/ServerParityPending/patch-created-activity-pending.json \ @@ -1989,6 +1990,7 @@ jobs: -e CACHE_STORE=file -e QUEUE_CONNECTION=database -e DW_PARITY_RUNNER_REVISION "$image" scripts/conformance/server-parity.mjs record \ --mode embedded --application-root /app --target embedded --output /evidence/embedded.json \ --exclude-fixture tests/Fixtures/ServerParity/update-validation.json \ + --exclude-fixture tests/Fixtures/ServerParity/service-calls.json \ --include-fixture tests/Fixtures/ServerParityPending/patch-insertion-activity-pending.json \ --include-fixture tests/Fixtures/ServerParityPending/patch-insertion-activity-completed.json \ --include-fixture tests/Fixtures/ServerParityPending/patch-created-activity-pending.json \ @@ -2002,6 +2004,7 @@ jobs: -e DW_PARITY_TOKEN=parity-test-token -e DW_PARITY_RUNNER_REVISION -e DW_PARITY_CLI_PHAR "$image" scripts/conformance/server-parity.mjs record \ --mode http --url http://parity-rust:8080 --target rust --output /evidence/rust.json \ --exclude-fixture tests/Fixtures/ServerParity/update-validation.json \ + --exclude-fixture tests/Fixtures/ServerParity/service-calls.json \ --include-fixture tests/Fixtures/ServerParityPending/patch-insertion-activity-pending.json \ --include-fixture tests/Fixtures/ServerParityPending/patch-insertion-activity-completed.json \ --include-fixture tests/Fixtures/ServerParityPending/patch-created-activity-pending.json \ @@ -2014,6 +2017,7 @@ jobs: -v "$PWD:/source:ro" -v "$PWD/parity-evidence:/evidence" -w /source "$image" \ scripts/conformance/server-parity.mjs compare /evidence/php.json /evidence/rust.json /evidence/embedded.json \ --exclude-fixture tests/Fixtures/ServerParity/update-validation.json \ + --exclude-fixture tests/Fixtures/ServerParity/service-calls.json \ --include-fixture tests/Fixtures/ServerParityPending/patch-insertion-activity-pending.json \ --include-fixture tests/Fixtures/ServerParityPending/patch-insertion-activity-completed.json \ --include-fixture tests/Fixtures/ServerParityPending/patch-created-activity-pending.json \ @@ -2023,13 +2027,19 @@ jobs: --include-fixture tests/Fixtures/ServerParityPending/workflow-task-transient-retry.json > parity-evidence/comparison.json || compare_status=$? # Server #723: update-validation records apart from the main corpus, and # its published PHP record runs with PHP's maintenance loop stopped. + # Server #796: so does service-calls until the reference carries that + # fix. Under the loop's writer contention published PHP has refused a + # limited operation's first call. update_validation_status=0 bash scripts/conformance/server-parity/update-validation-sqlite.sh http://parity-server:8080 http://parity-rust:8080 || update_validation_status=$? + service_calls_status=0 + bash scripts/conformance/server-parity/update-validation-sqlite.sh http://parity-server:8080 http://parity-rust:8080 service-calls || service_calls_status=$? # A native fixture difference fails the step only after the # comparison has run, so one difference cannot hide the rest. test "$native_record_status" -eq 0 test "$compare_status" -eq 0 test "$update_validation_status" -eq 0 + test "$service_calls_status" -eq 0 - name: Compare published patch, legacy-marker and range-repair profiles if: ${{ matrix.part == 'restarts' }} env: @@ -2041,7 +2051,7 @@ jobs: 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) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) bash scripts/conformance/server-parity/published-sdk-patch-profile.sh sqlite http://parity-server:8080 http://parity-rust:8080 bash scripts/conformance/server-parity/repeated-patch-profile.sh sqlite http://parity-server:8080 http://parity-rust:8080 bash scripts/conformance/server-parity/legacy-marker-profile.sh sqlite http://parity-server:8080 http://parity-rust:8080 @@ -2122,7 +2132,7 @@ jobs: 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) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) bash scripts/ci/run-composer.sh --rm -v "$PWD:/source" -w /source -- \ install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress mkdir -p parity-state parity-evidence @@ -2166,7 +2176,7 @@ jobs: RESOURCE_SCOPE: dw-parity-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} run: | set -euo pipefail - image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) bash scripts/ci/run-composer.sh --rm -v "$PWD:/source" -w /source -- \ install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress mkdir -p parity-state parity-evidence @@ -2269,7 +2279,7 @@ jobs: RESOURCE_SCOPE: dw-parity-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} run: | set -euo pipefail - image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) bash scripts/ci/run-composer.sh --rm -v "$PWD:/source" -w /source -- \ install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress mkdir -p parity-state parity-evidence @@ -2387,7 +2397,7 @@ jobs: run: | set -euo pipefail mkdir -p parity-evidence parity-summary - image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) + image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) docker run --rm --user="$(id -u):$(id -g)" --network=none --cpus=1 --memory=256m --memory-swap=256m \ --entrypoint node -e PARITY_JOB_OUTCOME -e GITHUB_SHA \ -v "$PWD:/source:ro" -v "$PWD/parity-evidence:/evidence:ro" -v "$PWD/parity-summary:/summary" \ diff --git a/docs/rust-server-port-checkpoints/2026-10-27-reference-2114.md b/docs/rust-server-port-checkpoints/2026-10-27-reference-2114.md new file mode 100644 index 000000000..1dc74adf2 --- /dev/null +++ b/docs/rust-server-port-checkpoints/2026-10-27-reference-2114.md @@ -0,0 +1,52 @@ +# October 27, the parity reference moves to published 2.11.4 + +- **Before.** The Shared Server fixtures compared native and embedded + against published Server 2.11.1 (Workflow 2.10.2). +- **Reference.** Published Server 2.11.4, Core 2.14.2, as General and + Security verified it: + - source `ad3ac5fda0a30a58084b2d68569f2d91e23fabf3`; + - linux/amd64 `sha256:bee30cde5b70a429561a49f25daf4ef71a1e405d43322f78ff1b8137029c80f3`; + - linux/arm64 `sha256:cc297aa7d1f81b8bfa1169de6980dabe58d9776dcf513f379dcba2a51b1f5cc7`. + + `tests/Fixtures/ServerParityBaselines/php-2.11.4.json` records it. The SDK, + CLI, Waterline and sample-app pins are unchanged. The takeover cohorts are + not affected: each names its own source release and layout (`php2111` + still takes over published 2.11.1 as `php-2.11.1`). 2.11.4 has no schema + change from 2.11.3. Its API route table is the same as 2.11.1's, so the + route inventory still holds. +- **Expectation changes**, all Core 2.14 behavior: + - `repair-pass`: the report adds the dispatch-outbox counts + (`service_call_dispatch_candidates`, `service_call_dispatches_enqueued`, + `service_call_dispatch_failures`). + - `service-calls`: + - an execute request's `wait_timeout_seconds` is bounded at 60; + - handler metadata names `service_call_idempotency_key` (null for a call + dispatched when it is accepted). +- **Native.** + - The repair report adds the three counts as zeros: native runs a call's + handler when it accepts the call, so it has no outbox. + - The execute bound is 60. + - Handler metadata carries the null key. + - Task-queue views report published 2.11.3's `poll_transport` as + `unavailable`, since native records no early poll disconnects. Every poll + returns the ordinary JSON response, declining the opt-in progress + transport; SDKs must accept both. +- **Boundary: Workflow #931.** `service-calls` no longer repeats an + idempotency key from another caller namespace. + - Published 2.11.1 created a separate call for that caller. + - 2.11.3 and 2.11.4 replay the first call and rebind its handler identity. + - The prepared correction refuses the foreign caller. + + Recorded against 2.11.4, that step and the three that follow from it were + the only differences in the whole selection. The step returns, with the + corrected answer for native too, once a release carries the correction. + Until then native keeps 2.11.1's separate call. +- **Checked.** + - CI's fixture selection (the corpus without `update-validation`) recorded + on SQLite against published 2.11.4. Before the boundary, only + `service-calls` differed. + - After it, `repair-pass`, `service-calls` and `storage-test` pass against + both 2.11.4 and native from this branch, and the two records compare + equal. + - The service-calls contract test passes 22/22. CI runs the full selection + on all four databases. diff --git a/docs/rust-server-port.md b/docs/rust-server-port.md index 917de2887..70e7d4008 100644 --- a/docs/rust-server-port.md +++ b/docs/rust-server-port.md @@ -30,32 +30,34 @@ Updated October 27, 2026. maintenance repair loop (opt-in, `DW_RUST_MAINTENANCE_LOOP=1`); takeover keeps runtime credentials and update validation tasks; takeover of published 2.11.3, fresh and upgraded from 2.9.x. -- **In the queue or next, in order.** - 1. [#799](https://github.com/durable-workflow/server/pull/799): the fixture - adapters' Cargo cache, saved only by pushes to main. - 2. Message-stream retention (PHP's `MessageStreamRetentionCleanup`), and - takeover that keeps message and workflow streams. - 3. The native release-candidate image (`Dockerfile.native`), its release - workflow and the takeover runbook's image commands. - 4. The parity reference moves to published 2.11.3. Native already matches it - on 62 of 65 fixtures. The move waits on two published 2.11.3 defects - General owns: the storage diagnostic (#789) and cross-caller idempotent - replay (Workflow #931). + - [#799](https://github.com/durable-workflow/server/pull/799): the fixture + adapters' Cargo cache, saved only by pushes to main. + - [#801](https://github.com/durable-workflow/server/pull/801): + message-stream retention (PHP's `MessageStreamRetentionCleanup`), and + takeover that keeps message and workflow streams. +- **In the queue or next.** + - [#803](https://github.com/durable-workflow/server/pull/803): the native + release-candidate image (`Dockerfile.native`), its release workflow and + the takeover runbook's image commands. + - The parity reference moves to published 2.11.4 + ([checkpoint](rust-server-port-checkpoints/2026-10-27-reference-2114.md)). - **Still refused at takeover:** workflow-to-workflow messages (`workflow_messages`, written only by embedded PHP workflows) and external payload references. PHP keeps references in about 30 table and column locations in their URI form, so taking them over is its own project; the recommendation is that the release candidate keeps refusing them. -- **Shared Server fixtures.** Native, embedded and published PHP 2.11.1 are +- **Shared Server fixtures.** Native, embedded and published PHP 2.11.4 are compared on SQLite, MySQL 8.0, MariaDB 10.11 and PostgreSQL 17. Where the published reference itself misbehaves, an explicit boundary separates it from native, and the PHP owner has an issue for each case. Recent ones: + - Workflow #931, an idempotency key repeated from another caller namespace + (left out of `service-calls` until a release carries the correction); - #796, `service-calls` under full-corpus SQLite load; - #794, workflow-bound Nexus handlers. No expectation is edited to make native pass. - **Parity surface.** Native answers 174 of the 175 API route methods in - published 2.11.1's route table. Eight of those are the prepared + published 2.11.1's route table, which 2.11.4 leaves unchanged. Eight of those are the prepared local-activity routes, which it answers with PHP's refusal for a bridge without the prepared role. Nexus workflow-bound handler kinds answer 501 until #794 settles their contract. An execution test sends each route @@ -63,7 +65,7 @@ Updated October 27, 2026. `501 rust_capability_not_implemented` must match [`unserved-php-routes.v2.11.1.txt`](../rust/tests/fixtures/unserved-php-routes.v2.11.1.txt). - **Remaining for the release candidate:** - - the 2.11.3 reference move; + - landing the 2.11.4 reference move; - the scope question with the operator: prepared local activities, workflow-bound Nexus handlers and external payload takeover; - publishing the native image (General runs the release); diff --git a/rust/src/runtime/repair_pass.rs b/rust/src/runtime/repair_pass.rs index 8c48a90b3..1b7459901 100644 --- a/rust/src/runtime/repair_pass.rs +++ b/rust/src/runtime/repair_pass.rs @@ -102,6 +102,11 @@ fn empty_report(request: &PassRequest, throttled: bool) -> Map { "deadline_expired_failures": [], "activity_timeout_candidates": 0, "activity_timeouts_enforced": 0, "activity_timeout_failures": [], "service_call_timeout_candidates": 0, "service_call_timeouts_enforced": 0, "service_call_timeout_failures": [], + // Published 2.11.3 (Core 2.14) also reports its service-call dispatch + // outbox. Native runs a call's handler when it accepts the call, so it + // never has a dispatch to enqueue. + "service_call_dispatch_candidates": 0, "service_call_dispatches_enqueued": 0, + "service_call_dispatch_failures": [], }); match value { Value::Object(map) => map, diff --git a/rust/src/runtime/service_calls.rs b/rust/src/runtime/service_calls.rs index ebedfab7c..a5978c0e2 100644 --- a/rust/src/runtime/service_calls.rs +++ b/rust/src/runtime/service_calls.rs @@ -64,7 +64,8 @@ const EXECUTE_RULES: &laravel_rules::Rules = &[ ("payload_codec", &[Rule::Nullable, Rule::String, Rule::Max(191)]), ("mode_override", &[Rule::Nullable, Rule::String, Rule::In(MODES)]), ("wait_for", &[Rule::Nullable, Rule::String, Rule::In(&["accepted", "completed"])]), - ("wait_timeout_seconds", &[Rule::Nullable, Rule::Integer, Rule::Min(0), Rule::Max(86400)]), + // Published 2.11.3 bounds an update-bound wait at 60 seconds (#775). + ("wait_timeout_seconds", &[Rule::Nullable, Rule::Integer, Rule::Min(0), Rule::Max(60)]), ("idempotency_key", &[Rule::Nullable, Rule::String, Rule::Max(191)]), ("caller_namespace", &[Rule::Nullable, Rule::String, Rule::Max(255)]), ("caller_workflow_instance_id", &[Rule::Nullable, Rule::String, Rule::Max(191)]), @@ -1732,7 +1733,10 @@ store_methods! { return Self::handler_failure(connection, call, "handler_target_missing", "Activity binding has no activity class or type.", attempt, max).await; }; let id = binding_string(&binding, &["activity_execution_id"]).unwrap_or_else(upper_ulid); - let mut metadata = json!({"activity_execution_id": id, "activity_class": class, "activity_type": kind}); + // Core 2.14 names the dispatch attempt's idempotency key; a call + // dispatched when it is accepted has none. + let mut metadata = json!({"activity_execution_id": id, "activity_class": class, "activity_type": kind, + "service_call_idempotency_key": null}); for (key, value) in [("connection", &execution.connection), ("queue", &execution.queue)] { if let Some(value) = binding_string(&binding, &[key]).or_else(|| value.clone()) { metadata[key] = json!(value); @@ -1755,7 +1759,7 @@ store_methods! { let handler = binding_string(&binding, &["carrier_handler", "handler", "handler_name"]); let carrier = binding_string(&binding, &["carrier", "carrier_name"]); let id = binding_string(&binding, &["carrier_request_id"]).unwrap_or_else(upper_ulid); - let mut metadata = json!({"carrier_request_id": id, "carrier_endpoint": endpoint}); + let mut metadata = json!({"carrier_request_id": id, "carrier_endpoint": endpoint, "service_call_idempotency_key": null}); if let Some(handler) = &handler { metadata["carrier_handler"] = json!(handler); } diff --git a/rust/src/runtime/task_queues.rs b/rust/src/runtime/task_queues.rs index 68d9129bd..7e8ad23a7 100644 --- a/rust/src/runtime/task_queues.rs +++ b/rust/src/runtime/task_queues.rs @@ -371,6 +371,14 @@ where "current_leases":self.current_leases(queue, at).await?,"repair":self.repair_stats(queue, at).await?})) } + /// `WorkerPollDiagnostics::snapshot` as published 2.11.3 reports it when + /// its observation store is unavailable. Native keeps no record of worker + /// polls that ended early, so it never claims `no_recent_observation`. + /// PHP's retention for an observation is 15 minutes. + fn poll_transport() -> Value { + json!({"status":"unavailable","retention_seconds":900,"last_disconnect":null}) + } + /// One queue's detail with recent flow, activity attempts and admission. async fn queue_view(&self, queue: &str, config: &QueryTaskConfig, detail: bool) -> Result { let at = now(); @@ -395,9 +403,9 @@ where }); Ok(if detail { json!({"name":queue,"pollers":pollers,"stats":stats,"current_leases":base["current_leases"], - "repair":repair,"admission":admission}) + "repair":repair,"admission":admission,"poll_transport":Self::poll_transport()}) } else { - json!({"name":queue,"stats":stats,"repair":repair,"admission":admission}) + json!({"name":queue,"stats":stats,"repair":repair,"admission":admission,"poll_transport":Self::poll_transport()}) }) } diff --git a/rust/tests/execution/command_requests.rs b/rust/tests/execution/command_requests.rs index e156b55e9..7a31c1856 100644 --- a/rust/tests/execution/command_requests.rs +++ b/rust/tests/execution/command_requests.rs @@ -80,9 +80,10 @@ async fn update_wait_timeout_is_bounded_before_admission() { } // Server #775: a service operation bound to a workflow update hands its wait -// to the update admission. Native dispatches no workflow-bound handler yet, so -// neither the execute route nor the workflow-task command can carry a longer -// wait here. +// to the update admission. Published 2.11.3 bounds an execute request's wait +// at 60 seconds before routing, so a longer one is a validation failure. +// Native dispatches no workflow-bound handler yet, so a wait within the bound +// reaches the unserved handler kind, and the workflow-task command is refused. #[tokio::test] async fn service_operations_cannot_carry_an_update_wait_past_the_ceiling() { let database = TestDatabase::new().await; @@ -103,7 +104,10 @@ async fn service_operations_cannot_carry_an_update_wait_past_the_ceiling() { let (status, created) = request(&app, "POST", path, body).await; assert_eq!(status, StatusCode::CREATED, "{created}"); } - let (status, unserved) = request(&app, "POST", "/api/service-endpoints/billing/services/approvals/operations/approve/execute", - json!({"wait_timeout_seconds":61})).await; + let execute = "/api/service-endpoints/billing/services/approvals/operations/approve/execute"; + let (status, refused) = request(&app, "POST", execute, json!({"wait_timeout_seconds":61})).await; + assert_eq!((status, refused["reason"].clone()), (StatusCode::UNPROCESSABLE_ENTITY, json!("validation_failed")), "{refused}"); + assert_eq!(refused["errors"]["wait_timeout_seconds"][0], "The wait timeout seconds field must not be greater than 60.", "{refused}"); + let (status, unserved) = request(&app, "POST", execute, json!({"wait_timeout_seconds":60})).await; assert_eq!((status, unserved["reason"].clone()), (StatusCode::NOT_IMPLEMENTED, json!("rust_capability_not_implemented")), "{unserved}"); } diff --git a/rust/tests/execution/service_calls.rs b/rust/tests/execution/service_calls.rs index 09785f5b7..eb1da06f8 100644 --- a/rust/tests/execution/service_calls.rs +++ b/rust/tests/execution/service_calls.rs @@ -312,7 +312,7 @@ async fn execute_requests_are_checked_like_the_controller() { (json!({"arguments": {"codec": "avro", "blob": 5}}), "arguments.blob", "The payload envelope blob must be a string."), (json!({"search_attributes": {"1bad": "x"}}), "search_attributes", "Search attribute key [1bad] must start with a letter"), (json!({"mode_override": "fast"}), "mode_override", "The selected mode override is invalid."), - (json!({"wait_timeout_seconds": 86401}), "wait_timeout_seconds", "The wait timeout seconds field must not be greater than 86400."), + (json!({"wait_timeout_seconds": 61}), "wait_timeout_seconds", "The wait timeout seconds field must not be greater than 60."), ] { let (status, refused) = execute(&app, "charge", body.clone()).await; assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY, "{body}: {refused}"); diff --git a/rust/tests/execution/waterline_reads.rs b/rust/tests/execution/waterline_reads.rs index be9146b37..9959b4b1f 100644 --- a/rust/tests/execution/waterline_reads.rs +++ b/rust/tests/execution/waterline_reads.rs @@ -24,6 +24,11 @@ async fn task_queue_views_follow_registrations_and_tasks() { assert_eq!(status, StatusCode::OK, "{queue}"); assert_eq!(queue["pollers"][0]["worker_id"], "queue-worker", "{queue}"); assert_eq!(queue["admission"]["query_tasks"]["status"], "accepting", "{queue}"); + // Published 2.11.3's poll diagnostics, reported as unobserved: native keeps + // no record of worker polls that ended early. + let unobserved = json!({"status":"unavailable","retention_seconds":900,"last_disconnect":null}); + assert_eq!(queue["poll_transport"], unobserved, "{queue}"); + assert_eq!(queues[0]["poll_transport"], unobserved, "{list}"); let (status, other) = get(&app, "queues", "/api/task-queues").await; assert_eq!(status, StatusCode::OK, "{other}"); assert_eq!(other["task_queues"], json!([]), "{other}"); diff --git a/scripts/conformance/server-parity.mjs b/scripts/conformance/server-parity.mjs index 175a27757..b3ca96a0d 100644 --- a/scripts/conformance/server-parity.mjs +++ b/scripts/conformance/server-parity.mjs @@ -44,7 +44,7 @@ Requires Node 20+ and PHP 8.3+ with the locked adapter installed. Each target must have its own isolated, already bootstrapped database. Set DW_PARITY_TOKEN for the HTTP target; it is never recorded. Use the same --prefix on all targets and a fresh database or prefix for a new run. --artifacts selects the exact -consumer tuple; the default is tests/Fixtures/ServerParityBaselines/php-2.11.1.json. +consumer tuple; the default is tests/Fixtures/ServerParityBaselines/php-2.11.4.json. Repeat --fixture PATH to record or compare an explicitly selected candidate set. --database-resource-limits accepts inspected memory_bytes and memory_swap_bytes as a JSON object; comparisons require equal effective @@ -81,7 +81,7 @@ try { } const record = { schema: 'durable-workflow.server-parity-record/v1', target: values.target, mode: values.mode, - runner_revision: values['runner-revision'], artifacts: json(values.artifacts ?? resolve(root, 'tests/Fixtures/ServerParityBaselines/php-2.11.1.json')), + runner_revision: values['runner-revision'], artifacts: json(values.artifacts ?? resolve(root, 'tests/Fixtures/ServerParityBaselines/php-2.11.4.json')), started_at: new Date().toISOString(), outcome: 'runner-blocked', fixture_hashes: hashes, cases: [], database_resource_limits: parseDatabaseResourceLimits(values['database-resource-limits']), }; diff --git a/scripts/conformance/server-parity/README.md b/scripts/conformance/server-parity/README.md index 7ca4e4f67..0cf7c2674 100644 --- a/scripts/conformance/server-parity/README.md +++ b/scripts/conformance/server-parity/README.md @@ -282,7 +282,7 @@ Docker kill and verifies exit code 137 before restarting the native process. For an adapter installed elsewhere, set `DW_PARITY_SDK_AUTOLOAD` to its locked `vendor/autoload.php`. `--artifacts` selects a frozen tuple manifest. The default -is `tests/Fixtures/ServerParityBaselines/php-2.11.1.json`; its SDK version must match +is `tests/Fixtures/ServerParityBaselines/php-2.11.4.json`; its SDK version must match the installed lock. A frozen tuple does not prove every listed consumer ran. The original `tests/Fixtures/ServerParity/php-baseline.json` remains an immutable 2.5.13 / Workflow 2.5.4 reference, together with its known NUL reproducer outside diff --git a/scripts/conformance/server-parity/role-profile.sh b/scripts/conformance/server-parity/role-profile.sh index 0fa79101c..1681daef4 100644 --- a/scripts/conformance/server-parity/role-profile.sh +++ b/scripts/conformance/server-parity/role-profile.sh @@ -8,7 +8,7 @@ test -n "$RESOURCE_SCOPE" scope_identity="$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$GITHUB_JOB" case "$RESOURCE_SCOPE" in *"$scope_identity"*) ;; *) exit 2 ;; esac backend="$1" -image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) +image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) rust_image=mirror.gcr.io/library/rust@sha256:ba81bc3eaa4422af576c0262515d96b0111a628a6ccc2c86557cf55c9a4bbee0 fixture=tests/Fixtures/ServerParityProfiles/role-tokens/admission-role-tokens.json credentials=(-e DW_AUTH_TOKEN=parity-role-legacy -e DW_WORKER_TOKEN=parity-role-worker diff --git a/scripts/conformance/server-parity/update-validation-sqlite.sh b/scripts/conformance/server-parity/update-validation-sqlite.sh index e4c4a285b..c861f1bf5 100644 --- a/scripts/conformance/server-parity/update-validation-sqlite.sh +++ b/scripts/conformance/server-parity/update-validation-sqlite.sh @@ -1,6 +1,11 @@ #!/usr/bin/env bash +# update-validation-sqlite.sh [fixture name] +# # Server #723. The SQLite job records the update-validation fixture apart from -# its main corpus, for all three targets, and compares the three records. +# its main corpus, for all three targets, and compares the three records. The +# optional third argument names another fixture recorded the same way: +# service-calls, whose published PHP record under SQLite writer contention has +# refused the first call to a limited operation with 429 (Server #796). # # The published PHP record runs with PHP's maintenance loop stopped. On SQLite # the API server and that loop share one database file. Under the loop's writer @@ -26,10 +31,13 @@ set -euo pipefail php_url="$1" native_url="$2" +name="${3:-update-validation}" +[[ "$name" =~ ^[a-z0-9-]+$ ]] [[ -n "$RESOURCE_SCOPE" ]] [[ -n "$DW_PARITY_RUNNER_REVISION" ]] -fixture=tests/Fixtures/ServerParity/update-validation.json -image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.1.json) +fixture="tests/Fixtures/ServerParity/$name.json" +test -f "$fixture" +image=$(jq -r '.server.images["linux/amd64"] | sub("^durableworkflow/server@"; "ghcr.io/durable-workflow/server@")' tests/Fixtures/ServerParityBaselines/php-2.11.4.json) . scripts/conformance/server-parity/php-maintenance-window.sh prefix=parity-v1 @@ -38,7 +46,7 @@ http_record() { docker run --rm --user=1000:1000 --entrypoint node --network "$RESOURCE_SCOPE" \ -v "$PWD:/source:ro" -v "$PWD/parity-evidence:/evidence" -w /source \ -e DW_PARITY_TOKEN=parity-test-token -e DW_PARITY_RUNNER_REVISION "$image" scripts/conformance/server-parity.mjs record \ - --mode http --url "$url" --target "$target" --output "/evidence/update-validation-$target.json" --fixture "$fixture" \ + --mode http --url "$url" --target "$target" --output "/evidence/$name-$target.json" --fixture "$fixture" \ --prefix "$prefix" } @@ -53,35 +61,35 @@ php_record() { record_finished=$(date -u +%Y-%m-%dT%H:%M:%S.%NZ) } -run_without_maintenance parity-evidence/update-validation-maintenance-schedule.txt -- php_record +run_without_maintenance "parity-evidence/$name-maintenance-schedule.txt" -- php_record if ((php_status != 0)); then - locked=parity-evidence/update-validation-php.attempt-1.locked.txt + locked="parity-evidence/$name-php.attempt-1.locked.txt" { docker logs --since "$record_started" --until "$record_finished" "$RESOURCE_SCOPE-server" 2>&1 || true; } \ | grep -m 5 'database is locked' | cut -c 1-400 > "$locked" || true if [[ ! -s "$locked" ]]; then rm -f "$locked" exit "$php_status" fi - for name in update-validation-php.json update-validation-php.failure-excerpt.txt; do - if [[ -e "parity-evidence/$name" ]]; then - mv "parity-evidence/$name" "parity-evidence/${name/update-validation-php./update-validation-php.attempt-1.}" + for file in "$name-php.json" "$name-php.failure-excerpt.txt"; do + if [[ -e "parity-evidence/$file" ]]; then + mv "parity-evidence/$file" "parity-evidence/${file/$name-php./$name-php.attempt-1.}" fi done - excerpt=$(tr '\n' ' ' < parity-evidence/update-validation-php.attempt-1.failure-excerpt.txt 2>/dev/null || true) - echo "::warning title=Published PHP update-validation record retried::The PHP record failed (${excerpt:-no excerpt}) after published PHP logged 'database is locked'; recording all three targets under a fresh prefix (Server #754)." + excerpt=$(tr '\n' ' ' < "parity-evidence/$name-php.attempt-1.failure-excerpt.txt" 2>/dev/null || true) + echo "::warning title=Published PHP $name record retried::The PHP record failed (${excerpt:-no excerpt}) after published PHP logged 'database is locked'; recording all three targets under a fresh prefix (Server #754)." prefix=parity-v1-retry - run_without_maintenance parity-evidence/update-validation-maintenance-schedule-retry.txt -- php_record + run_without_maintenance "parity-evidence/$name-maintenance-schedule-retry.txt" -- php_record ((php_status == 0)) || exit "$php_status" fi docker run --rm --user=1000:1000 --entrypoint node \ -v "$PWD:/source:ro" -v "$PWD/parity-state:/state" -v "$PWD/parity-evidence:/evidence" -w /source \ -e DB_CONNECTION=sqlite -e DB_DATABASE=/state/embedded.sqlite -e DW_MODE=embedded \ -e CACHE_STORE=file -e QUEUE_CONNECTION=database -e DW_PARITY_RUNNER_REVISION "$image" scripts/conformance/server-parity.mjs record \ - --mode embedded --application-root /app --target embedded --output /evidence/update-validation-embedded.json --fixture "$fixture" \ + --mode embedded --application-root /app --target embedded --output "/evidence/$name-embedded.json" --fixture "$fixture" \ --prefix "$prefix" http_record "$native_url" rust docker run --rm --user=1000:1000 --entrypoint node \ -v "$PWD:/source:ro" -v "$PWD/parity-evidence:/evidence" -w /source "$image" \ scripts/conformance/server-parity.mjs compare \ - /evidence/update-validation-php.json /evidence/update-validation-rust.json /evidence/update-validation-embedded.json \ - --fixture "$fixture" > parity-evidence/update-validation-comparison.json + "/evidence/$name-php.json" "/evidence/$name-rust.json" "/evidence/$name-embedded.json" \ + --fixture "$fixture" > "parity-evidence/$name-comparison.json" diff --git a/tests/Fixtures/ServerParity/repair-pass.json b/tests/Fixtures/ServerParity/repair-pass.json index 8bc98874c..d18609afb 100644 --- a/tests/Fixtures/ServerParity/repair-pass.json +++ b/tests/Fixtures/ServerParity/repair-pass.json @@ -233,7 +233,10 @@ "activity_timeout_failures": [], "service_call_timeout_candidates": 0, "service_call_timeouts_enforced": 0, - "service_call_timeout_failures": [] + "service_call_timeout_failures": [], + "service_call_dispatch_candidates": 0, + "service_call_dispatches_enqueued": 0, + "service_call_dispatch_failures": [] } ], "trimmed filters": [ @@ -269,7 +272,10 @@ "activity_timeout_failures": [], "service_call_timeout_candidates": 0, "service_call_timeouts_enforced": 0, - "service_call_timeout_failures": [] + "service_call_timeout_failures": [], + "service_call_dispatch_candidates": 0, + "service_call_dispatches_enqueued": 0, + "service_call_dispatch_failures": [] } ], "empty instance": [ @@ -304,7 +310,10 @@ "activity_timeout_failures": [], "service_call_timeout_candidates": 0, "service_call_timeouts_enforced": 0, - "service_call_timeout_failures": [] + "service_call_timeout_failures": [], + "service_call_dispatch_candidates": 0, + "service_call_dispatches_enqueued": 0, + "service_call_dispatch_failures": [] } ], "run ids not a list": [ diff --git a/tests/Fixtures/ServerParity/service-calls.json b/tests/Fixtures/ServerParity/service-calls.json index 493ee5e9e..9580dc93b 100644 --- a/tests/Fixtures/ServerParity/service-calls.json +++ b/tests/Fixtures/ServerParity/service-calls.json @@ -2,7 +2,7 @@ "schema": "durable-workflow.server-parity-fixture/v1", "id": "service-calls", "review_state": "reviewed", - "contract": "Published Server 2.11.1 Nexus service-call execution (ServiceCatalogController over Workflow 2.10.2's DefaultServiceControlPlane, operator): POST .../operations/{operation}/execute validates like Laravel (mode, wait, ids, labels, memo, search attributes, payload codec and an Avro arguments payload or {codec, blob} envelope; a provider external_storage reference is refused by the runtime transport before routing), then admits at the service boundary and writes an audit row for every decision. Required roles, caller-namespace deny and allow lists answer 403 with the target redacted (show on the audit row is not redacted), an open circuit 503 circuit_open with retry 30, an invalid shared limit 503 service_boundary_admission_unavailable. Shared admission limits count per caller boundary: a per-minute rate refuses 429 rate_limit_exceeded once the window is spent, a concurrency limit 429 concurrency_limit_exceeded while a lease is taken; an async call keeps its lease, a sync call returns it when the request ends. An activity_execution or invocable_http call stays accepted with its handler stamped (an uppercase ULID unless the binding names one); a binding without a target is a handler failure (409), retried in the request up to retry_policy.max_attempts unless its type is non-retryable. A deadline policy sets call_deadline_at; one already past is admitted and timed out (200, accepted). GET .../service-calls/{id} reads a call of the operation by trimmed id; POST .../cancel cancels an open call once (repeat or terminal 409 service_call_terminal, a policy with mode none 409 cancellation_not_allowed), validating the reason after the lookup. A repeated idempotency key replays the first call in the describe shape, re-checking authorization (a refusal writes a new audit row). The catalog reads list the recorded calls by scope, status and outcome filter or bucket, newest accepted first (each list holds only accepted or only rejected calls, since a rejected call's null accepted_at sorts by the database's NULL order), and read one call's detail with its caller link; an unknown call is 404 catalog_object_not_found. Generated ids and times are placeholders; @own is the fixture's namespace. Embedded applications have no HTTP service calls.", + "contract": "Published Server 2.11.1 Nexus service-call execution (ServiceCatalogController over Workflow 2.10.2's DefaultServiceControlPlane, operator): POST .../operations/{operation}/execute validates like Laravel (mode, wait, ids, labels, memo, search attributes, payload codec and an Avro arguments payload or {codec, blob} envelope; a provider external_storage reference is refused by the runtime transport before routing), then admits at the service boundary and writes an audit row for every decision. Required roles, caller-namespace deny and allow lists answer 403 with the target redacted (show on the audit row is not redacted), an open circuit 503 circuit_open with retry 30, an invalid shared limit 503 service_boundary_admission_unavailable. Shared admission limits count per caller boundary: a per-minute rate refuses 429 rate_limit_exceeded once the window is spent, a concurrency limit 429 concurrency_limit_exceeded while a lease is taken; an async call keeps its lease, a sync call returns it when the request ends. An activity_execution or invocable_http call stays accepted with its handler stamped (an uppercase ULID unless the binding names one); a binding without a target is a handler failure (409), retried in the request up to retry_policy.max_attempts unless its type is non-retryable. A deadline policy sets call_deadline_at; one already past is admitted and timed out (200, accepted). GET .../service-calls/{id} reads a call of the operation by trimmed id; POST .../cancel cancels an open call once (repeat or terminal 409 service_call_terminal, a policy with mode none 409 cancellation_not_allowed), validating the reason after the lookup. A repeated idempotency key replays the first call in the describe shape, re-checking authorization (a refusal writes a new audit row). Reusing a key from another caller namespace is outside this fixture until the Workflow #931 correction is published: published 2.11.3 and 2.11.4 replay the first call and rebind its handler identity, and the correction refuses the foreign caller. The catalog reads list the recorded calls by scope, status and outcome filter or bucket, newest accepted first (each list holds only accepted or only rejected calls, since a rejected call's null accepted_at sorts by the database's NULL order), and read one call's detail with its caller link; an unknown call is 404 catalog_object_not_found. Generated ids and times are placeholders; @own is the fixture's namespace. Embedded applications have no HTTP service calls.", "workflow_type": "parity.v1.echo", "input": { "message": "service calls λ", @@ -386,7 +386,7 @@ "path": "service-endpoints/pay/services/billing/operations/charge/execute", "body": { "mode_override": "fast", - "wait_timeout_seconds": 86401 + "wait_timeout_seconds": 61 } }, { @@ -727,15 +727,6 @@ "arguments": "wwHioz3/VYAiNwoIZG9uZQ==" } }, - { - "label": "replay other caller", - "method": "POST", - "path": "service-endpoints/pay/services/billing/operations/replay-target/execute", - "body": { - "idempotency_key": "k1", - "caller_namespace": "other" - } - }, { "label": "cancel replayed", "method": "POST", @@ -1490,7 +1481,7 @@ "The selected mode override is invalid." ], "wait_timeout_seconds": [ - "The wait timeout seconds field must not be greater than 86400." + "The wait timeout seconds field must not be greater than 60." ] }, "validation_errors": { @@ -1498,7 +1489,7 @@ "The selected mode override is invalid." ], "wait_timeout_seconds": [ - "The wait timeout seconds field must not be greater than 86400." + "The wait timeout seconds field must not be greater than 60." ] } } @@ -1674,6 +1665,7 @@ "activity_execution_id": "{ULID:charge}", "activity_class": "charge-activity", "activity_type": "charge-activity", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -1777,6 +1769,7 @@ "activity_execution_id": "{ULID:charge with options}", "activity_class": "charge-activity", "activity_type": "charge-activity", + "service_call_idempotency_key": null, "connection": "cx", "queue": "qx", "service_call_attempts": [ @@ -1882,6 +1875,7 @@ "activity_execution_id": "{ULID:charge sync}", "activity_class": "charge-activity", "activity_type": "charge-activity", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -1985,6 +1979,7 @@ "activity_execution_id": "{ULID:charge override}", "activity_class": "charge-activity", "activity_type": "charge-activity", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -2088,6 +2083,7 @@ "activity_execution_id": "act-fixed-1", "activity_class": "C", "activity_type": "t", + "service_call_idempotency_key": null, "queue": "bq", "service_call_attempts": [ { @@ -2191,6 +2187,7 @@ "boundary_key": "@own|@own|pay|billing|notify", "carrier_request_id": "{ULID:notify}", "carrier_endpoint": "https://carrier.example/notify", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -2294,6 +2291,7 @@ "boundary_key": "@own|@own|pay|billing|notify-bound", "carrier_request_id": "car-fixed-1", "carrier_endpoint": "https://c.example/x", + "service_call_idempotency_key": null, "carrier_handler": "h", "carrier": "c", "service_call_attempts": [ @@ -2400,6 +2398,7 @@ "activity_execution_id": "{ULID:charge}", "activity_class": "charge-activity", "activity_type": "charge-activity", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -2494,6 +2493,7 @@ "activity_execution_id": "{ULID:charge}", "activity_class": "charge-activity", "activity_type": "charge-activity", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -3234,6 +3234,7 @@ "activity_execution_id": "{ULID:timed}", "activity_class": "t", "activity_type": "t", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -3577,6 +3578,7 @@ "activity_execution_id": "{ULID:charge}", "activity_class": "charge-activity", "activity_type": "charge-activity", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -3672,6 +3674,7 @@ "activity_execution_id": "{ULID:charge}", "activity_class": "charge-activity", "activity_type": "charge-activity", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -3798,6 +3801,7 @@ "activity_execution_id": "{ULID:nocancel}", "activity_class": "t", "activity_type": "t", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -3903,6 +3907,7 @@ "activity_execution_id": "{ULID:nocancel}", "activity_class": "t", "activity_type": "t", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -4227,6 +4232,7 @@ "activity_execution_id": "{ULID:rated}", "activity_class": "t", "activity_type": "t", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -4379,6 +4385,7 @@ "activity_execution_id": "{ULID:rated other caller}", "activity_class": "t", "activity_type": "t", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -4497,6 +4504,7 @@ "activity_execution_id": "{ULID:held}", "activity_class": "t", "activity_type": "t", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -4648,6 +4656,7 @@ "activity_execution_id": "{ULID:held sync}", "activity_class": "t", "activity_type": "t", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -4765,6 +4774,7 @@ "activity_execution_id": "{ULID:held sync again}", "activity_class": "t", "activity_type": "t", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -4951,6 +4961,7 @@ "activity_execution_id": "{ULID:replay first}", "activity_class": "charge-activity", "activity_type": "charge-activity", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -5043,109 +5054,6 @@ "failure_message": null } ], - "replay other caller": [ - 200, - { - "id": "{id:replay other caller}", - "namespace": "@own", - "endpoint_id": "{id:register endpoint}", - "endpoint_name": "pay", - "service_id": "{id:register service}", - "service_name": "billing", - "operation_id": "{id:register replay-target}", - "operation_name": "replay-target", - "caller_namespace": "other", - "caller_workflow_instance_id": null, - "caller_workflow_run_id": null, - "target_namespace": "@own", - "linked_workflow_instance_id": null, - "linked_workflow_run_id": null, - "linked_workflow_update_id": null, - "status": "accepted", - "outcome": "accepted", - "outcome_category": "accepted", - "outcome_reason": "accepted", - "outcome_message": null, - "outcome_metadata": { - "boundary_key": "other|@own|pay|billing|replay-target" - }, - "service_error_type": null, - "caller_observed_error_type": null, - "typed_error_message": null, - "policy_name": "server.shared-service-boundary", - "retry_after_seconds": null, - "operation_mode": "async", - "resolved_binding_kind": "activity_execution", - "resolved_target_reference": "{ULID:replay other caller}", - "caller_principal_subject": "legacy-token", - "caller_principal_method": "token", - "caller_principal_roles": [ - "admin" - ], - "caller_principal_tenant": null, - "caller_principal_claims": { - "legacy_credential": true - }, - "payload_codec": null, - "input_payload_reference": null, - "output_payload_reference": null, - "failure_payload_reference": null, - "failure_message": null, - "idempotency_key": "k1", - "deadline_policy": null, - "idempotency_policy": null, - "cancellation_policy": null, - "retry_policy": null, - "boundary_policy": null, - "metadata": { - "boundary_key": "other|@own|pay|billing|replay-target", - "activity_execution_id": "{ULID:replay other caller}", - "activity_class": "charge-activity", - "activity_type": "charge-activity", - "service_call_attempts": [ - { - "attempt": 1, - "status": "accepted", - "outcome": "accepted", - "outcome_reason": "accepted", - "started_at": "{µs}", - "retry_scheduled": false - } - ], - "retry_attempt_count": 1 - }, - "service_call_attempts": [ - { - "attempt": 1, - "status": "accepted", - "outcome": "accepted", - "outcome_reason": "accepted", - "started_at": "{µs}", - "retry_scheduled": false - } - ], - "retry_attempt_count": 1, - "accepted_at": "{µs}", - "call_deadline_at": null, - "started_at": null, - "completed_at": null, - "failed_at": null, - "cancelled_at": null, - "created_at": "{time}", - "updated_at": "{time}", - "service_call_id": "{id:replay other caller}", - "accepted": true, - "idempotent_replay": false, - "handler": { - "accepted": true, - "kind": "activity_execution", - "activity_execution_id": "{ULID:replay other caller}", - "activity_class": "charge-activity", - "activity_type": "charge-activity" - }, - "reason": null - } - ], "cancel replayed": [ 200, { @@ -5205,6 +5113,7 @@ "activity_execution_id": "{ULID:replay first}", "activity_class": "charge-activity", "activity_type": "charge-activity", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -5348,6 +5257,7 @@ "activity_execution_id": "{ULID:replay second key}", "activity_class": "charge-activity", "activity_type": "charge-activity", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -5504,6 +5414,7 @@ "activity_execution_id": "{ULID:replay second key}", "activity_class": "charge-activity", "activity_type": "charge-activity", + "service_call_idempotency_key": null, "service_call_attempts": [ { "attempt": 1, @@ -6762,6 +6673,7 @@ "activity_execution_id": "{ULID:charge with options}", "activity_class": "charge-activity", "activity_type": "charge-activity", + "service_call_idempotency_key": null, "connection": "cx", "queue": "qx", "service_call_attempts": [ diff --git a/tests/Fixtures/ServerParityBaselines/php-2.11.4.json b/tests/Fixtures/ServerParityBaselines/php-2.11.4.json new file mode 100644 index 000000000..06d4cd6c9 --- /dev/null +++ b/tests/Fixtures/ServerParityBaselines/php-2.11.4.json @@ -0,0 +1,20 @@ +{ + "schema": "durable-workflow.server-port-artifacts/v1", + "server": { + "version": "2.11.4", + "source_commit": "ad3ac5fda0a30a58084b2d68569f2d91e23fabf3", + "images": { + "linux/amd64": "durableworkflow/server@sha256:bee30cde5b70a429561a49f25daf4ef71a1e405d43322f78ff1b8137029c80f3", + "linux/arm64": "durableworkflow/server@sha256:cc297aa7d1f81b8bfa1169de6980dabe58d9776dcf513f379dcba2a51b1f5cc7" + } + }, + "workflow": "2.14.2", + "sdk_php": "2.2.6", + "sdk_python": "2.5.0", + "sdk_rust": "3.4.1", + "cli": "2.2.0", + "waterline": "2.3.1", + "sample_app_commit": "4427a537e092f9d92c96d2a4867b9186e0e75ccd", + "capacity_suite": "1.9.0", + "performance_status": "not_measured" +} diff --git a/tests/Unit/ServerParityFixtureSelectionTest.mjs b/tests/Unit/ServerParityFixtureSelectionTest.mjs index 3c5ca16b1..cccbfb1b8 100644 --- a/tests/Unit/ServerParityFixtureSelectionTest.mjs +++ b/tests/Unit/ServerParityFixtureSelectionTest.mjs @@ -1,5 +1,5 @@ // Fixture selection for parity records and comparisons, and the SQLite job's -// separate update-validation record (Server #723). +// separate update-validation and service-calls records (Server #723, #796). import test from 'node:test'; import assert from 'node:assert/strict'; import {readFileSync, readdirSync} from 'node:fs'; @@ -56,16 +56,24 @@ function mainCorpusCommands(job) { return job.split(/(?=docker run )/).filter(command => /server-parity\.mjs (record|compare)/.test(command)); } -test('the SQLite job covers update-validation only through its separate record', () => { +test('the SQLite job covers update-validation and service-calls only through their separate records', () => { const sqlite = jobs()['execution-and-codec']; const commands = mainCorpusCommands(sqlite); assert.equal(commands.length, 4, 'php, embedded and rust records plus their comparison'); - for (const command of commands) assert.match(command, /--exclude-fixture tests\/Fixtures\/ServerParity\/update-validation\.json/); + for (const command of commands) { + assert.deepEqual(command.match(/--exclude-fixture \S+/g), [ + '--exclude-fixture tests/Fixtures/ServerParity/update-validation.json', + '--exclude-fixture tests/Fixtures/ServerParity/service-calls.json', + ]); + } + assert.ok(corpus.includes('service-calls.json')); assert.match(sqlite, /update_validation_status=0\n\s+bash scripts\/conformance\/server-parity\/update-validation-sqlite\.sh http:\/\/parity-server:8080 http:\/\/parity-rust:8080 \|\| update_validation_status=\$\?/); + assert.match(sqlite, /service_calls_status=0\n\s+bash scripts\/conformance\/server-parity\/update-validation-sqlite\.sh http:\/\/parity-server:8080 http:\/\/parity-rust:8080 service-calls \|\| service_calls_status=\$\?/); assert.match(sqlite, /test "\$update_validation_status" -eq 0/); + assert.match(sqlite, /test "\$service_calls_status" -eq 0/); }); -test('the MySQL and PostgreSQL jobs keep update-validation in their main corpus', () => { +test('the MySQL and PostgreSQL jobs keep update-validation and service-calls in their main corpus', () => { const all = jobs(); for (const name of ['mysql-execution', 'postgres-storage']) { assert.ok(mainCorpusCommands(all[name]).length > 0, name); @@ -75,18 +83,20 @@ test('the MySQL and PostgreSQL jobs keep update-validation in their main corpus' test('the separate record stops maintenance for the PHP record only and compares all three targets', () => { const script = readFileSync(resolve(root, 'scripts/conformance/server-parity/update-validation-sqlite.sh'), 'utf8'); - assert.match(script, /^fixture=tests\/Fixtures\/ServerParity\/update-validation\.json$/m); + // update-validation by default, or the named reviewed fixture. + assert.match(script, /^name="\$\{3:-update-validation\}"\n\[\[ "\$name" =~ \^\[a-z0-9-\]\+\$ \]\]$/m); + assert.match(script, /^fixture="tests\/Fixtures\/ServerParity\/\$name\.json"\ntest -f "\$fixture"$/m); assert.match(script, /^\. scripts\/conformance\/server-parity\/php-maintenance-window\.sh$/m); // The PHP record, and its one retry after a lost SQLite write (#754). const windows = script.match(/^ *run_without_maintenance .*$/gm).map(line => line.trim()); assert.deepEqual(windows, [ - 'run_without_maintenance parity-evidence/update-validation-maintenance-schedule.txt -- php_record', - 'run_without_maintenance parity-evidence/update-validation-maintenance-schedule-retry.txt -- php_record', + 'run_without_maintenance "parity-evidence/$name-maintenance-schedule.txt" -- php_record', + 'run_without_maintenance "parity-evidence/$name-maintenance-schedule-retry.txt" -- php_record', ]); assert.match(script, /^php_record\(\) \{\n php_status=0\n record_started=\$\(date -u \+%Y-%m-%dT%H:%M:%S\.%NZ\)\n http_record "\$php_url" php \|\| php_status=\$\?\n record_finished=\$\(date -u \+%Y-%m-%dT%H:%M:%S\.%NZ\)\n\}$/m); assert.match(script, /docker logs --since "\$record_started" --until "\$record_finished" "\$RESOURCE_SCOPE-server"/); assert.match(script, /^http_record "\$native_url" rust$/m); - assert.match(script, /--mode embedded --application-root \/app --target embedded --output \/evidence\/update-validation-embedded\.json --fixture "\$fixture"/); - assert.match(script, /update-validation-php\.json \/evidence\/update-validation-rust\.json \/evidence\/update-validation-embedded\.json \\\n\s+--fixture "\$fixture"/); + assert.match(script, /--mode embedded --application-root \/app --target embedded --output "\/evidence\/\$name-embedded\.json" --fixture "\$fixture"/); + assert.match(script, /"\/evidence\/\$name-php\.json" "\/evidence\/\$name-rust\.json" "\/evidence\/\$name-embedded\.json" \\\n\s+--fixture "\$fixture" > "parity-evidence\/\$name-comparison\.json"/); assert.match(script, /^set -euo pipefail$/m); }); diff --git a/tests/Unit/ServerParityImagePullTest.mjs b/tests/Unit/ServerParityImagePullTest.mjs index 441dc79e1..aa5f7e552 100644 --- a/tests/Unit/ServerParityImagePullTest.mjs +++ b/tests/Unit/ServerParityImagePullTest.mjs @@ -32,7 +32,7 @@ test('each container-heavy fixture job pulls its pinned images first, with retri const pull = steps[1]; assert.match(pull, /- name: Pull the pinned images, retrying registry errors\n/, name); assert.match(pull, /bash scripts\/ci\/pull-images\.sh \\\n/, name); - assert.match(pull, /"\$\(baseline php-2\.11\.1\)"/, name); + assert.match(pull, /"\$\(baseline php-2\.11\.4\)"/, name); for (const image of extra) assert.ok(pull.includes(image), `${name} pulls ${image}`); // Every Rust image the job runs is in its pull list, so a digest bump // cannot leave the pull behind. diff --git a/tests/Unit/ServerParityServiceCallsContractTest.mjs b/tests/Unit/ServerParityServiceCallsContractTest.mjs index 562f2980f..aea85a9d0 100644 --- a/tests/Unit/ServerParityServiceCallsContractTest.mjs +++ b/tests/Unit/ServerParityServiceCallsContractTest.mjs @@ -50,7 +50,7 @@ const observation = () => { }; test('the service calls accept the published answers', () => { - assert.deepStrictEqual(checkServiceCalls(fixture, observation(), id), {scope: 'service_calls', cases: 91}); + assert.deepStrictEqual(checkServiceCalls(fixture, observation(), id), {scope: 'service_calls', cases: 90}); }); test('the fixture covers execute, show, cancel and every reachable refusal', () => { diff --git a/tests/Unit/ServerParityUpdateValidationRetryTest.mjs b/tests/Unit/ServerParityUpdateValidationRetryTest.mjs index 2ff2d0a0d..d000b4792 100644 --- a/tests/Unit/ServerParityUpdateValidationRetryTest.mjs +++ b/tests/Unit/ServerParityUpdateValidationRetryTest.mjs @@ -1,6 +1,7 @@ // Server #754: the SQLite job's separate update-validation record retries the // published PHP record once, and only when that record failed after PHP -// logged "database is locked". Native and embedded records never retry. +// logged "database is locked". Native and embedded records never retry. The +// separate service-calls record (Server #796) runs the same script by name. import test from 'node:test'; import assert from 'node:assert/strict'; import {chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync} from 'node:fs'; @@ -71,7 +72,7 @@ case "$1" in esac `; -function run(outcomes) { +function run(outcomes, ...name) { const dir = mkdtempSync(join(tmpdir(), 'update-validation-retry-')); for (const name of ['scripts', 'tests']) symlinkSync(join(root, name), join(dir, name)); for (const name of ['bin', 'parity-evidence', 'parity-state']) mkdirSync(join(dir, name)); @@ -80,7 +81,7 @@ function run(outcomes) { for (const tool of ['docker', 'jq']) chmodSync(join(dir, 'bin', tool), 0o755); writeFileSync(join(dir, 'state'), 'running\n'); writeFileSync(join(dir, 'records'), ''); - const result = spawnSync('bash', ['scripts/conformance/server-parity/update-validation-sqlite.sh', 'http://parity-server:8080', 'http://parity-rust:8080'], { + const result = spawnSync('bash', ['scripts/conformance/server-parity/update-validation-sqlite.sh', 'http://parity-server:8080', 'http://parity-rust:8080', ...name], { cwd: dir, encoding: 'utf8', env: {...process.env, PATH: `${join(dir, 'bin')}:${process.env.PATH}`, OUTCOMES: outcomes, RESOURCE_SCOPE: 'dw-parity-7-2-sqlite', GITHUB_RUN_ID: '7', GITHUB_RUN_ATTEMPT: '2', GITHUB_JOB: 'sqlite', @@ -156,3 +157,24 @@ test('a native or embedded failure is never retried, even after a PHP retry', () assert.deepEqual(result.records, records, outcomes); } }); + +test('a named fixture records and retries under its own evidence names', () => { + const result = run('php1=fail-locked', 'service-calls'); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(result.records, ['php parity-v1', 'php parity-v1-retry', 'embedded parity-v1-retry', 'rust parity-v1-retry']); + assert.match(result.stdout, /^::warning title=Published PHP service-calls record retried::/m); + for (const name of ['service-calls-php.json', 'service-calls-embedded.json', 'service-calls-rust.json', 'service-calls-comparison.json', + 'service-calls-php.attempt-1.json', 'service-calls-php.attempt-1.locked.txt', 'service-calls-maintenance-schedule-retry.txt']) { + assert.equal(result.evidence(name), true, name); + } + assert.equal(result.evidence('update-validation-php.json'), false); + assert.equal(result.state, 'running'); +}); + +test('a fixture name outside the reviewed corpus records nothing', () => { + for (const name of ['no-such-fixture', '../ServerParityPending/workflow-task-transient-retry', 'Service-Calls']) { + const result = run('', name); + assert.notEqual(result.status, 0, name); + assert.deepEqual(result.records, [], name); + } +});