Skip to content

Refactor: umbp metadata store rebase#437

Open
TianDi101 wants to merge 15 commits into
mainfrom
refactor/umbp-metadata-store-rebase
Open

Refactor: umbp metadata store rebase#437
TianDi101 wants to merge 15 commits into
mainfrom
refactor/umbp-metadata-store-rebase

Conversation

@TianDi101

Copy link
Copy Markdown
Collaborator

Motivation

Technical Details

Test Plan

Test Result

Submission Checklist

TianDi101 and others added 15 commits June 29, 2026 06:11
Preparatory type changes ahead of the IMasterMetadataStore refactor. No
behavior change; the rest of the refactor becomes purely structural.

0a. Hoist NodeTierKey (from GlobalBlockIndex) and NodeMatch (from
    ExternalKvBlockIndex, with MatchedHashCount) to types.h. Temporary
    `using` aliases left in both classes so existing callers compile;
    removed in Phase 5.

0b. Add ClientRegistration and HeartbeatResult (with nested
    enum Status { APPLIED, SEQ_GAP, UNKNOWN }) to types.h. Additive;
    these are the input/result vocabulary the Phase 1 interface uses.

0c. Migrate boundary-crossing timestamps from steady_clock to
    system_clock (hazard #7): ClientRecord {last_heartbeat,registered_at},
    BlockMetrics {created_at,last_accessed_at}, EvictionCandidate
    last_accessed_at, BlockEntry lease/access atomics + GrantLease/
    BatchLookupForRouteGet durations, Router lease_duration_, and
    master_server NowNs(). Process-local duration/timeout clocks (rpc
    latency timer, ssd read-lease, peer allocator deadlines) intentionally
    stay steady_clock. Fixes the one audit fallout in
    test_route_put_strategy.cpp that constructed the migrated fields.

Full UMBP C++ unit suite (local + distributed) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Land the abstract IMasterMetadataStore interface header consolidating
the four master-side state holders (GlobalBlockIndex, ClientRegistry,
ExternalKvBlockIndex, ExternalKvHitIndex) behind one contract. No
consumers wired yet.

- master_metadata_store.h: lifted from the draft, depends only on
  types.h. Adds the two hit-count methods the draft dropped
  (GetExternalKvHitCounts, GarbageCollectHits) so the live
  GetExternalKvHitCounts RPC path is preserved, and adds a `now`
  parameter to MatchExternalKv so count_as_hit=true can stamp last_seen.
- Hoist EvictionCandidate from global_block_index.h into types.h (part
  of the store contract; mirrors the phase 0 NodeTierKey/NodeMatch
  hoist) so the interface depends only on types.h.
- Compile/instantiation gates: self-compile TU, GMock
  MockMasterMetadataStore (reused in phase 3), and a
  signature-completeness test exercising every method through
  IMasterMetadataStore&.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement IMasterMetadataStore in-process by folding the four former state
holders (GlobalBlockIndex, ClientRegistry, ExternalKvBlockIndex,
ExternalKvHitIndex) behind a single std::shared_mutex.

- Block locations + LRU/lease (per-entry atomics mutated under a shared lock,
  keeping the RouteGet hot path concurrent); lease/access timestamps are now
  caller-supplied system_clock (hazard #7).
- ApplyHeartbeat single-seq CAS; SEQ_GAP keeps liveness but not caps/seq
  (hazard #1). ExpireStaleClients flips ALIVE->EXPIRED and keeps the row,
  cascading block + external-KV cleanup atomically (hazard #3).
- RegisterExternalKvIfAlive fuses the alive-check with the write (TOCTOU fix).
- MatchExternalKv(count_as_hit=true) is the one formerly-shared path that
  takes the unique lock; hit last_seen is system_clock and feeds
  GarbageCollectHits.
- EnumerateLruForEviction = Option A (full scan + sort + greedy byte budget,
  no maintained index) so tie-timestamps never drop candidates.

Adds the §6a behavioral suite (31 cases) written against IMasterMetadataStore&
so it is reused for the Redis backend; all green, concurrency cases clean
under ThreadSanitizer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the direct GlobalBlockIndex / ClientRegistry / ExternalKvBlockIndex /
ExternalKvHitIndex references in every master-side consumer with a single
IMasterMetadataStore&. The four old classes are still present, so this isolates
rewire regressions from the Phase 4 deletion.

- Router: holds one store_ instead of index_ + registry_. RoutePut /
  BatchRoutePut read ListAliveClients() and BatchExistsBlock(); BatchRouteGet
  now passes an explicit system_clock::now() into BatchLookupBlockForRouteGet
  (the old GlobalBlockIndex read the clock internally — the timestamp now
  crosses the store boundary, hazard #7).
- EvictionManager: constructor takes the store. RunOnce passes its existing
  per-(node,tier) byte budget (already computed down to the low watermark)
  straight into EnumerateLruForEviction and drops its own std::sort + greedy
  budget walk, since the store returns candidates LRU-ordered and
  budget-trimmed. The standalone overloaded-set is gone — the budget map's
  keys already identify the overloaded buckets.
- MasterServer: the four state members collapse to one
  unique_ptr<IMasterMetadataStore> (InMemoryMasterMetadataStore), declared
  before router_/service_ so it outlives their references. gRPC handlers
  rewired to store_ calls. The Heartbeat handler flattens EventBundle[] into
  per-bundle single-seq ApplyHeartbeat calls and short-circuits on SEQ_GAP
  (§3e). The client-expiry reaper moves out of ClientRegistry into MasterServer
  (per-tick store_->ExpireStaleClients(cutoff)); the hit-index GC tick becomes
  store_->GarbageCollectHits(now - max_age) — both cutoffs on the system_clock
  basis so they compare against last_heartbeat / last_seen.
- Add UnregisterExternalKvByNode to the interface, in-memory impl, and mock:
  it backs the live RevokeAllExternalKvBlocksForNode RPC (whole-node external-KV
  wipe that leaves the client record + block locations intact), which the §1b
  draft had dropped.

Tests:
- test_router_dedup migrated to construct InMemoryMasterMetadataStore instead
  of GlobalBlockIndex + ClientRegistry.
- Store suite extended with the previously-uncovered methods:
  UnregisterExternalKvByNode (distinct from UnregisterClient), GetPeerAddress
  (ALIVE + EXPIRED + unknown), GetClientTags, and ListAliveClients content;
  the interface "every method callable" gate now names UnregisterExternalKvByNode.
- InMemoryMasterMetadataStore behavioral suite green (36 cases) via the
  standalone g++ build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UMBP C++ tests were split across two trees with two CMake wirings and a
duplicate GoogleTest FetchContent: the standard tests/cpp/umbp and a second
suite under src/umbp/tests (built unconditionally, with its own gtest fetch).

Move the src/umbp/tests suite into tests/cpp/umbp/distributed/ (master/peer/
index/store/router logic = distributed layer), merge its target definitions
into distributed/CMakeLists.txt (keeping gtest_discover_tests registration),
and drop the src/umbp/tests subdirectory and its duplicate googletest fetch.
The whole UMBP suite now builds under the single BUILD_TESTS + BUILD_UMBP gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that all consumers route through IMasterMetadataStore (Phase 3),
remove the four superseded master-side state holders and their
class-specific test suites:

  - GlobalBlockIndex, ClientRegistry, ExternalKvBlockIndex,
    ExternalKvHitIndex (header + cpp each)
  - their Phase 0a `using NodeTierKey` / `using NodeMatch` aliases
    (lived inside the deleted headers)
  - GlobalBlockIndex::GetMetrics (zero callers outside the class)

Behavioral coverage moved to the store suite
(test_in_memory_master_metadata_store), so the five class-specific
tests are dropped. Two suites that still constructed the old classes
as fixtures are migrated to InMemoryMasterMetadataStore instead:

  - test_ssd_reliability: ApplyEvents/Lookup/BatchLookupForRouteGet ->
    RegisterClient + ApplyHeartbeat (ascending seq) + LookupBlock /
    BatchLookupBlockForRouteGet
  - test_umbp_tags Suite 1: ClientRegistry -> store
    RegisterClient/ApplyHeartbeat/GetClientTags/UnregisterClient

BlockMetrics and EvictionCandidate stay in types.h (still used by the
store's BlockEntry, eviction_manager, and test_types).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n docker

The UMBP C++ tests were consolidated under tests/cpp/umbp/distributed
(commit 00652fa); remove the now-duplicate originals left behind under
src/umbp/tests (their CMakeLists was already deleted in that commit, so
these files were orphaned and unbuilt).

Also add .rocprofv3/ to .dockerignore so profiler output stays out of the
docker build context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Salvaged from the dropped "add profiling logs" commit (7892877c), which
was built on main's pre-#382/#403 routing API and was auto-dropped as
redundant during the rebase onto main. Only the parts not already on main
are kept here; the RoutePut half (per-key Select vs BatchSelect) is gone
because main replaced TierAwareMostAvailableStrategy::Select with
ConfigurableRoutePutStrategy::SelectBatch, leaving nothing to compare.

- RouteGetStrategy gains BatchSelect(): one virtual dispatch for a whole
  BatchRouteGet instead of one Select() per key. Default impl loops over
  Select() so custom strategies that only override Select() keep working;
  Random/TierPriority override it with a tight internal loop. Empty
  candidate lists are left as a default Location ("not routed") and skip
  Select(), mirroring the router's pre-batch skip.
- Hoist the per-strategy selection cores into PickRandomReplica /
  PickTierPriorityReplica shared by Select and BatchSelect. The per-call
  MORI_UMBP_DEBUG lines that ran SummarizeLocations() unconditionally
  (macro args evaluated even when the level suppresses output) are
  commented out — they dominated the hot path.
- Router::BatchRouteGet calls get_strategy_->BatchSelect once over the
  whole batch.
- Add BatchSelect unit tests and a RouteGet-only micro-bench
  (bench_route_strategy.cpp, not a CTest target).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three RouterDedup cases that main carried in src/umbp/tests/test_router_dedup.cpp
were dropped when that file was deleted in the test consolidation (they used the
old GlobalBlockIndex/ClientRegistry API). Re-add them against
InMemoryMasterMetadataStore so the coverage survives the refactor:

- RoutePutMarksAlreadyExistsForIndexedKey: single-key RoutePut delegates to the
  batch path, so dedup still applies (indexed key -> kAlreadyExists, unknown ->
  kRouted).
- BatchRoutePutDedupDoesNotConsumeCapacity: a dedup hit consumes no projected
  capacity, so a same-size new key on a node with exactly one free slot still
  routes.
- BatchRoutePutDoesNotWriteBackProjectedCapacity: batch-local capacity
  deductions are not written back to the store's client record.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r-store tests to new API

The rebase added a unique_ptr<MasterEvictStrategy> member to MasterServerConfig
while the type is only forward-declared, so its implicit destructor failed to
compile in every TU lacking evict_strategy.h — including the umbp_common and
umbp_master libraries. Declare the special members out-of-line in config.h and
define them = default in master_server.cpp (now includes evict_strategy.h).

Port the three remaining tests to the policy-neutral store API
EnumerateEvictionCandidates(buckets, EvictionOrder, max_per_bucket, now):
- test_in_memory_master_metadata_store: Budget byte-map helper -> Buckets list;
  cap is now a count, not a byte budget (byte-budget trimming is the strategy's
  job); rename EvictionLruOrderAndBudget->AndCap, OnlyBudgetedBuckets->OnlyRequestedBuckets.
- mock_master_metadata_store: swap EnumerateLruForEviction MOCK_METHOD for the
  4-arg EnumerateEvictionCandidates; drop dead BudgetMap/LruResult aliases.
- test_master_metadata_store_interface: update ON_CALL + the interface call.
- test_master_evict_strategy: drop the deleted global_block_index.h include
  (EvictionCandidate/Location now live in types.h); Clock steady->system_clock
  to match EvictionCandidate::last_accessed_at.

Builds clean and all umbp ctest pass (cross_node_smoke excluded; needs 2 nodes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve the refactor-vs-perf collision: #440 optimized the legacy
GlobalBlockIndex/ClientRegistry that this branch deletes in favor of
IMasterMetadataStore. Keep the deletions and port #440's non-sharding
wins onto the new store; defer block-index sharding to a follow-up so
it stays separate from this conflict resolution.

Applied from #440:
- Add IMasterMetadataStore::GetAlivePeerView() (node->peer for ALIVE
  clients) + InMemory impl + GMock; router BatchRouteGet and
  master MatchExternalKv use it instead of copying full ClientRecords.
- Keep the gRPC sync-server poller pool sizing (UMBP_MASTER_MIN/MAX_POLLERS).
- Drive RemoveBlocksByNodeLocked off the node_to_keys_ reverse index
  (O(node keys) vs full scan); benefits Unregister/ExpireStaleClients.
- Drop CLEAR_AT_TIER everywhere to match main's event protocol.
- Keep kvevent master-pressure bench; drop the dead
  bench_global_block_index_route_get target (class removed).
- Port the old ClientRegistry peer-view/count tests onto the store
  interface suite; test_peer_dram_allocator additions carried via rename.

Deferred to a separate commit (A2): re-introduce per-shard locking for
the block index inside InMemoryMasterMetadataStore. The single mutex_
(cross-store atomicity for the Redis/HA backend) is preserved unchanged;
dropped the now-unimplemented UMBP_MASTER_INDEX_SHARDS doc entry.
Apply clang-format to the GetAlivePeerView impl and the ported
peer-view store test (pre-commit clang-format hook).
clang-format (pre-commit --all-files, the config CI runs) reflows the
EnumerateEvictionCandidates / SelectVictims calls and re-aligns the
EvictionOrder enum comments. Pre-existing refactor-branch formatting the
per-file hook run did not cover.
…hash

Split InMemoryMasterMetadataStore into two lock domains: meta_mutex_ (clients
+ external-kv + hit counts) and N key-hashed block shards
(UMBP_MASTER_INDEX_SHARDS, default 32, =1 = single lock), restoring PR #440's
per-shard heartbeat concurrency the store merge had serialized behind one mutex.

Cross-domain writes (ApplyHeartbeat / UnregisterClient / ExpireStaleClients)
run meta first, then the block section after releasing meta_mutex_ — never
nested, so heartbeats to different shards apply in parallel. Atomic per domain,
not globally atomic; the reader windows are benign and match main/#440 (see the
contract in master_metadata_store.h). Public API and event semantics unchanged.
Re-adds the UMBP_MASTER_INDEX_SHARDS doc row and two concurrency conformance
tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants