diff --git a/docs/en/antalya/cas/architecture/backend.md b/docs/en/antalya/cas/architecture/backend.md
index b1d88843471e..cc0740ea73da 100644
--- a/docs/en/antalya/cas/architecture/backend.md
+++ b/docs/en/antalya/cas/architecture/backend.md
@@ -108,15 +108,17 @@ storage, and GC would silently stop reclaiming.
`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 further gates run as the battery's opening steps, and one
+the mount refuses to become writable. The one tolerated exception is a versioning probe that cannot
+answer at all, described in the first bullet below. Two further gates run as the battery's opening steps, and one
sits genuinely alongside it. The distinction matters: because the versioning check runs *inside* the
battery, skipping the battery used to skip it too, which is exactly why the third gate exists.
-- `checkPoolPreconditions` — inside the battery. On the `GCS`-dialect combination only, requires bucket versioning to be
- *verifiably* off. A confirmed `Enabled` and an inconclusive probe both throw: `CAS` cannot assume
- the safe answer here, because what it would do on a versioned bucket is delete objects it believes
- it reclaimed. A probe is inconclusive when the credential may not read the bucket's versioning
- configuration, or when the backend cannot answer at all.
+- `checkPoolPreconditions` — inside the battery. On the `GCS`-dialect combination only, checks that
+ bucket versioning is off. A confirmed `Enabled` throws: what `CAS` would do on a versioned bucket is
+ delete objects it believes it reclaimed. An inconclusive probe — the credential may not read the
+ bucket's versioning configuration, or the backend cannot answer at all — logs a warning and lets
+ the mount proceed, since it is not evidence of a versioned bucket; verifying it then falls to the
+ operator, as it already does for soft delete.
- `checkSkipAccessCheckSupport` — alongside the battery, in the skip branch of `Pool::open`, since it
is the gate that decides whether the battery may be skipped at all. It asks whether the backend may serve a writable mount that skips the
battery at all. The `GCS`-dialect combination refuses, so `skip_access_check = true` cannot reach a
diff --git a/docs/en/antalya/cas/architecture/garbage-collection.md b/docs/en/antalya/cas/architecture/garbage-collection.md
index c918a0a35f14..a604104c7796 100644
--- a/docs/en/antalya/cas/architecture/garbage-collection.md
+++ b/docs/en/antalya/cas/architecture/garbage-collection.md
@@ -226,6 +226,7 @@ the user-facing configuration surface.
| Setting | Default | Bounds |
|---|---|---|
| `cas_gc_meta_pool_size` | 16 | bounded pool for condemn-marker writes |
+| `cas_gc_read_concurrency` | 16 | bounded pool for the fold's read-ahead; `1` disables |
## Observability {#observability}
diff --git a/docs/en/antalya/cas/architecture/manifests-and-refs.md b/docs/en/antalya/cas/architecture/manifests-and-refs.md
index df86d82e575d..ba476cffe88a 100644
--- a/docs/en/antalya/cas/architecture/manifests-and-refs.md
+++ b/docs/en/antalya/cas/architecture/manifests-and-refs.md
@@ -101,7 +101,7 @@ swept for that root.
flowchart TD
A["LIST one page of cas/manifests/ freeze candidates with exact GET"] --> 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 -->|"same epoch, min_active_build_sequence 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"]
diff --git a/docs/en/antalya/cas/architecture/mounts-and-leases.md b/docs/en/antalya/cas/architecture/mounts-and-leases.md
index d778756ce323..eb852b070da0 100644
--- a/docs/en/antalya/cas/architecture/mounts-and-leases.md
+++ b/docs/en/antalya/cas/architecture/mounts-and-leases.md
@@ -9,7 +9,7 @@ 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
+is no ZooKeeper/Keeper client anywhere in this protocol — `MountLeaseRenewer` is a local lease
*renewer*, not a Keeper client.
## `cas_server_root_id` — the identity {#server-root-id}
@@ -61,32 +61,36 @@ Two failure modes this closes:
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.
+ than corrupting the pool — this is the default behavior, with `cas_unsafe_remount_no_delay` off.
+ With it on, a same-uuid claim over such a slot reclaims at once instead of observing (see
+ `cas_unsafe_remount_no_delay` in the configuration reference).
## 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`, `write_attempt_id`, `hostname`, `pid`, `started_at_ms`, renewal `seq`,
-`expires_at_ms`, `min_active` (the build-watermark floor), and `gc_fenced`.
+`expires_at_ms`, `min_active_build_sequence` (the build-watermark floor), and `gc_fenced`.
- **Logical renewal identity.** Each holder-originated body has a fresh nonzero
`write_attempt_id`. One logical renewal fixes one immutable `(key, bytes, expected token,
write_attempt_id)` tuple before I/O. Every physical retry repeats it byte-for-byte; a later GC
fence preserves the observed ID, while reclaim and successor bodies mint new IDs.
- **Resolve before retry.** A transient or ambiguous conditional `PUT` is followed by one exact
- `GET`. The keeper adopts the result only when the complete body, including `write_attempt_id`,
+ `GET`, except that an attempt whose transport error names a failed connection is reissued first
+ after a flat pause and settled by the reissue's own answer (a 2xx) or by the exact `GET` that
+ follows its `412`. The renewer adopts the result only when the complete body, including `write_attempt_id`,
equals its immutable request. If the predecessor token is still current, another identical `PUT`
may follow bounded backoff. A same-pair twin, GC-fenced body, successor, foreign holder, or absent
body is never treated as this renewal.
- **Absolute deadline.** Renewal uses `CLOCK_BOOTTIME`, not `CLOCK_MONOTONIC`, so a VM resumed from
suspend correctly observes itself expired. Its absolute deadline is the minimum of the existing
request-operation budget and the last confirmed lease deadline minus the safety margin. The
- controller checks that one configured attempt still fits before each backend `PUT` or resolving
+ controller checks that one attempt envelope still fits before each backend `PUT` or resolving
`GET`, after each interruptible backoff, and before accepting success. A retry, `GET`, response
timestamp, or wall-clock step never extends authority.
-- **Cadence.** The runtime normally starts a logical renewal every `mount_renew_period` (default
- 10 s), with TTL `mount_lease_ttl_ms` (default 30 s, TTL/3 renewal ratio). The next beat is anchored
+- **Cadence.** The runtime normally starts a logical renewal every `cas_mount_renew_period_ms` (default
+ 10 s), with TTL `cas_mount_lease_ttl_ms` (default 30 s, TTL/3 renewal ratio). The next beat is anchored
at the committed body's pre-I/O BOOTTIME start. A slow recovery therefore causes an immediate
catch-up beat when the nominal cadence has elapsed; it does not wait a fresh full period after the
response.
@@ -94,10 +98,10 @@ watermark — there is no separate watermark object. `MountLease` fields: `serve
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.
+ `2 × envelope + safety_margin` fits inside the remaining lease (a write and its settlement read),
+ rejecting with `BAD_ARGUMENTS` at request-admission time rather than mid-flight.
-**Losing the lease is neither read-only mode nor a process abort.** `MountLeaseKeeper` is a
+**Losing the lease is neither read-only mode nor a process abort.** `MountLeaseRenewer` is a
synchronous durable-slot state machine. A committed result advances its token, sequence, confirmed
BOOTTIME deadline, and cadence anchor. Any admitted deterministic failure, confirmed conflict, or
ambiguity left at the deadline/attempt limit moves it to `RenewalTerminal`; it cannot mint another
@@ -105,7 +109,7 @@ body or publish a clean farewell. Owner cancellation before any request is the o
`NotAttempted` result and leaves clean release possible. Cancellation after a request was sent is
terminal because that request may still land.
-After the keeper call returns, `CasMountRuntime` consumes the result. A terminal result trips the
+After the renewer call returns, `CasMountRuntime` consumes the result. A terminal result trips the
local fence (latches `lost`, bumps the fence generation, moves the in-process runtime to
`TransientNotLive`) and latches one self-remount generation. A confirmed foreign/successor or
same-pair conflict remains a typed fail-closed error; it is never adopted. A real fence still costs
@@ -115,9 +119,25 @@ inside authority already proved by the last confirmed lease.
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.
+for `TTL + floor(TTL/20) + period` — close to, but not identical to, the threshold a re-mounting
+server uses to wait out a predecessor, which observes `TTL + floor(TTL/20) + max(1,
+floor(period/2))`. Both thresholds are evaluated purely on the observer's own clock and its own
+configured `TTL`/`period`; nothing about the writer's timing travels on the wire. The stamped
+`expires_at_ms` never participates in either decision — it is a writer-stamped diagnostic used by
+`system.cas_mounts` and by the non-authoritative decommission epoch-recovery precheck, never an
+authorization; local fencing is derived instead from the confirmed request's pre-I/O `BOOTTIME`
+anchor plus the TTL, and wall-clock `now` stays audit-only.
+
+Every server sharing a pool must therefore run the identical `cas_mount_lease_ttl_ms` and
+`cas_mount_renew_period_ms`: a member or GC leader configured with a shorter threshold than its
+peers can fence out a healthy peer whose token-update gap merely exceeds that shorter threshold —
+a peer renewing frequently stays live, one that missed a renewal does not. Change these values only
+with every member of the pool stopped; a graceful restart removes only that member's own startup
+observation and does not make mixed thresholds safe. With the defaults (TTL 30 s, period 10 s,
+margin 2 s), `TTL − margin − period − 2 × envelope = 4 s` is the scheduling-lateness budget before
+the first renewal attempt of a period can begin, where `envelope = attempt_timeout + 2 × cap` and
+`cap` is `attempt_timeout` when the disk's `connect_timeout_ms` is `0`, else
+`min(connect_timeout_ms, attempt_timeout)` (7 s with defaults).
## The two monotone counters {#counters}
@@ -133,9 +153,9 @@ 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
+itself: no mount means no deletion authority means nothing is swept. `min_active_build_sequence`, 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.
+`min_active_build_sequence` is the farewell/retired sentinel, not a real build.
## Mount claim outcomes {#claim-outcomes}
@@ -153,9 +173,10 @@ a `MountClaimResult::Kind` together with a `MountPriorState` describing which ce
| `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`) |
+| `Clean` | the predecessor's own graceful farewell (`min_active_build_sequence == 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 |
+| `UncleanUnsafe` | the operator's explicit `cas_unsafe_remount_no_delay` authorization carried the slot's exact token — not a certificate of death |
## Behavioral mount-slot model {#mount-state-machines}
@@ -166,12 +187,13 @@ the claim outcomes above and is shown here as behavior, not as a type in the cod
stateDiagram-v2
[*] --> Absent
Absent --> Live: claimMount putIfAbsent, seq=1
- Live --> Live: keeper beat, putOverwrite seq+1
+ Live --> Live: renewer 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)
+ Live --> Terminated: certified drain, terminal farewell (expires_at=now, min_active_build_sequence=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
+ Live --> Live: same-uuid claim under cas_unsafe_remount_no_delay, no observation
Fenced --> Fenced: same uuid and epoch claim, FencedSelf, no write
Live --> Absent: decommission tail, mount then epoch then owner tombstone
Terminated --> [*]
@@ -202,24 +224,24 @@ under a live mount is an operator-level event.
**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 synchronous keeper start, materialization grace if the
-predecessor was unclean (default 30 s), arm the fence, then create and release the runtime-owned
-renewal and remount workers before the writable pool becomes externally visible. If the grace period
-consumed the TTL, one fresh synchronous renewal re-anchors the deadline before the fence is armed.
+`allocateWriterEpoch`, mount claim and synchronous renewer start, arm the fence, then create and
+release the runtime-owned renewal and remount workers before the writable pool becomes externally
+visible. If the claim consumed the TTL, one fresh synchronous renewal re-anchors the deadline
+before the fence is armed.
Failure to construct either worker joins the partial pair, closes the fence, and fails the writable
open. No incident path constructs a thread.
The renewal and remount workers are separate and long-lived under one stable `CasMountRuntime`.
`scheduleRemount` increments a requested-generation latch and wakes the persistent remount worker,
-including while an older generation is active. Before keeper replacement, remount requests
-`ParkRequested` and waits for the renewal driver to report `Parked`, which proves that no keeper call
+including while an older generation is active. Before renewer replacement, remount requests
+`ParkRequested` and waits for the renewal driver to report `Parked`, which proves that no renewer call
is in flight. A successful remount handles only its snapshotted generation; a newer request is
processed before renewal resumes.
**Clean unmount:** request stop and join both persistent workers, drain the ref lanes, and only if
-the drain *certified* quiescence call `MountLeaseKeeper::release` on an `Active` keeper to write the
-terminal farewell (`expires_at_ms` already expired, `min_active = UINT64_MAX`). That sentinel is what
-lets a successor reclaim instantly. A `RenewalTerminal` keeper, an unresolved ref write, or a sent
+the drain *certified* quiescence call `MountLeaseRenewer::release` on an `Active` renewer to write the
+terminal farewell (`expires_at_ms` already expired, `min_active_build_sequence = UINT64_MAX`). That sentinel is what
+lets a successor reclaim instantly. A `RenewalTerminal` renewer, an unresolved ref write, or a sent
renewal ambiguity writes no farewell — an unearned farewell would let a successor start mutating
while a stale conditional request from the predecessor is still in flight.
diff --git a/docs/en/antalya/cas/architecture/read-path.md b/docs/en/antalya/cas/architecture/read-path.md
index beb2d87d99fb..49a6864a0109 100644
--- a/docs/en/antalya/cas/architecture/read-path.md
+++ b/docs/en/antalya/cas/architecture/read-path.md
@@ -36,30 +36,33 @@ decoding the whole body is cheaper than any partial-read machinery would be.
| Cache | Keyed by | Setting | Default | What still hits the network |
|---|---|---|---|---|
-| Manifest decode cache | `(ManifestId, Token)` | `cas_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 | `cas_part_folder_cache_bytes`, `cas_part_folder_cache_max_entries`, `cas_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 `cas_part_folder_validate` (`always` \| `never` \| `age `) |
+| Manifest decode cache | `ManifestId` | `cas_manifest_decode_cache_bytes` | 128 MiB | Nothing on a hit; one `GET` on a miss |
+| Part-folder view cache (`Cas::CachedPartFolderAccess`, `Parts/PartFolderAccess.h`) | Part ref key | `cas_part_folder_cache_bytes`, `cas_part_folder_cache_max_entries`, `cas_part_folder_cache_max_entry_bytes` | 64 MiB / 10 000 entries / 16 MiB | Nothing on a validated hit; a `ForceFresh` access bypasses the retained view and rebuilds from the manifest decode cache |
-**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:
+**A cache hit costs no request.** A manifest id is minted once and its body is written once, so one
+id names one content forever and a cached decode can be served without asking the object store:
```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"]
+ A["readManifestShared(ManifestId)"] --> B{"decode cache lookup by ManifestId"}
+ B -->|hit| C["return the cached decode -- no request"]
+ B -->|miss| D["GET the body"]
+ D -->|"absent"| E["throw FILE_DOESNT_EXIST -- a live ref must never name a missing object"]
+ D -->|"present"| F{"body's own ref and namespace match the key?"}
+ F -->|no| G["throw CORRUPTED_DATA"]
+ F -->|yes| H["decode, insert into the cache keyed by ManifestId, 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.
+On a miss, the `GET` is followed by the two identity checks in the diagram, each `CORRUPTED_DATA` on
+failure, and only a fully validated decode enters the cache. A live ref that names a missing body is
+detected on a miss, by the garbage collector before it deletes a manifest, and by `fsck`; a reader
+holding a cached decode for a manifest the collector has since removed sees a snapshot-consistent
+manifest and fails with a typed error when it reads a blob that is gone. Write paths that carry
+entries forward from a committed part (hardlinks, renames, single-file rewrites, relink) adopt the
+source blobs on the strength of the source ref's live edge, which the collector honours; deleting
+objects out of band, behind the collector's back, is outside that contract and is what `fsck`
+reports. Setting either cache's byte budget to `0` disables retention while leaving the
+`GET`-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
diff --git a/docs/en/antalya/cas/architecture/replication.md b/docs/en/antalya/cas/architecture/replication.md
index 371ea74b39f7..a9f3221badf7 100644
--- a/docs/en/antalya/cas/architecture/replication.md
+++ b/docs/en/antalya/cas/architecture/replication.md
@@ -31,11 +31,11 @@ sequenceDiagram
participant Snd as Sender
participant S3 as Shared pool
- R->>Snd: GET part, cas_pool_uuid = R's pool uuid, client_protocol_version = 11
+ R->>Snd: GET part, cas_pool_uuid = every pool of R's policy, 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
+ Snd-->>R: cookies cas_relink = part_manifest_v2, cas_source_token = ..., cas_pool_uuid = the matched pool -- body = manifest bytes
Note over Snd: sender is fire-and-forget -- it releases the part here
rect rgba(120,160,255,0.12)
@@ -63,7 +63,7 @@ sequenceDiagram
| # | 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 |
+| 1 | Pool identity | The receiver advertises `cas_pool_uuid` — the pool uuids of every content-addressed disk of its storage policy that is not read-only, as one list — and the sender offers relink only if its own disk's pool uuid is **in** it, naming that uuid in a `cas_pool_uuid` response cookie. 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 |
@@ -77,6 +77,40 @@ cannot be entered twice for one fetch. Byte-fetched files content-address and de
anyway, so falling back never loses the dedup property, only the zero-byte-move property for that
one fetch.
+## Where a relinked part lands {#relink-placement}
+
+The offer decides the disk. Once the sender has named the pool, the receiver places the part on the
+first disk of its storage policy that belongs to that pool, and reserves space there directly — ahead
+of everything the policy would otherwise consult: volume order, JBOD balancing,
+`max_data_part_size_bytes`, and `TTL ... TO DISK|VOLUME` move rules. A part that is already in the
+pool never travels as bytes merely because the policy would have put it somewhere else.
+
+A TTL rule is not ignored, it is deferred: the background mover sees a part that is not in its TTL
+destination and moves it there afterwards. The bytes then travel once, as a read from the pool on the
+receiver, and the sender is never loaded.
+
+Two things do not bend to the offer. A disk the caller supplied (zero-copy `MOVE` re-fetching a shared
+part onto the move's destination) is never overridden — a content-addressed disk cannot reach that path
+at all, since it does not support zero-copy replication. And a read-only disk is never a candidate: its
+pool is advertised only if some other disk of that pool in the policy is writable, and when none is, the
+sender streams bytes and the ordinary placement applies.
+
+A pool disk that is not live — its mount lease lost, its identity lost, or the storage shut down — is
+still the target. The relink's own write gate refuses it and the fetch fails; a replication-queue fetch
+is retried by the queue, while a manual `FETCH PART` or `FETCH PARTITION` reports the error to the user.
+The part is never quietly placed on another disk instead. This is the behaviour a single-disk
+content-addressed policy always had, and a mixed policy now shares it.
+
+The byte-fetch fallback after a relink that failed for a mechanism reason (a corrupted manifest, a
+body-absent precommit, a ref conflict) re-requests the bytes on the same pool disk, where they
+content-address and deduplicate against the pool — the placement outlives the relink. A manifest of a
+newer format generation is not degraded to bytes today (a tracked gap, `[relink-fallback-unknown-format-version]`
+in the backlog).
+
+During a rolling upgrade a sender that predates the pool-set advertise compares the whole `cas_pool_uuid`
+value with its own pool id, so a receiver whose policy holds several pools gets bytes from such a sender
+until it is upgraded; a receiver with one pool is unaffected, its advertise is byte-for-byte the old one.
+
## What actually seals "commit before release" {#relink-seal}
The receiver's `+1` — its precommit binding — is durable **before** the sender is asked anything,
@@ -109,7 +143,8 @@ content, and that root can confirm its exact refs.
`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
+*committed* source. `republishRef` resolves the source ref freshly and reads its manifest through
+the manifest cache, 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
diff --git a/docs/en/antalya/cas/architecture/storage-layout.md b/docs/en/antalya/cas/architecture/storage-layout.md
index e4d20725836b..b136552acf37 100644
--- a/docs/en/antalya/cas/architecture/storage-layout.md
+++ b/docs/en/antalya/cas/architecture/storage-layout.md
@@ -45,7 +45,7 @@ namespace's shape and never interprets its contents.
| `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 |
+| `gc/server-roots//mount` | mount lease (incl. `min_active_build_sequence` watermark) | `cas_mount_lease` | mount |
| `roots/` | loose mountpoint object, verbatim | — (never interpreted) | upper layers |
| `staging//…` | S3-native upload staging scratch | — | writer, own mount only |
diff --git a/docs/en/antalya/cas/bucket-requirements.md b/docs/en/antalya/cas/bucket-requirements.md
index 67a3795ab682..700d0a94d371 100644
--- a/docs/en/antalya/cas/bucket-requirements.md
+++ b/docs/en/antalya/cas/bucket-requirements.md
@@ -32,11 +32,13 @@ Bucket **versioning is not required** — in fact it must be **disabled** on the
dialect (see below), because a token-exact delete on a versioned bucket archives a noncurrent
generation instead of reclaiming storage, silently stopping GC reclamation.
-On the generation-token dialect that requirement is checked, and checked strictly: a writable mount
-proceeds only when the probe *confirms* versioning is disabled. A bucket reported as versioned and a
-probe that could not answer — the credential may not read the bucket's versioning configuration, or
-the backend cannot report it — both refuse the mount. `CAS` does not assume the safe answer, because
-the failure it would be assuming away is `GC` deleting objects it believes it reclaimed.
+On the generation-token dialect that requirement is checked at mount. A bucket reported as versioned
+refuses the mount. A probe that could not answer — the credential may not read the bucket's versioning
+configuration (`storage.buckets.get` on GCS), or the backend cannot report it — does not: the mount
+proceeds and logs a warning naming what it could not verify, because an unreadable configuration is
+not evidence of a versioned bucket, and refusing on it would turn a missing IAM grant into an outage.
+In that case confirming that versioning is disabled is your responsibility, exactly as soft delete is
+below; grant the permission if you want the mount to verify it for you.
Because that check is part of the mount battery, `skip_access_check = true` is refused on a writable
generation-token disk. Mount the disk read-only if you need to start before the access check can
@@ -54,6 +56,68 @@ Soft delete does not leave the deleted generation live, so it does not break exa
the way versioning does. What it does is delay physical reclamation until the retention period
expires: `GC` reports space as reclaimed while the bill still reflects it.
+## Request rate, and the limit that is not the one you expect {#request-rate}
+
+Google Cloud Storage publishes two kinds of ceiling, and the one that constrains `CAS` is the
+smaller and less-known of them.
+
+A bucket starts at roughly **1000 object writes per second** — uploads, updates and deletes — and
+roughly **5000 object reads per second**, counting listings and metadata reads as reads. Those
+ceilings are not fixed: Cloud Storage raises them by splitting the index range behind the bucket,
+which it says takes "on the order of minutes" to detect and act on. Buckets with a hierarchical
+namespace start up to eight times higher.
+
+Separately, Cloud Storage applies **a much smaller limit to repeated writes to the same object
+name**. Google documents that this limit exists but does not publish its value. Measured against a
+live bucket from this codebase, it begins to bite at approximately one mutation per second on a
+single key, and it does not participate in the auto-scaling above — splitting an index range cannot
+help a single name.
+
+That second limit is the one `CAS` meets first, because two of its objects are single fixed names
+written on a hot path:
+
+| Object | One per | Written on |
+|---|---|---|
+| `cas/ns/state//_ckpt` | table | every durable ref-log transaction, plus namespace birth, epoch seal and snapshot |
+| `cas/ref_catalog` | pool | twice per `CREATE TABLE` and twice per `DROP TABLE` |
+
+Blob bodies and their metadata sidecars are named by content hash and are therefore spread the way
+Google's own guidance asks for: it recommends "completely random object names" for the best load
+distribution, and a hashed prefix where names would otherwise be sequential. Ref-log transactions are
+sequential within a namespace but are written under a per-namespace prefix, so they scale with the
+number of tables rather than sharing one index range.
+
+### What this means for a deployment {#rate-consequences}
+
+- **A single table commits at about one transaction per second** on Google Cloud Storage. Inserts,
+ merges and mutations on that table queue behind the checkpoint write; they do not fail, but the
+ lane's throughput is capped and each flush's tail takes longer than it would on a store without
+ the per-name limit.
+- **A pool performs about one table lifecycle transition per second.** Concurrent `CREATE TABLE` or
+ `DROP TABLE` beyond that rate contends on the pool-wide catalog. Test suites that create hundreds
+ of tables in parallel are the case that provokes this; ordinary production DDL is not.
+- **Ramping up gradually is Google's documented expectation.** Its guidance is to increase the
+ request rate "no faster than doubling the rate over a period of 20 minutes", and to pause or
+ reduce the rate when latency or error rates rise. A pool that goes from idle to full write load in
+ one step will see throttling before the bucket has redistributed the load.
+
+### Throttling is a retryable condition, not a failure {#rate-errors}
+
+Cloud Storage signals a rate it will not serve with HTTP `429`, `408`, or a `5xx` status, and its
+retry guidance names all three, together with socket timeouts and TCP disconnects, as retryable with
+exponential backoff and jitter. Every mutable-object write `CAS` issues carries a generation
+precondition, which places it in Google's *conditionally idempotent* class — a retry either applies
+exactly once or fails the precondition, never applies twice. Retrying them is therefore safe by
+Google's own rule, not merely by ours.
+
+### Reads over a wide-area link want a cache disk {#rate-reads}
+
+The read ceiling is high enough that `CAS` does not approach it, but latency is a separate matter.
+A cacheless `CAS` disk pays a round trip per column file per part: measured against a bucket in
+another region, a `SELECT` issued about 725 ranged reads and took 3.6 seconds at the median and 15.7
+seconds at the ninety-ninth percentile. Put a `cache` disk in front of the `CAS` disk for any
+deployment where the bucket is not local to the server.
+
## Platform support {#platform-support}
The deterministic request-construction coverage is green, but the
diff --git a/docs/en/antalya/cas/configuration.md b/docs/en/antalya/cas/configuration.md
index d7ee12130993..bf0fe17f7dd3 100644
--- a/docs/en/antalya/cas/configuration.md
+++ b/docs/en/antalya/cas/configuration.md
@@ -15,7 +15,9 @@ A `CAS` disk is an `object_storage` disk with `metadata_type` set to `cas` and a
`cas_server_root_id`. The recommended shape layers a `type=cache` disk in front of it — the local
filesystem cache absorbs repeated reads of the same blob, while the `CAS` disk underneath stays the
single source of truth the pool's other members and GC also read from. The storage policy references
-the **cached** disk, not the raw `CAS` disk directly:
+the **cached** disk, not the raw `CAS` disk directly. `http_keep_alive_timeout` and
+`http_keep_alive_max_requests` are set here for the reason explained under
+[recommended keep-alive settings](#recommended-keep-alive-settings):
```xml
@@ -29,6 +31,8 @@ the **cached** disk, not the raw `CAS` disk directly:
https://bucket.s3.amazonaws.com/cas/......
+ 30
+ 10000cache
@@ -92,17 +96,80 @@ entirely before release. Treat this table as a snapshot of the current build, no
| `cas_blob_hash` | `cityhash128` | Pool blob content-hash function (`cityhash128` \| `xxh3-128` \| `sha256`). Recorded in the pool at creation; a mismatching config is refused at mount |
| `cas_blob_hash_allow_new` | `false` | Explicit opt-in to admit a new hash algorithm into an existing pool. One-way: once admitted, the pool carries both algorithms permanently |
| `skip_access_check` | `false` | Skip the boot-time capability probe (start now, fix later). Only the preflight probe is skipped — the conditional-write correctness check still runs on every writable mount. **Not available on a writable generation-token (GCS) disk**, which refuses to mount with it: there, the probe battery is the only proof that a token-exact delete carries its generation precondition. Mount such a disk read-only if you need to defer the check |
+| `cas_mount_lease_ttl_ms` | `30000` | Milliseconds for which a mount lease remains valid after a successful claim or renewal (≥ 1). Lower values shorten stale-mount recovery but reduce tolerance for object-storage and scheduling delays |
+| `cas_mount_renew_period_ms` | `10000` | Milliseconds between background mount-lease renewals (≥ 1). It must leave enough time for two attempt envelopes (a renewal write and its settlement read) and the lease safety margin before the TTL expires: `period + 2 × envelope + margin < TTL` |
| `cas_gc_snapshot_generations_to_keep` | `3` | GC snapshot generations retained |
| `cas_gc_shards` | `1` | Blob-hash-prefix reducer shards (≥ 1). Recorded in the pool at creation; a mismatching config is refused at mount |
| `gcs_max_conditional_put_bytes` | 1 GiB | Largest conditional non-blob `PUT` on a generation-token store, including create-if-absent metadata/control artifacts and conditional replacements. Blob publication is unconditional, uses ordinary multipart, and is not subject to this cap |
| `cas_part_folder_cache_bytes` | 64 MiB | Part-folder view cache byte budget (`0` disables retention) |
| `cas_part_folder_cache_max_entries` | `10000` | Part-folder view cache entry cap |
| `cas_part_folder_cache_max_entry_bytes` | 16 MiB | Oversized part-folder views bypass retention above this size |
-| `cas_part_folder_validate` | `always` | Cache body re-proof policy (`always` \| `never` \| `age `). **Leave at `always`**: the other modes trade the fail-closed body-existence check for an optimization — this is a trust decision about unverified data, not a performance knob |
| `cas_manifest_decode_cache_bytes` | 128 MiB | Manifest decode cache byte budget (`0` disables) |
| `cas_gc_meta_pool_size` | `16` | Bounded pool size for GC per-hash freshness-meta writes |
+| `cas_gc_read_concurrency` | `16` | Bounded pool size for the GC fold's read-ahead of checkpoints, ref logs, manifests and zero-candidate HEADs; `1` disables |
+| `cas_attempt_timeout_ms` | `5000` | Budget for one HTTP attempt of a writable Native mount's control-plane requests (read, head, list, remove, conditional write), at least 1. Together with the connect cap it forms the attempt envelope (`cas_attempt_timeout_ms + 2 × cap`; the cap is `cas_attempt_timeout_ms` itself when the disk's `connect_timeout_ms` is `0`, else `min(connect_timeout_ms, cas_attempt_timeout_ms)`) that the lease arithmetic reserves: one TCP connect and one TLS handshake under the cap each, send/receive bounded per socket operation by `cas_attempt_timeout_ms`. With background renewal the cadence check requires `cas_mount_renew_period_ms + 2 × envelope + cas_lease_safety_margin_ms < cas_mount_lease_ttl_ms`, which puts an effective ceiling on the frozen connect cap: under the defaults (TTL 30000, period 10000, margin 2000) the envelope must stay under 9000, so a disk `connect_timeout_ms` of 2000 ms or more refuses to open writable — lower the connect timeout or raise the TTL if you hit this |
+| `cas_lease_safety_margin_ms` | `2000` | Startup-only margin validated against the mount lease TTL: the attempt envelope + `cas_lease_safety_margin_ms` must be strictly less than the mount lease TTL, and `cas_mount_renew_period_ms` + 2 × envelope + `cas_lease_safety_margin_ms` too, or the disk refuses to open writable |
+| `cas_unsafe_remount_no_delay` | `0` | Reclaim a mount slot that carries this server's own uuid at once after a hard restart, without observing the slot's token for the lease TTL. Unsafe whenever two processes can hold the same `server_uuid` (a copied uuid file, a stalled predecessor). After such a reclaim the predecessor can still start conditional writes until its own cutoff (`confirmed deadline − cas_lease_safety_margin_ms − 2 × envelope`) or until its next renewal meets the token guard, and a request it already sent may still materialize later. That is not a data hazard: ref-log keys carry `(writer_epoch, sequence)` and creates are conditional, so two writers can never commit different bodies to one key, and recovery's epoch seal settles any straggler (recovery fails closed after 64 successive seal-create attempts displaced by newly materializing old-epoch transactions). The exposure is availability, not data. Intended for test stands and deployments that guarantee one process per uuid |
| `cas_staging_backend` | `local` | Blob staging backend (`local` \| `s3`); `s3` is opt-in and requires native same-store copy on writable mount |
+All servers sharing a pool must run the same `cas_mount_lease_ttl_ms` and `cas_mount_renew_period_ms`.
+Startup reclaim and GC's fence-out both judge liveness by the mount slot's write token holding stable
+on the observer's own `CLOCK_BOOTTIME`, using the observer's own threshold — nothing about a writer's
+timing travels on the wire. Startup observes `cas_mount_lease_ttl_ms + floor(cas_mount_lease_ttl_ms /
+20) + max(1, floor(cas_mount_renew_period_ms / 2))`; GC observes `cas_mount_lease_ttl_ms +
+floor(cas_mount_lease_ttl_ms / 20) + cas_mount_renew_period_ms`. A pool member or GC leader
+configured with a shorter threshold than its peers can therefore fence out a healthy peer whose
+token-update gap exceeds that shorter threshold — a peer renewing frequently stays live, one that
+missed a renewal does not. Change these values only with every member of the pool stopped: a
+graceful restart removes only that member's own startup observation and does not make mixed
+thresholds safe.
+
+A shorter TTL reduces the tolerance for object-storage delays; a shorter renewal period increases it
+(renewal starts earlier) at the cost of more background traffic. With the defaults,
+`cas_mount_lease_ttl_ms − cas_lease_safety_margin_ms − cas_mount_renew_period_ms − 2 × envelope =
+4000` ms is the scheduling-lateness budget before the first renewal attempt of a period can begin,
+where `envelope = cas_attempt_timeout_ms + 2 × cap` (7000 ms with defaults) and `cap` is
+`cas_attempt_timeout_ms` when the disk's `connect_timeout_ms` is `0`, else
+`min(connect_timeout_ms, cas_attempt_timeout_ms)` (1000 ms with defaults); the renewal then keeps
+retrying until `confirmed deadline − cas_lease_safety_margin_ms`.
+
+The `expires_at_ms` stamped into the mount object is a writer-stamped diagnostic used by
+`system.cas_mounts` and by the non-authoritative decommission epoch-recovery precheck; it never
+authorizes a reclaim or a GC fence-out. Local fencing is derived instead from the confirmed
+request's pre-I/O `CLOCK_BOOTTIME` anchor plus the TTL.
+
+## Recommended keep-alive settings {#recommended-keep-alive-settings}
+
+On a `CAS` disk, set `http_keep_alive_timeout` to `30` and `http_keep_alive_max_requests` to `10000`,
+alongside the disk's other settings:
+
+```xml
+
+
+
+
+ object_storage
+ s3
+ cas
+ {replica}
+ https://example-bucket.s3.amazonaws.com/cas/
+ ...
+ ...
+ 30
+ 10000
+
+
+
+
+```
+
+The generic S3 default, `http_keep_alive_max_requests = 100`, is the whole lifetime of a
+connection under `CAS`'s control-plane request rate rather than a headroom margin: every ~100
+requests, a connection is torn down and recreated, and its local port then cycles through
+`TIME_WAIT`. Under sustained load this churn exhausts the ephemeral port range
+(`EADDRNOTAVAIL`) and starves the mount-lease renewal request. Raising the two settings above
+removes that churn, with no measured cost.
+
## Advanced GC pacing settings {#advanced-gc-pacing-settings}
These settings bound individual phases of a `GC` round. The first two accept any `UInt64` value;
@@ -112,6 +179,7 @@ for the remaining caps, `0` means unbounded.
|---|---|---|---|
| `cas_manifest_sweep_list_budget_keys` | `1000` | `UInt64` | Orphan-manifest sweep `LIST` budget per round |
| `cas_manifest_sweep_delete_budget_keys` | `100` | `UInt64` | Orphan-manifest sweep `DELETE` budget per round |
+| `cas_gc_bulk_delete_chunk_keys` | `1000` | `1`–`1000` | Keys per batch delete request in GC's write-once families (owner-removed manifest bodies, covered ref logs and snapshots) |
| `cas_gc_round_graduation_budget` | `5000` | `0` = unbounded | Blob-graduation (`condemned` → `delete_pending`) cohort cap per round |
| `cas_gc_round_redelete_budget` | `5000` | `0` = unbounded | Exact-token re-delete cohort cap for prior `delete_pending` rows per round |
| `cas_gc_round_sweep_namespace_budget` | `20` | `0` = unbounded | Distinct namespaces per orphan-manifest sweep page whose protection view may be built |
diff --git a/docs/en/antalya/cas/index.md b/docs/en/antalya/cas/index.md
index 2bc71046494b..cb1d563eebf0 100644
--- a/docs/en/antalya/cas/index.md
+++ b/docs/en/antalya/cas/index.md
@@ -64,6 +64,14 @@ Two consequences for planning:
Each prefix is a fully independent pool (its own refs, leases, and `GC`), so rounds stay short
regardless of the total fleet size.
+:::tip
+For replicated tables on `CAS`, enable
+[`execute_merges_on_single_replica_time_threshold`](/operations/settings/merge-tree-settings#execute_merges_on_single_replica_time_threshold).
+This lets one replica perform each merge while the others wait for and fetch the resulting part,
+avoiding redundant merge work across replicas. Set the threshold higher than the usual merge
+duration for your workload.
+:::
+
## Status {#status}
`CAS` is **experimental**. It ships in Altinity Antalya builds. Experimental means the on-disk
diff --git a/docs/en/antalya/cas/operations/debugging.md b/docs/en/antalya/cas/operations/debugging.md
index cbc8b0c693f0..cb2e48f4f3b3 100644
--- a/docs/en/antalya/cas/operations/debugging.md
+++ b/docs/en/antalya/cas/operations/debugging.md
@@ -112,8 +112,6 @@ SELECT event_time_microseconds, event_type, outcome, reason,
detail['write_attempt_id'] AS write_attempt_id,
detail['attempts_sent'] AS attempts_sent,
detail['classification'] AS classification,
- detail['deadline_source'] AS deadline_source,
- detail['stop_cause'] AS stop_cause,
detail['attempt_no'] AS remount_attempt,
detail['step'] AS remount_step,
detail['error'] AS error
@@ -123,15 +121,33 @@ WHERE disk_name = 'cas'
ORDER BY event_time_microseconds;
```
-Interpret the sequence as follows:
-
-- `retrying -> recovered` with the same `write_attempt_id` means an in-budget blip recovered in the
- existing epoch; `classification = 'committed_by_get'` means exact `GET` proved a landed request,
- while `committed_after_retry` means a later identical physical `PUT` completed.
-- A `failed` renewal carries the decisive `unresolved_reason`, `deadline_source`, `stop_cause`, and
- `classification`. `external_lease_deadline`, `cancelled`, `conflict`,
- `fence_or_lifecycle_lost`, and `attempts_exhausted` are different operator diagnoses; do not
- collapse them into a generic timeout.
+A `watermark_renew` row now carries only two detail keys beyond the identifying ones:
+`attempts_sent` (the number of physical HTTP attempts the whole logical renewal made) and
+`classification`. There is no per-attempt `retrying` row any more — a renewal that recovers after
+one or more physical attempts produces exactly one `recovered` row when it settles, not a `retrying`
+row followed by a `recovered` one — and the older `unresolved_reason`, `deadline_source`, and
+`stop_cause` keys are gone; everything they used to distinguish is now named directly by
+`classification`. Interpret the sequence as follows:
+
+- `outcome = 'recovered'` means an in-budget renewal landed, in the same epoch. `classification`
+ says how: `committed_by_read` means an exact `GET` proved a landed request; `committed_after_retry`
+ means a later identical physical `PUT` completed and the response itself proved it.
+- `outcome = 'failed'` carries the decisive `classification`: `external_lease_deadline` (the
+ confirmed lease's own safety margin, not the request policy, ran out first — check object-store
+ latency or `BOOTTIME` advancement before anything else), `request_deadline` (the ninety-second
+ request policy exhausted first), `unresolved` (every attempt was ambiguous and never settled by
+ the time the operation gave up), `conflict` (an exact resolve read found another body — a
+ same-pair twin, a GC-fenced body, a successor epoch, or a foreign holder), `cancelled` (a
+ renewal in flight was cancelled by shutdown or a remount park request; expected during graceful
+ shutdown), `fence_or_lifecycle_lost` (another local fence loss or a terminal lifecycle transition
+ closed admission while the operation was active), `deterministic_failure` (the store's own
+ answer proved the write never applied), and `vanished` (an exact resolve read proved the mount
+ slot absent — the pool directory was removed or renamed out of band, or a decommission raced the
+ renewal). `terminal_unclassified` means the renewal terminated through a path that assigned no
+ classification; that is a defect to report together with the surrounding rows, not an operator
+ condition. Do not collapse these into a generic timeout — the action
+ differs by classification, and only `external_lease_deadline` and `request_deadline` are about a
+ deadline at all.
- A following `mount_remount` row names the whole-chain `attempt_no` and final `step`. An `ok` row
restored `Live` under the reported fresh `writer_epoch`; a `failed` row's `step` and optional
`error` identify where that whole-chain attempt stopped.
diff --git a/docs/en/antalya/cas/operations/migration.md b/docs/en/antalya/cas/operations/migration.md
index df51e79144a3..e8258b96d61d 100644
--- a/docs/en/antalya/cas/operations/migration.md
+++ b/docs/en/antalya/cas/operations/migration.md
@@ -19,7 +19,9 @@ disk and its data are untouched until a partition is explicitly moved.
A storage policy can carry both an ordinary disk and a `CAS` disk as separate volumes. `ALTER TABLE
... MOVE PARTITION ... TO DISK` then moves data between them without an `INSERT`/`DROP` cycle. As on
the [configuration](/antalya/cas/configuration#disk-config) page, the recommended shape layers a
-`type=cache` disk over the `CAS` disk, and the policy's volume references the **cached** disk name:
+`type=cache` disk over the `CAS` disk, and the policy's volume references the **cached** disk name.
+`http_keep_alive_timeout` and `http_keep_alive_max_requests` are set here for the reason explained
+under [recommended keep-alive settings](/antalya/cas/configuration#recommended-keep-alive-settings):
```xml
@@ -37,6 +39,8 @@ the [configuration](/antalya/cas/configuration#disk-config) page, the recommende
https://bucket.s3.amazonaws.com/cas/......
+ 30
+ 10000cache
diff --git a/docs/en/antalya/cas/operations/monitoring.md b/docs/en/antalya/cas/operations/monitoring.md
index 58600e8e058c..8bde03328575 100644
--- a/docs/en/antalya/cas/operations/monitoring.md
+++ b/docs/en/antalya/cas/operations/monitoring.md
@@ -82,9 +82,12 @@ SETTINGS system_events_show_zero_values = 1;
```
`system.cas_log` records only nontrivial logical renewals. A `watermark_renew` row has outcome
-`retrying`, `recovered`, or `failed`, with detail keys `server_root_id`, `writer_epoch`, `seq`, a
-shortened `write_attempt_id`, `attempts_sent`, `elapsed_ms`, `remaining_confirmed_budget_ms`,
-`unresolved_reason`, `deadline_source`, `stop_cause`, and `classification`. Ordinary first-attempt
+`recovered` or `failed` — there is no per-attempt `retrying` row; the terminal event is the whole
+story — with detail keys `server_root_id`, `writer_epoch`, `seq`, a shortened `write_attempt_id`,
+`attempts_sent`, `elapsed_ms`, `remaining_confirmed_budget_ms`, and `classification`. The older
+`unresolved_reason`, `deadline_source`, and `stop_cause` keys no longer exist; `classification`
+carries what they used to say between them (see [debugging](/antalya/cas/operations/debugging#trace-renewal-remount)
+for the full value list). Ordinary first-attempt
success produces no row. Every `mount_remount` attempt produces one final row with outcome `ok` or
`failed` and details `attempt_no`, `step`, `server_root_id`, optional `writer_epoch`, and optional
`error`.
@@ -121,6 +124,12 @@ changed shard needing a fold and no graduation was due — a cheap round, not a
`Finish` row is worth a steady watch: it is fold clamps surfaced and survived, so a non-zero value
that persists across rounds is more interesting than an isolated one.
+A dashboard alert that filters on `outcome = 'Error'` alone misses `Aborted` and `Stopped` rows too
+— see the [`outcome` column](/operations/system-tables/cas_gc_log#columns) for what each one means.
+A round that is recurring `Aborted` rather than `Error` still deserves attention: it keeps retrying,
+but the underlying transient condition (backend unavailability, a lost lease, a competing leader)
+has not gone away.
+
Which phase dominates round duration or the `LIST` budget — reproduced from the
[per-phase rows](/operations/system-tables/cas_gc_log#per-phase-rows) reference:
diff --git a/docs/en/antalya/cas/operations/troubleshooting.md b/docs/en/antalya/cas/operations/troubleshooting.md
index be541add39f8..1113ec1855dd 100644
--- a/docs/en/antalya/cas/operations/troubleshooting.md
+++ b/docs/en/antalya/cas/operations/troubleshooting.md
@@ -16,16 +16,16 @@ tools.
| Symptom | Diagnosis | Action |
|---|---|---|
-| A server keeps losing its mount lease and self-remounting | Check `system.cas_mounts` for the server's own `state`/`expires_at`, then correlate `watermark_renew` and `mount_remount` in `system.cas_log`; losing the lease trips a local fence and latches a remount generation | Read `classification`, `deadline_source`, and `stop_cause` before changing anything. Look for object-store latency consuming the confirmed lease or BOOTTIME advancement; see [the decision flow](#mount-renewal-remount-flow) and [the mount lease](/antalya/cas/architecture/mounts-and-leases#mount-lease) |
-| Writes slow down or stall under load, with no exception reaching the client | S3 `SlowDown`/`ServiceUnavailable`/`RequestTimeout`/`InternalError` (5xx) responses are not on `CasRequestController`'s definite-failure whitelist (only malformed-request, entity-too-large, and access-denied are), so they classify as `Unresolved` and are retried automatically. Confirm with `sum(ProfileEvents['CASConditionalWriteUnresolved'])` rising alongside `sum(ProfileEvents['CASConditionalWriteAttempts'])` over `system.query_log` for the affected window (or `ProfileEvent_CASConditionalWriteUnresolved` in `system.metric_log` for a cumulative view across queries), and check `system.blob_storage_log` for `disk_name = ''` rows with a nonzero `error_code` around the same window | Nothing to configure per-request: the controller retries the same `(key, bytes)` with capped-exponential backoff (200ms initial, capped at 5s) for up to 16 attempts inside a 90-second operation deadline, and the mount-lease renewer keeps extending the fence across the disruption — this is the "blips, throttling, partial outages" case the write path is built to survive. Confirm the mount lease itself is still renewing (`system.cas_mounts.expires_at` moving forward, `last_success_age_seconds` not climbing) — if it is, this is expected and self-resolving. If `SlowDown` responses are sustained rather than transient, check the bucket's request-rate limits against the pool's actual PUT/GET rate (see [bucket requirements](/antalya/cas/bucket-requirements)) and consider lowering `cas_blob_upload_pool_size` to reduce concurrent upload traffic; a write only surfaces a client-visible `NETWORK_ERROR` if the 90-second deadline is exhausted before the store recovers, and that error is retried by the ordinary merge/insert backoff, not silently dropped |
+| A server keeps losing its mount lease and self-remounting | Check `system.cas_mounts` for the server's own `state`/`expires_at`, then correlate `watermark_renew` and `mount_remount` in `system.cas_log`; losing the lease trips a local fence and latches a remount generation | Read the failed renewal's `classification` before changing anything — it alone now says why (see [the decision flow](#mount-renewal-remount-flow)). Look for object-store latency consuming the confirmed lease or BOOTTIME advancement; see [the mount lease](/antalya/cas/architecture/mounts-and-leases#mount-lease) |
+| Writes slow down or stall under load, with no exception reaching the client | S3 `SlowDown`/`ServiceUnavailable`/`RequestTimeout`/`InternalError` (5xx) responses are not on the request engine's `isDefinitelyRefusedWrite` definite-failure list (only malformed-request, entity-too-large, and access-denied that no credential refresh can fix are), so they classify as ambiguous and are retried automatically. Confirm with `sum(ProfileEvents['CASConditionalWriteUnresolved'])` rising alongside `sum(ProfileEvents['CASConditionalWriteAttempts'])` over `system.query_log` for the affected window (or `ProfileEvent_CASConditionalWriteUnresolved` in `system.metric_log` for a cumulative view across queries), and check `system.blob_storage_log` for `disk_name = ''` rows with a nonzero `error_code` around the same window | Nothing to configure per-request: the request engine retries the same `(key, bytes)` with capped-exponential backoff (200ms initial, capped at 5s, full jitter) until the 90-second operation deadline — there is no separate attempts ceiling, only the deadline — and the mount-lease renewer keeps extending the fence across the disruption — this is the "blips, throttling, partial outages" case the write path is built to survive. Confirm the mount lease itself is still renewing (`system.cas_mounts.expires_at` moving forward, `last_success_age_seconds` not climbing) — if it is, this is expected and self-resolving. If `SlowDown` responses are sustained rather than transient, check the bucket's request-rate limits against the pool's actual PUT/GET rate (see [bucket requirements](/antalya/cas/bucket-requirements)) and consider lowering `cas_blob_upload_pool_size` to reduce concurrent upload traffic; a write only surfaces a client-visible `NETWORK_ERROR` if the 90-second deadline is exhausted before the store recovers, and that error is retried by the ordinary merge/insert backoff, not silently dropped |
| `GC` never seems to reclaim space after tables are dropped | `SELECT * FROM system.cas_gc_log WHERE event_type='Finish' ORDER BY event_time DESC LIMIT 5` — check `outcome`; also `SELECT is_leader FROM system.cas_mounts` on this node | If `outcome != 'Success'`/`'Deferred'`, see [reading GC health](/antalya/cas/operations/monitoring#gc-health); if this node is not the leader (`is_leader = 0`), it never reclaims for this disk — check the peer holding leadership. Reclamation also needs at least two full rounds past condemnation by design (the grace period is rounds, not acks) — a single manual `SYSTEM CAS GC RUN` will not finish it |
| A dangling-access exception or `CORRUPTED_DATA` on read | Run `clickhouse-disks cas-fsck --detail` and check `dangling` specifically — it is the one class that means data loss, distinct from `unreachable`/`awaiting-gc`, which are just waiting for graduation | A nonzero `dangling` count is a real incident: collect the `--detail` output (see [what to collect before filing a bug](/antalya/cas/operations/debugging#filing-a-bug)) before taking any destructive action |
| `SYSTEM CAS FSCK` or `clickhouse-disks cas-fsck` times out on a large pool | The scan is bounded by `--timeout` (default 600s / the `SYSTEM` form has no override); a large `roots/` prefix can make the scan slow | Retry with `--partial` to see the counts accumulated so far instead of aborting empty-handed, or `--namespace ` to scope the scan to a subset of namespaces |
| `SYSTEM CAS DROP POOL MEMBER` returns a non-empty `warnings` column | A per-object drain step could not confirm emptiness; the mount slot is left terminated but not fully drained, as a resume anchor | Rerun the same command — it is resumable and skips namespaces already marked removed, reporting them under `namespaces_already_removed` |
| Writes or `ALTER`s on a `CAS` disk fail with a `READONLY`-class error | The disk's metadata storage rejects every mutating entry point; this is deliberate for a disk opened with `true`, used by every offline `clickhouse-disks` tool | Confirm whether the disk was intentionally configured read-only (offline inspection, `cas-fsck`, `cas-gc-dryrun`, `cas-gc-rebuild`, `cas-drop-member` all require it); a production disk serving writes must not carry `true` |
| A table stays unavailable after a transient network error during startup | `AsyncLoader` has no retry/requeue path for a failed table load job: a transient S3 `NETWORK_ERROR` during `CAS` ref-table startup recovery can leave the job permanently `FAILED` | Restart the server, or issue a fresh load for the table; this is a one-shot job design, not a `CAS`-specific bug |
-| A mounted pool directory was removed or renamed out of band | Renewal observes an absent, foreign, successor, or otherwise conflicting mount body and terminates the keeper with a typed fail-closed exception; the runtime closes the local write fence and requests remount rather than adopting the body | Never remove or rename a live pool's storage path. To retire a member permanently use [`SYSTEM CAS DROP POOL MEMBER`](/antalya/cas/operations/migration#decommission) instead of raw filesystem operations; collect the `watermark_renew` classification and subsequent `mount_remount` step |
-| Stale-looking part metadata after an out-of-band change to the pool | The part-folder view cache may be serving a retained (not re-validated) view | Set the disk-level `cas_part_folder_cache_bytes = 0` as a diagnostic kill switch to disable retention, and run `fsck`/integrity checks with `cas_part_folder_validate = always` so every read re-proves the body |
+| A mounted pool directory was removed or renamed out of band | Renewal observes an absent, foreign, successor, or otherwise conflicting mount body and terminates the keeper with a typed fail-closed exception; the runtime closes the local write fence and requests remount rather than adopting the body | Never remove or rename a live pool's storage path. To retire a member permanently use [`SYSTEM CAS DROP POOL MEMBER`](/antalya/cas/operations/migration#decommission) instead of raw filesystem operations; collect the `watermark_renew` classification (`vanished` for an absent body, `conflict` for a foreign, successor, or otherwise conflicting one) and the subsequent `mount_remount` step |
+| Stale-looking part metadata after an out-of-band change to the pool | The part-folder view cache may be serving a retained (not re-validated) view | Set the disk-level `cas_part_folder_cache_bytes = 0` to disable view retention and `cas_manifest_decode_cache_bytes = 0` to make every manifest read fetch the body, then run `fsck`; both are diagnostic kill switches, not steady-state settings |
| A wide merge (many thousands of columns) fails with a port-exhaustion error from the network layer | Each column in a wide part can cost a separate object-store operation in one merge, and a very wide part can issue on the order of the column count in requests, exhausting local ephemeral TCP ports under load | Reduce concurrent merge parallelism on that table, or increase the host's ephemeral port range; this is a general high-fan-out-merge limit, not specific to content addressing |
## Mount renewal and remount decision flow {#mount-renewal-remount-flow}
@@ -33,28 +33,30 @@ tools.
Start with the `watermark_renew` timeline described in
[debugging](/antalya/cas/operations/debugging#trace-renewal-remount), then follow the matching case:
-1. **Recovered blip.** `retrying` is followed by `recovered` for the same shortened
- `write_attempt_id`; `CASMountRenewalRecovered` rises while `CASMountLeaseLost` and all remount
- counters stay flat. No intervention is needed unless the rate is sustained; investigate backend
- throttling/latency before the blips consume the lease budget.
+1. **Recovered blip.** A single `outcome = 'recovered'` row (there is no separate `retrying` row to
+ look for) with `classification` of `committed_by_read` or `committed_after_retry`;
+ `CASMountRenewalRecovered` rises while `CASMountLeaseLost` and all remount counters stay flat. No
+ intervention is needed unless the rate is sustained; investigate backend throttling/latency before
+ the blips consume the lease budget.
2. **External lease-safety exhaustion.** The failed row has
- `classification = 'external_lease_deadline'` and
- `deadline_source = 'external_lease_safety'`; `CASMountRenewalDeadlineExceeded` and
+ `classification = 'external_lease_deadline'`; `CASMountRenewalDeadlineExceeded` and
`CASMountLeaseLost` rise. The runtime correctly refused to manufacture authority beyond the last
confirmed lease. Check object-store latency and BOOTTIME/suspend history, then follow the ensuing
- remount.
-3. **Cancellation.** `stop_cause = 'cancelled'` after a sent request is terminal and suppresses a
- clean farewell because the request may still land. Cancellation before any request is
- `NotAttempted`, remains `Active`, and emits no failed aggregate row; during graceful shutdown that
- is the expected clean-release path.
+ remount. `classification = 'request_deadline'` is the sibling case: the ninety-second request
+ policy exhausted first rather than the lease's own safety margin.
+3. **Cancellation.** `classification = 'cancelled'` after a sent request is terminal and suppresses a
+ clean farewell because the request may still land. Cancellation before any request remains
+ `Active` and emits no failed aggregate row; during graceful shutdown that is the expected
+ clean-release path.
4. **Confirmed conflict.** `classification = 'conflict'` means exact resolution found another body;
inspect `server_root_id`, `writer_epoch`, `seq`, and `write_attempt_id`. Same-pair twins, GC-fenced
bodies, successor epochs, and foreign holders all remain fail closed. Do not delete or rewrite the
mount key by hand.
-5. **Fence or lifecycle loss.** `stop_cause = 'fence_or_lifecycle_lost'` means another local loss,
+5. **Fence or lifecycle loss.** `classification = 'fence_or_lifecycle_lost'` means another local loss,
remount park request, or terminal lifecycle closed admission while the operation was active. A
parked result reuses the already-requested recovery generation and must not double-count
- `CASMountLeaseLost`.
+ `CASMountLeaseLost`. `classification = 'unresolved'` is a related but distinct case: every attempt
+ stayed ambiguous and the operation gave up without ever settling one way or the other.
6. **Whole-chain remount failure.** Read the following `mount_remount` row. Its `attempt_no`, `step`,
and optional `error` identify the failed owner/catalog/epoch/claim/install/quiescence/fence step.
The current protocol retries the whole chain with bounded backoff; it does not preserve per-step
diff --git a/docs/en/antalya/cas/quick-start.md b/docs/en/antalya/cas/quick-start.md
index a54d9345fbb0..0491f6972e8b 100644
--- a/docs/en/antalya/cas/quick-start.md
+++ b/docs/en/antalya/cas/quick-start.md
@@ -56,7 +56,9 @@ literal string, as above, is enough; on a replicated cluster where every replica
disk's `endpoint` already uses, giving each replica a distinct subtree from one template.
**S3 endpoint variant.** Swap `object_storage_type` to `s3` and add the usual object-storage
-connection keys; nothing else in this config changes:
+connection keys, plus `http_keep_alive_timeout` and `http_keep_alive_max_requests` — see
+[recommended keep-alive settings](/antalya/cas/configuration#recommended-keep-alive-settings) for
+why; nothing else in this config changes:
```xml
@@ -67,6 +69,8 @@ connection keys; nothing else in this config changes:
https://bucket.s3.amazonaws.com/cas/......
+ 30
+ 10000
```
diff --git a/docs/en/operations/storing-data.md b/docs/en/operations/storing-data.md
index 1f2b85d24244..a3503bc1eb2c 100644
--- a/docs/en/operations/storing-data.md
+++ b/docs/en/operations/storing-data.md
@@ -467,7 +467,9 @@ and the [`system.cas_gc_log`](/operations/system-tables/cas_gc_log),
[content-addressed storage documentation](/antalya/cas) for the architecture, operations
runbooks, and a live-validated quick start.
-Configuration:
+Configuration: `http_keep_alive_timeout` and `http_keep_alive_max_requests` are set here for the
+reason explained under
+[recommended keep-alive settings](/antalya/cas/configuration#recommended-keep-alive-settings).
```xml
@@ -476,6 +478,8 @@ Configuration:
cashttps://s3.eu-west-1.amazonaws.com/clickhouse-eu-west-1.clickhouse.com/data/1
+ 30
+ 10000server-{replica}disks/s3_cas/cas_scratch/
@@ -485,7 +489,6 @@ Configuration:
60167108864
- always
```
@@ -542,15 +545,14 @@ disk-level and server-level settings surface.
view cache.
- `cas_part_folder_cache_max_entry_bytes` — `16` MiB by default. Maximum size of a single cached
part-folder view entry.
-- `cas_part_folder_validate` — `always` (default), `never`, or `age `. Controls how often a
- `ForceFresh` read re-proves a cached manifest body via a `HEAD` request: `always` re-proves every
- time (the original, pre-optimization behavior), `never` trusts the cache without re-proving, and
- `age ` re-proves only once the cached entry is older than the given number of seconds.
- `cas_manifest_decode_cache_bytes` — `128` MiB by default. Byte bound for the decoded-manifest cache.
`0` disables decode caching entirely (a diagnostic mode).
- `cas_gc_meta_pool_size` — `16` by default. Bounded thread-pool size for the GC's per-hash freshness-meta
writes (condemn/spare/delete), so a mass `DROP` condemning millions of blobs does not run fully
sequentially.
+- `cas_gc_read_concurrency` — `16` by default. Bounded thread-pool size for the GC fold's read-ahead of
+ checkpoints, ref logs, manifest bodies and zero-candidate `HEAD`s. The fold's decisions stay on the
+ round thread in their original order; only the fetches overlap. `1` disables read-ahead.
- `skip_access_check` — `false` by default. Skips the disk's `CAS` capability probe ("start now,
fix later"). The server-level `skip_access_check` flag skips the generic disk access check;
this disk key governs the `CAS` capability probe.
diff --git a/docs/en/operations/system-tables/cas_gc_log.md b/docs/en/operations/system-tables/cas_gc_log.md
index 5fd04b4fb11d..e92c2a6e3bd6 100644
--- a/docs/en/operations/system-tables/cas_gc_log.md
+++ b/docs/en/operations/system-tables/cas_gc_log.md
@@ -38,20 +38,21 @@ specified (it is enabled by default in the shipped `config.xml`).
- `gc_id` ([String](/sql-reference/data-types/string)) — The GC scheduler instance id (which mounter ran the round).
- `trigger` ([Enum8](/sql-reference/data-types/enum)) — `Scheduled` (background tick) or `Manual` (`SYSTEM` command).
- `round` ([UInt64](/sql-reference/data-types/int-uint)) — The GC round number (`0` on a `Start` row).
-- `outcome` ([Enum8](/sql-reference/data-types/enum)) — `Unknown` (on a `Start` row), `Success` (led, folded, and completed), `NotALeader` (another replica holds the GC lease), `Deferred` (led but took the skip-unchanged fast path — no fold ran, because no changed shard reached the fold threshold and no graduation was due), or `Error` (the round threw).
+- `outcome` ([Enum8](/sql-reference/data-types/enum)) — `Unknown` (on a `Start` row), `Success` (led, folded, and completed), `NotALeader` (another replica holds the GC lease), `Deferred` (led but took the skip-unchanged fast path — no fold ran, because no changed shard reached the fold threshold and no graduation was due), `Aborted` (the round threw a transient error — backend unavailability, a lost lease, a concurrent leader; the next scheduled round retries it), `Stopped` (a transient error observed after the disk began shutting down: the round was cut short so the shutdown need not wait for it, and the next start re-derives its work), or `Error` (the round threw a non-transient error — investigate).
- `candidates_marked` ([UInt64](/sql-reference/data-types/int-uint)) — Objects retired (marked) this round.
- `objects_deleted` ([UInt64](/sql-reference/data-types/int-uint)) — Objects physically deleted this round.
- `objects_absent` ([UInt64](/sql-reference/data-types/int-uint)) — Retire candidates found already absent.
- `objects_replaced` ([UInt64](/sql-reference/data-types/int-uint)) — `412`-saves (a resurrection won the race against the delete).
- `objects_spared` ([UInt64](/sql-reference/data-types/int-uint)) — Candidates spared because their in-degree was greater than zero at recheck.
-- `manifests_deleted` ([UInt64](/sql-reference/data-types/int-uint)) — Owner-removed manifest bodies physically deleted this round, counted separately from blob deletes.
+- `manifests_deleted` ([UInt64](/sql-reference/data-types/int-uint)) — Owner-removed manifest bodies deleted or found already absent this round (a batch delete of write-once keys cannot tell the two apart), counted separately from blob deletes.
- `entries_condemned` ([UInt64](/sql-reference/data-types/int-uint)) — Retired entries newly condemned this round (retired-cursor pipeline stage 1).
- `entries_graduated` ([UInt64](/sql-reference/data-types/int-uint)) — Retired entries newly floor-passed and republished `delete_pending` this round (pipeline stage 2; deleted the next round).
- `entries_redeleted` ([UInt64](/sql-reference/data-types/int-uint)) — Pending exact-token blob deletes executed this round (pipeline stage 3).
- `fence_outs` ([UInt64](/sql-reference/data-types/int-uint)) — Expired mounts fenced out by this round's heartbeat floor.
- `anomalies` ([UInt64](/sql-reference/data-types/int-uint)) — Fold clamps surfaced (and survived) this round. A steady non-zero value warrants a look at the round log details.
- `duration_ms` ([UInt64](/sql-reference/data-types/int-uint)) — The round wall-clock duration (on a `Finish` row).
-- `error` ([String](/sql-reference/data-types/string)) — The exception text when `outcome = 'Error'`.
+- `error` ([String](/sql-reference/data-types/string)) — The exception text when `outcome = 'Aborted'`, `'Stopped'` or `'Error'`. On a `Stopped` row it names the request the shutdown refused, not the shutdown itself.
+- `error_code` ([Int32](/sql-reference/data-types/int-uint)) — The exception code when `outcome = 'Aborted'`, `'Stopped'` or `'Error'`; `0` otherwise. Key monitoring on this column rather than on the `error` text. On an `Aborted`, `Stopped` or `Error` row the counters still report everything the round completed before it threw, and `round != 0` on such a row means the round's closing compare-and-swap committed and the failure hit only post-commit cleanup.
- `ProfileEvents` ([Map(LowCardinality(String), UInt64)](/sql-reference/data-types/map)) — On a `Start`/`Finish` row, the per-round `ProfileEvents` delta (the `CAS*` counters and S3/disk events for this round). On a `Phase` row, **that phase's** delta, so `GROUP BY phase` over `ProfileEvents['S3ListObjects']` attributes the round's `LIST` budget to the phase that spent it.
- `round_id` ([String](/sql-reference/data-types/string)) — The correlator for every row of one round attempt: its `Start`, each of its `Phase` rows, and its `Finish`. Minted per attempt, so unlike `round` it exists even for a round that never committed and for a round that never led. Group by this column to reconstruct one round.
- `phase` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — The GC phase this row describes; empty on `Start`/`Finish`. See [Per-phase rows](#per-phase-rows) for the phase list.
diff --git a/docs/en/operations/system-tables/cas_log.md b/docs/en/operations/system-tables/cas_log.md
index 17616cd9b329..d4742cda279e 100644
--- a/docs/en/operations/system-tables/cas_log.md
+++ b/docs/en/operations/system-tables/cas_log.md
@@ -25,13 +25,13 @@ specified (it is enabled by default in the shipped `config.xml`).
- `event_date` ([Date](/sql-reference/data-types/date)) — Event date.
- `event_time` ([DateTime](/sql-reference/data-types/datetime)) — Event time.
- `event_time_microseconds` ([DateTime64(6)](/sql-reference/data-types/datetime64)) — Event time with microseconds precision.
-- `event_type` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — The CAS decision/event, e.g. `blob_put`, `blob_reuse_adopt`, `root_remove`, `indegree_zero`, `gc_retire_decision`, `gc_recheck_verdict`, `blob_delete`, `dangling_access`, `corrupt_dangle`.
+- `event_type` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — The CAS decision/event, e.g. `blob_put`, `blob_reuse_adopt`, `root_remove`, `indegree_zero`, `gc_retire_decision`, `gc_recheck_verdict`, `blob_delete`, `dangling_access`, `corrupt_dangle`, `watermark_renew`, `mount_remount`.
- `disk_name` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — The content-addressed disk / pool the event belongs to.
- `namespace` ([String](/sql-reference/data-types/string)) — `roots/` (server/table); empty if not applicable.
- `ref_name` ([String](/sql-reference/data-types/string)) — Part name / ref the event concerns; empty if not applicable.
- `object_kind` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — One of `none`, `blob`, `manifest`, `root`, `snapshot`.
- `object_hash` ([String](/sql-reference/data-types/string)) — Content hash (lowercase hex) of the object; empty if not applicable.
-- `token` ([String](/sql-reference/data-types/string)) — Incarnation token (`ETag`) involved; empty if not applicable.
+- `token` ([String](/sql-reference/data-types/string)) — On events about a stored object, the incarnation involved, rendered uniformly as `:` (e.g. `etag:"a1b2c3"` on S3-compatible stores, `generation:1234` on GCS); the part-build lifecycle events reuse the column for the 128-bit build id in hex; empty if not applicable.
- `round` ([UInt64](/sql-reference/data-types/int-uint)) — GC round (`0` if not applicable).
- `generation` ([UInt64](/sql-reference/data-types/int-uint)) — GC snapshot generation (`0` if not applicable).
- `at_version` ([UInt64](/sql-reference/data-types/int-uint)) — Manifest `shard_version` of the driving journal record (`0` if not applicable).
@@ -39,7 +39,7 @@ specified (it is enabled by default in the shipped `config.xml`).
- `reason` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — Human-readable rationale for the decision. Templated across rows, so it is `LowCardinality`.
- `thread_id` ([UInt64](/sql-reference/data-types/int-uint)) — OS thread that emitted the event.
- `query_id` ([String](/sql-reference/data-types/string)) — Query id for correlation with [`system.query_log`](/operations/system-tables/query_log); empty if not applicable.
-- `detail` ([Map(LowCardinality(String), String)](/sql-reference/data-types/map)) — Structured event-specific facts, e.g. `condemn_round`, `superseded_token`, `code`, `site`.
+- `detail` ([Map(LowCardinality(String), String)](/sql-reference/data-types/map)) — Structured event-specific facts, e.g. `condemn_round`, `superseded_token`, `code`, `site`, or — on `watermark_renew` — `attempts_sent` and `classification`; see [debugging](/antalya/cas/operations/debugging#trace-renewal-remount) for the mount-renewal detail keys.
## Example {#example}
diff --git a/programs/disks/CommandCaInspect.cpp b/programs/disks/CommandCaInspect.cpp
index b4a3a24dbb89..32df388f2cdb 100644
--- a/programs/disks/CommandCaInspect.cpp
+++ b/programs/disks/CommandCaInspect.cpp
@@ -48,11 +48,17 @@ class CommandCaInspect final : public ICommand
if (!ca->isReadOnly())
throw Exception(ErrorCodes::BAD_ARGUMENTS, "cas-inspect: open the CA disk read-only");
- const auto got = ca->store()->backend().get(key);
+ /// `store()` hands back a snapshot of the pool pointer; `openRequests()`/`layout()` return
+ /// references into that Pool object. Keeping the shared_ptr alive for the whole operation,
+ /// rather than letting each `store()` call's temporary expire, is what keeps those references
+ /// valid and pins both calls to the SAME pool if a concurrent remount swaps it out from under `ca`.
+ const Cas::PoolPtr pool = ca->store();
+ Cas::CasOperation op = pool->openRequests().admit();
+ const auto got = op.read(key, Cas::Retry::standard());
if (!got)
throw Exception(ErrorCodes::BAD_ARGUMENTS, "cas-inspect: key '{}' does not exist", key);
- const Cas::Layout & layout = ca->store()->layout();
+ const Cas::Layout & layout = pool->layout();
std::optional resolved_life;
std::optional life_id;
if (const auto parsed = layout.parseRefObjectKey(key))
@@ -61,7 +67,7 @@ class CommandCaInspect final : public ICommand
life_id = *parsed_ckpt;
if (life_id)
{
- const Cas::CasRefCatalog::Snapshot cut = Cas::CasRefCatalog::read(ca->store()->backend(), layout);
+ const Cas::CasRefCatalog::Snapshot cut = Cas::CasRefCatalog::read(op, layout);
resolved_life = cut.life_index.resolve(*life_id);
}
diff --git a/src/Common/CurrentMetrics.cpp b/src/Common/CurrentMetrics.cpp
index fe754c0334cb..a36be7839914 100644
--- a/src/Common/CurrentMetrics.cpp
+++ b/src/Common/CurrentMetrics.cpp
@@ -235,6 +235,8 @@
M(CASPartFolderCacheEntries, "Entries retained by the CA part-folder view cache") \
M(CASManifestDecodeCacheBytes, "Bytes retained by the CA manifest decode cache") \
M(CASManifestDecodeCacheEntries, "Entries retained by the CA manifest decode cache") \
+ M(CASHotKeyCacheBytes, "Bytes retained by the CA hot-key lane's cache of last known objects") \
+ M(CASHotKeyCacheEntries, "Entries retained by the CA hot-key lane's cache of last known objects") \
M(CASBlobUploadPoolThreads, "Number of threads in the CA blob upload thread pool.") \
M(CASBlobUploadPoolThreadsActive, "Number of threads in the CA blob upload thread pool running a task.") \
M(CASBlobUploadPoolThreadsScheduled, "Number of queued or active jobs in the CA blob upload thread pool.") \
diff --git a/src/Common/ErrorCodes.cpp b/src/Common/ErrorCodes.cpp
index f9ecd77e997c..cfdc94b31795 100644
--- a/src/Common/ErrorCodes.cpp
+++ b/src/Common/ErrorCodes.cpp
@@ -677,8 +677,18 @@
M(1007, ILLEGAL_STREAM) \
M(1008, TEMPORARY_DATA_NOT_IN_CACHE) \
M(1009, PENDING_MUTATIONS_NOT_ALLOWED) \
+ /* 1010 and 1011 predate the fork's error-code range policy stated below, and are kept as-is \
+ * rather than renumbered: they currently collide with upstream ClickHouse's own 1010 \
+ * (UNIQUE_KEY_DENSE_INDEX_UNREADABLE) and 1011 (HANDLER_ALREADY_EXISTS). */ \
M(1010, EXPORT_PARTITION_ALREADY_EXPORTED) \
M(1011, PARTITION_EXPORT_FAILED) \
+ /* 1012 and 1013 are intentionally skipped: they collide with upstream ClickHouse's \
+ * HANDLER_DOESNT_EXIST and AMBIGUOUS_HANDLER. Fork-specific error codes live in the 1030-1099 \
+ * range, chosen to sit well above upstream's maximum error code (1017 at the time this range \
+ * was reserved) so upstream can keep adding codes below it without colliding with the fork's. \
+ * A new fork error code goes in this range, not below 1030. CAS codes occupy 1037-1038. */ \
+ M(1037, CAS_WRITE_UNATTRIBUTED) \
+ M(1038, CAS_DELETE_MARKER) \
/* See END */
#ifdef APPLY_FOR_EXTERNAL_ERROR_CODES
@@ -695,7 +705,7 @@ namespace ErrorCodes
APPLY_FOR_ERROR_CODES(M)
#undef M
- constexpr ErrorCode END = 1011;
+ constexpr ErrorCode END = 1038;
ErrorPairHolder values[END + 1]{};
struct ErrorCodesNames
diff --git a/src/Common/FailPoint.cpp b/src/Common/FailPoint.cpp
index 1309bc1872c5..f54467a95676 100644
--- a/src/Common/FailPoint.cpp
+++ b/src/Common/FailPoint.cpp
@@ -235,7 +235,9 @@ static struct InitFiu
REGULAR(tcp_handler_fail_connection_setup) \
REGULAR(distributed_plan_status_check_reenqueue_fault) \
REGULAR(cas_relink_receiver_force_mechanism_failure) \
- PAUSEABLE_ONCE(cas_relink_receiver_pause_before_confirm)
+ PAUSEABLE_ONCE(cas_relink_receiver_pause_before_confirm) \
+ REGULAR(cas_relink_sender_omit_pool_cookie) \
+ REGULAR(cas_relink_receiver_drop_forced_disk)
namespace FailPoints
{
diff --git a/src/Common/ProfileEvents.cpp b/src/Common/ProfileEvents.cpp
index 0b1807b9659e..1ce7bdb60a2b 100644
--- a/src/Common/ProfileEvents.cpp
+++ b/src/Common/ProfileEvents.cpp
@@ -800,6 +800,10 @@ The server successfully detected this situation and will download merged part fr
M(CASRefBatchedMutations, "Number of CAS ref mutations committed through the per-namespace batching queue. Growth indicates reference-write activity.", ValueType::Number) \
M(CASRefBatchScopeCuts, "Number of CAS ref batches cut short by scope limits. Growing values indicate smaller batches and more write overhead.", ValueType::Number) \
M(CASRefQueueWaitMicroseconds, "Total time CAS ref writers spent queued, in microseconds. A rising value indicates ref-write contention or backend latency.", ValueType::Microseconds) \
+ M(CASHotKeyQueueWaitMicroseconds, "Total time CAS writers of a shared key spent queued in the hot-key lane before holding it or leaving, in microseconds. A rising value with a flat write rate means the holder is slow, not the store.", ValueType::Microseconds) \
+ M(CASHotKeyCacheStarts, "Number of hot-key lane holds that started from the pool's last known object instead of a read.", ValueType::Number) \
+ M(CASHotKeyReadStarts, "Number of hot-key lane holds that started from a read of the key.", ValueType::Number) \
+ M(CASHotKeyCacheVerdictsReread, "Number of verdicts (a refusal or a decline) a hot-key lane decide rendered on a cached object and that were re-rendered on a fresh read instead of delivered.", ValueType::Number) \
M(CASRefRecoveryRestarts, "Number of CAS ref-table recovery retries after a snapshot or log vanished during reading. A non-zero value indicates concurrent cleanup or backend inconsistency.", ValueType::Number) \
M(CASRefRecoveryRetries, "Number of CAS ref-table recovery attempts retried after a transient object-store error before the table's load fails. A non-zero value indicates transient object-store disruption during table startup.", ValueType::Number) \
M(CASRefAppendWedged, "Number of CAS ref-log append lanes that exhausted retries after an uncertain PUT. A non-zero value indicates ref-log progress may be stalled.", ValueType::Number) \
@@ -808,6 +812,11 @@ The server successfully detected this situation and will download merged part fr
M(CASRefAppendDefiniteFailure, "Number of CAS ref-log appends rejected with certainty. A non-zero value indicates invalid requests or backend rejection requiring investigation.", ValueType::Number) \
M(CASRefAppendSealRejected, "Number of CAS ref-log transactions conclusively rejected by a successor's epoch seal occupying the id they derived. This is the protocol working -- the writer was deposed and its operation was never acknowledged -- but a lane that keeps counting here is a writer that has lost its mount and does not yet know it.", ValueType::Number) \
M(CASRefAppendOccupantUnreadable, "Number of CAS ref-log appends that met a DIFFERENT object at the id they derived and could not read it to tell a successor's epoch seal from a breach of mount write-exclusivity. The decision is deferred to the next attempt, which re-derives the same id. Sustained growth means a real breach may be going unreported: the loud interference path is only reached once the occupant can be read.", ValueType::Number) \
+ M(CASRelinkConfirmRefusedRefMutationInFlight, "Number of CAS fetch-by-relink confirms this server answered Unknown because a queued or in-flight ref-lane mutation names the asked-about ref or the whole namespace. Expected under write load; the receiver retries the fetch.", ValueType::Number) \
+ M(CASRelinkConfirmRefusedLaneWedged, "Number of CAS fetch-by-relink confirms answered Unknown because the namespace's ref lane holds an unresolved append (a wedge). Lasts until the next flush or a remount resolves it.", ValueType::Number) \
+ M(CASRelinkConfirmRefusedLaneBroken, "Number of CAS fetch-by-relink confirms answered Unknown because the namespace's ref lane is in NeedsRecovery, Closed or Faulted state, or is Writing with nothing carved. A growing value outside induced faults is a lane defect, not load.", ValueType::Number) \
+ M(CASRelinkConfirmRefusedStateLockBusy, "Number of CAS fetch-by-relink confirms answered Unknown because the ref table's state lock was held. Under write load the usual holder is the table's own append leader, arming or installing a chunk; otherwise a recovery, a listing or a snapshot publish. The confirm never waits for it.", ValueType::Number) \
+ M(CASRelinkConfirmRefusedMountCannotSpeak, "Number of CAS fetch-by-relink confirms answered Unknown because this mount cannot speak for the namespace: its ref table is unrecovered or mid-recovery, its catalog life was invalidated, its runtime was superseded by a remount, or its mount fence is no longer held. Neither a lane defect nor write load. A growing value means this writer is losing, or has already lost, its claim to the namespace.", ValueType::Number) \
M(CASRefNeedsRecovery, "Number of CAS ref append lanes moved to `NeedsRecovery` because a known-durable transaction could not be installed. Such a lane refuses writes, snapshots, and confirmation until durable replay completes.", ValueType::Number) \
M(CASRefSweepDeferred, "Number of stale-precommit sweeps deferred after a read-only failure. A non-zero value indicates cleanup is waiting for a later trigger.", ValueType::Number) \
M(CASRefSweepRearmed, "Number of failed or partial stale-precommit sweeps scheduled for retry. Growing values indicate persistent cleanup or backend errors.", ValueType::Number) \
@@ -818,7 +827,7 @@ The server successfully detected this situation and will download merged part fr
M(CASRefLogBodyGets, "Number of CAS ref-log bodies read and decoded during GC. Growth indicates more reference history to process.", ValueType::Number) \
M(CASRefManifestBodyFoldGets, "Number of manifest bodies read while GC follows reference edges. High values indicate cache misses or many referenced manifests.", ValueType::Number) \
M(CASRefEmittedEdges, "Number of reachability edges emitted while GC folds CAS reference history. Growth indicates more reference relationships to process.", ValueType::Number) \
- M(CASRefCleanupObjectsDeleted, "Number of old CAS ref logs and snapshots deleted after safe coverage was confirmed. Growth indicates cleanup progress.", ValueType::Number) \
+ M(CASRefCleanupObjectsDeleted, "Number of old CAS ref logs and snapshots deleted after safe coverage was confirmed. Includes keys that were already absent, since a batch delete of write-once keys cannot tell the two apart. Growth indicates cleanup progress.", ValueType::Number) \
M(CASRefSnapshotPutBytes, "Total bytes written to CAS ref-table snapshots. A high value indicates frequent or large snapshot publication.", ValueType::Bytes) \
M(CASRefSnapshotTailLogs, "Number of CAS ref-log entries compacted into published snapshots. Growth indicates snapshot maintenance work.", ValueType::Number) \
M(CASRefSnapshotPublishDispatched, "Number of background CAS ref-table snapshot publications started. High values indicate frequent threshold or read-triggered publishing.", ValueType::Number) \
@@ -869,6 +878,9 @@ The server successfully detected this situation and will download merged part fr
M(CASGCGetStream, "Number of streaming CAS GC GET requests. Grows with large collection or recovery reads.", ValueType::Number) \
M(CASGCDelete, "Number of CAS GC DELETE requests. Grows with successful cleanup attempts.", ValueType::Number) \
M(CASGCList, "Number of CAS GC LIST requests. Growing values indicate more collection enumeration.", ValueType::Number) \
+ M(CASGCReadAheadHit, "Number of CAS GC fold reads and HEADs answered by the fold's read-ahead. Growth means the round's small-object round trips overlapped instead of serializing.", ValueType::Number) \
+ M(CASGCReadAheadMiss, "Number of CAS GC fold reads and HEADs performed inline because nothing was hinted for the key. A large value against hits means a hint set is narrower than the walk.", ValueType::Number) \
+ M(CASGCReadAheadWasted, "Number of CAS GC read-ahead results fetched and never taken: a namespace held below its lookahead, or a HEAD candidate that kept an edge. Bounded by the read-ahead window per namespace.", ValueType::Number) \
M(CASServerPut, "Number of CAS server-object PUT requests. Grows with server metadata writes.", ValueType::Number) \
M(CASServerPutDeduplicated, "Number of deduplicating CAS server-object PUT requests. Growth indicates reused server objects.", ValueType::Number) \
M(CASServerOverwrite,"Number of CAS server-object overwrite requests. Growing values indicate repeated replacement writes.", ValueType::Number) \
@@ -908,6 +920,7 @@ The server successfully detected this situation and will download merged part fr
M(CASMetaResurrectClean, "Number of condemned-body replacement paths that entered Clean metadata reconciliation. Counts the reason entry, not a guaranteed metadata reset.", ValueType::Number) \
M(CASGCMetaOps, "Number of per-hash metadata operations executed by CAS GC. Growing values indicate more GC candidates or metadata work.", ValueType::Number) \
M(CASGCEnumerationPages, "Number of CAS GC LIST pages fetched while enumerating the object universe. Growing values indicate a larger universe or more frequent scans.", ValueType::Number) \
+ M(CASBulkDeleteRequests, "Number of CAS batch delete requests: one DeleteObjects carrying up to 1000 write-once keys (manifest bodies, ref logs, ref snapshots). The per-key class counters (CASManifestDelete, CASRootDelete) say how many keys each request carried.", ValueType::Number) \
M(CASGCRefWalkPlansBuilt, "Number of complete catalog-authoritative CAS ref walk plans constructed by ordinary GC and rebuild. A regular or rebuilding invocation that reaches the post-LIST catalog cut increments this exactly once, including a round that later defers.", ValueType::Number) \
M(CASGCUnmatchedAdoptedParentLives, "Number of adopted-parent CAS ref-life rows dropped because the post-LIST catalog cut has no matching physical life. Each occurrence is inert for planning and suppression and is logged with its exact physical life id; a persistent nonzero rate indicates old generation state is outliving catalog removal.", ValueType::Number) \
M(CASGCStuckRemovals, "Number of adopted CAS GC rounds that observed a Removing namespace at or beyond the diagnostic age threshold without terminal cleanup evidence. Incremented and warned every such round; diagnostic only, with no effect on folding, suppression, appends, or deletion.", ValueType::Number) \
@@ -927,6 +940,15 @@ The server successfully detected this situation and will download merged part fr
M(CASConditionalWriteDefiniteFailure, "Number of CAS conditional writes rejected with certainty before applying. A non-zero value indicates invalid requests, oversized entities, or access denial.", ValueType::Number) \
M(CASConditionalWriteUnresolved, "Number of CAS conditional writes with an unknown outcome after conflict, timeout, connection loss, or server error. A non-zero value indicates backend instability or state requiring resolution.", ValueType::Number) \
M(CASConditionalWriteFenceLostPostWrite, "Number of CAS writes that succeeded but lost the final mount-fence check. A non-zero value indicates late responses after the mount lifecycle changed.", ValueType::Number) \
+ M(CASRequestAttempt, "Number of physical requests the CAS request contract started. Each one was admitted by the mount fence and reserved against the call's deadline before it was sent.", ValueType::Number) \
+ M(CASRequestReissue, "Number of CAS requests re-sent: after a jittered backoff for an ordinary failure, after a flat pause for a connect-failure hint, or at once with no pause at all for a first-attempt fuse. Growth means the object store is throttling, failing, or contended.", ValueType::Number) \
+ M(CASRequestConflictPause, "Number of clean lost races the CAS request contract repaid after a flat jitter instead of a growing backoff: the resolve read had settled the conflict and no transport fault preceded it.", ValueType::Number) \
+ M(CASRequestResolveRead, "Number of requests the CAS request contract made to settle a refused precondition or an ambiguous write: a body read, or a HEAD where the caller needs only presence. A connect-hinted attempt reissues without one.", ValueType::Number) \
+ M(CASRequestGaveUp, "Number of CAS writes that ended without a proven outcome, at a deadline, on a lost mount fence, or unresolved. A non-zero value means callers are being asked to retry later.", ValueType::Number) \
+ M(CASRequestRefused, "Number of CAS writes the store itself refused, proving they never applied: a malformed request, an entity too large, or an access or credential denial that no credential refresh was performed for, either because the disk has no refresh mechanism or because this write had already spent its one refresh.", ValueType::Number) \
+ M(CASRequestFenceLostPostWrite, "Number of CAS writes that were proven durable but lost the mount fence before the call could claim them. A non-zero value indicates late responses after the mount lifecycle changed.", ValueType::Number) \
+ M(CASRequestConnectFailureHint, "Number of CAS write attempts whose transport error named a failed connection (no free local port, refused or unreachable peer, connect timeout). Under a reissuing policy the engine reissues them after a flat pause without a settle read, when the deadline and the fence admit it. Growth means the server cannot open connections to the object store.", ValueType::Number) \
+ M(CASRequestFirstAttemptFuse, "Number of CAS control requests whose first HTTP attempt matched the adaptive first-attempt timeout; the engine reissues them at once as attempt 2 when the policy and the gates permit. Growth means the object store does not answer a fresh connection within the first-attempt timeout.", ValueType::Number) \
M(CASMountRenewalAttempts, "Number of physical conditional renewal PUTs sent for CAS mount leases. This counts transport attempts, not logical renewals.", ValueType::Number) \
M(CASMountRenewalRetries, "Number of physical conditional renewal PUTs sent after the first attempt of one logical CAS mount-lease renewal.", ValueType::Number) \
M(CASMountRenewalResolved, "Number of CAS mount-lease renewals whose committed outcome was proved by an exact resolving GET.", ValueType::Number) \
@@ -946,7 +968,6 @@ The server successfully detected this situation and will download merged part fr
M(CASRefRecoveryStragglerAdopted, "Number of straggler ref-log transactions a recovery compare-and-swap walk met at the slot it tried to seal and adopted, re-sealing at the new T+1. Non-zero means writes from a dying epoch were still materializing when recovery ran.", ValueType::Number) \
M(CASRefRecoveryCancelled, "Number of CAS ref-table recovery attempts abandoned because a self-remount requested cancellation before re-arming the mount fence. Non-zero means remounts are overlapping recoveries; nothing is written or installed on this path.", ValueType::Number) \
M(CASRefRecoveryStreamHole, "Number of times CAS ref-table recovery found a 404 BELOW a durable same-epoch witness -- a hole in a stream INV-1 makes dense. Restarted while the restart budget lasts (a racing cleanup is the innocent explanation), then reported as corruption. Any sustained non-zero value is data loss, not noise.", ValueType::Number) \
- M(CASPartFolderValidateSkipped, "Number of CAS part-folder validation HEADs skipped by policy or a fresh retained view. High values reduce reads but can delay detecting external changes.", ValueType::Number) \
M(CASBlobAdoptTrusted, "Number of CAS blob adoptions trusted through a durable manifest edge without per-file probes. Growth indicates manifest-based relinking.", ValueType::Number) \
M(S3GetObjectTagging, "Number of S3 API GetObjectTagging calls.", ValueType::Number) \
M(S3HeadObjectMicroseconds, "Time of S3 API HeadObject execution.", ValueType::Microseconds) \
diff --git a/src/Common/setThreadName.h b/src/Common/setThreadName.h
index 4959fb535647..bfeb53141384 100644
--- a/src/Common/setThreadName.h
+++ b/src/Common/setThreadName.h
@@ -35,7 +35,7 @@ namespace DB
M(CAS_ANOMALY_DIAG, "CasAnomalyDiag") \
M(CAS_GC_HEARTBEAT, "CasGcHeartbeat") \
M(CAS_GC_SCHEDULER, "CasGcSched") \
- M(CAS_LEASE_KEEPER, "CasLeaseKeeper") \
+ M(CAS_LEASE_RENEWER, "CasLeaseRenewer") \
M(CAS_REF_SNAPSHOT_PUBLISH, "CasRefSnapPub") \
M(CAS_REMOUNT, "CasRemount") \
M(CGROUP_MEMORY_OBSERVER, "CgrpMemUsgObsr") \
diff --git a/src/Disks/DiskObjectStorage/MetadataStorages/ContentAddressed/Backend/CasBackend.h b/src/Disks/DiskObjectStorage/MetadataStorages/ContentAddressed/Backend/CasBackend.h
index e40521134377..96911defc15f 100644
--- a/src/Disks/DiskObjectStorage/MetadataStorages/ContentAddressed/Backend/CasBackend.h
+++ b/src/Disks/DiskObjectStorage/MetadataStorages/ContentAddressed/Backend/CasBackend.h
@@ -1,11 +1,18 @@
#pragma once
+#include
+#include
#include
+#include
#include
#include
+#include
+#include
#include
#include
+#include
+#include
+#include
#include
-#include