Skip to content

[Store] Walk tenants through shard-backed cursors and build the removes on them - #4796

Merged
Aionw merged 3 commits into
kvcache-ai:mainfrom
Aionw:refactor/tenant-iteration
Oct 21, 2026
Merged

Aionw merged 3 commits into
kvcache-ai:mainfrom
Aionw:refactor/tenant-iteration

Conversation

@Aionw

@Aionw Aionw commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Step 10 of #4557: MasterService walks a tenant and its objects through shard-backed cursors, and Remove, RemoveByRegex, BatchRemove and both RemoveAll overloads are built on that walk and on the per-key accessor from step 9 (#4717).

The removes each walked the 1024 shards by hand, found the tenant's state, iterated its map, and carried their own copy of the lease, replica and replication-task checks and of the HA mark-removed-and-persist step.

  • Iteration primitives. VisitTenants calls back with each tenant that holds an object, holding no lock, so the callback may reach any key. ReadCursor(tenant) and WriteCursor(tenant) walk one tenant's objects under each shard's read or write lock and hand out the same Object view (key, generation, ref(), metadata, state) as the route's cursor, so the swap in step 15 keeps the loop bodies as they are. A write cursor may change an object's metadata and state but not publish or tear one down; a body that acts later keeps ref() and comes back through MetadataAccessorRW(service, tenant_id, ref).
  • Removes on the walk. RemoveByRegex and both RemoveAll overloads go through one RemoveTenantObjects: a ReadCursor pass names the objects that look removable, and each is checked again and removed under its own write lock, so an object republished meanwhile is left alone. BatchRemove holds each key through MetadataAccessorRW instead of grouping the keys by shard, so like the single-key Remove it runs the invalid-memory-replica cleanup first. All four share CheckRemovable and MarkRemovedAndPersist.
  • ObjectRef accessor. It no longer runs the invalid-memory-replica cleanup: that cleanup's KV sync bumps the tenant epoch, which would make RemoveAll suppress its own cleared. Its only other callers are the durable finalizers, which run with the OpLog on, where the cleanup is a no-op.
  • Empty tenant rows. RemoveAll now visits only tenants that hold an object, so it no longer sweeps the empty rows a failed standby restore left behind; the restore rollback drops them itself.
  • Smaller behavior notes. The RemoveAll test hook fires once per tenant instead of once per shard. RemoveByRegex logs the keys it skips at VLOG(1) instead of LOG(WARNING).

Until the swap, RemoveAll(force) walks every shard once per tenant (VisitTenants then one ReadCursor each), where it used to visit each shard once; the namespace table removes that multiplier. No RPC, standby snapshot or OpLog format changes.

Part of the code comes from #4370 by @CAICAIIs.

Module

  • Mooncake Store (mooncake-store)

Type of Change

  • Refactor

How Has This Been Tested?

Test commands:

make -C build -j14 -k
cd build && ctest -j8 --timeout 240 --output-on-failure
ctest -R '^pybind_client_test$' --timeout 600   # timed out at 240 s under -j8

Test results:

  • Unit tests pass

  • Integration tests pass (if applicable)

  • Manual testing done (describe below)

  • New master_service_tenant_iteration_test (7 cases): VisitTenants names only tenants holding objects, a ReadCursor visits each object of its tenant once, leaving a cursor early releases its shard, a WriteCursor changes state in place, RemoveByRegex stays in its tenant, RemoveAll walks every tenant, and RemoveAll leaves leased objects unless forced.

  • Full ctest on top of origin/main (37642c5): 250 of 254 pass. pybind_client_test timed out under -j8 and passes alone (272 s). The other three are known local environment failures, unrelated to this change: tcp_transport_test and transfer_metadata_test need a network/RDMA environment this host lacks, and ordered_oplog_writer_test does not link locally (libaio). Built with GCC 16 (Release, BUILD_UNIT_TESTS=ON).

  • Formatting: ./scripts/code_format.sh --check --changed-lines --base origin/main is clean with clang-format 20.1.8. pre-commit run --files <changed files>: codespell passes; cmake-format only rewrites lines in mooncake-store/tests/CMakeLists.txt that this PR does not touch (already on main), so those edits are left out.

Checklist

AI Assistance Disclosure

  • AI tools were used (specify below)

AI assistance was used for the refactor and to draft this description. The submitter has reviewed every changed line and can defend the change end to end.

