Repository navigation
Conversation
Two processes of one server_uuid could adopt the same writer epoch: when our counter write timed out and a twin wrote the same next value, the resolve read found identical bytes and reported our write as landed. Each allocation decision now writes a fresh random `allocation_nonce`, so only our own write matches our bytes. The field is a plain key that 26.6.4 readers skip. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every owner write sets `owner_life` to the read value + 1, so no two owner writes carry the same bytes and a delayed If-Match cannot land after a later write; `retirement_verified` records a finished decommission. Both are optional keys that 26.6.4 readers skip. Every claimer re-reads the owner after its mount write and requires its own uuid and life; otherwise it fences its own body and fails, so a successor does not wait the lease out. A remount that finds another life stops for good. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decommission no longer deletes owner, epoch or mount: the counter keeps epochs from repeating across a member's lives, and the farewell body keeps a paused claimer from minting a lower epoch into an absent slot. Each run tombstones the owner right after its claim, bounded by its lease, confirms the claim, drains on its fenced mount plane, writes its farewell and marks the retirement verified. A lost claim rolls the tombstone back unless an earlier run verified it. After an unresolved tombstone the admin withholds its farewell, so the victim does not run before a late tombstone lands. The result column slot_removed becomes member_retired and namespaces_pending_gc is appended. The admin refuses a member without an epoch counter instead of minting one, and a mount that skips the capability probe now logs it. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mount's startup sweep deleted every staging object under its prefix, including a successor's in-flight uploads when two processes of one member overlapped. Keys are now staging/<srid>/<writer_epoch>/<random>.tmp and the sweep deletes only epochs below the live one; flat keys count as epoch 0 and a nested member's keys are left alone. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A decommission keeps the epoch counter, so allocateWriterEpoch never mints over an absent counter for it and EpochMintPolicy goes. The vanished-slot pacing and the observation prune stay, because a 26.6.4 decommission or a hand delete still removes a mount slot. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MountLeaseRenewer::renewOn sampled the stop flag before the write. A stop that landed after the sample refused the admission with nothing sent, yet the renewal went terminal and finishTeardown skipped the farewell. Read the flag after the refusal instead. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a rejoin under a new server_uuid the slot can hold the previous uuid's farewell or self-fence. claimMount now reclaims such a terminal body below its epoch once a fresh owner read shows the claimer's uuid and life, not retired. A start and a self-remount pass that expectation; the decommission admin does not. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A terminated or fenced slot whose owner is tombstoned reads as `retired`, and the new last column `retirement_verified` says whether a decommission verified it. A terminal row whose owner does not decode reads as `corrupt`. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A start over a verified retirement whose data is gone takes the owner at the next life under its own server_uuid, same or new, and continues the kept epoch counter. A new uuid rewrites the mount body before the owner. An unverified tombstone, a held slot, leftover data or missing control objects refuse with the action to take. Remounts never rejoin, and a start without a server uuid does not rejoin. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47bb142 to
e203482
Compare
Brings in #2566 (member lifecycle PR A, final review) and #2517 (renewal until a definitive answer). - `MountLeaseRenewer::renewOn`: the stop flag is read once, after the refusal, inside the #2517 call series, so a stop racing the admission stays releasable (93adba9). - The decommissioned-owner refusal keeps this branch's text: with the epoch nonce and `owner_life`, a retired id can come back through the owner rewrite in the troubleshooting docs. - #2566 tests for the terminal-state refusal and ids with `/` join the `CASDecommission` fixture. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uite `CASDecommission` is a `TEST_F` fixture on this branch; gtest refuses a plain `TEST` in the same suite. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the base merge of PR B: #2566 (member lifecycle PR A, final review) and #2517 (renewal until a definitive answer). - The foreign-owner refusal takes the #2566 text, which names `SYSTEM CAS DROP POOL MEMBER` for a dead owner. - Docs keep this branch's rejoin rules and add the #2566 rows for the terminal-slot claim and ids with `/`. - The unverified-tombstone test expects this branch's `decommission is not finished` refusal. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #2574 Audit ReviewVerdictNo new High defect in the rejoin itself. The High finding from #2567 is still in this head, and it matters more now. The rejoin protocol is careful. It checks the counter and slot first, requires a verified retirement, refuses a held slot, removes the old access probe and checks for leftovers. For a new uuid it writes the mount before the owner, and it bounds its re-decisions. Every race I traced either fails closed or leaves a state that the next start resolves. The trouble is the other way back into the pool. A decommission that loses its claim after the drain has started rolls the tombstone back. The member then restarts as an ordinary start, over a partly deleted life, and skips every check this PR adds. On top of that there is one Medium (also inherited, and wider now) and two new Lows, both about diagnosability.
What the change doesBefore this PR, a server whose
The ordinary start then continues: Model
FindingsRecovery and rollbackHigh (inherited from #2567, unchanged at this head): a decommission that loses its claim mid-drain un-retires the member, and its next start skips the rejoin gate.
Observability and diagnosabilityMedium (inherited from #2567, broadened by this PR): a still-running process of a retired id stays
Low (new): a rejoin writes the mount slot and the owner without a CAS event.
Low (new): the
Checked and clean
Coverage summary
Operational readinessScope: restart of a decommissioned Flow 1: Restart the same host after
|
PR #2574 CI VerificationVerdictCI blocks approval. One failure is a The
Job counts: 56 succeeded, 3 failed, 1 cancelled, 107 skipped. How the list was reduced
Findings
|
| Commit | Where | Result |
|---|---|---|
3a2641dc70 |
#2567, before the base merge | OK (30 ms) |
d45685f0ca |
#2567 head, after dc3396dc |
FAILED |
46d9e99d64 |
#2574 head | FAILED |
The product code is consistent: the abort cleanup is intended, and the staging-epoch property the test protects still holds. The test's expectation is stale. Fix it in PR B: count only the refused write's keys, or keep the first transaction alive across the assertion, or use leakWriterStagedObject the way the CASDecommissionStaging tests do. Either way, this check stays red after the base merge that clears the Iceberg abort.
This failure is not new to #2574. PR-2567-CI-VERIFICATION.md lists only two failing tests on d45685f0 and does not mention it, so that report should be corrected too. The PR description's "CAS* unit tests … ASan 2751, all passed" predates the merge.
This PR's own tests. The diff from base adds 130 TEST/TEST_F cases across PR B and this PR. All 130 ran in this job: 129 passed and the one above failed. That includes all 19 CASRejoin.*, both CASListMounts.*, and the new CASMount foreign-reclaim tests. The log has 2,746 CAS* tests run and all 2,746 passed.
IcebergMetadataGenerator.ModifyColumnAfterItselfWithoutTypeIsNoop: pre-existing-flaky
/ClickHouse/src/DataTypes/IDataType.h:380:67: runtime error: member call on null pointer of type 'DB::IDataType'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /ClickHouse/src/DataTypes/IDataType.h:380:67
This is a deterministic base bug, not flakiness. It fails the same way on 16 of 17 PR heads against this base, including unrelated ones such as #2573 ("Create custom disks outside storage_policies_mutex"). The one head where it passes is #2614, which merged c72ea99e999 ("Added nullcheck for type before calling unknown type check") into the base on Oct 21. That was after this PR last merged the base. The diff touches no Iceberg or IDataType file. Merging the current base clears it.
It does block one more thing: UBSan aborts the gtest binary, so any test ordered after it did not run on this head. All of this PR's tests ran before it (see above).
RegressionTestsRelease / OAuth / oauth: unknown
Job 118343500828. oauth/regression.py started at 22:32:50 with --output new-fails and printed nothing more. At the job's timeout_minutes: 210 it was cancelled: ##[error]The operation was canceled. at 02:01:09. No suite artifacts were uploaded under REFs/2574/merge/46d9e99d…/regression/.../oauth/.
On this job, across the PR heads on this base:
| Who | Runs | Result |
|---|---|---|
| 16 other PR heads (Oct 8–20) | 16 | 16 success, 46–62 min |
#2567 head d45685f0, started 3 minutes after this one |
1 | success, 48 min |
| #2574 | 1 | hung, cancelled at 210 min |
There is no code path. The OAuth suite's configs contain no CAS disk, and this diff only touches the CAS disk, system.cas_mounts and docs. Still, 0 of 16 against 1 of 1 cannot be called flaky from one run, and the hang left no log that names a mechanism.
What would resolve it: rerun the OAuth job. A pass makes it infrastructure (a hung Keycloak or the runner). A second hang needs regression-test-database-investigation with the CI database password, to find which scenario stalled.
Grype (GrypeScanKeeper, GrypeScanServer (-alpine)): pre-existing-flaky
The keeper image reports 4 High/Critical findings and the Alpine server image reports 1, including CVE-2026-85091, CVE-2026-84782, CVE-2026-84784 and CVE-2026-72897. The same counts appear on #2594, #2577 and #2567, all built as 26.6.8.20001. Their #2614 images (26.6.8.20002, the current base) report 0. The diff changes no Dockerfile or package list. A rebuild after merging the base should clear both. Jobs: 118351150468 and 118353852535.
What passed that matters for this PR
test_cas_drop_pool_member: all four tests passed inIntegration tests (amd_asan_ubsan, db disk, old analyzer, 2/8)(118356079923). That includestest_retired_member_rejoins_empty(same uuid twice, then a new uuid, three GC rounds each).- CAS stateless jobs:
amd_binary, cas storage,amd_binary, cas s3 storage, andamd_asan_ubsan, cas s3 storage1/2 and 2/2. - CAS stress jobs:
amd_debugandamd_asan_ubsan, both with CAS S3 storage. - The regression suites the PR selected: Parquet, ParquetS3 (minio and aws_s3), Iceberg 1 and 2, S3Export part and partition, Swarms.
Coverage gaps
- No CAS regression suite ran.
ci_regression_casis unchecked in the PR body, soCAS,CASSuite,CASAlter,CASLightweightDelete,TieredStorageCASand theCASS3Cache*jobs were all skipped. For a change to the CAS mount and owner protocol, this is the largest gap. - TSan, MSan, Aarch64 and coverage are excluded by the PR's CI options. The
tsan/msanCAS stateless and stress jobs andUnit tests (tsan)did not run. - The targeted integration job ran nothing ("No failed tests found from previous runs").
Recommendation
Do not approve on CI grounds yet.
- Fix
StagingEpochMatchesAdmissionGenerationin CAS: decommission keeps a member's control objects; owner life and epoch nonce #2567: the test, not the product. This branch picks the fix up through its next merge of CAS: decommission keeps a member's control objects; owner life and epoch nonce #2567. - Merge the current
antalya-26.6-20261008(97ac10160bor later). That clears the Iceberg UBSan abort and should clear both Grype scans. - Rerun OAuth. If it hangs again, investigate it with the CI database.
- Tick
ci_regression_casfor the next run.
…ation Since #2566 an abandoned transaction deletes its S3 staging object, so the first write's key was gone by the time the test counted the keys under the old epoch and the count was 0 on every run (`Unit tests (asan_ubsan)` of #2567 and #2574 after the base merge). The first transaction now lives until the final assertion; the property under test — a self-remount between the two reads refuses the write and stages nothing under the old epoch — is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com>
The helper reads the `UINT64_MAX` sentinel in `min_active_build_sequence`, the build watermark, and "retired" in its field comment collided with the owner tombstone (`retired_at_ms`, `member_retired`) this series adds: every clean shutdown writes a farewell, and that does not retire the member. The name now says what the body records — the holder released the slot cleanly — and the comment says it is a sentinel in the watermark, independent of `gc_fenced`. Review: #2567 (comment) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com>
Brings the StagingEpochMatchesAdmissionGeneration fix and the `isCleanRelease` rename; the two `isFarewell` call sites this branch added are renamed with it. Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com>
A CAS server whose
server_root_idwas decommissioned withSYSTEM CAS DROP POOL MEMBERnow rejoins the pool as an empty member on its next start, with the same or a new server uuid. Before, the id was refused for good and needed a hand-edited owner object or a newserver_root_id.Built on PR B; until it merges, this diff also shows PR B's commits. Base:
antalya-26.6-20261008.Related: #2567
Changes
Pool::openover a tombstoned owner takes the id over whenepochandmountexist, the retirement is verified, the slot is terminal and the old life's data is gone. The owner moves to a new life byIf-Match, the epoch counter continues, and the member starts empty. A conflict re-reads the owner and decides again. The rejoin logs a warning with the old and new uuid and life.epochormount(an id retired by 26.6.4, or deleted by hand; checked first, so such an id is never sent to a rerun), an unverified tombstone, a held slot, data of the old life, and an owner or slot that changes on every decision.ForeignOwner, and the decommission tool never takes a foreign body.roots/<srid>/clickhouse_access_check_<uuid>key. Any other object there still blocks the rejoin.previous life remainsnames the part left and, for catalog rows and manifests,SYSTEM CAS GC RUN '<disk>'on the GC leader. The foreign-owner refusal also names a duplicateserver_root_idbetween two running servers.system.cas_mountsshows a decommissioned member asretired(one moreGETper terminated or fenced row) and adds the last columnretirement_verified: 1 once the retirement is verified, else 0. A rerun ofSYSTEM CAS DROP POOL MEMBERis forretiredwithretirement_verified = 0only.system.cas_mounts,SYSTEM CAS DROP POOL MEMBER). Decommission is no longer called permanent: the data is gone for good, the id is not.Risks / notes
retiringstate: a tombstoned owner's terminal row isretired, andretirement_verified(new last column) tells verified from not. Queries that matchedretiringfrom the earlier revision of this PR must read the column.server_root_id. Its local table definitions attach with no rows.roots/object, leaves a slot the next start refuses, and a decommission rerun cannot clear it (the admin never takes a foreign body). The refusal says what to delete by hand.clickhouse diskswithout a server uuid file does not rejoin: it keeps the refusal.cas_server_root_id. RunSYSTEM CAS DROP POOL MEMBERonly from upgraded servers: a 26.6.4 admin concurrent with a same-uuid rejoin can deleteepochandmountunder it.retiredwithretirement_verified = 0; PR B's fenced tombstone narrows the window.DROP POOL MEMBERof them in that window is refused.Testing
epochandmountis refused with "configure a new<cas_server_root_id>" and no rerun advice;retiredwithretirement_verified0 and 1 inlistMounts; one test over everyServerRootLeftover(control objects unchanged, probe key built fromtoString(UUID)).test_cas_drop_pool_member: after each post-rejoin insert,SYSTEM CAS GC RUNon every node, then the rows are rechecked; a verified retirement readsretiredwithretirement_verified = 1. Module: 3/3.CAS*unit tests: release 2747, ASan 2751, all passed. Every commit builds and passes releaseCAS*.CaEpochAllocCoreround 14): with the tombstone allowed to land until GC fences the admin's slot, a clean farewell after an unresolved tombstone puts it on a running member (NoTombOverLive); with the farewell withheld the late-landing, paused-run and 26.6.4 rows are green under the rerun rule. The default battery (187 rows) meets every expectation.retiredwithretirement_verified = 0and the rerun converges; a kill after the verified mark showsretirement_verified = 1and needs no rerun; every rejoin comes back empty and its new rows survive three GC rounds;dangling = 0after every cycle; soak oraclePHASE3 OK. The one red is theNoWaitrefusal text, which does not name the state to wait for; that text belongs to the base series, not this PR.Commits (on top of PR B)
ba336d2c60a9Let a CAS start reclaim another uuid's terminal body of its own ownerb4ede038732fShow retired CAS members in system.cas_mounts85df7b155084Rejoin a decommissioned CAS server root at server starte20348282ab0Document CAS member rejoin and the retired stateChangelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):
A CAS server whose
server_root_idwas decommissioned withSYSTEM CAS DROP POOL MEMBERnow comes back on its next start as an empty member, with the same or a new server uuid, instead of being refused.system.cas_mountsshows a decommissioned member asretired, with a newretirement_verifiedcolumn.Documentation entry for user-facing changes
CI/CD Options
Exclude tests:
Regression jobs to run: