Skip to content

[Store] Defer invalid handle cleanup from segment unmount RPC#2877

Open
Aionw wants to merge 10 commits into
kvcache-ai:mainfrom
Aionw:codex/async-invalid-handle-cleanup-upstream
Open

[Store] Defer invalid handle cleanup from segment unmount RPC#2877
Aionw wants to merge 10 commits into
kvcache-ai:mainfrom
Aionw:codex/async-invalid-handle-cleanup-upstream

Conversation

@Aionw

@Aionw Aionw commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Description

MasterService::UnmountSegment currently calls ClearInvalidHandles() synchronously on the RPC path. The cleanup walks every metadata shard and examines every key, so its cost scales with the total number of keys managed by the master rather than only the keys stored on the segment being unmounted.

This became visible in a stress scenario with four master RPC threads, ten store clients, approximately eight million keys in total, and continuous batch put/get/exists traffic at a 1:1:1 ratio. Unmounting eight store services concurrently caused multiple RPC workers to perform full metadata scans, during which unrelated clients observed connection timeouts. Profiling showed that invalid-handle scanning and metadata destruction dominated the unmount period.

This PR separates immediate replica invalidation from physical metadata removal:

  • Each segment allocator registration owns a shared SegmentLifetime.
  • Buffers bind to the exact registration used for allocation and check its atomic availability state before exposing a descriptor.
  • PrepareUnmountSegment invalidates the registration immediately, while draining and graceful unmount only remove it from the allocation pool and keep existing replicas readable until final unmount.
  • Get, batch get, exists, batch exists, regex, and admin query paths exclude completed replicas whose segment is unavailable.
  • The unmount RPC commits the segment state and schedules a coalescing InvalidHandleCleanup worker instead of scanning every metadata shard synchronously.
  • GetAllKeys hides metadata without an available completed replica by default and retains filter_invalid=false for callers that need the physical metadata view.
  • Registration identity distinguishes same-name segments and CXL segments that share one underlying allocator.

The existing AllocatorManager::getAllocators() interface remains unchanged. The new registration view is used only by allocation and segment lifecycle paths that need lifetime tracking.

The behavior change is intentionally limited: replicas become invisible immediately after unmount preparation, while their physical metadata is removed asynchronously. As a result, physical metadata counters may be eventually consistent for a short period. If an object still has another available replica, reads continue using only the surviving replica. The existing synchronous cleanup in the client monitor remains unchanged.

Module

  • Transfer Engine (mooncake-transfer-engine)
  • Mooncake Store (mooncake-store)
  • Mooncake EP (mooncake-ep)
  • Mooncake PG (mooncake-pg)
  • Integration (mooncake-integration)
  • P2P Store (mooncake-p2p-store)
  • Python Wheel (mooncake-wheel)
  • Common (mooncake-common)
  • Mooncake RL (mooncake-rl)
  • CI/CD
  • Docs
  • Other

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Breaking change
  • Documentation update
  • Performance improvement
  • Other

How Has This Been Tested?

The affected test targets were built from the current upstream main. Tests cover lifetime propagation, same-name segment isolation, CXL shared-allocator behavior, immediate query visibility after unmount, partial-replica survival, asynchronous worker shutdown, admin key filtering, and mounted-segment serialization compatibility.

Test commands:

cmake --build build --target allocation_strategy_test segment_test master_service_test serializer_test -j8
./build/mooncake-store/tests/allocation_strategy_test
./build/mooncake-store/tests/segment_test
./build/mooncake-store/tests/master_service_test --gtest_filter='MasterServiceTest.UnmountSegmentHidesUnavailableReplicas:MasterServiceTest.ReadableAfterPartialUnmountWithReplication:MasterServiceTest.GetAllKeysFiltersUnavailableMetadataByDefault'
./build/mooncake-store/tests/serializer_test

Test results:

  • Unit tests pass
  • Integration tests pass (if applicable)
  • Manual testing done (describe below)

Results:

  • Allocation strategy: 70 passed, 2 existing conditional tests skipped
  • Segment: 15 passed
  • Master service targeted regression tests: 3 passed
  • Serializer: 8 passed

Checklist

  • I have performed a self-review of my own code
  • I have formatted my code using ./scripts/code_format.sh
  • I have run pre-commit run --all-files and all hooks pass
  • I have updated the documentation (if applicable)
  • I have added tests to prove my changes are effective
  • For changes >500 LOC: I have filed an RFC issue

AI Assistance Disclosure

  • No AI tools were used
  • AI tools were used (specify below)

OpenAI Codex assisted with adapting the change to the latest upstream code, reviewing the implementation, running the listed tests, and drafting this pull request description. The submitter reviewed the resulting code and test results.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces a SegmentLifetime tracking mechanism to immediately invalidate and hide replicas of unmounted segments from query paths, deferring the physical metadata cleanup asynchronously. The review feedback focuses on optimizing performance by avoiding heap allocations in the default constructor of SegmentLifetime on the data path, and improving safety by adding defensive bounds checks in AllocatorManager to prevent potential out-of-bounds crashes if registrations and allocators get out of sync.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread mooncake-store/include/allocator.h Outdated
Comment thread mooncake-store/include/allocation_strategy.h Outdated
Comment thread mooncake-store/include/allocation_strategy.h Outdated
@Aionw
Aionw marked this pull request as ready for review July 13, 2026 09:39
@Aionw
Aionw requested review from XucSh, YiXR, stmatengss and ykwd as code owners July 13, 2026 09:39

@he-yufeng he-yufeng 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.

Overall this is a solid approach to the unmount-scan cost — separating immediate visibility from physical cleanup via the per-registration SegmentLifetime flag is the right shape. Things I checked and that hold up:

  • All descriptor-returning paths are covered and mutually consistent. GetReplicaList/BatchGetReplicaList/regex/the *ForAdmin variants go through GetVisibleReplicaDescriptors; ExistKey/BatchExistKey/GetAllKeys through HasVisibleCompletedReplica. I checked these two can't disagree during the async window: is_completed_and_available() gates on has_invalid_mem/nof_handle()!buffer->isAllocatorValid(), the same predicate get_available_descriptor() uses. So Exist won't say "present" while Get says "not ready".
  • Per-registration lifetimes correctly disambiguate same-name segments and CXL client segments sharing one underlying allocator, since each addAllocator mints a fresh SegmentAllocator/lifetime.
  • getDescriptorIfAvailable re-checks isAllocatorValid() after building the descriptor — good guard against the flag flipping mid-build.
  • PrepareGracefulUnmount/SetSegmentStatusByName passing invalidate=false keeps draining replicas readable, matching the design.

One thing I'd like you to confirm before it merges: snapshot-restored replica buffers don't get the lifetime bound. Serializer<AllocatedBuffer>::deserialize reconnects allocator_ to the remounted buf_allocator, but the buffer's segment_lifetime_ stays default-constructed (always-available), and the registration created in SegmentSerializer::Deserialize (segment.cpp ~944) is a separate lifetime. bindSegmentLifetime only runs in SegmentAllocator::allocate(), so a restored buffer is never bound. After a master restart, a restored replica's unmount-invalidation therefore falls back to allocator_.expired() rather than the flag.

For the common allocator-per-segment case this still invalidates at unmount (the allocator is destroyed once the registration and buf_allocator are released), so no functional regression there. But it (a) breaks the "immediate flag" invariant for restored buffers, and (b) for a shared underlying allocator that outlives a single unmount — CXL client segments, the case this PR explicitly calls out — the restored replica stays isAllocatorValid() == true and remains visible after unmount, i.e. a stale descriptor.

Could you confirm the restore → unmount path (particularly CXL client segments) is covered? The small defensive fix would be to rebind the registration's lifetime onto the restored buffers where the registration is created in SegmentSerializer::Deserialize, so the invariant holds regardless of how a buffer came to exist. A regression test that snapshots → restores → unmounts a memory segment and asserts the object is no longer visible would lock it in.

@ykwd

ykwd commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

@Aionw @Icedcoco The following CI failed, could you take a look:
The following tests FAILED:
40 - master_metrics_test (Failed)
63 - master_service_test_for_snapshot (Failed)
Errors while running CTest

@Aionw
Aionw requested review from 00fish0 and Libotry as code owners July 14, 2026 02:24
@Aionw

Aionw commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

CI failure is expected to be fixed in latest commit.

@codecov-commenter

codecov-commenter commented Jul 14, 2026

Copy link
Copy Markdown

@ykwd

ykwd commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Since this is a critical change to the master service, I also invited @yokinoshitayoki for a double check.

@ykwd
ykwd requested a review from yokinoshitayoki July 14, 2026 04:48
Comment thread mooncake-store/src/master_service.cpp Outdated
Comment thread mooncake-store/src/segment.cpp
Comment thread mooncake-store/src/master_snapshot_manager.cpp Outdated
@Aionw

Aionw commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Scope update: I removed the snapshot-specific fixes from this PR, including the pre-fork unavailable-replica sweep, restored lifetime/registration rebinding, LOCAL_DISK-only client restore tracking, and the related snapshot round-trip tests.

The reason is to keep #2877 focused on asynchronous invalid-handle cleanup and generation-safe live segment lifetime management. Snapshot consistency and restore behavior have separate correctness and compatibility considerations, so they will be addressed in a dedicated follow-up PR instead of expanding the scope here.

The only changes retained under snapshot-related paths are minimal compatibility updates: existing unmount tests now wait for the asynchronous cleanup worker before fixture persistence, and the catalog provider uses the unified replica availability API.

* @return ErrorCode::OK if exists
*/
auto GetAllKeys(const std::string& tenant_id)
auto GetAllKeys(const std::string& tenant_id, bool filter_invalid = true)

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.

It seems that filter_invalid is currently exposed only on MasterService. The admin HTTP handler ignores the request, and GetAllKeysForAdmin() always uses the default value, so users cannot request filter_invalid=false.

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.

5 participants