Aionw and others added 2 commits October 20, 2026 15:05
Add the iteration primitives that the remaining phase-3 steps of kvcache-ai#4557
build on. VisitTenants names every tenant that holds an object and calls
back with no lock held. ReadCursor and WriteCursor walk one tenant's
objects under each shard's read or write lock, and hand out the same
Object view (key, generation, ref, metadata, state) as the route's
cursor, so the swap to NamespaceTable keeps the loop bodies as they are.

Only the tests use them so far.

Co-authored-by: CAICAIIs <3360776475@qq.com>
RemoveByRegex and both RemoveAll overloads now walk the tenant with
ReadCursor, keep the ObjectRef of each object that looks removable, and
come back to it under its own write lock to check it again and remove
it. BatchRemove holds each key through MetadataAccessorRW instead of
grouping the keys by shard, so like the single-key Remove it now runs
the invalid-memory-replica cleanup first: a key left with no valid
replica comes back OBJECT_NOT_FOUND. The four removes share the lease,
replica and replication-task checks and the HA
mark-removed-and-persist step.

The ObjectRef accessor no longer runs the invalid-memory-replica
cleanup: the cleanup's KV sync bumps the tenant epoch, which would make
RemoveAll suppress its own `cleared`. Its only other callers are the
durable finalizers, which run with the oplog on, where the cleanup was
already a no-op.

RemoveAll now visits only the tenants that hold an object, so it no
longer drops the empty tenant rows a failed standby restore left
behind. The restore rollback drops them itself.

The RemoveAll test hook now fires once per tenant instead of once per
shard, and RemoveByRegex logs the keys it skips at VLOG(1).

Co-authored-by: CAICAIIs <3360776475@qq.com>

@Icedcoco Icedcoco left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together. I understand this PR is groundwork for the #4557 migration, and the tenant-scoped cursors seem like a useful step in that direction.

I had a question about RemoveTenantObjects: it collects all eligible ObjectRefs for a tenant before deleting them. For tenants with a large number of objects, that could require a sizable temporary allocation. Do you think buffering the full candidate set is acceptable for the expected workloads, or would bounded batches be worth considering?

One other point for discussion: MarkRemovedAndPersist marks replicas as REMOVED before appending the oplog entry, but doesn’t restore their status if the append fails. I noticed the previous inline removal code had the same behavior, so this may be outside the scope of this PR. Since the logic is now centralized, though, should we handle the rollback here or track it separately?

Curious to hear your thoughts on both.

MarkRemovedAndPersist marked the object's completed replicas REMOVED
before appending the REMOVE. The append can still fail after the slot
is reserved (the writer stopping or failed, or the entry rejected), and
then no durable callback ever drops those replicas: the object is left
unreadable, and no remove can take it, since every remove wants all
replicas complete.

Collect the replica ids first and mark the replicas only once the append
succeeds, so a failed append leaves the object as it was. The durable
finalizer takes the object's shard lock, which the caller holds, so it
cannot run before the marks land.
@Aionw

Aionw commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

@Icedcoco Thanks for the careful read!

Buffering the candidates. It's a consequence of the cursor contract: the cursor holds the shard lock across the loop body, so the body can neither tear an object down nor block on the oplog reservation. It keeps the ObjectRef and comes back under the object's own write lock, which matches how the NamespaceTable cursor will behave after the #4557 swap. An ObjectRef is just the key plus an 8-byte generation, a small fraction of the metadata already held for those objects, and RemoveAll() frees it between tenants. In exchange, the scan now takes only shard read locks, where main held each shard's write lock for its whole scan-and-erase, which is friendlier to online requests. Bounded batches would need a cursor that can resume from a position. The single-pass cursor deliberately doesn't offer one, so I'd revisit this once the NamespaceTable cursor lands, if it turns out to matter for these admin-only paths.

REMOVED without rollback. Good catch. Commit can still fail after a successful reservation (writer terminal, stopping, or entry validation), which left the replicas REMOVED with no durable callback to drop them. Since the logic is centralized now, I fixed it here in ea30bd1 rather than adding a rollback: MarkRemovedAndPersist collects the replica ids, queues the REMOVE, and marks the replicas removed only after the append succeeds. That's safe because the durable finalizer takes the same shard write lock the caller holds, so it can't run before the marks land. The other mark-then-commit sites outside the remove paths are left for a follow-up.

@Icedcoco Icedcoco left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM pending CI

@CAICAIIs CAICAIIs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@Aionw
Aionw merged commit 0ed2fdf into kvcache-ai:main Oct 21, 2026
57 of 60 checks passed
@Aionw
Aionw deleted the refactor/tenant-iteration branch October 21, 2026 06:16
zxpdemonio added a commit to zxpdemonio/Mooncake that referenced this pull request Oct 21, 2026
…20261021

* upstream/main: (507 commits)
  [Reshard] Plan Store KV reuse across TP, PP and host layouts (kvcache-ai#4762)
  [Store] Walk tenants through shard-backed cursors and build the removes on them (kvcache-ai#4796)
  [Store-RS][Bugfix] Match TENT Request Layout to the C ABI (kvcache-ai#4732)
  [TransferEngine] Recover auto GID after data-path retry exhaustion (kvcache-ai#4290)
  [Store] Add the namespace layer over the object route (kvcache-ai#4721)
  [Store] Fix silent loss of offload keys deferred to the ungrouped pool (kvcache-ai#3006) (kvcache-ai#3009)
  [Store] Remove redundant alignment headroom from DFS shard allocation (kvcache-ai#4753)
  [CI/Build] Gate ROCm hardware tests and bound nightly Docker builds (kvcache-ai#4750)
  [Bugfix][Store] Fix double-counting of segment OpLog metrics in OpLogApplier (kvcache-ai#4771)
  [Store] Skip hot-cache writeback for device-resident read destinations (kvcache-ai#4764)
  [Reshard] Add bounded in-flight execution for weight transfers (kvcache-ai#4628)
  [Store] Avoid racing with detached hot-cache fills during key scans (kvcache-ai#4334)
  [Store] HA: drain the master on SIGTERM/SIGINT before exit (part 1 of kvcache-ai#4710) (kvcache-ai#4745)
  TENT: report a notification the receiver could not take (kvcache-ai#4754)
  [Store] Reach object metadata through a per-key accessor contract (kvcache-ai#4717)
  [Store][Bugfix] Fix DFS sync client test retry failure (kvcache-ai#4781)
  [Wheel] Keep integer dtype for typed_ragged rows when a row is an empty list (kvcache-ai#4611)
  [Doc] Sync README news onto the docs homepage (kvcache-ai#4767)
  [CI] remove stale Torch version binding after PG JIT (kvcache-ai#4760)
  [TransferEngine] Add buffer-protocol and page-gathered batch transfer APIs (kvcache-ai#4763)
  ...
zxpdemonio added a commit to zxpdemonio/Mooncake that referenced this pull request Oct 21, 2026
…-ranges-20261021

* upstream/main: (394 commits)
  [Reshard] Plan Store KV reuse across TP, PP and host layouts (kvcache-ai#4762)
  [Store] Walk tenants through shard-backed cursors and build the removes on them (kvcache-ai#4796)
  [Store-RS][Bugfix] Match TENT Request Layout to the C ABI (kvcache-ai#4732)
  [TransferEngine] Recover auto GID after data-path retry exhaustion (kvcache-ai#4290)
  [Store] Add the namespace layer over the object route (kvcache-ai#4721)
  [Store] Fix silent loss of offload keys deferred to the ungrouped pool (kvcache-ai#3006) (kvcache-ai#3009)
  [Store] Remove redundant alignment headroom from DFS shard allocation (kvcache-ai#4753)
  [CI/Build] Gate ROCm hardware tests and bound nightly Docker builds (kvcache-ai#4750)
  [Bugfix][Store] Fix double-counting of segment OpLog metrics in OpLogApplier (kvcache-ai#4771)
  [Store] Skip hot-cache writeback for device-resident read destinations (kvcache-ai#4764)
  [Reshard] Add bounded in-flight execution for weight transfers (kvcache-ai#4628)
  [Store] Avoid racing with detached hot-cache fills during key scans (kvcache-ai#4334)
  [Store] HA: drain the master on SIGTERM/SIGINT before exit (part 1 of kvcache-ai#4710) (kvcache-ai#4745)
  TENT: report a notification the receiver could not take (kvcache-ai#4754)
  [Store] Reach object metadata through a per-key accessor contract (kvcache-ai#4717)
  [Store][Bugfix] Fix DFS sync client test retry failure (kvcache-ai#4781)
  [Wheel] Keep integer dtype for typed_ragged rows when a row is an empty list (kvcache-ai#4611)
  [Doc] Sync README news onto the docs homepage (kvcache-ai#4767)
  [CI] remove stale Torch version binding after PG JIT (kvcache-ai#4760)
  [TransferEngine] Add buffer-protocol and page-gathered batch transfer APIs (kvcache-ai#4763)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants