diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index 4ac46c2929c3..c2eb5c010bf1 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/master.yml
@@ -2860,6 +2860,516 @@ jobs:
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, sequential)' --workflow "MasterCI" --ci --timestamp
+ stateless_tests_amd_binary_cas_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_binary, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjYXMgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_binary, cas s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, cas s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_BINARY_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, cas s3 storage, parallel)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_1_2:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_asan_ubsan, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYXNhbl91YnNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAxLzIp') }}
+ name: "Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 1/2)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 1/2)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_ASAN_UBSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_ASAN_UBSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 1/2)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_2_2:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_asan_ubsan, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYXNhbl91YnNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAyLzIp') }}
+ name: "Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 2/2)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 2/2)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_ASAN_UBSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_ASAN_UBSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 2/2)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_amd_tsan_cas_s3_storage_parallel_1_2:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester, 16c]
+ needs: [build_amd_tsan, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfdHNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAxLzIp') }}
+ name: "Stateless tests (amd_tsan, cas s3 storage, parallel, 1/2)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_tsan, cas s3 storage, parallel, 1/2)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_TSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_TSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_tsan, cas s3 storage, parallel, 1/2)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_amd_tsan_cas_s3_storage_parallel_2_2:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester, 16c]
+ needs: [build_amd_tsan, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfdHNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAyLzIp') }}
+ name: "Stateless tests (amd_tsan, cas s3 storage, parallel, 2/2)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_tsan, cas s3 storage, parallel, 2/2)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_TSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_TSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_tsan, cas s3 storage, parallel, 2/2)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_amd_msan_cas_s3_storage_parallel_1_3:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
+ needs: [build_amd_msan, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfbXNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAxLzMp') }}
+ name: "Stateless tests (amd_msan, cas s3 storage, parallel, 1/3)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_msan, cas s3 storage, parallel, 1/3)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_MSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_MSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_msan, cas s3 storage, parallel, 1/3)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_amd_msan_cas_s3_storage_parallel_2_3:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
+ needs: [build_amd_msan, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfbXNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAyLzMp') }}
+ name: "Stateless tests (amd_msan, cas s3 storage, parallel, 2/3)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_msan, cas s3 storage, parallel, 2/3)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_MSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_MSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_msan, cas s3 storage, parallel, 2/3)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_amd_msan_cas_s3_storage_parallel_3_3:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
+ needs: [build_amd_msan, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfbXNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAzLzMp') }}
+ name: "Stateless tests (amd_msan, cas s3 storage, parallel, 3/3)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_msan, cas s3 storage, parallel, 3/3)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_MSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_MSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_msan, cas s3 storage, parallel, 3/3)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_arm_binary_cas_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester-aarch64, 16c]
+ needs: [build_arm_binary, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhcm1fYmluYXJ5LCBjYXMgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (arm_binary, cas s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (arm_binary, cas s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_ARM_BIN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_ARM_BIN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, cas s3 storage, parallel)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_amd_binary_cas_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_binary, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjYXMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_binary, cas storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, cas storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_BINARY_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, cas storage, parallel)' --workflow "MasterCI" --ci --timestamp
+
stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_1_8:
runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
needs: [build_amd_llvm_coverage_per_test, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
@@ -5995,7 +6505,7 @@ jobs:
finish_workflow:
runs-on: [self-hosted, altinity-on-demand, altinity-style-checker]
- needs: [ast_fuzzer_amd_debug, ast_fuzzer_amd_msan, ast_fuzzer_amd_tsan, ast_fuzzer_arm_asan_ubsan, build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_llvm_coverage_per_test, build_amd_msan, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_debug, build_arm_msan, build_arm_release, build_arm_tsan, build_arm_ubsan, buzzhouse_amd_debug, buzzhouse_amd_msan, buzzhouse_amd_tsan, buzzhouse_arm_asan_ubsan, clickbench_amd_release, clickbench_arm_release, compatibility_check_amd_release, compatibility_check_arm_release, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, install_packages_amd_release, install_packages_arm_release, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_1_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_2_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_3_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_4_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_5_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_6_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_7_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_8_8, integration_tests_amd_msan_10_10, integration_tests_amd_msan_1_10, integration_tests_amd_msan_2_10, integration_tests_amd_msan_3_10, integration_tests_amd_msan_4_10, integration_tests_amd_msan_5_10, integration_tests_amd_msan_6_10, integration_tests_amd_msan_7_10, integration_tests_amd_msan_8_10, integration_tests_amd_msan_9_10, integration_tests_amd_tsan_1_6, integration_tests_amd_tsan_2_6, integration_tests_amd_tsan_3_6, integration_tests_amd_tsan_4_6, integration_tests_amd_tsan_5_6, integration_tests_amd_tsan_6_6, integration_tests_arm_binary_distributed_plan_1_4, integration_tests_arm_binary_distributed_plan_2_4, integration_tests_arm_binary_distributed_plan_3_4, integration_tests_arm_binary_distributed_plan_4_4, sign_release_amd_release, sign_release_arm_release, source_upload, sqllogic_test, sqlstorm_test, sqltest, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_1_2, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_2_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_distributed_plan_s3_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_sequential, stateless_tests_amd_debug_parallel, stateless_tests_amd_debug_sequential, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_1_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_2_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_3_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_4_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_5_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_6_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_7_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_8_8, stateless_tests_amd_msan_wasmedge_parallel_1_4, stateless_tests_amd_msan_wasmedge_parallel_2_4, stateless_tests_amd_msan_wasmedge_parallel_3_4, stateless_tests_amd_msan_wasmedge_parallel_4_4, stateless_tests_amd_msan_wasmedge_sequential_1_2, stateless_tests_amd_msan_wasmedge_sequential_2_2, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_amd_tsan_s3_storage_parallel_1_2, stateless_tests_amd_tsan_s3_storage_parallel_2_2, stateless_tests_amd_tsan_s3_storage_sequential_1_2, stateless_tests_amd_tsan_s3_storage_sequential_2_2, stateless_tests_amd_tsan_sequential_1_2, stateless_tests_amd_tsan_sequential_2_2, stateless_tests_arm_asan_ubsan_azure_parallel, stateless_tests_arm_asan_ubsan_azure_sequential_1_2, stateless_tests_arm_asan_ubsan_azure_sequential_2_2, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential, stress_test_amd_asan_ubsan, stress_test_amd_debug, stress_test_amd_msan, stress_test_amd_tsan, stress_test_arm_asan_ubsan, stress_test_arm_asan_ubsan_s3, stress_test_arm_debug, stress_test_arm_msan, stress_test_arm_release, stress_test_arm_tsan, stress_test_arm_ubsan, stress_test_azure_amd_msan, stress_test_azure_amd_tsan, unit_tests_asan_ubsan, unit_tests_asan_ubsan_function_prop_fuzzer, unit_tests_msan, unit_tests_msan_function_prop_fuzzer, unit_tests_tsan, unit_tests_tsan_function_prop_fuzzer]
+ needs: [ast_fuzzer_amd_debug, ast_fuzzer_amd_msan, ast_fuzzer_amd_tsan, ast_fuzzer_arm_asan_ubsan, build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_llvm_coverage_per_test, build_amd_msan, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_debug, build_arm_msan, build_arm_release, build_arm_tsan, build_arm_ubsan, buzzhouse_amd_debug, buzzhouse_amd_msan, buzzhouse_amd_tsan, buzzhouse_arm_asan_ubsan, clickbench_amd_release, clickbench_arm_release, compatibility_check_amd_release, compatibility_check_arm_release, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, install_packages_amd_release, install_packages_arm_release, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_1_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_2_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_3_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_4_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_5_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_6_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_7_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_8_8, integration_tests_amd_msan_10_10, integration_tests_amd_msan_1_10, integration_tests_amd_msan_2_10, integration_tests_amd_msan_3_10, integration_tests_amd_msan_4_10, integration_tests_amd_msan_5_10, integration_tests_amd_msan_6_10, integration_tests_amd_msan_7_10, integration_tests_amd_msan_8_10, integration_tests_amd_msan_9_10, integration_tests_amd_tsan_1_6, integration_tests_amd_tsan_2_6, integration_tests_amd_tsan_3_6, integration_tests_amd_tsan_4_6, integration_tests_amd_tsan_5_6, integration_tests_amd_tsan_6_6, integration_tests_arm_binary_distributed_plan_1_4, integration_tests_arm_binary_distributed_plan_2_4, integration_tests_arm_binary_distributed_plan_3_4, integration_tests_arm_binary_distributed_plan_4_4, sign_release_amd_release, sign_release_arm_release, source_upload, sqllogic_test, sqlstorm_test, sqltest, stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_1_2, stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_2_2, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_1_2, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_2_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_binary_cas_s3_storage_parallel, stateless_tests_amd_binary_cas_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_sequential, stateless_tests_amd_debug_parallel, stateless_tests_amd_debug_sequential, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_1_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_2_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_3_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_4_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_5_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_6_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_7_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_8_8, stateless_tests_amd_msan_cas_s3_storage_parallel_1_3, stateless_tests_amd_msan_cas_s3_storage_parallel_2_3, stateless_tests_amd_msan_cas_s3_storage_parallel_3_3, stateless_tests_amd_msan_wasmedge_parallel_1_4, stateless_tests_amd_msan_wasmedge_parallel_2_4, stateless_tests_amd_msan_wasmedge_parallel_3_4, stateless_tests_amd_msan_wasmedge_parallel_4_4, stateless_tests_amd_msan_wasmedge_sequential_1_2, stateless_tests_amd_msan_wasmedge_sequential_2_2, stateless_tests_amd_tsan_cas_s3_storage_parallel_1_2, stateless_tests_amd_tsan_cas_s3_storage_parallel_2_2, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_amd_tsan_s3_storage_parallel_1_2, stateless_tests_amd_tsan_s3_storage_parallel_2_2, stateless_tests_amd_tsan_s3_storage_sequential_1_2, stateless_tests_amd_tsan_s3_storage_sequential_2_2, stateless_tests_amd_tsan_sequential_1_2, stateless_tests_amd_tsan_sequential_2_2, stateless_tests_arm_asan_ubsan_azure_parallel, stateless_tests_arm_asan_ubsan_azure_sequential_1_2, stateless_tests_arm_asan_ubsan_azure_sequential_2_2, stateless_tests_arm_binary_cas_s3_storage_parallel, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential, stress_test_amd_asan_ubsan, stress_test_amd_debug, stress_test_amd_msan, stress_test_amd_tsan, stress_test_arm_asan_ubsan, stress_test_arm_asan_ubsan_s3, stress_test_arm_debug, stress_test_arm_msan, stress_test_arm_release, stress_test_arm_tsan, stress_test_arm_ubsan, stress_test_azure_amd_msan, stress_test_azure_amd_tsan, unit_tests_asan_ubsan, unit_tests_asan_ubsan_function_prop_fuzzer, unit_tests_msan, unit_tests_msan_function_prop_fuzzer, unit_tests_tsan, unit_tests_tsan_function_prop_fuzzer]
if: ${{ !cancelled() && needs.config_workflow.outputs.pipeline_status != '' }}
name: "Finish Workflow"
outputs:
@@ -6148,6 +6658,16 @@ jobs:
- stateless_tests_amd_tsan_s3_storage_sequential_2_2
- stateless_tests_arm_binary_parallel
- stateless_tests_arm_binary_sequential
+ - stateless_tests_amd_binary_cas_s3_storage_parallel
+ - stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_1_2
+ - stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_2_2
+ - stateless_tests_amd_tsan_cas_s3_storage_parallel_1_2
+ - stateless_tests_amd_tsan_cas_s3_storage_parallel_2_2
+ - stateless_tests_amd_msan_cas_s3_storage_parallel_1_3
+ - stateless_tests_amd_msan_cas_s3_storage_parallel_2_3
+ - stateless_tests_amd_msan_cas_s3_storage_parallel_3_3
+ - stateless_tests_arm_binary_cas_s3_storage_parallel
+ - stateless_tests_amd_binary_cas_storage_parallel
- stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_1_8
- stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_2_8
- stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_3_8
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 07f1a6856aa6..f3ac34c197d7 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -2405,6 +2405,496 @@ jobs:
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, sequential)' --workflow "PR" --ci --timestamp
+ stateless_tests_amd_binary_cas_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjYXMgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_binary, cas s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, cas s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_BINARY_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, cas s3 storage, parallel)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_1_2:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYXNhbl91YnNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAxLzIp') }}
+ name: "Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 1/2)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 1/2)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_ASAN_UBSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_ASAN_UBSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 1/2)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_2_2:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYXNhbl91YnNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAyLzIp') }}
+ name: "Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 2/2)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 2/2)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_ASAN_UBSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_ASAN_UBSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 2/2)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_amd_tsan_cas_s3_storage_parallel_1_2:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfdHNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAxLzIp') }}
+ name: "Stateless tests (amd_tsan, cas s3 storage, parallel, 1/2)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_tsan, cas s3 storage, parallel, 1/2)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_TSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_TSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_tsan, cas s3 storage, parallel, 1/2)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_amd_tsan_cas_s3_storage_parallel_2_2:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfdHNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAyLzIp') }}
+ name: "Stateless tests (amd_tsan, cas s3 storage, parallel, 2/2)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_tsan, cas s3 storage, parallel, 2/2)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_TSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_TSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_tsan, cas s3 storage, parallel, 2/2)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_amd_msan_cas_s3_storage_parallel_1_3:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_msan, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfbXNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAxLzMp') }}
+ name: "Stateless tests (amd_msan, cas s3 storage, parallel, 1/3)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_msan, cas s3 storage, parallel, 1/3)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_MSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_MSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_msan, cas s3 storage, parallel, 1/3)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_amd_msan_cas_s3_storage_parallel_2_3:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_msan, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfbXNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAyLzMp') }}
+ name: "Stateless tests (amd_msan, cas s3 storage, parallel, 2/3)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_msan, cas s3 storage, parallel, 2/3)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_MSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_MSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_msan, cas s3 storage, parallel, 2/3)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_amd_msan_cas_s3_storage_parallel_3_3:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_msan, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfbXNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAzLzMp') }}
+ name: "Stateless tests (amd_msan, cas s3 storage, parallel, 3/3)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_msan, cas s3 storage, parallel, 3/3)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_MSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_MSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_msan, cas s3 storage, parallel, 3/3)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_arm_binary_cas_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester-aarch64, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhcm1fYmluYXJ5LCBjYXMgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (arm_binary, cas s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (arm_binary, cas s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_ARM_BIN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_ARM_BIN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, cas s3 storage, parallel)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_amd_binary_cas_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjYXMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_binary, cas storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, cas storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_BINARY_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, cas storage, parallel)' --workflow "PR" --ci --timestamp
+
stateless_tests_arm_asan_ubsan_azure_parallel:
runs-on: [self-hosted, altinity-on-demand, altinity-func-tester-aarch64, 16c]
needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
@@ -5545,7 +6035,7 @@ jobs:
finish_workflow:
runs-on: [self-hosted, altinity-on-demand, altinity-style-checker]
- needs: [ast_fuzzer_amd_debug, ast_fuzzer_amd_debug_targeted, ast_fuzzer_amd_debug_targeted_old_compatibility, ast_fuzzer_amd_msan, ast_fuzzer_amd_tsan, ast_fuzzer_arm_asan_ubsan, build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_msan, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_debug, build_arm_msan, build_arm_release, build_arm_tsan, build_arm_ubsan, build_toolchain_pgo_bolt_aarch64, build_toolchain_pgo_bolt_amd64, buzzhouse_amd_debug, buzzhouse_amd_msan, buzzhouse_amd_tsan, buzzhouse_arm_asan_ubsan, ci_tests, compatibility_check_amd_release, compatibility_check_arm_release, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, install_packages_amd_release, install_packages_arm_release, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_1_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_2_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_3_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_4_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_5_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_6_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_7_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_8_8, integration_tests_amd_asan_ubsan_targeted, integration_tests_amd_msan_10_10, integration_tests_amd_msan_1_10, integration_tests_amd_msan_2_10, integration_tests_amd_msan_3_10, integration_tests_amd_msan_4_10, integration_tests_amd_msan_5_10, integration_tests_amd_msan_6_10, integration_tests_amd_msan_7_10, integration_tests_amd_msan_8_10, integration_tests_amd_msan_9_10, integration_tests_amd_tsan_1_6, integration_tests_amd_tsan_2_6, integration_tests_amd_tsan_3_6, integration_tests_amd_tsan_4_6, integration_tests_amd_tsan_5_6, integration_tests_amd_tsan_6_6, integration_tests_arm_binary_distributed_plan_1_4, integration_tests_arm_binary_distributed_plan_2_4, integration_tests_arm_binary_distributed_plan_3_4, integration_tests_arm_binary_distributed_plan_4_4, keeper_stress_tests_pr, quick_functional_tests, source_upload, sqllogic_test, sqlstorm_test, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_1_2, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_2_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_distributed_plan_s3_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_sequential, stateless_tests_amd_debug_parallel, stateless_tests_amd_debug_sequential, stateless_tests_amd_msan_wasmedge_parallel_1_4, stateless_tests_amd_msan_wasmedge_parallel_2_4, stateless_tests_amd_msan_wasmedge_parallel_3_4, stateless_tests_amd_msan_wasmedge_parallel_4_4, stateless_tests_amd_msan_wasmedge_sequential_1_2, stateless_tests_amd_msan_wasmedge_sequential_2_2, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_amd_tsan_s3_storage_parallel_1_2, stateless_tests_amd_tsan_s3_storage_parallel_2_2, stateless_tests_amd_tsan_s3_storage_sequential_1_2, stateless_tests_amd_tsan_s3_storage_sequential_2_2, stateless_tests_amd_tsan_sequential_1_2, stateless_tests_amd_tsan_sequential_2_2, stateless_tests_arm_asan_ubsan_azure_parallel, stateless_tests_arm_asan_ubsan_azure_sequential_1_2, stateless_tests_arm_asan_ubsan_azure_sequential_2_2, stateless_tests_arm_asan_ubsan_targeted, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential, stress_test_amd_asan_ubsan, stress_test_amd_debug, stress_test_amd_msan, stress_test_amd_tsan, stress_test_arm_asan_ubsan, stress_test_arm_asan_ubsan_s3, stress_test_arm_debug, stress_test_arm_msan, stress_test_arm_release, stress_test_arm_tsan, stress_test_arm_ubsan, unit_tests_asan_ubsan, unit_tests_asan_ubsan_function_prop_fuzzer, unit_tests_msan, unit_tests_msan_function_prop_fuzzer, unit_tests_tsan, unit_tests_tsan_function_prop_fuzzer]
+ needs: [ast_fuzzer_amd_debug, ast_fuzzer_amd_debug_targeted, ast_fuzzer_amd_debug_targeted_old_compatibility, ast_fuzzer_amd_msan, ast_fuzzer_amd_tsan, ast_fuzzer_arm_asan_ubsan, build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_msan, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_debug, build_arm_msan, build_arm_release, build_arm_tsan, build_arm_ubsan, build_toolchain_pgo_bolt_aarch64, build_toolchain_pgo_bolt_amd64, buzzhouse_amd_debug, buzzhouse_amd_msan, buzzhouse_amd_tsan, buzzhouse_arm_asan_ubsan, ci_tests, compatibility_check_amd_release, compatibility_check_arm_release, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, install_packages_amd_release, install_packages_arm_release, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_1_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_2_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_3_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_4_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_5_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_6_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_7_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_8_8, integration_tests_amd_asan_ubsan_targeted, integration_tests_amd_msan_10_10, integration_tests_amd_msan_1_10, integration_tests_amd_msan_2_10, integration_tests_amd_msan_3_10, integration_tests_amd_msan_4_10, integration_tests_amd_msan_5_10, integration_tests_amd_msan_6_10, integration_tests_amd_msan_7_10, integration_tests_amd_msan_8_10, integration_tests_amd_msan_9_10, integration_tests_amd_tsan_1_6, integration_tests_amd_tsan_2_6, integration_tests_amd_tsan_3_6, integration_tests_amd_tsan_4_6, integration_tests_amd_tsan_5_6, integration_tests_amd_tsan_6_6, integration_tests_arm_binary_distributed_plan_1_4, integration_tests_arm_binary_distributed_plan_2_4, integration_tests_arm_binary_distributed_plan_3_4, integration_tests_arm_binary_distributed_plan_4_4, keeper_stress_tests_pr, quick_functional_tests, source_upload, sqllogic_test, sqlstorm_test, stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_1_2, stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_2_2, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_1_2, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_2_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_binary_cas_s3_storage_parallel, stateless_tests_amd_binary_cas_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_sequential, stateless_tests_amd_debug_parallel, stateless_tests_amd_debug_sequential, stateless_tests_amd_msan_cas_s3_storage_parallel_1_3, stateless_tests_amd_msan_cas_s3_storage_parallel_2_3, stateless_tests_amd_msan_cas_s3_storage_parallel_3_3, stateless_tests_amd_msan_wasmedge_parallel_1_4, stateless_tests_amd_msan_wasmedge_parallel_2_4, stateless_tests_amd_msan_wasmedge_parallel_3_4, stateless_tests_amd_msan_wasmedge_parallel_4_4, stateless_tests_amd_msan_wasmedge_sequential_1_2, stateless_tests_amd_msan_wasmedge_sequential_2_2, stateless_tests_amd_tsan_cas_s3_storage_parallel_1_2, stateless_tests_amd_tsan_cas_s3_storage_parallel_2_2, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_amd_tsan_s3_storage_parallel_1_2, stateless_tests_amd_tsan_s3_storage_parallel_2_2, stateless_tests_amd_tsan_s3_storage_sequential_1_2, stateless_tests_amd_tsan_s3_storage_sequential_2_2, stateless_tests_amd_tsan_sequential_1_2, stateless_tests_amd_tsan_sequential_2_2, stateless_tests_arm_asan_ubsan_azure_parallel, stateless_tests_arm_asan_ubsan_azure_sequential_1_2, stateless_tests_arm_asan_ubsan_azure_sequential_2_2, stateless_tests_arm_asan_ubsan_targeted, stateless_tests_arm_binary_cas_s3_storage_parallel, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential, stress_test_amd_asan_ubsan, stress_test_amd_debug, stress_test_amd_msan, stress_test_amd_tsan, stress_test_arm_asan_ubsan, stress_test_arm_asan_ubsan_s3, stress_test_arm_debug, stress_test_arm_msan, stress_test_arm_release, stress_test_arm_tsan, stress_test_arm_ubsan, unit_tests_asan_ubsan, unit_tests_asan_ubsan_function_prop_fuzzer, unit_tests_msan, unit_tests_msan_function_prop_fuzzer, unit_tests_tsan, unit_tests_tsan_function_prop_fuzzer]
if: ${{ !cancelled() && needs.config_workflow.outputs.pipeline_status != '' }}
name: "Finish Workflow"
outputs:
@@ -5687,6 +6177,16 @@ jobs:
- stateless_tests_amd_tsan_s3_storage_sequential_2_2
- stateless_tests_arm_binary_parallel
- stateless_tests_arm_binary_sequential
+ - stateless_tests_amd_binary_cas_s3_storage_parallel
+ - stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_1_2
+ - stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_2_2
+ - stateless_tests_amd_tsan_cas_s3_storage_parallel_1_2
+ - stateless_tests_amd_tsan_cas_s3_storage_parallel_2_2
+ - stateless_tests_amd_msan_cas_s3_storage_parallel_1_3
+ - stateless_tests_amd_msan_cas_s3_storage_parallel_2_3
+ - stateless_tests_amd_msan_cas_s3_storage_parallel_3_3
+ - stateless_tests_arm_binary_cas_s3_storage_parallel
+ - stateless_tests_amd_binary_cas_storage_parallel
- stateless_tests_arm_asan_ubsan_azure_parallel
- stateless_tests_arm_asan_ubsan_azure_sequential_1_2
- stateless_tests_arm_asan_ubsan_azure_sequential_2_2
diff --git a/.github/workflows/pull_request_community.yml b/.github/workflows/pull_request_community.yml
index 6f81498b69bd..29eec6d222e5 100644
--- a/.github/workflows/pull_request_community.yml
+++ b/.github/workflows/pull_request_community.yml
@@ -2073,6 +2073,486 @@ jobs:
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, sequential)' --workflow "Community PR" --ci --timestamp
+ stateless_tests_amd_binary_cas_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_binary, build_amd_debug, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjYXMgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_binary, cas s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, cas s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_BINARY
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, cas s3 storage, parallel)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_1_2:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYXNhbl91YnNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAxLzIp') }}
+ name: "Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 1/2)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 1/2)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_ASAN_UBSAN
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_ASAN_UBSAN
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 1/2)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_amd_asan_ubsan_cas_s3_storage_parallel_2_2:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYXNhbl91YnNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAyLzIp') }}
+ name: "Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 2/2)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 2/2)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_ASAN_UBSAN
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_ASAN_UBSAN
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_asan_ubsan, cas s3 storage, parallel, 2/2)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_amd_tsan_cas_s3_storage_parallel_1_2:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester, 16c]
+ needs: [build_amd_debug, build_amd_tsan, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfdHNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAxLzIp') }}
+ name: "Stateless tests (amd_tsan, cas s3 storage, parallel, 1/2)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_tsan, cas s3 storage, parallel, 1/2)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_TSAN
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_TSAN
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_tsan, cas s3 storage, parallel, 1/2)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_amd_tsan_cas_s3_storage_parallel_2_2:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester, 16c]
+ needs: [build_amd_debug, build_amd_tsan, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfdHNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAyLzIp') }}
+ name: "Stateless tests (amd_tsan, cas s3 storage, parallel, 2/2)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_tsan, cas s3 storage, parallel, 2/2)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_TSAN
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_TSAN
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_tsan, cas s3 storage, parallel, 2/2)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_amd_msan_cas_s3_storage_parallel_1_3:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
+ needs: [build_amd_debug, build_amd_msan, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfbXNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAxLzMp') }}
+ name: "Stateless tests (amd_msan, cas s3 storage, parallel, 1/3)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_msan, cas s3 storage, parallel, 1/3)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_MSAN
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_MSAN
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_msan, cas s3 storage, parallel, 1/3)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_amd_msan_cas_s3_storage_parallel_2_3:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
+ needs: [build_amd_debug, build_amd_msan, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfbXNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAyLzMp') }}
+ name: "Stateless tests (amd_msan, cas s3 storage, parallel, 2/3)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_msan, cas s3 storage, parallel, 2/3)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_MSAN
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_MSAN
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_msan, cas s3 storage, parallel, 2/3)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_amd_msan_cas_s3_storage_parallel_3_3:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
+ needs: [build_amd_debug, build_amd_msan, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfbXNhbiwgY2FzIHMzIHN0b3JhZ2UsIHBhcmFsbGVsLCAzLzMp') }}
+ name: "Stateless tests (amd_msan, cas s3 storage, parallel, 3/3)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_msan, cas s3 storage, parallel, 3/3)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_MSAN
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_MSAN
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_msan, cas s3 storage, parallel, 3/3)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_arm_binary_cas_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester-aarch64, 16c]
+ needs: [build_amd_debug, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhcm1fYmluYXJ5LCBjYXMgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (arm_binary, cas s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (arm_binary, cas s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_ARM_BIN
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_ARM_BIN
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, cas s3 storage, parallel)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_amd_binary_cas_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_binary, build_amd_debug, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjYXMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_binary, cas storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, cas storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_BINARY
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, cas storage, parallel)' --workflow "Community PR" --ci --timestamp
+
integration_tests_amd_asan_ubsan_db_disk_old_analyzer_1_8:
runs-on: [self-hosted, altinity-on-demand, altinity-func-tester, 16c]
needs: [build_amd_asan_ubsan, build_amd_debug, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
diff --git a/.github/workflows/release_builds.yml b/.github/workflows/release_builds.yml
index 8f6f6f22525d..5e943c0cd19e 100644
--- a/.github/workflows/release_builds.yml
+++ b/.github/workflows/release_builds.yml
@@ -1334,9 +1334,162 @@ jobs:
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, sequential)' --workflow "Release Builds" --ci --timestamp
+ stateless_tests_amd_binary_cas_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_binary, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjYXMgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_binary, cas s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, cas s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_BINARY_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, cas s3 storage, parallel)' --workflow "Release Builds" --ci --timestamp
+
+ stateless_tests_arm_binary_cas_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester-aarch64, 16c]
+ needs: [build_arm_binary, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhcm1fYmluYXJ5LCBjYXMgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (arm_binary, cas s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (arm_binary, cas s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_ARM_BIN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_ARM_BIN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, cas s3 storage, parallel)' --workflow "Release Builds" --ci --timestamp
+
+ stateless_tests_amd_binary_cas_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_binary, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjYXMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_binary, cas storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, cas storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_BINARY_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, cas storage, parallel)' --workflow "Release Builds" --ci --timestamp
+
finish_workflow:
runs-on: [self-hosted, altinity-on-demand, altinity-style-checker]
- needs: [build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_msan, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_debug, build_arm_msan, build_arm_release, build_arm_tsan, build_arm_ubsan, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, install_packages_amd_release, install_packages_arm_release, sign_release_amd_release, sign_release_arm_release, source_upload, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential]
+ needs: [build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_msan, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_debug, build_arm_msan, build_arm_release, build_arm_tsan, build_arm_ubsan, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, install_packages_amd_release, install_packages_arm_release, sign_release_amd_release, sign_release_arm_release, source_upload, stateless_tests_amd_binary_cas_s3_storage_parallel, stateless_tests_amd_binary_cas_storage_parallel, stateless_tests_arm_binary_cas_s3_storage_parallel, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential]
if: ${{ !cancelled() && needs.config_workflow.outputs.pipeline_status != '' }}
name: "Finish Workflow"
outputs:
@@ -1433,6 +1586,9 @@ jobs:
- source_upload
- stateless_tests_arm_binary_parallel
- stateless_tests_arm_binary_sequential
+ - stateless_tests_amd_binary_cas_s3_storage_parallel
+ - stateless_tests_arm_binary_cas_s3_storage_parallel
+ - stateless_tests_amd_binary_cas_storage_parallel
- finish_workflow
- GrypeScanServer
- GrypeScanKeeper
diff --git a/ci/defs/altinity_jobs.py b/ci/defs/altinity_jobs.py
index 1d736abaa17e..ab7db9c881be 100644
--- a/ci/defs/altinity_jobs.py
+++ b/ci/defs/altinity_jobs.py
@@ -1,6 +1,7 @@
from praktika import Artifact, Job
from ci.defs.defs import TEMP_DIR, ArtifactNames, RunnerLabels
+from ci.defs.job_configs import common_ft_job_config
class AltinityArtifactNames:
@@ -69,3 +70,53 @@ class AltinityJobConfigs:
command="python3 ./ci/jobs/source_upload.py",
timeout=3600,
)
+ # Stateless tests with a content-addressed disk as the default MergeTree storage.
+ cas_functional_tests_jobs = common_ft_job_config.parametrize(
+ # CAS over S3: RustFS, not MinIO OSS, because the incarnation pool needs
+ # enforced conditional deletes.
+ Job.ParamSet(
+ parameter="amd_binary, cas s3 storage, parallel",
+ runs_on=RunnerLabels.AMD_MEDIUM_CPU,
+ requires=[ArtifactNames.CH_AMD_BINARY_GH],
+ ),
+ # The sanitizer lanes are sharded because an unsharded one exceeds the 6h
+ # GitHub job timeout and is killed before it uploads any results.
+ *[
+ Job.ParamSet(
+ parameter=f"amd_asan_ubsan, cas s3 storage, parallel, {batch}/{total_batches}",
+ runs_on=RunnerLabels.AMD_MEDIUM_CPU,
+ requires=[ArtifactNames.CH_AMD_ASAN_UBSAN_GH],
+ )
+ for total_batches in (2,)
+ for batch in range(1, total_batches + 1)
+ ],
+ *[
+ Job.ParamSet(
+ parameter=f"amd_tsan, cas s3 storage, parallel, {batch}/{total_batches}",
+ runs_on=RunnerLabels.AMD_MEDIUM,
+ requires=[ArtifactNames.CH_AMD_TSAN_GH],
+ )
+ for total_batches in (2,)
+ for batch in range(1, total_batches + 1)
+ ],
+ *[
+ Job.ParamSet(
+ parameter=f"amd_msan, cas s3 storage, parallel, {batch}/{total_batches}",
+ runs_on=RunnerLabels.FUNC_TESTER_AMD,
+ requires=[ArtifactNames.CH_AMD_MSAN_GH],
+ )
+ for total_batches in (3,)
+ for batch in range(1, total_batches + 1)
+ ],
+ Job.ParamSet(
+ parameter="arm_binary, cas s3 storage, parallel",
+ runs_on=RunnerLabels.ARM_MEDIUM_CPU,
+ requires=[ArtifactNames.CH_ARM_BINARY_GH],
+ ),
+ # CAS over local object storage.
+ Job.ParamSet(
+ parameter="amd_binary, cas storage, parallel",
+ runs_on=RunnerLabels.AMD_MEDIUM_CPU,
+ requires=[ArtifactNames.CH_AMD_BINARY_GH],
+ ),
+ )
diff --git a/ci/jobs/functional_tests.py b/ci/jobs/functional_tests.py
index e4aaa09c4348..590ffa7ce0c9 100644
--- a/ci/jobs/functional_tests.py
+++ b/ci/jobs/functional_tests.py
@@ -142,6 +142,8 @@ def run_tests(
"old analyzer": "--analyzer",
"WasmEdge": "--wasm-engine wasmedge",
"s3 storage": "--s3-storage",
+ "cas storage": "--cas-storage",
+ "cas s3 storage": "--cas-s3-storage",
"DatabaseReplicated": "--db-replicated",
"DatabaseOrdinary": "--db-ordinary",
"wide parts enabled": "--wide-parts",
@@ -155,6 +157,8 @@ def run_tests(
OPTIONS_TO_TEST_RUNNER_ARGUMENTS = {
"s3 storage": "--s3-storage --no-stateful",
+ "cas storage": "--cas-storage",
+ "cas s3 storage": "--cas-s3-storage",
"ParallelReplicas": "--no-zookeeper --no-shard --no-parallel-replicas",
"AsyncInsert": " --no-async-insert",
"DatabaseReplicated": " --no-stateful --replicated-database",
@@ -241,6 +245,7 @@ def main():
is_targeted_check = False
is_bugfix_validation = False
is_s3_storage = False
+ is_cas_s3 = False
is_azure_storage = False
is_database_replicated = False
is_shared_catalog = False
@@ -294,8 +299,13 @@ def main():
is_excluded_from_llvm = True
if "per_test_coverage" in to:
is_per_test_coverage = True
- if "s3 storage" in to:
+ if "s3 storage" in to and "cas" not in to:
+ # The CAS-over-s3 variant ("cas s3 storage") installs
+ # only its own default policy and must not pull in the s3 stateful-data / encrypted
+ # storage machinery, so it is deliberately excluded from is_s3_storage.
is_s3_storage = True
+ if "cas s3 storage" in to:
+ is_cas_s3 = True
if "azure" in to:
is_azure_storage = True
if "DatabaseReplicated" in to:
@@ -639,6 +649,14 @@ def main():
def start():
res = CH.start_minio(test_type="stateless") and CH.start_azurite()
+ if res and is_cas_s3:
+ # The CA-over-S3 pool lives on RustFS (M-W D-W8): the incarnation pool
+ # needs ENFORCED conditional deletes, which MinIO OSS lacks (the
+ # fail-closed capability probe rejects it). start_rustfs wipes its data
+ # dir per run, so no pool state bleeds between runs (the local-CA
+ # analogue is the per-run server-store wipe). MinIO keeps the non-CA
+ # s3 disks.
+ res = CH.start_rustfs()
res = res and CH.start()
res = res and CH.wait_ready()
if res:
diff --git a/ci/jobs/scripts/check_style/various_checks.sh b/ci/jobs/scripts/check_style/various_checks.sh
index 3660b8acd193..1d208fff563d 100755
--- a/ci/jobs/scripts/check_style/various_checks.sh
+++ b/ci/jobs/scripts/check_style/various_checks.sh
@@ -230,6 +230,32 @@ done
# CLICKHOUSE_URL already includes "?"
git grep -P 'CLICKHOUSE_URL(|_HTTPS)(}|}/|/|)\?' $ROOT_PATH/tests/queries/0_stateless/*.sh && echo "CLICKHOUSE_URL already includes '?', use '&' to append query parameters"
+# A bare double quote inside a `-q """ ... """` block ends the string early.
+# Bash treats """ as an empty string followed by an open quote, so everything up to the next quote is
+# one argument. A quote anywhere inside -- including in a SQL comment -- closes it there, and the rest
+# of the block becomes shell words. The script stays syntactically valid, so `bash -n` and shellcheck
+# both pass; the only symptom is that the client receives a truncated query and reports a syntax error
+# pointing at whatever followed the quote. Escape it as \" or use single quotes.
+python3 - "$ROOT_PATH" <<'PYEOF'
+import glob, os, re, sys
+
+for path in sorted(glob.glob(os.path.join(sys.argv[1], "tests/queries/0_stateless/*.sh"))):
+ inside = False
+ with open(path, encoding="utf-8", errors="replace") as handle:
+ for number, line in enumerate(handle, 1):
+ if not inside:
+ if re.search(r'(-q|--query)\s+"""\s*$', line.strip()):
+ inside = True
+ continue
+ # The block can close mid-line (`... LIMIT 10;"""`), so only what precedes the
+ # closing delimiter is still inside it.
+ body, closed, _ = line.partition('"""')
+ if '"' in re.sub(r'\\"', "", body):
+ print(f"{path}:{number}: bare double quote inside a -q \"\"\" block ends the SQL early")
+ if closed:
+ inside = False
+PYEOF
+
# Large files checked into git.
# Every byte committed is cloned by every contributor forever and cannot be removed without history rewriting.
# Binary blobs (JARs, archives, .so, datasets) should be downloaded at test time or built from source.
diff --git a/ci/jobs/scripts/clickhouse_proc.py b/ci/jobs/scripts/clickhouse_proc.py
index 88a4e56c97ba..cbc8ad5984b4 100644
--- a/ci/jobs/scripts/clickhouse_proc.py
+++ b/ci/jobs/scripts/clickhouse_proc.py
@@ -9,6 +9,7 @@
import threading
import traceback
import uuid
+import zipfile
from collections import defaultdict
from pathlib import Path
from typing import List
@@ -43,6 +44,7 @@ class ClickHouseProc:
MINIO_LOG = f"{temp_dir}/minio.log"
AZURITE_LOG = f"{temp_dir}/azurite.log"
KAFKA_LOG = f"{temp_dir}/kafka.log"
+ RUSTFS_LOG = f"{temp_dir}/rustfs.log"
LOGS_SAVER_CLIENT_OPTIONS = "--max_memory_usage 10G --max_threads 1 --max_rows_to_read=0 --max_result_rows 0 --max_result_bytes 0 --max_bytes_to_read 0 --max_execution_time 0 --max_execution_time_leaf 0 --max_estimated_execution_time 0"
DMESG_LOG = f"{temp_dir}/dmesg.log"
# TODO: run servers in dedicated wds to keep trash localised
@@ -163,6 +165,77 @@ def start_minio(self, test_type):
print("Failed to start minio")
return False
+ RUSTFS_VERSION = "1.0.0-beta.12"
+
+ def download_rustfs(self, rustfs_bin):
+ machine = platform.machine()
+ if machine not in ("x86_64", "aarch64", "arm64"):
+ print(f"unsupported architecture for rustfs [{machine}]")
+ return False
+ arch = "aarch64" if machine in ("aarch64", "arm64") else "x86_64"
+ url = (
+ f"https://github.com/rustfs/rustfs/releases/download/{self.RUSTFS_VERSION}"
+ f"/rustfs-linux-{arch}-musl-v{self.RUSTFS_VERSION}.zip"
+ )
+ zip_path = f"{temp_dir}/rustfs.zip"
+ if not Shell.check(
+ f"curl -sSfL --retry 3 --retry-delay 5 -o {zip_path} {url}", verbose=True
+ ):
+ print(f"failed to download rustfs from {url}")
+ return False
+ # The release zip contains the single `rustfs` binary at its root.
+ with zipfile.ZipFile(zip_path) as archive:
+ archive.extract("rustfs", temp_dir)
+ os.remove(zip_path)
+ os.chmod(rustfs_bin, 0o755)
+ return True
+
+ def start_rustfs(self):
+ # RustFS backs the CAS-over-S3 pool because the incarnation pool needs enforced
+ # conditional operations (a wrong-token DELETE must fail with 412) that MinIO OSS lacks;
+ # MinIO keeps serving the non-CAS s3 disks on its own port. Binary and data dir live
+ # under ci/tmp, which CI wipes per run, so no pool state bleeds between runs.
+ rustfs_bin = f"{temp_dir}/rustfs"
+ if not Path(rustfs_bin).is_file() and not self.download_rustfs(rustfs_bin):
+ print(f"rustfs binary not found at {rustfs_bin} and download failed")
+ return False
+ data_dir = f"{temp_dir}/rustfs_data"
+ Shell.check(f"rm -rf {data_dir} && mkdir -p {data_dir}", verbose=True)
+ # The background data-scanner and auto-heal manager do no useful work on a single-disk
+ # ephemeral pool, but their namespace locks produced multi-minute bursts of 503
+ # ServiceUnavailable that stalled client I/O. Client GET/PUT/LIST/DELETE do not depend on
+ # either. The RUSTFS_ENABLE_* spellings are deprecated since 1.0.0-beta.8.
+ # Raise the open-files limit for the same reason start_azurite does: under parallel load
+ # the server holds thousands of S3 connections, and at the default soft limit (1024)
+ # rustfs runs out of fds and refuses new TCP connections in bursts.
+ command = (
+ "(ulimit -n 1048576 2>/dev/null || ulimit -n $(ulimit -Hn)) && "
+ f"RUSTFS_SCANNER_ENABLED=false RUSTFS_HEAL_ENABLED=false "
+ f"{rustfs_bin} server --address 0.0.0.0:11121 "
+ f"--access-key clickhouse --secret-key clickhouse {data_dir}"
+ )
+ with open(self.RUSTFS_LOG, "w") as log_file:
+ self.rustfs_proc = subprocess.Popen(
+ command, stdout=log_file, stderr=subprocess.STDOUT, shell=True
+ )
+ print(f"Started rustfs asynchronously with PID {self.rustfs_proc.pid}")
+
+ if not Shell.check(
+ "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:11121/ | grep -qE '403|200'",
+ verbose=False,
+ retries=6,
+ ):
+ print("Failed to start rustfs")
+ return False
+ # The `test` bucket the storage policy expects.
+ res = Shell.check(
+ "/mc alias set carustfs http://localhost:11121 clickhouse clickhouse && /mc mb --ignore-existing carustfs/test",
+ verbose=True,
+ )
+ if not res:
+ print("Failed to create rustfs test bucket")
+ return res
+
def start_azurite(self):
# Raise the open files limit before launching azurite-rs.
# Each concurrent test query opens a TCP connection plus an in-memory
@@ -938,6 +1011,8 @@ def prepare_logs(self, info, all=False):
res.append(self.AZURITE_LOG)
if Path(self.KAFKA_LOG).exists():
res.append(self.KAFKA_LOG)
+ if Path(self.RUSTFS_LOG).exists():
+ res.append(self.RUSTFS_LOG)
if Path(self.DMESG_LOG).exists():
res.append(self.DMESG_LOG)
if Path(self.CH_LOCAL_ERR_LOG).exists():
@@ -1211,6 +1286,29 @@ def dump_system_tables(self):
Shell.check(
f"sed -i 's|.*|{self.CH_LOCAL_ERR_LOG}|' /etc/clickhouse-server/config.xml"
)
+ # Open any CAS disk read-only: a writable open claims server-root ownership and fails
+ # closed against the real server's persisted owner uuid, while a read-only open skips the
+ # claim and is all a dump needs. Keyed on the `cas` marker
+ # rather than on disk names, so it covers every CAS disk however this job names it.
+ # `grep -R` and `sed --follow-symlinks` are required: `tests/config/install.sh` symlinks
+ # these configs into `config.d`, and `-r`/plain `sed` would silently match nothing.
+ Shell.check(
+ "grep -Rl 'cas' /etc/clickhouse-server/ 2>/dev/null "
+ "| xargs -r sed -i --follow-symlinks 's|cas|castrue|g'"
+ )
+ # Report loudly if the substitution stops matching: a declared but not read-only CAS disk
+ # means this scrape is about to die on ownership. Reports; does not abort the dump.
+ if Shell.check(
+ "grep -Rlq 'cas' /etc/clickhouse-server/",
+ verbose=False,
+ ) and not Shell.check(
+ "grep -Rlq 'castrue' /etc/clickhouse-server/",
+ verbose=False,
+ ):
+ print(
+ "WARNING: a CAS disk is declared but the read-only marker was not inserted "
+ "-- `clickhouse local` will claim server-root ownership and this scrape will fail"
+ )
# FIXME: Hack for s3_with_keeper (note, that we don't need the disk,
# the problem is that whenever we need disks all disks will be
# initialized [1])
@@ -1226,8 +1324,12 @@ def dump_system_tables(self):
self.restore_system_metadata_files_from_remote_database_disk()
+ # `**`, not `*`: dynamic cache disks created by tests nest their path, e.g.
+ # `filesystem_caches/disks/cache_03517/status` — a one-level glob missed exactly that file,
+ # and the scrape died on its flock (`StatusFile.cpp` "Another server instance ... is already
+ # running") when the server had not released it.
cache_status_files = glob.glob(
- f"{self.ch_var_lib_dir}/filesystem_caches/*/status"
+ f"{self.ch_var_lib_dir}/filesystem_caches/**/status", recursive=True
)
if cache_status_files:
print(
diff --git a/ci/workflows/backport_branches.py b/ci/workflows/backport_branches.py
index 528a47cbe32e..5f891943d5c9 100644
--- a/ci/workflows/backport_branches.py
+++ b/ci/workflows/backport_branches.py
@@ -1,9 +1,15 @@
from praktika import Workflow
from ci.defs.defs import DOCKERS, SECRETS, ArtifactConfigs
+from ci.defs.altinity_jobs import AltinityJobConfigs
from ci.defs.job_configs import JobConfigs
from ci.jobs.scripts.workflow_hooks.filter_job import should_skip_job
+FUNCTIONAL_TESTS_JOBS = [
+ *JobConfigs.functional_tests_jobs,
+ *AltinityJobConfigs.cas_functional_tests_jobs,
+]
+
workflow = Workflow.Config(
name="BackportPR",
event=Workflow.Event.PULL_REQUEST,
@@ -24,7 +30,7 @@
JobConfigs.docker_keeper,
*JobConfigs.install_check_jobs,
*JobConfigs.compatibility_test_jobs,
- *[job for job in JobConfigs.functional_tests_jobs if "amd_asan_ubsan" in job.name],
+ *[job for job in FUNCTIONAL_TESTS_JOBS if "amd_asan_ubsan" in job.name],
*[
job
for job in JobConfigs.unittest_jobs
diff --git a/ci/workflows/master.py b/ci/workflows/master.py
index b3ffe686f331..b98ffb96fa66 100644
--- a/ci/workflows/master.py
+++ b/ci/workflows/master.py
@@ -13,6 +13,11 @@
from ci.jobs.scripts.workflow_hooks.filter_job import should_skip_job
from ci.workflows.pull_request import REGULAR_BUILD_NAMES
+FUNCTIONAL_TESTS_JOBS = [
+ *JobConfigs.functional_tests_jobs,
+ *AltinityJobConfigs.cas_functional_tests_jobs,
+]
+
# Add long retention tags to subset of artifacts
clickhouse_binaries_with_tags = []
for artifact in ArtifactConfigs.clickhouse_binaries + ArtifactConfigs.clickhouse_stripped_binaries:
@@ -55,7 +60,7 @@
*JobConfigs.compatibility_test_jobs,
*[
j
- for j in JobConfigs.functional_tests_jobs
+ for j in FUNCTIONAL_TESTS_JOBS
if "coverage" not in j.name
],
# *JobConfigs.functional_test_llvm_coverage_jobs,
diff --git a/ci/workflows/pull_request.py b/ci/workflows/pull_request.py
index ae88a3b3a244..a3d063400716 100644
--- a/ci/workflows/pull_request.py
+++ b/ci/workflows/pull_request.py
@@ -14,11 +14,16 @@
from ci.jobs.scripts.workflow_hooks.filter_job import should_skip_job
from ci.jobs.scripts.workflow_hooks.trusted import can_be_tested
-ALL_FUNCTIONAL_TESTS = [job.name for job in JobConfigs.functional_tests_jobs]
+FUNCTIONAL_TESTS_JOBS = [
+ *JobConfigs.functional_tests_jobs,
+ *AltinityJobConfigs.cas_functional_tests_jobs,
+]
+
+ALL_FUNCTIONAL_TESTS = [job.name for job in FUNCTIONAL_TESTS_JOBS]
FUNCTIONAL_TESTS_PARALLEL_BLOCKING_JOB_NAMES = [
job.name
- for job in JobConfigs.functional_tests_jobs
+ for job in FUNCTIONAL_TESTS_JOBS
if any(
substr in job.name
for substr in (
@@ -40,7 +45,7 @@
REGULAR_BUILD_NAMES = [job.name for job in JobConfigs.build_jobs]
PLAIN_FUNCTIONAL_TEST_JOB = [
- j for j in JobConfigs.functional_tests_jobs if "amd_debug, parallel" in j.name
+ j for j in FUNCTIONAL_TESTS_JOBS if "amd_debug, parallel" in j.name
][0]
workflow = Workflow.Config(
@@ -91,7 +96,7 @@
if j.name not in FUNCTIONAL_TESTS_PARALLEL_BLOCKING_JOB_NAMES
else []
)
- for j in JobConfigs.functional_tests_jobs
+ for j in FUNCTIONAL_TESTS_JOBS
if "coverage" not in j.name
],
*[
diff --git a/ci/workflows/pull_request_community.py b/ci/workflows/pull_request_community.py
index af76e6ac9c4b..71fe9bbca8ed 100644
--- a/ci/workflows/pull_request_community.py
+++ b/ci/workflows/pull_request_community.py
@@ -2,12 +2,18 @@
from praktika import Workflow, Artifact
from ci.defs.defs import BASE_BRANCH, DOCKERS, ArtifactConfigs, JobNames
+from ci.defs.altinity_jobs import AltinityJobConfigs
from ci.defs.job_configs import JobConfigs
from ci.jobs.scripts.workflow_hooks.filter_job import should_skip_job
+FUNCTIONAL_TESTS_JOBS = [
+ *JobConfigs.functional_tests_jobs,
+ *AltinityJobConfigs.cas_functional_tests_jobs,
+]
+
FUNCTIONAL_TESTS_PARALLEL_BLOCKING_JOB_NAMES = [
job.name
- for job in JobConfigs.functional_tests_jobs
+ for job in FUNCTIONAL_TESTS_JOBS
if any(
substr in job.name
for substr in (
@@ -24,7 +30,7 @@
]
PLAIN_FUNCTIONAL_TEST_JOB = [
- j for j in JobConfigs.functional_tests_jobs if "amd_debug, parallel" in j.name
+ j for j in FUNCTIONAL_TESTS_JOBS if "amd_debug, parallel" in j.name
][0]
def _normalize_gh_aliases(items):
@@ -74,7 +80,7 @@ def _normalize_gh_aliases(items):
if j.name not in FUNCTIONAL_TESTS_PARALLEL_BLOCKING_JOB_NAMES
else []
)
- for j in JobConfigs.functional_tests_jobs if 'coverage' not in j.name
+ for j in FUNCTIONAL_TESTS_JOBS if 'coverage' not in j.name
],
*[
job.set_run_after(FUNCTIONAL_TESTS_PARALLEL_BLOCKING_JOB_NAMES)
diff --git a/ci/workflows/release_branches.py b/ci/workflows/release_branches.py
index 2969c8f10b30..78f43c15d600 100644
--- a/ci/workflows/release_branches.py
+++ b/ci/workflows/release_branches.py
@@ -1,9 +1,15 @@
from praktika import Workflow
from ci.defs.defs import BINARIES_WITH_LONG_RETENTION, DOCKERS, SECRETS, ArtifactConfigs
+from ci.defs.altinity_jobs import AltinityJobConfigs
from ci.defs.job_configs import JobConfigs
from ci.jobs.scripts.workflow_hooks.filter_job import should_skip_job
+FUNCTIONAL_TESTS_JOBS = [
+ *JobConfigs.functional_tests_jobs,
+ *AltinityJobConfigs.cas_functional_tests_jobs,
+]
+
builds_for_release_branch = [
job
for job in JobConfigs.build_jobs
@@ -31,7 +37,7 @@
JobConfigs.docker_server,
JobConfigs.docker_keeper,
*JobConfigs.install_check_master_jobs,
- *[job for job in JobConfigs.functional_tests_jobs if "asan" in job.name],
+ *[job for job in FUNCTIONAL_TESTS_JOBS if "asan" in job.name],
*[job for job in JobConfigs.unittest_jobs if "fuzzer" not in job.name],
*[
job
diff --git a/ci/workflows/release_builds.py b/ci/workflows/release_builds.py
index 5e6165f9a744..d11b1324968e 100644
--- a/ci/workflows/release_builds.py
+++ b/ci/workflows/release_builds.py
@@ -5,6 +5,11 @@
from ci.defs.job_configs import JobConfigs
from ci.jobs.scripts.workflow_hooks.filter_job import should_skip_job
+FUNCTIONAL_TESTS_JOBS = [
+ *JobConfigs.functional_tests_jobs,
+ *AltinityJobConfigs.cas_functional_tests_jobs,
+]
+
# Add long retention tags to subset of artifacts
clickhouse_binaries_with_tags = []
for artifact in ArtifactConfigs.clickhouse_binaries + ArtifactConfigs.clickhouse_stripped_binaries:
@@ -45,7 +50,7 @@
AltinityJobConfigs.source_upload_job,
*[
job
- for job in JobConfigs.functional_tests_jobs
+ for job in FUNCTIONAL_TESTS_JOBS
if any(t in job.name for t in ("release", "binary"))
],
],
diff --git a/docs/en/antalya/cas/architecture/backend.md b/docs/en/antalya/cas/architecture/backend.md
new file mode 100644
index 000000000000..fab4d23e323b
--- /dev/null
+++ b/docs/en/antalya/cas/architecture/backend.md
@@ -0,0 +1,100 @@
+---
+description: 'The Cas::Backend storage seam, its token contract, the per-provider conditional-write dialects, and the mount-time capability probe.'
+sidebar_label: 'Backend abstraction'
+sidebar_position: 11
+slug: /antalya/cas/architecture/backend
+title: 'CAS Architecture — Backend Abstraction'
+doc_type: 'reference'
+---
+
+# Backend abstraction {#backend-abstraction}
+
+Every protocol described elsewhere in this set — blobs, manifests, refs, mounts, GC — is written
+against one interface, `Cas::Backend` (`Backend/CasBackend.h`). It is a token-aware storage seam:
+every present key has exactly one current incarnation identified by an opaque `Token`, and
+`putOverwrite`/`casPut` succeed only against the expected current token (or expected absence).
+
+## The interface {#interface}
+
+| Method | Contract |
+|---|---|
+| `get` / `getStream` | Read bytes (or a forward-only stream, for write-once objects) plus the token of the incarnation read |
+| `head` | Existence, size, token, and metadata without reading the body |
+| `putIfAbsent` / `putIfAbsentStream` | Create-if-absent (`If-None-Match: *`); `PreconditionFailed` is a returned outcome, never an exception |
+| `putOverwrite` | Replace the current object only when its token equals `expected`; a mismatch is a returned outcome |
+| `casPut` | `expected == nullopt` ⇒ create-if-absent CAS (used for the first write of a root object); a set `expected` conditionally replaces that exact incarnation |
+| `deleteExact` | Delete only the incarnation named by `token`; a token mismatch (`TokenMismatch`) leaves the object untouched and is distinguished from `NotFound` |
+| `list` | One page of keys under a prefix, resumed by the backend's own cursor |
+| `supportsListTokens` | Whether `list` can surface a per-key incarnation token, letting GC discovery skip an unchanged root shard without a `GET` |
+| `promoteStaged` / `resurrect` | `promoteStaged`: write-once server-side copy from S3 staging (optional, defaults to `NOT_IMPLEMENTED`). `resurrect`: unconditional re-upload displacing a condemned incarnation from a caller-supplied reader (streamed on remote object storage, materialized one-at-a-time on the local emulated mode); size-checked before publication, and fresh-tagged so pending deletes of the old incarnation cannot remove it |
+
+`deleteExact`, `putIfAbsent`/`putIfAbsentStream`, and `putOverwrite`/`casPut` are safety-critical:
+they are what makes exact-token deletes, write-once creation, and mutual exclusion hold. Every
+other method is protocol hygiene.
+
+**`TOKEN ⟹ CONTENT`** is the one contract item the capability probe cannot check: a token must
+uniquely identify the byte content of the incarnation it labels, so that a repeated token never
+means different bytes. The read-path decode cache skips a re-read on a token match, so a backend
+that recycled tokens across different content would serve stale manifests — a wrong-result bug, not
+merely inefficiency. `S3` `ETag`s are content-derived; the in-memory and emulated backends mint a
+strictly monotonic sequence that is never reused. This remains a standing requirement of every
+backend implementation, not a property the probe verifies.
+
+## Provider dialects {#dialects}
+
+`ObjectStorageBackend` (`Backend/CasObjectStorageBackend.cpp`) wraps one `IObjectStorage` and picks
+its token dialect from `IObjectStorage::conditionalOpsUseGenerationTokens()`:
+
+| Dialect | Token type | How a conditional write is expressed |
+|---|---|---|
+| `AWS` (default) | `ETag` | `If-None-Match: *` / `If-Match: ` sent as-is |
+| `GCS` | `Generation` | The backend rewrites conditional headers before the request goes out: `If-None-Match: *` becomes `x-goog-if-generation-match: 0`, and `If-Match: ` becomes `x-goog-if-generation-match: ` (`applyGcsConditionalDialectToRequest`, `IO/S3/GCSConditionalDialect.cpp`) |
+
+The GCS dialect is opted into by client configuration (`http_client = gcs_hmac` or `gcp_oauth`), not
+auto-detected from the endpoint host. It also rejects one shape outright: a **conditional
+`CompleteMultipartUpload`** throws rather than silently dropping the precondition, because GCS
+ignores preconditions on that call — a measured, documented gap, not a hypothetical one. `CAS`'s
+conditional writes therefore always take the single-`PUT` path on a generation-dialect backend.
+
+This bounds conditional writes only: the write-once create is therefore single-part and limited by
+`gcs_max_conditional_put_bytes` on a generation dialect, while the unconditional resurrect takes the
+ordinary multipart path and has no size limit on any backend.
+
+Every request carrying a rewritten header also has its AWS auth headers stripped and every
+remaining `x-amz-*` header renamed to `x-goog-*`, since GCS rejects a mixed header set.
+
+Azure Blob Storage's REST API documents equivalent conditional headers (`If-None-Match`,
+`If-Match`), but no third dialect exists in this backend yet — `IObjectStorage`'s Azure
+implementation does not currently wire up a `CAS` conditional path, so Azure is untested by the
+capability probe below, not merely a slower-verified third case.
+
+## Exact-token delete, per provider {#exact-token-delete}
+
+`deleteExact(key, token)` is realized as a conditional `DELETE` naming the token as a precondition:
+an `If-Match`-style delete on `AWS` (`ETag`), a generation-match delete on `GCS`. A precondition
+failure — `S3::isPreconditionFailedError` — is reported as `DeleteOutcome::TokenMismatch`, never as
+an exception; the object is left untouched. `DeleteOutcome::created_delete_marker` reports whether
+the backend created a delete marker instead of actually removing the object, which the capability
+probe rejects: a bucket with versioning enabled would let `CAS` "delete" a blob without freeing any
+storage, and GC would silently stop reclaiming.
+
+## The capability probe {#capability-probe}
+
+`runCapabilityProbe` (`Backend/CasProbe.cpp`) runs a throwaway-key battery against every writable
+mount, described in full on the [bucket requirements](/antalya/cas/bucket-requirements) page. It is
+fail-closed: any check that does not pass throws `NOT_IMPLEMENTED` naming the specific failure, and
+the mount refuses to become writable. Two mount-time gates sit alongside it:
+
+- `checkPoolPreconditions` — on the `GCS`-dialect combination only, verifies bucket versioning is
+ off (a confirmed `Enabled` throws; an inconclusive check proceeds under an assumption, logged at
+ `WARNING`, that versioning is off).
+- `checkConditionalWriteSingleAttemptSupport` — refuses to mount writable unless the underlying
+ object storage supports a single-HTTP-attempt retry profile for conditional writes. A hidden SDK
+ retry can outlive the writer's mount lease and obscure whether a conditional operation actually
+ committed, so retries on the conditional path must be explicit CAS state-machine transitions, not
+ transparent client behavior.
+
+A third, optional probe (`probeConditionalCopy`) checks whether the backend enforces a write-once
+conditional server-side copy. It only matters for `staging_backend = s3`: when the probe reports
+`false`, S3-native staging silently falls back to local staging rather than refusing to mount, since
+enforcement here is an optimization, not a correctness requirement of the disk itself.
diff --git a/docs/en/antalya/cas/architecture/blob-protocol.md b/docs/en/antalya/cas/architecture/blob-protocol.md
new file mode 100644
index 000000000000..64a62511afba
--- /dev/null
+++ b/docs/en/antalya/cas/architecture/blob-protocol.md
@@ -0,0 +1,232 @@
+---
+description: 'How CAS writes, deduplicates, and reclaims a blob: conditional-write sequencing, the writer-versus-GC race, and the deterministic-artifact adoption pin.'
+sidebar_label: 'Blob protocol'
+sidebar_position: 3
+slug: /antalya/cas/architecture/blob-protocol
+title: 'CAS Architecture — Blob Protocol'
+doc_type: 'reference'
+---
+
+# CAS architecture — blob protocol {#blob-protocol}
+
+A blob is the unit of content-addressed storage: one part file's bytes, keyed by a hash of its
+own content. This page covers how a blob gets written exactly once, how a duplicate write is
+turned into a no-op, and how a writer and a `GC` round racing over the same blob are kept safe
+without ever comparing multi-gigabyte bodies. Object layout and the four durable object kinds
+are covered on the [overview page](/antalya/cas/architecture/); `GC`'s fold and round structure
+is covered on the GC page.
+
+## Conditional-write sequence {#conditional-write-sequence}
+
+Every blob body lives at a key derived purely from its content hash
+(`blobs///`, `CasLayout::blobKey`), with a sidecar `.meta` object at the
+same key plus `.meta`. Because the key already encodes the digest, the backend never needs a
+compare-and-swap on content — only on *presence* (`PUT` with `If-None-Match: *`) or on a specific
+prior incarnation (`PUT`/`DELETE` with `If-Match: `).
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant Writer
+ participant S3 as Object store
+
+ Writer->>Writer: hash source, derive key from digest
+ alt dedup cache hit OR size >= deduplication_head_first_min_bytes
+ Writer->>S3: HEAD blobs/algo/hex
+ alt body present
+ Writer->>S3: GET .meta (point read, body never streamed)
+ Writer->>Writer: adopt current token if Clean or absent
+ else body absent
+ Writer->>S3: putIfAbsentStream (If-None-Match: star)
+ end
+ else small, no cache hit
+ Writer->>S3: putIfAbsentStream (If-None-Match: star) directly
+ end
+ S3-->>Writer: Done -- fresh upload, write Clean meta
+ S3-->>Writer: PreconditionFailed -- someone occupies the key
+ opt on PreconditionFailed
+ Writer->>S3: HEAD blobs/algo/hex
+ Writer->>S3: GET .meta -- adopt the occupant's token as a dependency
+ end
+```
+
+Ordered steps (`Pool/CasPartWriteTxn.cpp:160-245` and `:427-779`):
+
+1. `requireAlive()` — the build is not abandoned, the namespace not dropped, the writer epoch
+ still live.
+2. **Adaptive dedup gate.** `HEAD` first if the dedup cache reports the content present, or the
+ object is at least `deduplication_head_first_min_bytes` (default 1 MiB). Below that threshold
+ a speculative conditional `PUT` is cheaper than a `HEAD` plus a `PUT`.
+3. On a `HEAD` hit, `observeAndAdmit` point-reads the `.meta` sidecar and adopts the live
+ incarnation — the body is never streamed for a dedup hit.
+4. Otherwise a bounded retry loop (up to 8 attempts) around `uploadFromSource`, which mints a
+ **fresh `incarnation_tag` per attempt** and does either a conditional server-side `COPY` from
+ S3 staging or a streaming `putIfAbsentStream`. The byte count is verified against the declared
+ source size.
+5. A 412 means someone occupies the key. Because the key embeds the content digest, **any
+ occupant is by definition the intended content** — ambiguity is resolved by one `HEAD`
+ (occupancy), never by comparing bodies.
+6. `Unresolved` (timeout, 5xx, connection loss) never acks. It throws retry-later — nothing was
+ published, so a body that lands late is inert debris for the orphan sweep.
+
+**Two writers uploading identical content** both derive the same key and both send
+`If-None-Match: *`. The object store serializes them: one gets `Done`, the other gets 412,
+`HEAD`s, point-reads `.meta`, and adopts the winner's token as its own dependency. The loser never
+published anything — a failed or cancelled sink publishes nothing — and its adopt is protected by
+its own durable precommit edge (see [the writer-versus-GC race](#writer-gc-race)). Both writers
+are safe; the only cost is one wasted upload attempt.
+
+## Dedup and the identity primitive {#dedup-identity}
+
+Two blobs are the same object if and only if they hash to the same digest under the pool's
+configured algorithm. Nothing else — not size, not `LIST` order, not a cheap prefix compare —
+is allowed to stand in for that check. This follows the same rule everywhere in CAS: identity
+is *proven* by hash equality, never *inferred* by a cheap signal, and re-hashing on read is the
+identity primitive wherever the correctness of a decision depends on it.
+
+The blob content hash is pluggable per pool, fixed at pool creation: `blob_hash` selects
+`cityhash128` (default), `xxh3-128`, or `sha256` (`parseBlobHashAlgo`,
+`Primitives/CasBlobDigest.h`). A blob is identified by the pair `BlobRef = (BlobHashAlgo, digest)`,
+never by a bare digest — a bare digest is ambiguous once more than one algorithm can appear in a
+pool. `blob_hash_allow_new` gates admitting a second algorithm into an already-populated pool's
+`algos_used` set; it defaults to off.
+
+`cityhash128` is not cryptographically collision-resistant. A pool shared across mutually
+untrusted writers should run `sha256` — CAS enforces no policy choice here; the operator picks
+the threat model via `blob_hash`. This is why the dedup admission gate is a `HEAD` (occupancy)
+rather than a body compare: it tells the writer *something* already claims this key, and the
+digest is the only claim CAS trusts.
+
+## The writer-versus-GC race {#writer-gc-race}
+
+This is the interleaving that gets the most reviewer attention, because a writer and a `GC` round
+can legitimately disagree about whether a blob is still needed.
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant W as Writer
+ participant S3 as Object store
+ participant GC as GC leader
+
+ Note over GC: round n -- fold finds in-degree 0
+ GC->>S3: HEAD blob -- capture exact token t1
+ GC->>S3: write .meta = Condemned round n
+
+ rect rgba(120,160,255,0.12)
+ Note over W,S3: a writer arrives wanting this content
+ W->>S3: append ref-log PRECOMMIT (durable +1 edge)
+ W->>S3: HEAD blob (present, token t1)
+ W->>S3: GET .meta
+ alt meta is Clean
+ W->>W: adopt t1 as dependency
+ Note over GC: next fold sees in-degree >= 1 -- spared
+ else meta is Condemned
+ W->>S3: PUT blob unconditional re-upload of writer own source, fresh incarnation tag -- token t2 not t1
+ W->>S3: CAS .meta back to Clean
+ end
+ end
+
+ Note over GC: round n+1 -- graduation, only if still zero
+ GC->>S3: re-verify in-degree, requires confirmed durable Condemned evidence for hash+token t1
+ Note over GC: publishes delete_pending
+
+ Note over GC: round n+2 -- the single content-delete site
+ GC->>S3: deleteExact(blob, t1)
+ alt writer resurrected
+ S3-->>GC: TokenMismatch -- nothing deleted, blob is live at t2
+ else genuinely dead
+ S3-->>GC: Deleted -- then drop the .meta
+ end
+```
+
+The invariant that makes every interleaving safe: **revival is re-upload only — never `GET` a
+condemned object to revive it.** A writer that finds `Condemned` metadata does not resurrect the
+existing body; it re-uploads its own source bytes under a fresh `incarnation_tag`, producing a
+new token that no prior `deleteExact` call can name. `GC` never streams a body it might delete,
+and a writer never trusts a body it did not itself just write.
+
+Why this closes the race in both directions:
+
+- A writer that **adopts** a token must have read a non-`Condemned` marker, and its precommit
+ edge was durable *before* that read. The next fold therefore sees in-degree ≥ 1 and spares the
+ blob.
+- A writer that **resurrects** changes the token. A stale `deleteExact(t1)` then returns
+ `TokenMismatch` and reclaims nothing — the delete names an exact incarnation, never "the object
+ at this key".
+- The delete lags condemnation by at least two full rounds, and publishing the one edge that
+ authorizes an irreversible delete requires confirmed durable `Condemned` evidence for that
+ exact `(hash, token)` pair. Without it `GC` never throws — it carries the entry and retries the
+ marker write on the next round.
+
+Both directions degrade to a spurious re-upload or a no-op delete. Neither can lose data or leave
+a dangling manifest entry.
+
+**One asymmetry worth flagging:** on a local (emulated) disk the resurrect path materializes the
+full `[header][payload]` in memory; resurrections are serialized, so at most one body is held whole
+in RAM at a time. On remote object storage the resurrect streams and holds nothing.
+
+### The `.meta` sidecar {#meta-sidecar}
+
+`.meta` has exactly two states: `Clean` (body present, may be referenced) and `Condemned`
+(`GC` observed zero in-degree; the body is still present and a writer may resurrect it). An
+*absent* `.meta` reads exactly like `Clean` — there is no third "unaccounted" state in the
+stored format; `unaccounted` is an `ca-fsck` classification, not something `GC` ever writes.
+
+The record carries `state`, `condemn_round`, and `size`, and deliberately carries **no token**:
+it is a per-hash hint, not a per-incarnation fact. All safety comes from the body's in-envelope
+`incarnation_tag` plus exact-token deletes; a stale marker costs at worst one spurious re-upload,
+never a lost delete or a false revival.
+
+## Deterministic artifacts and the adoption pin {#deterministic-artifacts}
+
+Some CAS objects are a pure function of their inputs: the `GC` source-edge run files (`cas_run`)
+and fold seals (`cas_fold_seal`). For these, `putDeterministicArtifact`
+(`Gc/CasBlobInDegree.cpp:341-352`) is the write-once helper:
+
+```cpp
+if (backend.putIfAbsent(key, bytes).outcome == PutOutcome::PreconditionFailed)
+{
+ const auto existing = backend.get(key);
+ if (!existing || existing->bytes != bytes)
+ throw Exception(ErrorCodes::CORRUPTED_DATA, ...);
+ /// byte-equal => our own deterministic replay; adopt (no-op).
+}
+```
+
+The idempotency argument: identical inputs produce byte-identical output, so a replayed round —
+leader deposed mid-round, round `CAS` aborted, crash-restart — re-derives exactly the same bytes.
+A 412 therefore means "already occupied by our own replay", verified by comparing the fetched
+bytes, not inferred from occupancy alone as blob uploads do. Divergent bytes are impossible under
+correct operation and fail closed as `CORRUPTED_DATA`.
+
+This is the format-evolution **adoption pin**, documented in the persisted-format registry
+(`Formats/README.md`): on a `putDeterministicArtifact` conflict, the writer re-encodes at the `v`
+of the *existing* object rather than at its own current build's version, so two writers on
+different builds replaying the same deterministic round still land on byte-identical output.
+
+The helper is explicitly **not** for observation-bearing artifacts — `GC` outcome logs carry
+`HEAD`-observed tokens on which two observers may legitimately disagree, so those use
+first-durable-write-wins byte-adopt semantics instead. And a blob body can never use this path:
+the fresh-tag rule means two attempts at the same logical create are allowed to legitimately
+differ, which is exactly what `putDeterministicArtifact`'s divergence check would reject.
+
+## Settings {#settings}
+
+All names below are unprefixed keys inside the disk's `cas` config block
+(`ContentAddressedSettings.cpp`, `LIST_OF_CONTENT_ADDRESSED_SETTINGS`); none carry a `cas_`/`ca_`
+prefix.
+
+| Setting | Controls | Default |
+|---|---|---|
+| `blob_hash` | Pool blob content-hash function (`cityhash128` \| `xxh3-128` \| `sha256`); fixed at pool creation | `cityhash128` |
+| `blob_hash_allow_new` | Explicit opt-in to admit a new hash algorithm into an existing pool's `algos_used` | `false` |
+| `deduplication_cache_bytes` | Byte budget of the blob-presence cache that feeds the dedup `HEAD`-first decision (`0` disables) | 64 MiB |
+| `deduplication_head_first_min_bytes` | Minimum blob size to try a `HEAD` before uploading the body | 1 MiB |
+| `staging_backend` | Blob staging backend (`local` \| `s3`); `s3` is opt-in | `local` |
+| `scratch_path` | Server-local scratch directory for the local-staging write-buffer spill; a relative value is anchored to the server data path | `""` |
+| `gcs_max_conditional_put_bytes` | Largest conditional write on a generation-token store (GCS forces those single-part); does not bound the unconditional resurrect | 1 GiB |
+
+`GC`-round budgets that gate condemnation and reclaim of these same blobs (graduation, redelete,
+sweep budgets) live on the GC architecture page, not here — they govern the `GC` side of the race
+in [Writer-versus-GC race](#writer-gc-race), not the write path.
diff --git a/docs/en/antalya/cas/architecture/correctness.md b/docs/en/antalya/cas/architecture/correctness.md
new file mode 100644
index 000000000000..1beb28b0039f
--- /dev/null
+++ b/docs/en/antalya/cas/architecture/correctness.md
@@ -0,0 +1,91 @@
+---
+description: 'What the TLA+ model corpus proves about CAS safety, the counterexamples that shaped the design, and how model-checking and soak/chaos testing complement each other.'
+sidebar_label: 'Correctness'
+sidebar_position: 12
+slug: /antalya/cas/architecture/correctness
+title: 'CAS Architecture — Correctness'
+doc_type: 'reference'
+---
+
+# CAS architecture — correctness {#correctness}
+
+`CAS` treats formal modelling as a pre-implementation gate, not after-the-fact documentation. No
+task that changes safety-relevant behavior starts until the relevant `TLA+` model is green, and
+"green" means every safety and liveness stage holds **and** every deliberately sabotaged variant
+(`sab_*`) violates the specific rule it targets. A sabotage that fails to reproduce its named
+counterexample is treated as seriously as a real violation — it means the model was not actually
+covering the case it claimed to cover. This is why every safety rule below ships with the
+counterexample that appears when you remove it.
+
+The full model index (source `.tla` files and proof-run records) lives at
+`docs/superpowers/models/`; this page is the reader-facing summary.
+
+## Model → invariant → counterexample {#model-invariant-counterexample}
+
+| Model (`docs/superpowers/models/`) | Invariant it proves | Counterexample it caught |
+|---|---|---|
+| `CaIncarnationCore.tla` | `INV_NO_DANGLE`, `INV_NO_LOSS`, `INV_NO_RETURN` — the safety spine for the whole GC core | `sab_unconddelete`: replacing the exact-token delete with an unconditional one lets a stale delete kill the live incarnation a resurrect just wrote |
+| `CaBuildRootPrecommit.tla` | `INV_NO_DANGLE_COMMITTED` — a committed manifest never references an absent blob | Reproduces the dangling-manifest hazard exactly: `WriteBlob → AdoptBlob → BuildDie → GcDelete → Commit` with no presence re-check publishes a manifest over a deleted blob |
+| `CaGcLeaseCore.tla` | `NoFalseSteal` — no leader steals leadership from a live, mid-round incumbent | Without the advisory heartbeat, a frozen `seq` during a round looks identical to a dead leader, and a second leader steals from the alive one |
+| `CaCasMountCore.tla` | Reclaim exclusivity for an expired mount | `sab_wallclockreclaim`: trusting the foreign mount body's wall-clock timestamp (instead of observing a stable token on the reclaimer's own monotonic clock) breaks exclusivity |
+| `CaB140DangleMerge.tla` | `INV_NO_LOSS` across a GC lease handoff | Trim-before-durable: a fold cursor trimmed from in-memory state (not the durable snapshot) skips a live edge across a lease handoff, and the referenced blob is deleted while still live |
+| `CaGcRootLocalPartManifestCore.tla` | `INV_NO_DANGLE` over the root-local part-manifest fold | `sab_lazyfenceunsafe`: reusing a stale parent fence position instead of a fresh all-shard fence dangles a live object |
+| `CaGcShardIncarnationCore.tla` | `INV_NO_DANGLING` — safety of registry-free namespace discovery | `sab_pathkeyedcursor`: dropping the per-shard incarnation from the fold cursor reintroduces an ABA hazard on delete-then-recreate at the same path |
+| `CaGcAckFloorZombie.tla` | `INV_NO_DANGLE` under two fully-interleaved GC leaders | `sab_eagerdelete`: a leader deleting its own fresh (not-yet-pending) graduations — the pre-amendment single-phase behavior — dangles when a deposed leader's pass overlaps a live one |
+| `CaGcRoundDeferCore.tla` | `NoOverDelete` — a deferred round may skip a rebuild only when nothing destructive is pending | `sab_graduate_on_stale`: dropping the "an unfolded delta covers this blob" guard lets a deferred round delete a blob its own unread history still protects |
+| `CaEdgeBeforeObserve.tla` | The writer/`GC` publish order is safe to simplify | `sab_late_edge`: allowing adoption before the precommit closure is durable (the pre-fix order) dangles |
+| `CaGcCondemnMarkerGate.tla` | Graduation requires confirmed durable `Condemned` evidence | Swallowing a failed asynchronous condemn-marker write let a writer adopt a token a later graduation was about to delete |
+| `CaRefTableSnapshotLogCore.tla` | Dense, per-life ref ids with an in-band `_ckpt` recovery frontier | `sab_scanistruth`: trusting a listing as the source of truth for "acked" reproduces the real production incident where a `LIST` omitted two already-durable, already-acknowledged ref entries |
+
+## Soak and chaos: the empirical oracle {#soak-and-chaos}
+
+Model-checking and the soak/chaos harness (`utils/ca-soak/`) catch different classes of error, and
+the design leans on both rather than either alone. `TLA+` proves a protocol's constraints before a
+line of `C++` exists — the two-coordinate namespace-incarnation proof and the build-root necessity
+proof were both design decisions made this way. The soak, running two `ReplicatedMergeTree`
+replicas against one shared pool under a seeded workload and a seeded fault injector, finds what an
+idealized model necessarily abstracts away: the dangling-manifest hazard and the resurrect-reupload orphan were
+both first observed live in `system.cas_log` during soak runs, before either got a focused model. Each
+quiesced soak checkpoint cross-checks `SQL` results against a model oracle and runs
+`clickhouse-disks ca-fsck` plus `ca-gc-dryrun`, asserting `dangling=0`.
+
+The relationship runs in both directions: the resurrect-reupload orphan (`utils/ca-soak` scenario
+S30, root-caused via `system.cas_log`) got a focused `TLA+` reproduction that proved the fix and
+was then retired once a deterministic `gtest` (`CASGCLeak.ResurrectReplacedIncarnationReclaimed`)
+covered the same scenario for less ongoing cost — the model did its job as a pre-implementation
+gate and the regression coverage moved to the cheaper, faster tool. A model's proven-safe shape
+also becomes the thing a later soak scenario is written to stress. The `0x1430c`
+incident — a `LIST` that omitted two already-durable ref entries, caught live by an instrumented
+probe rather than reproduced by brute-force enumeration — is the clearest example: it is what made
+`sab_scanistruth` a permanent, named counterexample rather than a one-off incident report.
+
+## What this buys a reader {#what-this-buys}
+
+None of this proves the shipped `C++` is bug-free — a model proves its own abstraction, and several
+entries in the index are annotated `MIXED` or `DRIFTED` where the concrete mechanism has moved on
+from what a model checks, with the audit trail kept precisely so that gap is visible rather than
+implied. What it does buy: every safety rule in the GC core has an explicit counterexample on
+record for the world where that rule is missing, and the corpus is itself periodically re-audited
+for faithfulness to the code — a model whose guarantee the code no longer needs is deleted rather
+than kept as false comfort.
+
+## Test coverage {#test-coverage}
+
+The implementation was built test-first (TDD), and the coverage is correspondingly dense:
+
+| Layer | Volume |
+|---|---|
+| Unit tests (`gtest`, `CAS*` suites) | ~1,900 test cases across ~130 files, covering formats, the write and read paths, the ref machinery, `GC`, recovery, and the backend contract |
+| Integration tests | 10 dedicated `test_cas_*` suites (shared pools, `GC` on S3, sharded `GC`, relink replication, fault-injected `INSERT` recovery, member decommission, and more) |
+| Stateless tests | dozens of dedicated `CAS` tests (pool integrity, leftovers, fsck, GC), in addition to the whole standard suite running on a `CAS`-default server (below) |
+
+## The whole test suite, on CAS by default {#stateless-suite-on-cas}
+
+Beyond the model corpus and the soak harness, the standard ClickHouse **stateless test suite runs
+green with `CAS` as the default `MergeTree` storage**: dedicated CI lanes
+(the `cas storage` and `cas s3 storage` job families — the latter covering ASan/TSan/MSan/UBSan
+and ARM against a real S3-compatible store) run every stateless test against a server whose default disk is
+a `CAS` pool. A small set of tests carries the `no-cas-storage` tag and is skipped in those lanes —
+tests that exercise a mechanism a content-addressed disk deliberately does not have (for example,
+`s3_plain` layouts or deliberately corrupted on-disk part chains). Everything else — the thousands
+of tests that define what `MergeTree` is supposed to do — passes unchanged on top of `CAS`.
diff --git a/docs/en/antalya/cas/architecture/design-history.md b/docs/en/antalya/cas/architecture/design-history.md
new file mode 100644
index 000000000000..1587b2fdd58a
--- /dev/null
+++ b/docs/en/antalya/cas/architecture/design-history.md
@@ -0,0 +1,61 @@
+---
+description: 'A condensed record of the paths CAS explored and rejected, and the major design pivots that produced the current architecture.'
+sidebar_label: 'Design history'
+sidebar_position: 13
+slug: /antalya/cas/architecture/design-history
+title: 'CAS Architecture — Design History'
+doc_type: 'reference'
+---
+
+# CAS architecture — design history {#design-history}
+
+This page is a condensed record of the roads not taken: what was tried, why it was abandoned, and
+the sequence of pivots that produced the architecture described elsewhere in this section.
+
+## Rejected paths {#rejected-paths}
+
+| What it was | Why it was abandoned | What replaced it |
+|---|---|---|
+| **Generation-in-the-key** (Epoch-Based Reclamation core; blob keys carried a generation, `blobs//`) | Required `O(files)` persistent `Keeper` writes per commit and colliding intent keys across writers building identical content; a stuck writer stalled reclamation pool-wide | The incarnation-token design: identity moved into the object body and delete precision into the backend token, removing the generation from every key |
+| **Merkle tree layer** (a `Tree` object kind, `trees/` prefix, `child_gen` carried inside a tree's own identity) | Depended on the generation-in-the-key core: a reclaim at any child propagated a new generation up the entire tree chain, and the tree layer was itself an extra surface for the same class of bug | Removed entirely; trees became manifest-internal, and `Blob` is the sole durable object kind besides the manifest and the ref |
+| **Integer in-degree refcount** (a mutable counter, incremented per reference, decremented per release) | The decide-to-reference-then-not-yet-durable window let the fold observe in-degree 0 for a still-live blob; a mutable counter also costs a `CAS` round-trip proportional to write volume | A derived count: `GC` folds a multiset of `+`/`-` source-edge deltas, so losing or duplicating a record can only delay reclamation, never accelerate one |
+| **Extending zero-copy replication instead of a new mechanism** | Zero-copy's structural costs (a commit spanning local disk, S3, and `Keeper`; a mutable refcount) are inherent to its design, not a bug to patch | `CAS` is an alternative to zero-copy, not a replacement: both remain available, `metadata_type = cas` is opt-in per disk, and no existing deployment needs to migrate |
+| **Per-incarnation body keys** (`blobs/xx/.`, an alternative to the in-body incarnation tag) | A resurrect reusing the condemned incarnation instead of minting a fresh one reintroduced the shared-key race; structurally this was generation-in-the-key again | The in-body `incarnation_tag` plus exact-token body delete, which keeps the generation out of every object key |
+| **Meta as the lifecycle linearizer** (a per-hash `.meta` object whose presence/absence *was* the authority for a blob's lifetime) | The marker is a point-read hint only, never consulted by reads; treating it as the linearizer would assert a guarantee the design does not make | The meta stays advisory: the in-body incarnation tag and exact-token delete are the real authority, and an absent meta reads identically to `Clean` |
+| **Raw immutable bodies with a three-state tombstone meta** | A resurrect displacing the body forced a terminal-tombstone handshake — a writer↔`GC` liveness coupling that could re-enable data loss | The settled one-key-per-hash design with an in-body incarnation tag |
+| **A persistent, append-only namespace registry for `GC` discovery** | Never deregistered on drop, so it grew monotonically forever; its fence cost scaled with namespaces ever created, not namespaces live | Discovery from the ref data itself, made safe by two independent coordinates: a durable per-shard incarnation plus a pool-global round |
+| **A separate all-shard fence-and-recheck phase per `GC` round** | Both phases cost `O(pool size)` GET+CAS every round regardless of churn — roughly 2.4 million requests at 100k tables | A causal ack-floor: one streaming merge per round, with no separate fence or recheck phase, cutting the request count by roughly three orders of magnitude |
+| **A pool-wide sparse ref-id allocator with completeness certificates bolted on** | Successive additive fixes kept growing without closing the root cause: absence is undecidable in a sparse id space | The invariants were changed instead of patched: dense per-life ids derived from applied state, an in-band epoch seal, and a `_ckpt` head object carrying the exact acknowledged frontier |
+
+## Turns at a glance {#turns-at-a-glance}
+
+| Date | Turn |
+|---|---|
+| 2026-06-01 | Starting point: "content-addressed storage for `MergeTree`" thesis and a working proof of concept |
+| 2026-06-07 – 10 | The generation-in-the-key core is abandoned; the incarnation-token design replaces it |
+| 2026-06-11 | The incarnation model passes exhaustive model checking with zero violations |
+| 2026-06-18 | A dangling manifest reference — a committed manifest naming an already-deleted blob — leads to replacing per-blob protection hints with structural build-root reachability |
+| 2026-06-24 – 26 | Formats begin converging on a single self-describing envelope (completed in July as the all-text, JSON-based codec set) |
+| 2026-06-26 | Root-local full-tree manifests collapse a forest of small `GC` objects into one hot/cold split |
+| 2026-07-01 | The namespace registry is deleted; discovery moves to the two-coordinate incarnation-and-round scheme |
+| 2026-07-02 | Fence-and-recheck `GC` rounds are replaced by the one-pass, causal ack-floor round |
+| 2026-07-06 – 10 | Writer/`GC` simplification: promote-time revalidation of tokened dependencies is proved redundant |
+| 2026-07-13 | Mount-lease handover becomes boundary-exclusive, closing the cross-epoch grace window without a timeout |
+| 2026-07-15 | All part files become content-addressed: the mutable file set drops to empty, and disk-transaction dispatch collapses to one precommit contract |
+| 2026-07-17 | An acknowledged `INSERT` that could be lost is traced to a removed durability guard and fixed |
+| 2026-07-26 | A `LIST` omitting two already-durable ref entries is caught during a soak run — the incident that settles the trust model for listings |
+| 2026-07-27 – 29 | The sparse-id certificate stack is abandoned; the invariants change instead — dense ids, an in-band epoch seal, and a `_ckpt` recovery frontier |
+| 2026-08-01 – 03 | Recovery stops reading listings entirely and works from authoritative objects; the listing trust model is finalized |
+
+## The pattern underneath {#the-pattern}
+
+A few reflexes recur across these pivots and still apply to new design work:
+
+- **Re-derive the invariant, don't patch the mechanism.** Every durable fix came from asking what
+ property must hold, not from patching the specific failure observed.
+- **Delay is acceptable, authorization is not.** A stale-but-honest observation can only ever
+ postpone a decision; the design consistently rejects any mechanism that could *accelerate* a
+ destructive action past its safety gates — a delay is a latency cost, a wrongful authorization is
+ data loss.
+- **A model that no longer matches the code is worse than no model.** Superseded models are
+ removed rather than kept: an unfaithful proof is false comfort, not documentation.
diff --git a/docs/en/antalya/cas/architecture/garbage-collection.md b/docs/en/antalya/cas/architecture/garbage-collection.md
new file mode 100644
index 000000000000..48be9c4e6b24
--- /dev/null
+++ b/docs/en/antalya/cas/architecture/garbage-collection.md
@@ -0,0 +1,263 @@
+---
+description: 'The CAS garbage collector: leadership as work de-duplication, the 18-phase round pipeline, condemnation and exact-token deletion, sharding, and observability.'
+sidebar_label: 'Garbage collection'
+sidebar_position: 8
+slug: /antalya/cas/architecture/garbage-collection
+title: 'CAS Architecture — Garbage Collection'
+doc_type: 'reference'
+---
+
+# CAS architecture — garbage collection {#garbage-collection}
+
+`GC` is the only place in `CAS` that ever deletes a blob body or a manifest body. It runs as a
+background, lease-paced loop per mount (`Gc::runRegularRound`, `Gc/CasGc.cpp`), folding ref-log
+history into blob in-degree, condemning what reaches zero, and deleting only after that
+condemnation has survived a full extra round. This page covers leadership, the round's 18 phases,
+condemnation and deletion, sharding, pruning, round cost, and observability. Manifest and ref
+mechanics that `GC` folds are covered on the
+[manifests-and-refs page](/antalya/cas/architecture/manifests-and-refs); the writer-versus-`GC`
+race over one blob is covered on the
+[blob-protocol page](/antalya/cas/architecture/blob-protocol#writer-gc-race).
+
+## Leadership {#leadership}
+
+There is **no separate `GC` lease object**. The lease lives inside `gc/state` itself as
+`{owner, seq}`.
+
+```mermaid
+stateDiagram-v2
+ [*] --> Reading: GET gc/state
+ Reading --> Creating: object absent, never observed before
+ Creating --> Leader: casPut create-if-absent, gc_shards fixed here, once
+ Reading --> Renewing: lease owner is me
+ Renewing --> Leader: casPut seq+1, guarded by the observed token
+ Reading --> Evaluating: foreign owner
+ Evaluating --> NotLeader: incumbent lease moved, or heartbeat moved, or steal not allowed
+ Evaluating --> Stealing: both frozen across a full observation window
+ Stealing --> Leader: casPut owner=me seq+1, on the observed token
+ Stealing --> NotLeader: lost the CAS, re-read and re-arm
+ Leader --> [*]: run the round
+```
+
+Two independent liveness signals are consulted before a steal: whether `(owner, seq)` moved since
+the last tick, and whether the separate `gc/hb` heartbeat moved. The heartbeat is compared only
+under the same remembered heartbeat owner, deliberately not against `lease.owner` — a deposed
+leader's heartbeat thread keeps pulsing, and that must not cause a live new leader's lease to be
+stolen. The paced background loop may steal; a manual `SYSTEM CAS GC RUN` may not, because the
+safety argument needs two observations separated by real wall time. Because every renew or steal
+bumps `seq`, `seq` doubles as the round's attempt id.
+
+**A deposed leader that keeps running cannot corrupt anything**, and the argument does not rely on
+exclusivity at all:
+
+1. `gc/state` is published by exactly **one** `CAS` per round; a deposed leader's `CAS` fails and
+ its entire round evaporates.
+2. Every fold artifact is written under that leader's own attempt number, invisible to every
+ reader, and reclaimed later by wholesale generation pruning.
+3. Destructive pre-`CAS` actions are justified only by previously published durable state, so they
+ are replay-idempotent.
+4. Deletes are exact-token, so a stale leader can never delete a newer incarnation.
+
+The lease is therefore **work de-duplication, not mutual exclusion**.
+
+## The round {#the-round}
+
+A round is one pass of 18 named phases ending in exactly one `gc/state` `CAS`
+(`Gc::runRegularRound`, `Gc/CasGc.cpp`).
+
+| # | Phase (`GcPhaseTimer` name) | What it does |
+|---|---|---|
+| 1 | `lease` | Acquire, renew or steal the lease inside `gc/state`. The only phase a not-a-leader round emits |
+| 2 | `pre_fold_ref_drain` | Resolve catalog `Removing` rows whose cleanup evidence the adopted parent already sealed; exact-CAS-delete the completed ones before anything else can act |
+| 3 | `heartbeat_floor` | One `LIST` of `gc/server-roots/`, one `GET` per mount slot, fence-out `PUT` for any mount whose write-token has held stable past the threshold |
+| 4 | `defer_decision` | One full `LIST` of `cas/ns/stream/`, build the catalog-keyed ref walk plan; decide `DEFER` (nothing changed, no graduation due) or continue to a full fold. A `DEFER` verdict still runs one namespace-janitor page — the same work phase 16 does on a folding round — with its deletes suppressed |
+| 5 | `parent_seal_read` | Capture the parent fold seal's run references before the fold mutates the in-memory generation/attempt, to detect a ref that moved off an already-pruned generation |
+| 6 | `fold_ref_group` | Regroup the one `LIST` from phase 4 into per-table listings — no I/O, the keys are already in hand |
+| 7 | `fold_seal_read` | `GET` and decode the adopted fold seal that anchors this fold's coverage |
+| 8 | `fold_ref_intake` | `GET` every new ref-log record and every referenced manifest, extracting blob source edges |
+| 9 | `fold_reduce` | The three-cursor merge over prior edges, new deltas and the parent's condemned rows: spare, condemn, graduate or redelete each candidate |
+| 10 | `fold_seal_write` | Write the new fold seal once, write-once deterministic, adopting a byte-identical replay instead of rewriting it |
+| 11 | `pending_deletes` | The single content-delete site: exact-token `deleteExact` of every entry the *previous* round marked `delete_pending`, plus the forensic outcome-log writes |
+| 12 | `meta_pool_wait` | Drain the bounded pool of async `.meta` condemn-marker writes queued during the fold |
+| 13 | `round_commit` | Retention-prune old generations, then publish the single `gc/state` `CAS` that adopts the whole round |
+| 14 | `handoff_reclaim` | Post-`CAS`: reclaim any generation that a ref moved off during this very round, before the ordinary wholesale prune would reach it |
+| 15 | `manifest_deletes` | Delete manifest bodies whose owner-removal minus-one edge the `CAS` in phase 13 just adopted |
+| 16 | `namespace_cleanup` | One bounded page of the perpetual namespace janitor, reclaiming dead-life debris |
+| 17 | `ref_object_cleanup` | Prune ref logs and snapshots once both fold coverage and a live snapshot make them safe to delete |
+| 18 | `orphan_sweep` | One cursor-paced page of the [orphan-manifest sweep](/antalya/cas/architecture/manifests-and-refs#orphan-sweep); wrapped so it can never fail the round |
+
+Phases 5 through 18 run only when phase 4 decides to fold. A `DEFER` verdict is not a bare no-op:
+it still runs one bounded namespace-janitor page with `suppress_destructive = true` — cursor
+progress and diagnostics only, no deletes — and then returns, publishing no fold artifact and no
+`gc/state` `CAS` at all:
+
+```mermaid
+flowchart LR
+ D4{"4 defer_decision"} -->|"nothing changed, no graduation due"| DEF["DEFER: one suppressed namespace-janitor page, then return"]
+ D4 -->|"changed shards, or graduation due"| FOLD["phases 5 through 18: full fold and round commit"]
+```
+
+Orderings that are load-bearing:
+
+- **2 before 4** — a row proved complete by the adopted parent is resolved before `DEFER` or any
+ successor plan can publish.
+- **15 after 13** — manifest bodies are deleted only after the `CAS` adopted their decrements.
+- **13's prune before the `CAS`** — a pre-`CAS` destructive action may rely only on already-
+ published state.
+
+**Clamp suppression.** `suppress_destructive = !anomalies.empty() || !carried_holds.empty() ||
+!frontier_complete` is computed once and threaded into the merge, current-life ref cleanup and the
+perpetual namespace janitor, so they cannot desynchronize. Under suppression there is no
+graduation, no redelete, and no ref or namespace deletion; condemnation and sparing continue,
+because both are non-destructive.
+
+**Fail-closed aborts.** A throw before the `CAS` means nothing is adopted: unapplied transactions,
+a cursor/apply mismatch, a missing adopted seal, a table with a snapshot but no surviving log and
+no cursor, a non-total condemned summary, and an observed delete marker (bucket versioning is on).
+
+## The one-pass commit {#gc-state}
+
+`gc/state` is the durable safety and round-adoption state: `round`, `gc_shards`,
+`snap_generation`, `snap_pruned_through`, `snap_attempt`, `manifest_sweep_cursor`, and the lease.
+Exactly one `CAS` per round publishes it; the fold itself performs no `CAS` of its own.
+
+**The fold seal *is* the coverage record**: generation, parent generation, one `ref_lives` row per
+catalog-admitted opaque life (coverage plus optional cleanup evidence), references to the
+source-edge run segments, and a per-shard condemned summary. It is encoded deterministically, so a
+replayed round produces byte-identical bytes and adopts its own output through the
+`putDeterministicArtifact` adoption pin (see the [blob-protocol page](/antalya/cas/architecture/blob-protocol#deterministic-artifacts)).
+There is **no separate retired-list object** — condemned entries ride the source-edge run as
+sentinel rows at `source_id = 0` — and **no run-file list outside the seal**; runs are resolved
+*through* the seal's references, never by key construction.
+
+## Finding orphans {#finding-orphans}
+
+In-degree is a set of source edges, not a refcount. A blob becomes a candidate when its edge set
+becomes empty and it was touched this pass: one `HEAD` captures the exact incarnation token and
+size that a future delete will name. A blob merely carried from the parent run pays no `HEAD`.
+
+**The grace period is measured in rounds, not acks:** an entry graduates once it has survived one
+full round (`condemn_round < current_round`). The heartbeat floor is liveness only and **never**
+gates graduation.
+
+**The 404 rule.** A body that is present but invalid is `CORRUPTED_DATA`, hard. A body that is
+missing is **never** a throw — the fold records and continues, and the caller decides by position:
+a precommit activation clamps as a barrier; a committed or removal fold clamps only that table.
+Prunes are likewise fail-open on 404.
+
+## Condemnation and deletion {#condemn-delete}
+
+```mermaid
+flowchart LR
+ A["round n: in-degree hits zero HEAD -- exact token t"] --> B["write .meta = Condemned round n async, bounded pool, drained pre-CAS"]
+ B --> C["retired with condemn_round = n+1"]
+ C --> D{"round n+1: re-verify"}
+ D -->|"in-degree recovered"| S["SPARED -- recovery wins, even past the floor"]
+ D -->|"still zero, confirmed durable Condemned evidence for hash and t"| G["GRADUATED -- delete_pending"]
+ D -->|"still zero, evidence unconfirmed"| C2["carried unchanged, retry the marker, never throw"]
+ D -->|"current token not equal to t"| SUP["SUPERSEDED -- a writer resurrected, re-condemn the CURRENT token"]
+ G --> E["round n+2, pre-CAS: deleteExact blob, t"]
+ E -->|"Deleted or Absent"| F["then drop the .meta"]
+ E -->|TokenMismatch| H["nothing deleted -- live at a newer token, leave the .meta alone"]
+```
+
+The `.meta` sidecar carries **no token** — it is a per-hash hint. The exact incarnation token lives
+in the condemned sentinel row inside the run, together with the condemn round and two flags,
+`delete_pending` and `marker_confirmed`. `GC`'s marker is add-only: `Clean → Condemned` yes, the
+reverse never, not even when sparing — only a writer that has already displaced the body may clear
+it. Minimum two full rounds separate condemnation from deletion, and `delete_pending` is terminal —
+an entry is never un-pended.
+
+## Sharding {#sharding}
+
+`gc_shards` is fixed at first lease acquire and immutable; decoders reject `0`. A blob routes by
+the **high** 64 bits of its digest, read big-endian.
+
+The role split is worth internalizing: the **coordinator** — the lease holder — owns discovery,
+round visibility, the single global fence, and the generation advance, because a publish into
+*one* namespace can protect a blob owned by *any* shard, so these span the whole universe and must
+not be sharded. **Reducers** own only their disjoint shard; their run-key namespaces never
+collide, so two servers could reduce different shards concurrently and reducer work needs no
+lease.
+
+A shard with an empty delta bucket and no condemned entries in the parent summary copies the
+parent's run references verbatim — zero run I/O, a "pure carry". A missing parent summary entry on
+a non-fresh pool is `CORRUPTED_DATA`, never silently treated as zero.
+
+## Pruning old objects {#pruning}
+
+- **Current-life ref logs and snapshots** (phase 17) — a log is deletable only when covered by
+ both durable fold coverage and a durable live snapshot; snapshots strictly older than the newest
+ observed one are deletable. There is no batch delete; it is `HEAD` plus `deleteExact` per key.
+- **Generations** (phase 13) — keep the last `gc_snapshot_generations_to_keep` (default 3; `0`
+ means keep everything, for forensics). Pruning is wholesale: `LIST` the generation prefix and
+ delete everything under it, including deposed-leader debris and attempt-scoped outcome sets. A
+ generation still referenced by the live seal is skipped, but the cursor still advances past it —
+ leak-freedom then rests on the post-`CAS` hand-off reclaim in phase 14.
+- **Manifests** — owner-removed bodies delete in phase 15; never-precommitted bodies go through the
+ [orphan-manifest sweep](/antalya/cas/architecture/manifests-and-refs#orphan-sweep) in phase 18.
+
+## What a round costs {#round-cost}
+
+Per **folding** round, with `N` live mounts, `S` ref tables and `S_changed` tables carrying new
+logs:
+
+| Operation | Count |
+|---|---|
+| `LIST cas/ns/stream/` | 1 full enumeration |
+| `LIST gc/server-roots/` | 1, plus 1 `GET` per mount |
+| `GET` the adopted fold seal | 5, explicitly instrumented |
+| `GET` ref logs | 1 per new log |
+| `GET` manifests | 1 per emitted edge — no manifest-body cache within a round |
+| `PUT` run segments | 1 per non-pure-carry shard, plus 1 fold seal |
+| `HEAD` blobs | 1 per newly condemned |
+| `DELETE` | 1 per graduate |
+| `CAS gc/state` | 1 |
+
+The measured `GET` formula is exact: total `GET`s equal ref-log body `GET`s plus manifest body
+`GET`s, i.e. `1 + edges_per_log`. An idle round is one `LIST` sweep, `N` heartbeat `GET`s, and one
+`CAS`. A deferred round is cheaper still: one `LIST`, three seal `GET`s, the lease `GET`/`PUT` and
+the heartbeat floor — no `gc/state` `CAS` at all.
+
+The round's work is internally self-regulated: anything a pass cannot finish is carried and retried
+by the next round's cursors, never dropped. The internal pacing knobs are deliberately not part of
+the user-facing configuration surface.
+
+| Setting | Default | Bounds |
+|---|---|---|
+| `gc_meta_pool_size` | 16 | bounded pool for condemn-marker writes |
+
+## Observability {#observability}
+
+`system.cas_gc_log` emits `Start`, `Finish` and per-`Phase` rows, correlated by `round_id` — not
+`round`, which is `0` on `Start` and does not exist at all on a not-a-leader round. Phase rows
+carry no verb columns by design: per-phase operation counts ride the row's own `ProfileEvents`
+delta, so grouping by phase over an S3 event attributes the LIST/GET/PUT/DELETE budget without
+inventing schema. `phase_metrics` carries the semantic counts no counter can supply (clamped
+tables, dead precommits skipped, pure-carry shards, generations visited). `Deferred` is kept
+distinct from `Success` precisely so "folded and found nothing" is distinguishable from "never
+folded". Every `GC`-related `ProfileEvent` carries the uppercase `CAS`/`CASGC` prefix — for example
+`CASGCRetiredCondemned`, `CASGCRetiredGraduated`, `CASGCRetiredRedeleted`,
+`CASGCClampSuppressedPasses`, `CASGCHeartbeatFenceOuts`.
+
+Alongside it, `system.cas_log` carries the audit trail: the condemn chain, fence-outs, anomalies
+(capped per round, each carrying the true total), and manifest deletes.
+
+`ca-fsck` distinguishes two classes that are easy to conflate: `dangling` — referenced but missing,
+data loss — versus `unreachable`/`awaiting-gc` — present, unreferenced, and
+simply waiting for graduation.
+
+## Operational surface {#operational-surface}
+
+| Command | Effect |
+|---|---|
+| `SYSTEM CAS GC RUN ''` | One synchronous round on the contacted node; only the lease holder makes progress |
+| `SYSTEM CAS GC STOP` / `SYSTEM CAS GC START` | Stop or resume future rounds on the same scheduler, preserving its identity |
+| `SYSTEM CAS GC REBUILD` (`clickhouse-disks ca-gc-rebuild`) | Fail-closed disaster-recovery path that every "GC refuses to run" error points at; deliberately over-protects — it prefers bounded leaks over risking an under-count. It cannot delete live data directly: deletions it produces still flow through the normal round's condemn, graduate, exact-token path |
+| `clickhouse-disks ca-gc-dryrun` | Opens the disk read-only, constructs a non-leader `GC`, and prints what would be deleted with a reason per entry. Write-free, resolves runs through the seal's references. Documented caveat: it does not fold new owner events, so away from quiescence it can **over-report** — the subset guarantee holds only at quiescence, and its output must never feed a real delete |
+
+`SYSTEM CAS DROP POOL MEMBER '' FROM DISK ''` — permanent removal of a dead
+replica, distinct from ordinary `GC` — is covered on the
+[mounts-and-leases page](/antalya/cas/architecture/mounts-and-leases#mount-lifecycle). `SYSTEM CAS
+FSCK` and its `dangling`/`unreachable` vocabulary are a read-only diagnostic pass, not part of the
+`GC` protocol itself.
diff --git a/docs/en/antalya/cas/architecture/index.md b/docs/en/antalya/cas/architecture/index.md
new file mode 100644
index 000000000000..15a9516c9ae7
--- /dev/null
+++ b/docs/en/antalya/cas/architecture/index.md
@@ -0,0 +1,114 @@
+---
+description: 'What CAS is, the Git-analogy mental model, the object model, and the safety invariants a reviewer should hold every CAS protocol against.'
+sidebar_label: 'Architecture overview'
+sidebar_position: 1
+slug: /antalya/cas/architecture/
+title: 'CAS Architecture — Overview'
+doc_type: 'reference'
+---
+
+# CAS architecture — overview {#overview}
+
+`CAS` ("content-addressed storage") is a `MetadataStorage` back-end for object-storage disks
+(`metadata_type = cas`) that stores every `MergeTree` part file once, addressed by the hash of
+its content. Many servers share one object-storage pool with no byte duplication, no zero-copy
+bookkeeping in `Keeper`, no per-replica local-disk reference state that grows with data volume,
+and no mutable per-blob refcount.
+
+It is still experimental — that is deliberate, not a caveat to apologize for. Pre-release means
+the format can still change cheaply, with zero compatibility scaffolding, and the design can
+still be iterated on invariants rather than migrations. The bet: all you need underneath is a
+good S3 bucket. No external coordinator, no metadata service, no Keeper state proportional to
+data — the pool is self-describing, and everything CAS needs to agree on (refs, leases, GC
+leadership, fencing tokens) is an object in the bucket.
+
+This page is the entry point of a 4-page set: it gives the mental model. Deeper detail on
+storage layout, the write/read protocols, and GC lives in the other three pages.
+
+## The Git analogy {#git-analogy}
+
+The fastest way to load the model is Git, which most readers already carry:
+
+| Git | CAS |
+|---|---|
+| blob (file content by hash) | **blob** — one part file's bytes, keyed by content hash |
+| tree (directory listing) | **part manifest** — the immutable file list of one part |
+| ref (`refs/heads/main`) | **ref** — `part name → manifest id`, the only mutable state |
+| `gc` / reachability | **GC round** — an in-degree fold over refs → manifests → blobs |
+
+Where the analogy breaks: Git's objects are locally addressed and GC runs against a single
+repository with no concurrent writers; CAS objects are addressed inside a shared, multi-writer
+object-storage pool, and its GC round has to reason about ambiguity (crashed writers,
+in-flight precommits, eventually-consistent `LIST`) that a local Git repository never faces.
+Git also has no equivalent of a CAS ref's precommit state — a CAS ref transition is durable
+before the blob it names is guaranteed reachable, never the other way round.
+
+## The object model {#object-model}
+
+Four durable object kinds exist in a pool: one mutable (the ref), three immutable
+(part manifest, blob, and a blob's condemnation-marker sidecar).
+
+```mermaid
+graph TD
+ R["Ref: part name maps to manifest id"]
+ M["Part manifest: file list of one part"]
+ B["Blob: one part file's bytes, keyed by content hash"]
+ BM["Blob meta: condemnation marker sidecar"]
+
+ R -->|names| M
+ M -->|entry references| B
+ B -.->|sidecar| BM
+```
+
+**The reachability rule, stated once:** a blob is live if and only if some live manifest names
+it, and a manifest is live if and only if some ref — committed or precommitted — names it. `GC`
+computes exactly this and nothing else.
+
+## Safety invariants {#safety-invariants}
+
+The full numbered list lives in the CAS agent guide; this is the reader-facing summary of the
+substance:
+
+| Invariant | What it means |
+|---|---|
+| No silent data loss | No path may delete an object a committed reference still names |
+| Revival is re-upload only | A condemned blob is never revived by copying it — only by re-uploading the original bytes under a fresh identity |
+| Exact-token deletes | Every delete names the exact object incarnation it removes, never "the object at this key" |
+| `TOKEN ⟹ CONTENT` | A repeated write token implies unchanged bytes — the backend must never let a token be reused over different content |
+| Fail closed on ambiguity | An operation that may have landed is never treated as one that did not |
+| One content-delete site | Exactly one place in the whole codebase ever deletes a blob body, gated on a previously published `GC` round |
+| `GC` never invents history | Cleaning up an abandoned write is the writer's job, not `GC`'s |
+| Over-count only | A lost or duplicated `GC` fold can only delay a reclaim, never bring one forward |
+| No dangle / no loss / no return | A live ref always resolves through present objects; a delete requires proven unreachability at an exact token; a retired object identity is never valid again (though the same logical key can return under a new token) |
+
+## Positioning: shared-nothing, not shared-state {#positioning}
+
+Each server owns the catalog rows under its own identity and writes only its own state objects
+— that part is shared-nothing, same as `ReplicatedMergeTree` today. What CAS adds is a single
+**shared** resource: the blob content space, addressed purely by content hash, which is
+write-once and conflict-free by construction — two servers writing the same content write the
+same key with the same bytes, so there is nothing to reconcile. The only mutual exclusion CAS
+needs anywhere is a conditional write (create-if-absent, or compare-and-swap on a token) against
+a single object.
+
+That is deliberately not a coordinator or a serializable metadata service: there is no external
+coordinator, and no `ZooKeeper`/`Keeper` usage inside the pool protocol itself. `Keeper` stays
+exactly where `ReplicatedMergeTree` already used it — replication log and part-set consensus —
+and its load does not grow with pool size, because the pool's own bookkeeping never touches it.
+
+## The subsystem pages {#subsystem-pages}
+
+| Page | Covers |
+|---|---|
+| [Storage layout](/antalya/cas/architecture/storage-layout) | Every S3 key shape, the object envelope, codecs, a worked example tree |
+| [Namespaces](/antalya/cas/architecture/namespaces) | Namespaces, `life_id`, the catalog, and their lifetime |
+| [Blob protocol](/antalya/cas/architecture/blob-protocol) | Conditional writes, deduplication, the writer-vs-GC race |
+| [Part lifecycle](/antalya/cas/architecture/part-lifecycle) | Build, precommit, upload, promote; crash points and their cleaners |
+| [Manifests and refs](/antalya/cas/architecture/manifests-and-refs) | Part manifests and the ref machinery: publish, fold, recovery |
+| [Mounts and leases](/antalya/cas/architecture/mounts-and-leases) | Server identity, the owner claim, the mount lease, fencing |
+| [Replication](/antalya/cas/architecture/replication) | Fetch-by-relink between replicas sharing one pool |
+| [Read path](/antalya/cas/architecture/read-path) | Ref resolution, manifest reads, ranged blob reads, the caches |
+| [Garbage collection](/antalya/cas/architecture/garbage-collection) | Leadership, the round, sharding, cost, observability |
+| [Backend abstraction](/antalya/cas/architecture/backend) | Provider dialects for conditional writes, the capability probe |
+| [Correctness](/antalya/cas/architecture/correctness) | TLA+ models, counterexamples, soak methodology, test coverage |
+| [Design history](/antalya/cas/architecture/design-history) | The rejected designs and the major pivots |
diff --git a/docs/en/antalya/cas/architecture/manifests-and-refs.md b/docs/en/antalya/cas/architecture/manifests-and-refs.md
new file mode 100644
index 000000000000..bd23b51e9a5f
--- /dev/null
+++ b/docs/en/antalya/cas/architecture/manifests-and-refs.md
@@ -0,0 +1,260 @@
+---
+description: 'Part manifest structure and lifecycle, the ref table as the only mutable state in a CAS pool, the publish protocol, and the orphan-manifest sweep.'
+sidebar_label: 'Manifests and refs'
+sidebar_position: 5
+slug: /antalya/cas/architecture/manifests-and-refs
+title: 'CAS Architecture — Manifests and Refs'
+doc_type: 'reference'
+---
+
+# CAS architecture — manifests and refs {#manifests-and-refs}
+
+A part manifest is the immutable file list of one `MergeTree` part; a ref is the mutable pointer
+from a part name to the manifest that currently backs it. Together they are the two object kinds
+that make a CAS pool's state machine: manifests never change, refs are the only place anything
+moves. This page covers what a manifest contains, how a manifest becomes reachable or becomes an
+orphan, how a ref mutation is published durably, and how a mounted server recovers a ref table
+after a crash or a fresh mount. The write/promote sequence that drives these primitives is on the
+[part-lifecycle page](/antalya/cas/architecture/part-lifecycle); how `GC` folds ref history into
+blob liveness is on the [garbage-collection page](/antalya/cas/architecture/garbage-collection).
+
+## Part manifests {#part-manifests}
+
+A manifest (`cas_part_manifest`, `Formats/CasPartManifestFormat.h`) has four top-level fields:
+`ref` (its own id, repeated in the body for fail-closed validation), `root_namespace_id` (the
+owning namespace, likewise repeated), `payload_digest` (integrity/debug only — never a key, never
+a dedup input, never a `GC` edge), and `entries` — strictly ascending by path after decode. Each
+entry is `{path, placement, BlobRef, blob_size, inline_bytes}`; the hash algorithm travels **per
+entry**, so one manifest may legitimately mix algorithms if the pool has more than one enabled.
+
+A manifest deliberately holds **no** offsets, no packed-file support, no projections field, no
+codec info, no parent-manifest link, no source edges, and no incarnation token. One blob is one
+file's bytes; a read window is `{blobKey, blob_header_len, blob_size}`. A projection is an
+ordinary entry whose path has a `.proj` component. The incarnation token is the backend `ETag`
+observed by a `HEAD`, never stored in the manifest.
+
+**The manifest id is neither a content hash nor random.** It is
+`ManifestRef = {writer_epoch, build_sequence, manifest_ordinal}` — durable writer epoch times
+monotone per-incarnation build sequence times monotone per-build ordinal — which gives "no
+manifest id reuse" by construction with no randomness needed. The `GC`-level identity is the pair
+`ManifestId = (RootNamespace, ManifestRef)`; two namespaces may legally carry the same
+`ManifestRef`.
+
+Backpressure caps are enforced before the body is written (`Pool/CasPartWriteTxn.cpp`):
+
+| Cap | Limit |
+|---|---|
+| Entries per manifest | 1 048 576 |
+| Encoded manifest text | 256 MiB |
+| Total inline bytes | 16 MiB |
+| Largest single inline entry | 1 MiB |
+
+A manifest is written once with a conditional create (`putIfAbsentStream`) and **never rewritten**.
+A different object at that key would be an id collision and is `CORRUPTED_DATA`, fail-closed,
+before any owner transition names it. Rewriting a part therefore writes a **new** manifest over
+the **same** blobs and moves the ref in one ref-log record — a repoint, covered in full on the
+[part-lifecycle page](/antalya/cas/architecture/part-lifecycle#repoint).
+
+## Manifest lifecycle and the orphan sweep {#manifest-lifecycle}
+
+```mermaid
+stateDiagram-v2
+ [*] --> Staged: stageManifest, body PUT write-once
+ Staged --> PrecommitOwned: precommitAdd, ref-log OwnerTransition, plus-one edges on fold
+ PrecommitOwned --> Committed: promote, Precommit to Committed, no edge, net zero
+ Committed --> OwnerRemoved: drop or repoint or namespace removal, minus-one edges
+ OwnerRemoved --> [*]: GC deletes the body after the decrements are sealed
+
+ Staged --> OrphanA: writer died before precommitAdd
+ OrphanA --> [*]: writer best-effort delete, else the orphan sweep
+
+ PrecommitOwned --> DanglingPrecommit: writer died before promote
+ DanglingPrecommit --> OwnerRemoved: binding removed by abandon or a successor stale-precommit sweep
+```
+
+Two disjoint failure classes matter here:
+
+- **Pre-precommit orphan.** The body exists but no ref-log record ever named it. It contributes no
+ edges and nobody protects it — this is exactly what the orphan sweep below reclaims.
+- **Dangling precommit.** The transaction died between `precommitAdd` and `promote`. Nothing wakes
+ it up on its own: a `PartWriteTxn` is never persisted. The binding must be removed by a ref-log
+ transaction — either the live writer's own `abandon`, or a fenced successor's stale-precommit
+ sweep, which removes precommits whose `manifest_ref.writer_epoch < live_epoch`
+ (`Pool/CasRefLedger.cpp`). Only after that minus-one folds does `GC` delete the body, on the
+ ordinary owner-removal path.
+
+The writer's own best-effort cleanup deliberately **skips** the precommit target once a precommit
+was even attempted — including an uncertain outcome — because deleting a body that turns out to be
+a live precommit would clamp `GC`'s fold barrier forever.
+
+### The orphan-manifest sweep {#orphan-sweep}
+
+Runs as the last phase of the `GC` round, cursor-paced and budgeted, wrapped so it can never fail
+a round (`Gc/CasOrphanManifestSweep.cpp`). Eligibility comes **exclusively** from the durable
+watermark in the mount lease — there is no age threshold and no time-based grace period anywhere
+in this protocol. No mount lease for the `server_root_id` means no deletion authority means
+nothing is swept for that root.
+
+```mermaid
+flowchart TD
+ A["LIST one page of cas/manifests/ budget: manifest_sweep_list_budget_keys"] --> B{"build-prefix eligible? durable watermark fact only"}
+ B -->|"epoch less than lease epoch"| ELIG["eligible, old-epoch debris"]
+ B -->|"same epoch, min_active clears build_seq"| ELIG
+ B -->|"no lease, or epoch ahead, or build may be live"| SKIP["skip"]
+ ELIG --> C["protection view: committed manifests plus live precommits plus manifests with an unfolded minus-one"]
+ C -->|"key protected"| SKIP2["skip"]
+ C -->|"not protected"| D["deleteExact key, token"]
+ D -->|Deleted| E["emit ManifestDelete audit event"]
+ D -->|"NotFound or TokenMismatch"| SKIP3["spared, a fresh owner reclaimed the key"]
+ E --> F["CAS gc/state with the advanced cursor"]
+```
+
+The protection view is built from the **same complete replay** that writer recovery uses, and a
+namespace whose view fails to build is added to an errored set with **all** of its deletions
+skipped — an empty owner set is never substituted for a failed one. The sweep deletes only
+manifest bodies and emits no blob deltas, correct precisely because a pre-precommit body never
+contributed a `+1`. Contrast with the owner-removal path, which is ordered the other way: fold the
+`-1` edges, adopt the decrements in the round `CAS`, *then* delete the body — a crash there leaks
+a body to this sweep, never a dangle.
+
+## Source edges: how a manifest makes blobs live {#source-edges}
+
+Blob liveness is a **set of source edges**, not a counter (`Gc/CasBlobInDegree.h`) — which is what
+makes `GC`'s fold idempotent. An edge id is `sourceEdgeId(ManifestId, path)`, a deterministic hash
+over the namespace, epoch, build sequence, ordinal and path — an edge *identity*, deliberately not
+a content hash and not reconstructable.
+
+Edges are never written at manifest-write time. They materialize only when `GC` folds a ref-log
+transaction that changes ownership: add-precommit means `+1` per blob entry; either removal means
+`-1`; **promote means no edge at all**, because the manifest never loses an owner, so it is net
+zero. Inline entries produce no edges — they have no separate object to reclaim.
+
+## The ref table {#ref-table}
+
+A ref is the only mutable state in the whole system, so this is where the concurrency design is
+concentrated.
+
+- **Name** — a canonical clean relative path, in practice the part directory name with an optional
+ `detached/` or `moving/` prefix.
+- **Value** — `{ref_name, ManifestRef, published_at_ms}`. There is **no** token/`ETag` in a ref
+ row; the cross-server "confirm token" is the text form `epoch:build:ordinal`.
+- **Scope** — one ref table per `RootNamespace`, i.e. per table per server root.
+- **Ownership slots** — a `ManifestRef` has at most one owner across the table, in one of two
+ slots: `Committed` or `Precommit`. Precommits are keyed by the pair `(ref_name, manifest_ref)`,
+ so several in-flight builds may legitimately contend for one ref name.
+
+In memory, `RefTableState` holds a copy-on-write map of committed rows, a set of precommits, an
+ownership index enforcing the one-owner rule, a lifecycle (`Live`/`Removed`), the greatest applied
+transaction id, and byte-size counters used for admission. Copying a state is a refcount bump, so
+a flush's trial and candidate copies cost proportional to touched rows, not the whole table.
+Network I/O is never performed while holding the state lock, so a reader sees either a whole
+transaction or none of it.
+
+Two immutable object kinds carry the durable form under `cas/ns/stream//` (see the
+[storage-layout key table](/antalya/cas/architecture/storage-layout#key-table)): a log object
+holds exactly one transaction, `{namespace, txn_id, ops[]}`; a snapshot object holds one live table image
+— sorted committed rows plus precommits. Mutable/path-addressed state lives separately under
+`cas/ns/state//`: the per-life `_ckpt` checkpoint and any namespace-owned `_files/`.
+
+`RefTxnId = {writer_epoch, ref_sequence}` renders as two fixed-width hex fields, so lexical key
+order equals tuple order. Ids are per-namespace and contiguous: within one `(namespace,
+writer_epoch)` they run `1, 2, 3, …` with no holes, and a new mount epoch restarts the sequence at
+`1`. A hole is therefore corruption, not an allocation artifact, and a non-successor id is rejected
+as `CORRUPTED_DATA`.
+
+The op vocabulary is deliberately tiny: `NamespaceBirth`, `OwnerTransition{old?, new?}`,
+`SetPublishedAt`, `RemoveNamespace`. There are exactly four legal `OwnerTransition` shapes — add
+precommit, remove precommit, remove committed, and promote — enumerated identically by the state
+machine and by `GC`'s edge extractor, so the two readers of the format cannot drift.
+
+Logs are pure conditional creates on write-once keys. There is no append-to-object and no
+`CAS`-swapped mutable pointer anywhere in the ref lane. The writer never deletes ref objects; only
+`GC` does, once coverage and a live snapshot both make a log safe to remove.
+
+Snapshots publish in the background, best-effort, one in flight per table, when the tail exceeds a
+log-count or log-byte threshold.
+
+## Publishing a ref mutation {#publish-protocol}
+
+All mutations funnel through one flat-combining lane, `CasRefLedger::appendRefOps`. A single flush
+carves a batch out of the queue and commits it as one or more transactions.
+
+```mermaid
+flowchart TD
+ Q["appendRefOps enqueues ops"] --> REC["ensure the table is recovered"]
+ REC --> FEN{"mount fence still live?"}
+ FEN -->|no| FAIL0["fail the whole carved queue, retry error"]
+ FEN -->|yes| W{"outstanding wedge?"}
+ W -->|yes| WR["resolve the wedge by its exact key first"]
+ WR -->|resolved durable| INST0["install candidate, clear wedge"]
+ WR -->|still unresolved| FAIL1["fail the queue, stay wedged, never allocate a new id"]
+ W -->|no| CARVE["two-phase carve: plan may throw, publish never throws"]
+ CARVE --> VAL["per-item validation: caps, shape, byte budget a failing item fails alone"]
+ VAL --> PREP["build candidate state and the complete wedge before the PUT"]
+ PREP --> PUT["putIfAbsent the ref-log key"]
+ PUT -->|Committed| OK["allocation-free install: swap state, bump counters, complete waiters"]
+ PUT -->|DefiniteFailure| GAP["fail survivors, id not consumed"]
+ PUT -->|"Unresolved, provably nothing sent"| NOSEND["do not wedge"]
+ PUT -->|"Unresolved, otherwise"| WEDGE["install the prepared wedge, survivors fail Uncertain"]
+ OK --> SNAP["maybe schedule a snapshot publish"]
+```
+
+The **wedge** is the mechanism that makes fail-closed ambiguity concrete: at most one per table,
+recording the single conditional `PUT` whose outcome is unknown, complete with the key and the
+sealed bytes. The next flush must resolve *that exact key* before it may allocate a new transaction
+id — an unresolved write can never silently become a gap, and the ledger never double-publishes.
+
+Crash points: between the `PUT` and the install, the object is durable and unapplied — the next
+mount's recovery replays it. Between a precommit and its promote, a dangling precommit is reclaimed
+by the successor's stale-precommit sweep, described above.
+
+## Recovery {#recovery}
+
+Recovery is lazy per table, on first touch (`Pool/CasRefLedger.cpp`), and reads only named,
+authoritative objects — there is no `LIST` anywhere in this path:
+
+1. **Exact `GET` of `_ckpt`.** The durable checkpoint is the sole source of the recovery grounding:
+ `chooseRecoveryGrounding` derives the base (a snapshot id, or genesis if there is none) and the
+ exact transaction to walk from purely from the checkpoint's own fields
+ (`committed_through`/`checkpoint_snapshot_id`/`life_epoch`) — recovery never enumerates its own
+ stream to find them.
+2. If the grounding names a snapshot, `GET` and decode it as the replay base.
+3. Walk forward by exact key from there, one transaction resident at a time: `GET`
+ `cas/ns/stream//-`, decode, apply, discard, advance to the next
+ arithmetic id. Every key this walk touches is a dense, deterministic successor of the last —
+ never a listed or guessed one.
+4. **Absence is a decision point, not an error.** Finding a slot empty is either the live epoch's
+ stream legitimately ending there, or — for a dead predecessor epoch — the exact slot where its
+ closing `EpochSeal` must be written before the table may be trusted; the two cases are
+ distinguished by whether the epoch being walked is still live, not by retrying a listing.
+5. Recovery may itself advance `_ckpt` as it replays, each time via a conditional write against the
+ checkpoint it last read; the write is re-verified with a fresh exact `GET` afterward, and a
+ concurrent winner's farther frontier is honored by restarting from that newer checkpoint rather
+ than trusting the write blindly.
+6. Transient network errors retry the whole attempt with capped backoff; corruption and logic
+ errors fail fast.
+
+For a mounted writer the recovered in-memory table is authoritative for reads of its own
+namespaces — there is no other writer of that namespace. S3 is authoritative for durability:
+in-memory state advances only after a durable `PUT`, and a caller's `appendRefOps` returns only
+after the durable install. In-flight precommits are visible only through the precommit set, never
+through an ordinary ref resolve.
+
+Two cross-process readers see a different, colder view, but only at the discovery boundary: `GC`
+and `ca-fsck` `LIST` once to discover which namespaces exist, staleness-bounded by whatever was
+durable at `LIST` time, so a namespace born after that `LIST` is invisible to this pass. Within
+each discovered namespace, the replay itself is not `LIST`-driven — it is the same exact-`GET`,
+`_ckpt`-grounded arithmetic walk described above, just called from a caller-supplied catalog entry
+instead of a live mount. The relink-confirm handshake (see the
+[replication page](/antalya/cas/architecture/replication#relink-gates)) does zero object-store I/O
+and answers `Yes` only against the resident, warm, fence-live in-memory table — `No` is not proof
+of the negative, only `Yes` is fence-gated.
+
+## Namespace removal {#namespace-removal}
+
+Namespace removal has no physical-empty handshake. The writer changes the catalog row from `Live`
+to `Removing`, appends the exact removals plus `RemoveNamespace`, and deletes nothing itself. The
+`GC` fold attaches cleanup evidence to that life row; a later invocation's pre-fold drain exact-CAS
+-deletes the matching `Removing` catalog row before any successor plan publishes. A perpetual
+namespace janitor and the orphan-manifest sweep reclaim physical debris independently — a same-name
+birth waits only for the catalog row to disappear, never for physical emptiness.
diff --git a/docs/en/antalya/cas/architecture/mounts-and-leases.md b/docs/en/antalya/cas/architecture/mounts-and-leases.md
new file mode 100644
index 000000000000..468a783297a6
--- /dev/null
+++ b/docs/en/antalya/cas/architecture/mounts-and-leases.md
@@ -0,0 +1,223 @@
+---
+description: 'How a CAS server establishes identity, claims its mount slot, and holds a renewable lease that fences stale writers out of the pool.'
+sidebar_label: 'Mounts and leases'
+sidebar_position: 4
+slug: /antalya/cas/architecture/mounts-and-leases
+title: 'CAS Architecture — Mounts and Leases'
+doc_type: 'reference'
+---
+
+Page 4 of 4 in the CAS architecture set. Covers server identity, the mount lease that fences
+writers, and the server-scoped control-plane objects. No external coordinator is involved: there
+is no ZooKeeper/Keeper client anywhere in this protocol — `MountLeaseKeeper` is a local lease
+*renewer*, not a Keeper client.
+
+## `server_root_id` — the identity {#server-root-id}
+
+Every content-addressed disk must be configured with an explicit `server_root_id`. It is
+validated and immutable, and deliberately **not** derived from `ServerUUID` — two replicas can
+otherwise regenerate the same `ServerUUID` from a wiped local state directory, which must not
+silently steal an existing identity.
+
+Validation (`validateServerRootId`, `Pool/CasServerRoot.h`) is fail-closed `BAD_ARGUMENTS`, no
+sanitizing fallback: non-empty, at most 255 bytes, no empty/`.`/`..` path segment, no `_files` or
+`_manifests` segment.
+
+It roots four subtrees and owns catalog names at or below ``:
+
+| Subtree | Contents |
+|---|---|
+| `gc/server-roots//` | owner, epoch, mount — the three control-plane objects below |
+| `roots//` | loose mountpoint objects, no namespace/catalog association |
+| `cas/manifests//` | part manifests |
+| `staging//` | S3-staging debris, outside every GC `LIST`, reclaimed only by this server's next mount |
+
+`blobs/` is **not** under the `server_root_id` — content is pool-global, which is what makes cross-server
+dedup work. Ref/namespace keys are also deliberately opaque and do not embed the `server_root_id`.
+
+Each replica sharing a backend endpoint must use a distinct `server_root_id`; omitting the setting
+is a startup error.
+
+## The owner claim {#owner-claim}
+
+`claimOwnerOrThrow` binds `server_root_id` ↔ `server_uuid` **permanently**. The owner object is never deleted
+and never reassigned — decommission only tombstones it in place.
+
+| Observed at `gc/server-roots//owner` | Action |
+|---|---|
+| present, same `server_uuid`, not tombstoned | proceed |
+| present, `retired_at_ms` set | `CORRUPTED_DATA` — explicitly decommissioned, refuses to resume |
+| present, different `server_uuid` | `CORRUPTED_DATA` — names the regenerated-uuid-file cause |
+| absent, subtree provably empty | `putIfAbsent` the owner (claim) |
+| absent, subtree non-empty | `CORRUPTED_DATA` — identity lost over existing data |
+| lost the `putIfAbsent` race | re-read; equal uuid proceeds, else `CORRUPTED_DATA` |
+
+"Provably empty" requires both an authoritative decoded catalog naming no life owned by `server_root_id` and
+a 1-key `LIST` probe finding nothing under `cas/manifests//` or `roots//`.
+
+Two failure modes this closes:
+
+- A **second server with a different `server_uuid`** is refused at this gate and can never take
+ over, regardless of lease expiry.
+- A **same-uuid live twin** (two processes sharing one uuid file and `server_root_id`) is caught separately, by
+ the mount claim's token-stability observation, and aborts with an operator-facing message rather
+ than corrupting the pool.
+
+## The mount lease {#mount-lease}
+
+One object, `gc/server-roots//mount`, carries **both** the liveness lease and the build
+watermark — there is no separate watermark object. `MountLease` fields: `server_uuid`,
+`writer_epoch`, `hostname`, `pid`, `started_at_ms`, renewal `seq`, `expires_at_ms`, `min_active`
+(the build-watermark floor), and `gc_fenced`.
+
+- **Cadence.** Renew every `mount_renew_period` (default 10 s), TTL `mount_lease_ttl_ms` (default
+ 30 s, TTL/3 renewal ratio). Each beat is a token-guarded `putOverwrite` bumping `seq + 1` —
+ `MountLeaseKeeper` never re-mints the object.
+- **Local fence clock.** `CLOCK_BOOTTIME`, not `CLOCK_MONOTONIC`, so a VM resumed from suspend
+ correctly observes itself expired. The deadline anchors at attempt-*start*, never at response
+ time.
+- **Per-write recheck.** Every durable write or delete captures the fence generation at admission
+ and rechecks it immediately before the object-store call and on every conditional retry. Reads
+ are not gated.
+- **Request-budget admission.** `refAppendFenceOk` refuses to *start* a ref-log attempt unless
+ `attempt_timeout + safety_margin` fits inside the remaining lease, rejecting with
+ `BAD_ARGUMENTS` at request-admission time rather than mid-flight.
+
+**Losing the lease is neither read-only mode nor an abort.** It trips the local fence (latches
+`lost`, bumps the fence generation, moves the in-process runtime to `TransientNotLive`) and
+schedules a self-remount with exponential backoff from 1 s to 30 s. Only a *foreign* `server_uuid`
+observed on the mount body is `LOGICAL_ERROR` — the owner anchor makes a foreign claim
+protocol-unreachable, so seeing one is an invariant violation, not a recoverable race. A
+`putOverwrite` that threw *before* observing any outcome does not fence while the confirmed
+deadline is still comfortably ahead — only a **confirmed** mismatch is immediately terminal. A real
+fence still costs only an epoch: recovery re-claims with a fresh one, bounded at 3 attempts. This
+is the fail-closed posture from the general CAS invariant: doubt about the source aborts, doubt
+about the mechanism may retry.
+
+GC's own view of a dead server is symmetric and clock-skew-immune: a slot becomes fence-eligible
+only after the leader observes the *same* renewal token hold stable, on its own monotonic clock,
+for `TTL + TTL/20 + cadence` — the identical formula a re-mounting server uses to wait out a
+predecessor. The stamped `expires_at_ms` never participates in that decision; wall-clock `now` is
+audit-only.
+
+## The two monotone counters {#counters}
+
+| Counter | Storage | Scope | Protects against |
+|---|---|---|---|
+| `writer_epoch` | durable, `gc/server-roots//epoch` (`ServerEpoch::next_writer_epoch`, CAS-bumped by `allocateWriterEpoch`) | across crashes and restarts | a same-`(uuid, epoch)` twin: a present mount under a normal claim attempt is `CORRUPTED_DATA` |
+| `build_seq` | in-memory only, `CasMountRuntime::next_build_seq`, reset to 1 on every process start | one process incarnation | orders builds *within* an epoch; combined with `writer_epoch` it gives GC a total order |
+
+The absent-epoch branch of `allocateWriterEpoch` is deliberately paranoid: absent with a
+non-empty subtree is `CORRUPTED_DATA` (reset hazard); absent with an empty subtree decides by an
+authoritative probe, never by plain-`get` absence, because a transport fault must not be flattened
+into "not found".
+
+Global build ordering is the **pair** `(writer_epoch, build_seq)` compared lexicographically — the
+exact comparison GC uses for eligibility. The durable authority for both is the mount object
+itself: no mount means no deletion authority means nothing is swept. `min_active`, the oldest
+in-flight `build_seq`, rides in the same mount object as the watermark floor; `UINT64_MAX` in
+`min_active` is the farewell/retired sentinel, not a real build.
+
+## Mount claim outcomes {#claim-outcomes}
+
+The implementation does not expose a single named durable-slot enum; `claimMount` instead returns
+a `MountClaimResult::Kind` together with a `MountPriorState` describing which certificate of death
+(if any) justified a reclaim:
+
+| `Kind` | Meaning |
+|---|---|
+| `Claimed` | fresh claim (absent slot), same-`(uuid, epoch)` refresh, or a certified reclaim |
+| `LiveDoubleStart` | same `server_uuid`, different `writer_epoch`, and no certificate of death yet — a live twin, wait it out |
+| `ForeignOwner` | different `server_uuid` — refused unconditionally |
+| `FencedSelf` | same `(uuid, epoch)`, but `gc_fenced` — terminal for *this* epoch; the caller must mint a fresh one |
+
+| `MountPriorState` | Certificate that justified the reclaim |
+|---|---|
+| `None` | no reclaim needed (fresh claim or same-epoch refresh) |
+| `Clean` | the predecessor's own graceful farewell (`min_active == UINT64_MAX`) |
+| `Fenced` | GC's own threshold-gated fence-out (`gc_fenced`) |
+| `UncleanObserved` | this claimant's own token-stability observation held for the full `TTL + drift` window |
+
+## Behavioral mount-slot model {#mount-state-machines}
+
+Two coupled state pictures. Neither is a literal source enum — the durable slot is derived from
+the claim outcomes above and is shown here as behavior, not as a type in the code:
+
+```mermaid
+stateDiagram-v2
+ [*] --> Absent
+ Absent --> Live: claimMount putIfAbsent, seq=1
+ Live --> Live: keeper beat, putOverwrite seq+1
+ Live --> Fenced: GC observes a stable token past threshold, gc_fenced=1, body preserved
+ Live --> Terminated: certified drain, terminal farewell (expires_at=now, min_active=MAX)
+ Fenced --> Live: same-uuid claim with a fresh writer_epoch, instant reclaim
+ Terminated --> Live: same-uuid claim with a fresh writer_epoch, instant reclaim
+ Live --> Live: same-uuid claim, proven-dead token via UncleanObserved
+ Fenced --> Fenced: same uuid and epoch claim, FencedSelf, no write
+ Live --> Absent: decommission tail, mount then epoch then owner tombstone
+ Terminated --> [*]
+```
+
+The in-process `PoolLifecycle` runtime, by contrast, is a literal enum (`CasMountRuntime.h`):
+
+```mermaid
+stateDiagram-v2
+ [*] --> Live: Pool constructed, fence unarmed
+ Live --> Live: mountWritable arms the fence
+ Live --> TransientNotLive: renewal failure, tripMountLost, lost=true
+ TransientNotLive --> Live: self-remount succeeds with a fresh epoch
+ TransientNotLive --> TransientNotLive: probe inconclusive, retry with backoff
+ TransientNotLive --> IdentityLost: pool meta and owner both authoritatively absent
+ TransientNotLive --> VanishedReplaced: foreign pool_id observed
+ Live --> VanishedForgotten: SYSTEM CAS FORGET
+ IdentityLost --> [*]
+ VanishedReplaced --> [*]
+ VanishedForgotten --> [*]
+```
+
+`IdentityLost`, `VanishedReplaced` and `VanishedForgotten` are terminal and absorbing: the remount
+and GC threads self-exit, and there is deliberately no auto-revive — an identity disappearing
+under a live mount is an operator-level event.
+
+## Mount, unmount, crash {#mount-lifecycle}
+
+**Writable open** runs in a strict order: bootstrap-residual proof, capability probe under a
+random per-mount prefix, pool-meta create-or-validate, `validateServerRootId`, owner claim,
+`allocateWriterEpoch`, mount claim and keeper adopt, materialization grace if the predecessor was
+unclean (default 30 s), arm the fence, start background renewal. If the grace period consumed the
+TTL, one fresh renewal re-anchors the deadline before the fence is armed.
+
+**Clean unmount:** stop and join the remount thread, drain the ref lanes, and only if the drain
+*certified* quiescence write the terminal farewell (`expires_at_ms` already-expired,
+`min_active = UINT64_MAX`). That sentinel is what lets a successor reclaim instantly. If the drain
+did not certify, the keeper stops renewing and writes no farewell — an unearned farewell would let
+a successor start mutating while a stale conditional write from the predecessor is still in
+flight.
+
+**Crash:** no farewell; the renewal token freezes. Recovery is either the same server restarting
+and waiting out the token-stability observation, or the GC leader fencing the slot first, after
+which any reclaim is instant.
+
+**Permanent removal** of a dead replica (`Cas::decommissionPoolMember`, driven by
+`SYSTEM CAS DROP POOL MEMBER '' FROM DISK ''`) claims the victim's mount slot
+as an administrative writer with a no-wait policy (refuses immediately if the member is alive),
+drops every ref-bearing namespace, sweeps manifest debris before the slot (deleting the mount
+removes the watermark authority), drains staging and roots, then — only with zero warnings —
+retires in order: mount, epoch, a final liveness re-check, owner tombstone.
+
+## `system.cas_mounts` {#mounts-table}
+
+A read-only view of the same heartbeat-floor computation GC uses: one `LIST` of
+`gc/server-roots/` plus one `GET` per slot, zero writes, per-row fail-open (an undecodable body
+becomes `state = 'corrupt'`, never an exception). Shows every `server_root_id` in the pool, including peers.
+
+| Column | Notes |
+|---|---|
+| `disk`, `server_root_id`, `server_uuid`, `hostname`, `process_id` | identity |
+| `writer_epoch`, `renewal_sequence`, `started_at`, `expires_at`, `min_active_build_sequence`, `gc_fenced` | lease state (`DateTime64(3)` columns; the millisecond-integer field names live only in the internal `MountLease` struct and the on-disk body) |
+| `state` | one of `live`, `expired`, `terminated`, `fenced`, `corrupt` |
+| `is_leader`, `pending_reclaim`, `last_success_age_seconds`, `wedged_namespace_count` | GC health, process-local; **`NULL` on every peer row** — a process-local fact must never be stamped onto another server's row |
+| `lifecycle`, `lifecycle_reason`, `lifecycle_detail`, `lifecycle_since` | the SQL surface for the in-process `PoolLifecycle` runtime above: `lifecycle` is one of `live`, `not_live`, `identity_lost`, `vanished`, `constructing`, `shutdown`; `lifecycle_reason` distinguishes `replaced` from `forgotten` for a `vanished` disk; `lifecycle_detail` carries the full diagnosis text; `lifecycle_since` is when the current non-live state began (`NULL` while live) |
+
+The lifecycle snapshot is I/O-free and ungated, so a not-live, never-started, or vanished disk
+still produces a row instead of silently disappearing from the table.
diff --git a/docs/en/antalya/cas/architecture/namespaces.md b/docs/en/antalya/cas/architecture/namespaces.md
new file mode 100644
index 000000000000..2f622ff0e329
--- /dev/null
+++ b/docs/en/antalya/cas/architecture/namespaces.md
@@ -0,0 +1,172 @@
+---
+description: 'What a namespace is, the opaque life_id that qualifies every object it owns, the pool-wide namespace catalog, and a namespace lifetime end to end from first write to catalog-row deletion.'
+sidebar_label: 'Namespaces'
+sidebar_position: 10
+slug: /antalya/cas/architecture/namespaces
+title: 'CAS Architecture — Namespaces'
+doc_type: 'reference'
+---
+
+# CAS architecture — namespaces {#namespaces}
+
+A namespace (`Cas::RootNamespace`) is the opaque, per-table, per-server-root string under which one
+table's part manifests and one ref table live — in practice something the wiring layer composes,
+such as `srv1/` for an ordinary table or `shadow//` for a `FREEZE`
+shadow. `CAS` never interprets its contents beyond a shape check (non-empty, no empty or reserved
+path segment, at most 512 bytes). The [manifests-and-refs page](/antalya/cas/architecture/manifests-and-refs#ref-table)
+covers the ref table one namespace owns; this page covers the namespace itself — its physical
+identity, the catalog that is the sole authority for whether it exists, and its full lifetime from
+first write to the catalog row's deletion.
+
+## `life_id`: the physical identity {#life-id}
+
+A namespace **name** can be reused — a table dropped and recreated keeps the same name. What must
+never be reused is the **physical identity** any durable object under that name is keyed by, so
+that a stale reader of the old incarnation can never be handed bytes belonging to the new one. That
+identity is `life_id`: an opaque, pool-wide, randomly minted 128-bit value (two `thread_local_rng`
+draws; retried on the astronomically unlikely zero draw, since `0` is reserved as "never a valid
+life"). Internally it is the catalog's `incarnation` field, aliased as `NamespaceLifePhysicalId`;
+paired with the namespace name it forms `NamespaceLifeId{ns, incarnation}`
+(`Primitives/CasNamespaceLifeId.h`).
+
+`NamespaceLifeId` deliberately has no default construction and no conversion from a bare namespace
+name: code holding only the name cannot address a ref object or a namespace file at all, so
+forgetting the life qualifier is a compile error, not a runtime aliasing bug. The only legitimate
+source of a `NamespaceLifeId` is `fromCatalogEntry` — reading it off one immutable catalog cut —
+which is what makes "this life belongs to this name" a catalog fact rather than something a caller
+could reconstruct incorrectly.
+
+`life_id` renders as 32 fixed-width lowercase hex digits and appears in exactly the two subtrees
+that are life-owned (see the [storage-layout key table](/antalya/cas/architecture/storage-layout#key-table)):
+
+| Subtree | Contents |
+|---|---|
+| `cas/ns/stream//` | The immutable `_log`/`_snap` ref-transaction history |
+| `cas/ns/state//` | The mutable `_ckpt` checkpoint and any namespace-owned `_files/` |
+
+Part manifests deliberately do **not** carry `life_id` — a manifest already has its own globally
+unique identity (`{writer_epoch, build_sequence, manifest_ordinal}` under the server root, see the
+[manifests-and-refs page](/antalya/cas/architecture/manifests-and-refs#part-manifests)) and needs no
+further qualification. Loose mountpoint objects under `roots/` are outside namespace ownership
+altogether and carry no `life_id` either.
+
+## The namespace catalog {#catalog}
+
+One pool-wide object, `cas/ref_catalog` (`Layout::refCatalogKey`), is the sole authority for which
+namespaces exist. It is read on every fold round and every ref-table recovery, and mutated by one
+token-`CAS` write per lifecycle transition. Its entries are canonically ordered by namespace bytes,
+strictly ascending, with no duplicate name — both the encoder and the decoder enforce this, so an
+out-of-order or duplicate-keyed catalog can never become durable.
+
+Each row (`CatalogEntry`) carries:
+
+| Field | Meaning |
+|---|---|
+| `ns` | The namespace name |
+| `state` | `Creating`, `Live`, or `Removing` — see below |
+| `incarnation` | The `life_id` for this row, nonzero, never reused |
+| `creator` | The mounted writer's fence identity (server root, writer epoch, admission fence generation) that is creating this row — **required** iff `state == Creating`, **forbidden** otherwise |
+| `removal_started_round` | The `GC` round observed when removal began — **required** iff `state == Removing`, absent otherwise |
+
+`NsState`'s three wire values (`Creating = 1`, `Live = 2`, `Removing = 3`) are append-only, exactly
+like every other persisted enum in `CAS`: a catalog object written by one build is read by another,
+so a value is never renumbered or repurposed.
+
+```mermaid
+stateDiagram-v2
+ [*] --> Creating: casAdmitEntry -- fresh random life_id, creator fence stamped
+ Creating --> Live: completeCreation -- publish genesis _ckpt, then flip, clear creator
+ Creating --> Creating: a live foreign creator fence -- retry later, no steal
+ Creating --> Live: reconcileStaleCreator finds the creator fence provably dead, a fresh opener steals and completes it
+ Live --> Removing: beginRemoving -- table drop, stamps removal_started_round
+ Removing --> [*]: GC drains the row once a fold sealed positive cleanup evidence
+ [*] --> Creating: a fresh createNamespace call, only once the old row is fully absent -- brand new life_id
+```
+
+A row's own state machine is linear per row (`Creating → Live → Removing → gone`); what makes the
+catalog non-linear as a whole is that a stalled `Creating` row can resolve two different ways
+depending on whether its creator fence is still alive, and that a name only becomes creatable again
+once its prior row is completely gone — both shown above.
+
+## Lifetime end to end {#lifetime}
+
+### Creation, on first write {#creation}
+
+There is no explicit "create namespace" statement; a namespace is born the first time anything
+resolves its ref table (`CasRefLedger::resolveNamespaceLife`, bounded at 32 loop attempts). If the
+catalog has no row for the name at all, the resolving mount admits a `Creating` entry stamped with
+its own creator fence and a freshly minted `life_id`
+(`CasRefCatalog::createNamespace` → `casAdmitEntry`). Two more steps make it usable:
+
+1. **Publish the genesis checkpoint.** The first `_ckpt` ever written for this `life_id` carries
+ `life_epoch = creator.writer_epoch` — the only writer that will ever know this namespace's
+ genesis epoch.
+2. **Flip to `Live`.** One token-`CAS` moves the row from `Creating` to `Live` and clears `creator`.
+
+Both steps re-check the resolving mount's own fence before writing, so a mount that lost its lease
+mid-creation reports `FencedOut` rather than silently completing. Several openers racing the same
+brand-new name all observe "no entry", but only one wins the admit; the rest see `Superseded` and
+simply re-read the catalog, landing on the winner's `Creating` row.
+
+A `Creating` row under a **different** mount's creator fence is not this opener's problem to force:
+if that fence is still provably alive, the opener retries later; only once the fence is provably
+dead (the same mount-lease terminality check `GC`'s heartbeat floor uses) does
+`reconcileStaleCreator` let a fresh opener steal the row onto its own fence and finish the two steps
+above itself.
+
+### Removal {#removal}
+
+Dropping a table (`DROP TABLE`, and every operation that reduces to it) calls
+`CasRefLedger::dropNamespace`. It closes the namespace's local positive-mutation lane first — new
+positive writers are refused while the in-flight ones drain — then transitions the catalog row from
+`Live` to `Removing` in one token-`CAS` (`beginRemoving`, stamping `removal_started_round` from the
+currently observed `GC` round), then appends **one** ref-log transaction that removes every current
+committed and precommit binding and ends with a terminal `RemoveNamespace` op. Removal is never
+refused by an admission check — Constraint 13 in the catalog's own spec — it always succeeds once
+the fence holds.
+
+Nothing is deleted by the writer at this point. No blob, no manifest, no ref-log object physically
+disappears here — only pointers move, exactly like an ordinary [`DROP TABLE`](/antalya/cas/architecture/part-lifecycle#operation-mapping)
+on any other ref.
+
+### What `GC` does with a `Removing` namespace {#gc-and-removal}
+
+The terminal `RemoveNamespace` transaction is folded like any other ref-log record, during the
+[round's fold phases](/antalya/cas/architecture/garbage-collection#the-round). Folding it stamps
+positive **cleanup evidence** directly onto that `life_id`'s row in the new fold seal — there is no
+physical listing and no `Pending`/`Completed` handshake; the evidence is a pure fact about which
+ref-log transaction folded.
+
+The **next** round's `pre_fold_ref_drain` phase is what actually removes the catalog row: it reads
+the just-adopted parent fold seal, and for every `Removing` row whose life carries durable cleanup
+evidence, it exact-`CAS`-deletes the catalog entry before that round does anything else. This
+two-round shape — evidence sealed in round *n*, catalog row deleted in round *n+1* — is why removal
+needs no separate physical-emptiness proof: by the time the row is deleted, a fold has already
+proven its ref history is fully drained.
+
+### What disappears, and when {#what-disappears}
+
+| Object class | Reclaimed by | When |
+|---|---|---|
+| Catalog row (`cas/ref_catalog` entry) | `GC` phase 2, `pre_fold_ref_drain` | The round after the fold that sealed cleanup evidence for this life |
+| Part manifest bodies | Ordinary owner-removal ([phase 15](/antalya/cas/architecture/garbage-collection#the-round)) for anything that had a committed or precommit binding, the [orphan-manifest sweep](/antalya/cas/architecture/manifests-and-refs#orphan-sweep) for anything that never got that far | As each owning ref is dropped by the removal transaction itself, independent of the catalog row |
+| Blob bodies | The ordinary condemn/graduate/delete pipeline | Whenever the manifests that named them stop being live, same as any other blob |
+| Ref stream/state objects (`_log`, `_snap`, `_ckpt`, `_files`) under the dead `life_id` | The perpetual namespace janitor ([phase 16](/antalya/cas/architecture/garbage-collection#the-round)) | Best-effort, one bounded `LIST` page at a time, whenever it next lists a key whose `life_id` a fresh catalog cut no longer names — independent of, and not gated on, catalog-row deletion |
+
+The janitor is leak-only: it never fails a round, never blocks progress on an unreadable key, and a
+crash mid-page simply leaves debris for its next page.
+
+### Recreate while removing {#recreate-while-removing}
+
+A fresh `createNamespace` call for a name whose catalog row is still `Live` or `Removing` is
+refused outright — internally this is a misuse `LOGICAL_ERROR`, because the higher-level open loop
+(`resolveNamespaceLife`) filters that case out first and reports a typed retry-later error instead:
+"creation waits for its terminal fold and catalog removal to complete". A caller that keeps
+resolving the same name simply keeps retrying until the row is gone.
+
+Once `pre_fold_ref_drain` has deleted the row, the name is free again, and the very next opener mints
+a **brand new**, independently random `life_id` — never the retired one. That is the whole answer to
+"what happens on recreate": the old physical identity is never revived, so every key ever written
+under it — its `_log`, its `_snap`, its `_ckpt`, its `_files` — stays permanently addressed by a
+value nothing will ever mint again, and a reader still holding the old `NamespaceLifeId` observes
+only stale-or-absent data, never a byte that belongs to the new incarnation.
diff --git a/docs/en/antalya/cas/architecture/part-lifecycle.md b/docs/en/antalya/cas/architecture/part-lifecycle.md
new file mode 100644
index 000000000000..5a61d0490dc1
--- /dev/null
+++ b/docs/en/antalya/cas/architecture/part-lifecycle.md
@@ -0,0 +1,148 @@
+---
+description: 'The part-add protocol from local build through blob upload to promote, its nine crash points and their cleaners, and how each MergeTree operation maps onto it.'
+sidebar_label: 'Part lifecycle'
+sidebar_position: 6
+slug: /antalya/cas/architecture/part-lifecycle
+title: 'CAS Architecture — Part Lifecycle'
+doc_type: 'reference'
+---
+
+# CAS architecture — part lifecycle {#part-lifecycle}
+
+Publishing a `MergeTree` part on a `CAS` disk is one durable protocol,
+`stageManifest → precommitAdd → putBlob → promote`, driven by `Cas::PartWriteTxn`
+(`Pool/CasPartWriteTxn.cpp`). This page walks that protocol end to end: local build, the durable
+order and why each step is where it is, every crash window and who cleans it up, and how each
+`MergeTree`-level operation (insert, merge, mutation, detach, …) maps onto it. Manifest structure
+and the ref table it writes into are covered on the
+[manifests-and-refs page](/antalya/cas/architecture/manifests-and-refs); the fetch-side protocol
+for replicated parts is on the [replication page](/antalya/cas/architecture/replication).
+
+## The protocol {#protocol}
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant MT as MergeTree
+ participant TX as CA transaction overlay
+ participant PW as PartWriteTxn
+ participant S3 as Object store
+
+ rect rgba(140,190,140,0.12)
+ Note over MT,S3: Phase A -- local build, nothing durable, nothing visible
+ MT->>TX: writeFile data.bin
+ TX->>TX: classify: blob class spills and hashes to scratch or S3 staging
+ MT->>TX: writeFile count.txt, columns.txt, ...
+ TX->>TX: buffer small files in memory as inline candidates
+ MT->>TX: moveDirectory tmp_insert to final name
+ Note over TX: pure overlay re-key, not a publish
+ end
+
+ rect rgba(120,160,255,0.12)
+ Note over MT,S3: Phase B -- publish, per part, serially
+ MT->>TX: commit
+ TX->>PW: stageManifest entries
+ PW->>S3: PUT manifest, write-once, no preliminary HEAD
+ PW->>S3: append ref-log PRECOMMIT, plus NamespaceBirth if needed
+ Note over PW: precommit durable, the observe gate opens
+ TX->>PW: fan out blob uploads, one task per unique BlobRef
+ par blob 1
+ PW->>S3: HEAD / conditional PUT / adopt
+ and blob 2
+ PW->>S3: ...
+ end
+ PW->>PW: merge upload results on the owning thread, one no-throw swap
+ TX->>PW: promote
+ PW->>S3: GET and validate the precommit manifest body
+ PW->>S3: append ref-log txn: retire old committed, Precommit to Committed, SetPublishedAt
+ Note over PW: commit durable, then retire the build sequence
+ end
+```
+
+**Phase A — staging.** The transaction is an eager overlay, not a queue: `writeFile` immediately
+classifies the path and either spills bytes to a hashing buffer or holds them in memory as an
+inline candidate. Blob-class files stage to local scratch by default, or — when `staging_backend`
+is `s3` and the mount-time conditional-copy probe passed — to an S3 staging object written as
+`[header][payload]`, so that the later promote is a verbatim server-side copy. The `tmp_ → final`
+rename is a pure overlay re-key; the durable publish happens only in `commit`.
+
+**Step 1 — `stageManifest`.** Caps (see the
+[manifests-and-refs page](/antalya/cas/architecture/manifests-and-refs#part-manifests)) are
+checked before the write; the id is minted as `{epoch, build_seq, ordinal++}`; the body goes out
+with a conditional create and no preliminary `HEAD`. Both a definite failure and an unresolved
+outcome throw retry-later.
+
+**Step 2 — `precommitAdd`.** The intent — target namespace, final ref name, manifest — is recorded
+before the append, because an unresolved append may have landed anyway. One ref-log transaction
+adds the precommit binding. A same-name birth is refused with retry-later while the catalog still
+says `Removing`; once the predecessor row is absent, creation receives a new opaque life id and
+starts its own stream. On return the precommit is durable, and only now may the writer adopt
+existing blobs.
+
+**Step 3 — blob upload fan-out.** One task per unique `BlobRef`, deterministic dispatch order, one
+pre-sized result slot per ref (see the write-path sequence on the
+[blob-protocol page](/antalya/cas/architecture/blob-protocol#conditional-write-sequence)). The
+calling thread only submits and joins, never occupies a pool slot, so a pool of size one degenerates
+to a correct serial run and can never deadlock. The contract is merge-nothing: if any task threw,
+nothing is merged and the first error in dispatch order is rethrown. Results are folded into the
+dependency set on the owning thread, into a copy, committed by one no-throw swap. Pool size is the
+server setting `cas_blob_upload_pool_size` (default 16).
+
+**Step 4 — `promote`.** Reads and revalidates the precommit manifest body once; sets the commit
+state to Uncertain before the append — past that point, failure is no longer proof of the negative
+— then checks that the precommit is still the live owner and revalidates leaves. Tokened leaves
+are skipped because they are edge-protected; tokenless leaves must be evidence adopts, trusted
+through the durable manifest edge with no per-file `HEAD`; anything else is a `LOGICAL_ERROR`. The
+whole thing lands as one ref-log record: optional retirement of the old committed binding, the pure
+Precommit-to-Committed owner move, and `SetPublishedAt`. Promotion emits no blob deltas — the
+manifest never loses an owner, so it is net zero.
+
+## Crash points and their cleaners {#crash-points}
+
+This table is the single best summary of the design's crash-safety story: every row leaks
+something recoverable; no row loses data or leaves a dangling reference.
+
+| # | Crash window | Left behind | Who cleans it |
+|---|---|---|---|
+| C1 | During staging | Local temp files, or S3 staging objects | Local: unconditional cleanup plus buffer destructor. S3: the mount's own staging sweep at next mount — never deleted on abort |
+| C2 | After `stageManifest`, before `precommitAdd` | An unreferenced manifest body | Writer's best-effort exact-token delete; durable backstop is the orphan-manifest sweep |
+| C3 | `precommitAdd` returned Unresolved | A possibly-live precommit binding | Intent recorded pre-append; `abandon` appends the exact removal, tolerating absence. The body is never writer-deleted |
+| C4 | Between `precommitAdd` and `promote` | A live precommit plus uploaded blobs | No resume path exists. Removed by `abandon`, else by a fenced successor's stale-precommit sweep |
+| C5 | Mid blob fan-out | Already-uploaded blobs | Nothing merged; blobs become `GC`-reclaimable debris; the part is not published |
+| C6 | `promote` append Unresolved | The ref may or may not be committed | Commit state Uncertain — the relink layer maps this to "retry the whole fetch", never to a byte fetch |
+| C7 | A later part throws after earlier parts published | A partial multi-part commit | Precise rollback: drop only the refs this call created, matching the exact manifest — never clobbers a concurrent writer's repoint |
+| C8 | Transaction destroyed uncommitted | Open builds | Destructor abandons every build |
+| C9 | Namespace dropped mid-build | — | One atomic flag; every further op fails closed at the alive check |
+
+## The repoint {#repoint}
+
+Writing into an already-committed part — an `ALTER`-style metadata rewrite, or any standalone
+write against a committed source — never mutates the existing manifest. It writes a **new**
+manifest over the (possibly partly reused) blob set and moves the ref to it in one ref-log record.
+Unchanged columns are adopted by hash through a tokenless evidence dependency with no `HEAD` and no
+`GET`; changed columns are fresh uploads. A repoint therefore costs zero bytes moved for the
+carry-forward portion of the file set — only the changed content re-uploads.
+
+## How each MergeTree operation maps {#operation-mapping}
+
+| Operation | CAS mechanics |
+|---|---|
+| `INSERT` | The canonical path above. Projections ride the parent part's transaction |
+| Merge | Identical for the output part. `.tmp_proj → .proj` is an entry-prefix re-key inside the staged manifest, not a rename |
+| Mutation | `createHardLink` per unchanged file: a source staged in *this* transaction copies the entry and its pending-blob record; a **committed** source records a tokenless evidence dependency with no `HEAD` and no `GET`. A mutation is a manifest rewrite where zero bytes move for the carry-forward |
+| `ALTER` / metadata rewrites | Standalone writes into a committed part, i.e. a repoint |
+| `DROP PART` | `removeDirectory` drops the ref and clears any per-file removal marks — one ref-drop, zero repoints |
+| `DROP TABLE` / `DETACHED` / `UNFREEZE` | A namespace or prefixed-ref drop. Blobs are never deleted here — removal is pointer-unlink plus deferred `GC` |
+| `RENAME TABLE` | Republishes every ref and verbatim file into the new namespace, then drops the old one. Not atomic across namespaces, but idempotent and re-drivable — true atomicity would need a move journal and is out of scope |
+| `FREEZE` / `BACKUP` / `RESTORE` / cross-disk `MOVE` | Each wraps the whole clone in one disk transaction, because a CAS part is one atomic unit |
+
+`FREEZE` is the one operation that materializes real bytes into a genuinely separate shadow
+namespace rather than reusing a table's own ref names — that shadow namespace is a `GC`
+reachability root, and `UNFREEZE` releases its refs.
+
+## Reads while a part is in flight {#in-flight-reads}
+
+Read-your-writes for a part still inside an open transaction is served by an explicit overlay
+rather than by any durable object — `tryGetInFlightStorageObjects`, `tryReadFileInFlight`,
+`listInFlightDirectory`. One deliberate subtlety: the bare part directory reports as absent in the
+overlay, so cleanup of a deduplication-rejected temporary part does not mistake it for a real part.
diff --git a/docs/en/antalya/cas/architecture/read-path.md b/docs/en/antalya/cas/architecture/read-path.md
new file mode 100644
index 000000000000..c92634dd9e74
--- /dev/null
+++ b/docs/en/antalya/cas/architecture/read-path.md
@@ -0,0 +1,84 @@
+---
+description: 'How a CAS read resolves a ref to a manifest and then to ranged blob reads, and the two caches — manifest decode and part-folder view — that sit on that path.'
+sidebar_label: 'Read path'
+sidebar_position: 9
+slug: /antalya/cas/architecture/read-path
+title: 'CAS Architecture — Read Path'
+doc_type: 'reference'
+---
+
+# CAS architecture — read path {#read-path}
+
+A `CAS` read never touches a classical local-metadata path: there is no local directory listing to
+consult, only a ref resolve followed by object-store reads. This page covers the three ways a file
+access is served, the full chain for the common case, the two caches that sit on that chain, and
+how a part still open inside a write transaction serves its own reads.
+
+## How a file access is served {#access-kinds}
+
+| Access kind | How it is served | S3 cost |
+|---|---|---|
+| Inline entry — small files such as `count.txt`, `columns.txt` | Decoded straight out of the manifest body | Zero additional operations |
+| Blob-backed file — `.bin`, marks, large `primary.idx` | Ranged `GET` bounded by `[header_len, header_len + blob_size)` | One `GET` per column file per part open |
+| Verbatim file — `roots/…` objects | Plain object read, no `CAS` indirection | One `GET` |
+
+The full chain for a blob-backed file is: resolve the ref, read the manifest, look up the path,
+build a blob view plan, ranged `GET`, then `ReadBufferFromFileView`. Because the payload always
+starts at a pool-constant offset (the manifest's `blob_header_len`), no header parse is needed to
+locate content — see the [envelope format](/antalya/cas/architecture/storage-layout#envelope-format)
+on the storage-layout page.
+
+Part manifests themselves are read whole after opening the object: there is no on-disk random
+access, `seek`, or streaming requirement for their entry records — a manifest is small enough that
+decoding the whole body is cheaper than any partial-read machinery would be.
+
+## The two caches {#caches}
+
+| Cache | Keyed by | Setting | Default | What still hits the network |
+|---|---|---|---|---|
+| Manifest decode cache | `(ManifestId, Token)` | `manifest_decode_cache_bytes` | 128 MiB | A mandatory `HEAD` on **every** access, cache hit or miss |
+| Part-folder view cache (`Cas::CachedPartFolderAccess`, `Parts/PartFolderAccess.h`) | Part ref key | `part_folder_cache_bytes`, `part_folder_cache_max_entries`, `part_folder_cache_max_entry_bytes` | 64 MiB / 10 000 entries / 16 MiB | Its `ForceFresh` policy re-proves the manifest body via that same mandatory `HEAD`, paced by `part_folder_validate` (`always` \| `never` \| `age `) |
+
+**The `HEAD` is mandatory even on a cache hit** — the page's most counter-intuitive fact, because it
+means a cache hit still costs one object-store round trip:
+
+```mermaid
+flowchart TD
+ A["readManifestShared(ManifestId)"] --> B["HEAD the manifest key"]
+ B -->|"absent"| C["throw FILE_DOESNT_EXIST -- a live ref must never name a missing object"]
+ B -->|"present, token t"| D{"cache lookup (ManifestId, t)"}
+ D -->|hit| E["return the cached decode -- no GET"]
+ D -->|miss| F["GET the body"]
+ F --> G{"body's own ref and namespace match the key?"}
+ G -->|no| H["throw CORRUPTED_DATA"]
+ G -->|yes| I["decode, insert into cache keyed by (ManifestId, t), return"]
+```
+
+The `HEAD` is what proves the live ref still names an existing object — the no-dangle invariant —
+and it supplies the token that keys the cache; only then is the decode cache consulted. On a miss,
+the `GET` is followed by the two identity checks in the diagram, each `CORRUPTED_DATA` on failure.
+Only a fully validated decode enters the cache. Setting either cache's byte budget to `0` disables
+retention while leaving the `HEAD`-and-validate sequence intact — a cache is purely an
+optimization, never a trust boundary.
+
+The part-folder view cache is invalidated on every promote and repoint, and is single-flight on a
+cold build: concurrent readers of the same not-yet-cached view coalesce into one build rather than
+racing independent `GET`s.
+
+## Reads while a part is still being written {#in-flight-reads}
+
+An in-flight part inside an open write transaction is not yet visible through the ordinary ref
+resolve — reading it goes through the same explicit overlay used for read-your-writes, covered on
+the [part-lifecycle page](/antalya/cas/architecture/part-lifecycle#in-flight-reads). The bare part
+directory itself reports as absent in that overlay, precisely so that cleanup of a rejected
+temporary part is never mistaken for a real, resolvable part.
+
+## Diagnostic and read-only access {#read-only-access}
+
+A read-only or diagnostic opener of a `CAS` disk (`ca-fsck`, `ca-gc-dryrun`, and similar tools)
+must not claim mount ownership, schedule `GC`, or mint writer state — read-only enforcement sits
+below the ordinary facade checks, at the backend layer itself. A mounted `Pool` caches its ref
+table and does not re-recover it on every read; a diagnostic tool that deliberately performs a
+fresh cold recovery on each pass can therefore observe a **less** stale ref table than a live
+mounted read, which is intentional for tools whose entire purpose is catching drift a live mount
+would not notice.
diff --git a/docs/en/antalya/cas/architecture/replication.md b/docs/en/antalya/cas/architecture/replication.md
new file mode 100644
index 000000000000..cc767f7fe115
--- /dev/null
+++ b/docs/en/antalya/cas/architecture/replication.md
@@ -0,0 +1,118 @@
+---
+description: 'Fetch by relink between two replicas sharing a pool: the gates in order, what actually seals commit-before-release, and detach/attach/drop.'
+sidebar_label: 'Replication'
+sidebar_position: 7
+slug: /antalya/cas/architecture/replication
+title: 'CAS Architecture — Replication'
+doc_type: 'reference'
+---
+
+# CAS architecture — replication {#replication}
+
+When two `ReplicatedMergeTree` replicas share a `CAS` pool, a fetch should move **no bytes** — the
+receiver already has access to the same blobs the sender does. The mechanism is a three-phase
+handshake, fetch by relink, layered directly on the ordinary interserver part-fetch protocol. This
+page covers the handshake, the gates that decide whether it fires, what actually makes it safe
+against a concurrent `GC` round, and how detach/attach/drop reduce to the same primitives. The
+writer owns table semantics and part publication (see the
+[part-lifecycle page](/antalya/cas/architecture/part-lifecycle)); `GC` owns ref-log folding and
+physical cleanup (see the [garbage-collection page](/antalya/cas/architecture/garbage-collection))
+— ordinary replication traffic never reads `gc/state` or waits on a `GC` round.
+
+## The handshake {#handshake}
+
+Only two of the three phases are round trips to the sender — the offer and the confirm. The
+publish and the promote are the receiver's own writes to the pool.
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant R as Receiver
+ participant Snd as Sender
+ participant S3 as Shared pool
+
+ R->>Snd: GET part, cas_pool_uuid = R's pool uuid, client_protocol_version = 11
+ Note over R: advertising 11 is a promise to confirm before promoting
+ Snd->>Snd: same disk pool uuid? identity, never endpoint plus prefix
+ Snd->>S3: resolve the offer once -- manifest bytes and confirm token from the SAME view
+ Snd-->>R: cookie cas_relink = part_manifest_v2, cookie cas_source_token = ..., body = manifest bytes
+ Note over Snd: sender is fire-and-forget -- it releases the part here
+
+ rect rgba(120,160,255,0.12)
+ Note over R,S3: T1 -- publish, the plus-one lands first
+ R->>S3: adopt entries by evidence, no HEAD, no bytes, stageManifest fresh receiver-local id, precommitAdd
+ Note over R: the sender's ManifestRef, namespace and digest are ignored -- only entries are used
+ end
+
+ rect rgba(255,190,120,0.15)
+ Note over R,Snd: T2 -- confirm
+ R->>Snd: POST cas_confirm = token
+ Snd->>Snd: confirmExactRef, zero object-store I/O, never throws
+ Snd-->>R: cookie cas_confirm_answer = yes or unproven
+ end
+
+ alt answer is yes
+ R->>S3: T3 -- promote, ref published
+ else anything else -- unproven, missing cookie, timeout, transport error
+ R->>R: throw a locally generated NETWORK_ERROR, retry later
+ Note over R: never a byte re-request -- that would go back to the very source whose state is in doubt
+ end
+```
+
+## The gates, in order {#relink-gates}
+
+| # | Gate | What it enforces |
+|---|---|---|
+| 1 | Pool identity | The receiver advertises `cas_pool_uuid`; the sender offers relink only if its own disk's pool uuid is **equal**. Matching by endpoint and prefix was tried and rejected — a minted pool uuid is the identity |
+| 2 | Protocol version 11 | On the receiver side, advertising it is a promise to run the confirm round trip before promoting |
+| 3 | One resolution for two outputs | The manifest bytes and the confirm token come from the **same** view. Two separate calls would allow a repoint in between and hand the receiver a token naming a manifest whose entries it never adopted |
+| 4 | The receiver trusts nothing from the wire but the entry list | The sender's manifest id, namespace and payload digest are ignored; the target namespace and ref come from the receiver's own router, and manifest path hygiene is validated at decode |
+| 5 | The confirm is I/O-free and fail-closed | A cold, evicted, unfenced or terminal mount answers `Unknown`. `No` and `Unknown` both go on the wire as `unproven`, because the fence check is evaluated last, so a `No` cannot be distinguished from "cannot prove it right now" |
+| 6 | Only the literal `yes` authorizes promotion | Everything else — including a timeout — is one outcome: throw and retry later |
+| 7 | Promote outcomes are three-way | `Committed` proceeds; a **proven** not-committed state (body-absent precommit, precommit no longer live owner, ref conflict) falls back to a byte fetch; `Unresolved` **throws**, because returning "fall back" there would publish the part twice |
+
+The byte-fetch fallback is bounded: it re-invokes the fetch with relink disabled, which stops the
+receiver advertising its pool uuid, which stops the sender offering relink — so the relink path
+cannot be entered twice for one fetch. Byte-fetched files content-address and dedup on arrival
+anyway, so falling back never loses the dedup property, only the zero-byte-move property for that
+one fetch.
+
+## What actually seals "commit before release" {#relink-seal}
+
+The receiver's `+1` — its precommit binding — is durable **before** the sender is asked anything,
+and any removal of the sender's own binding is appended strictly after that `+1` is in the ref
+log. That ordering, steps T1 then T2 then T3, is the whole seal.
+
+This does **not** establish that every subsequent `GC` fold *sees* that `+1` under every listing
+behavior: a configuration with one incomplete listing page can, in principle, let a fold miss a
+freshly published edge. A confirmed relink therefore proves only "the source still holds exactly
+this manifest right now", not "no future fold can ever miss this edge" — `ca-fsck`'s
+reachable-but-absent scan is the backstop for that gap, not the relink protocol itself. Relink
+also races `GC` in the ordinary sense any writer does: between the sender encoding its offer and
+the receiver's promote, `GC` on the shared pool may condemn a blob that was live only through the
+sender's own ref. The [writer-versus-GC race](/antalya/cas/architecture/blob-protocol#writer-gc-race)
+on the blob-protocol page is what makes that interleaving safe — revival is re-upload only, and the
+receiver's evidence-adopt is protected by its own durable precommit edge exactly like any other
+writer's adopt.
+
+A fetch whose source part is still a live, held `DataPartPtr` on the sender's own replica — the
+common case for a local, same-process relink — keeps the source pinned through the destination's
+commit by ordinary part-lifetime rules, independent of the ref-log seal above.
+
+## Detach, attach, drop {#detach-attach-drop}
+
+A detached part is **not** a separate namespace — it is a ref in the table's own namespace with a
+`detached/` prefix (the same is true of `moving/`). Only `FREEZE` uses a genuinely separate shadow
+namespace, which the ownership check deliberately refuses to claim, so a frozen part can never be
+relink-confirmed.
+
+`DETACH`, `ATTACH`, `delete_tmp_` cleanup, and merge-result renames all reduce to the same two
+moves: re-key any *staged* source into the destination, then `republishRef(src → dst)` for any
+*committed* source. `republishRef` re-reads the source manifest freshly, publishes an
+equivalent-entry manifest under the destination ref — a **new** manifest id, with blobs untouched
+and adopted by evidence — then drops the source ref. A destination that already exists with
+identical entries just drops the source, an idempotent re-drive; one with different entries
+throws.
+
+Manifests are therefore per-ref and never moved: a detach creates a new manifest for
+`detached/` and retires the old one, and the blobs' net in-degree is unchanged.
diff --git a/docs/en/antalya/cas/architecture/storage-layout.md b/docs/en/antalya/cas/architecture/storage-layout.md
new file mode 100644
index 000000000000..9b4336a6bf2f
--- /dev/null
+++ b/docs/en/antalya/cas/architecture/storage-layout.md
@@ -0,0 +1,160 @@
+---
+description: 'S3 key layout and on-disk text-object formats used by the content-addressed storage (CAS) MergeTree disk backend.'
+sidebar_label: 'Storage layout'
+sidebar_position: 2
+slug: /antalya/cas/architecture/storage-layout
+title: 'CAS Architecture — Storage Layout'
+doc_type: 'reference'
+---
+
+# CAS architecture — storage layout {#storage-layout}
+
+Every key in a pool is built by one class, `Cas::Layout` (`Formats/CasLayout.h`), which owns
+exactly the pool prefix. Every persisted object opens with a one-line JSON envelope header, and
+control-plane bodies are JSON Lines — one JSON object per line, sorted where the object is a log
+or a set of entries (`Formats/README.md`; see [Envelope format](#envelope-format) below for which
+parts are a single JSON object versus JSON Lines versus raw payload bytes). The format is
+deliberately this plain: any object can be fetched and read with ordinary line-oriented tools
+while debugging, and a new field is additive — a tolerant reader skips it — so the format evolves
+without a migration.
+
+## Key table {#key-table}
+
+All key patterns are shown under the pool prefix. A **namespace** is the opaque per-table string
+under which one `MergeTree` table's part manifests and ref history live: for a live table it is
+the table's canonical disk path (`store//`, `@cas@`-marked) prefixed by the owning
+server's `server_root_id`, and a backup gets its own `shadow/…` namespace instead; `Cas::Layout`
+only validates a namespace's shape and never interprets its contents.
+
+| Key pattern | Object | Codec | Writer |
+|---|---|---|---|
+| `_pool_meta` | pool identity + floors | `cas_pool_meta` | pool create/admit |
+| `blobs///` | blob envelope + payload | `cas_blob` | uploads |
+| `blobs///.meta` | blob freshness sidecar | `cas_blob_meta` | dedup/GC |
+| `cas/ns/stream//_log/-.zst` | ref transaction log | `cas_ref_log` | writer commit path |
+| `cas/ns/stream//_snap/-.zst` | complete ref table snapshot | `cas_ref_snap` | writer/GC fold |
+| `cas/ns/state//_ckpt` | mutable per-life checkpoint | `cas_ref_ckpt` | writer/GC fold |
+| `cas/ns/state//_files/` | namespace-owned verbatim file | — (raw passthrough) | upper layers |
+| `cas/manifests//-/.zst` | part manifest | `cas_part_manifest` | part build |
+| `gc/state` | GC state (incl. GC lease) | `cas_gc_state` | GC |
+| `gc/hb` | GC leader heartbeat | `cas_gc_hb` | GC |
+| `gc/maintenance_state` | leak-only namespace-janitor cursor | `cas_gc_maintenance_state` | future janitor |
+| `gc/gen//attempt//fold_seal` | fold seal (deterministic) | `cas_fold_seal` | GC |
+| `gc/gen//attempt//blob_target//` | GC source-edge run segment | `cas_run` | GC |
+| `gc/gen//attempt//outcomes//.zst` | GC outcome log | `cas_gc_outcomes` | GC |
+| `gc/server-roots//owner` | server-root owner singleton | `cas_owner` | mount |
+| `gc/server-roots//epoch` | server-root epoch singleton | `cas_epoch` | mount |
+| `gc/server-roots//mount` | mount lease (incl. `min_active` watermark) | `cas_mount_lease` | mount |
+| `roots/` | loose mountpoint object, verbatim | — (never interpreted) | upper layers |
+| `staging//…` | S3-native upload staging scratch | — | writer, own mount only |
+
+`` is `ch128`, `xxh3`, or `sha256` — the hash algorithm is a path segment because one pool may
+legally hold blobs under several algorithms at once. `` is a flat two-character S3 key
+shard for request-fan-out, unrelated to the separate `gc_shards` GC-internal reduction fan-out
+(which appears only inside `gc/gen/…` keys and routes by the digest's high 64 bits, read
+big-endian). Discovery LISTs use fixed prefixes: `cas/ns/stream/`, `cas/ns/`, `cas/manifests/`,
+`blobs/` (deliberately without the algorithm segment, so one recursive LIST covers every
+algorithm), `roots/`, `gc/server-roots/`. `staging/` is a top-level sibling that no GC LIST ever
+touches — it is reclaimed only by its own server's next mount.
+
+## Envelope format {#envelope-format}
+
+Every persisted CAS metadata object is text: a header line, a body, and an optional trailer.
+
+```
+{"type":"cas_