Repository navigation
Rejoin a decommissioned CAS server root on server start - #2574
Conversation
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@ 8000 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 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>
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>
Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # docs/en/antalya/cas/architecture/mounts-and-leases.md # docs/en/antalya/cas/operations/troubleshooting.md # src/Disks/DiskObjectStorage/MetadataStorages/ContentAddressed/Pool/CasServerRoot.cpp # src/Disks/DiskObjectStorage/MetadataStorages/ContentAddressed/Pool/CasServerRoot.h
A rejoin rewrote the mount slot and the owner with no CAS event, so the audit trail showed a slot whose uuid changed with no writer. Each rejoin write now emits a `MountClaim` event (`rejoin_mount_rewrite`, `rejoin_owner`) with the retired and the new uuid and life. The `previous life remains` refusal for catalog rows named nothing, and a row GC holds never goes away, so the advice to run GC could not help. It now names the first rows with their state and points at `CASGCStuckRemovals`. Tests also pin that a still-running process of the old life stops in `IdentityLost` after a rejoin under a new uuid. Review: #2574 (comment) Signed-off-by: Mikhail Filimonov <mfilimonov@altinity.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the audit. Status per finding, on 57c7944:
CI: |
The merge-base changed after approval.
The merge-base changed after approval.
The merge-base changed after approval.
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.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, and each of its two writes emits aMountClaimevent (rejoin_mount_rewrite,rejoin_owner) with both uuids and lives.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 (for catalog rows, the first namespaces and their state) and, for catalog rows and manifests,SYSTEM CAS GC RUN '<disk>'on the GC leader; a row held by GC points atCASGCStuckRemovals. 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.IdentityLostat its next remount, also after a rejoin under a new uuid.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), the catalog-rows refusal names the row); the two rejoin events with both uuids and lives; the old process after a new-uuid rejoin stops inIdentityLostand writes nothing; a fence during the drain keeps the tombstone and the start is refused withdecommission is not finished.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 on the head: release 2797, ASan 2801, all passed. The merge of PR B alone: release 2796, ASan 2800. Every commit throughe20348282ab0builds and passes releaseCAS*on its own.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 state734d2bcd885Merge cas-363-member-lifecycle (CAS: decommission keeps a member's control objects; owner life and epoch nonce #2567) into cas-member-lifecycle-pr-c57c7944383bReport CAS rejoins in the event stream and name leftover catalog rowsChangelog 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: