Compare commits

...

69 Commits

Author SHA1 Message Date
Shelron 036075ccfa
[Bugfix]: route cache allocator capacity adapt (#1958)
* fix: route cache allocator capacity

* type cast
2026-04-23 12:50:19 +08:00
skygragon aad5111781
[Store] Support degraded startup when master is unavailable (#1930)
* [Store] Support degraded startup when master is unavailable

Problem:
P2P client startup fails when master is unavailable, blocking the
entire application. This is problematic for scenarios where the
client should be able to start independently and wait for master
to become available.

Solution:
1. Refactor P2PClientService::Init to support degraded startup:
   - Try ConnectToMaster/RegisterClient, allow failure
   - Set HA state to FULL or DEGRADED based on connection result
   - Skip MountSegment in degraded mode
   - Heartbeat thread will recover when master becomes available

2. Optimize initialization order:
   - ConnectToMaster/RegisterClient first
   - StartHeartbeat before InitTransferEngine (faster heartbeat)
   - InitTransferEngine and InitStorage after heartbeat starts

3. Add HARecoveryManager::SetState() for setting initial state

4. Make connection_interrupted_ atomic for thread-safe access

Changes:
- ha_recovery_manager.h: Add SetState() method
- client_service.h/cpp: Support degraded startup in heartbeat logic
- p2p_client_service.cpp: Refactor Init flow with degraded support

Co-Authored-By: Claude (antchat/GLM-5) <noreply@anthropic.com>

* [Store] Refactor HA recovery manager and client service

1. Remove unused master_server_entry parameter from ReconnectToMaster.
   In non-HA mode, current_master_address is already initialized to
   master_server_entry, making the parameter redundant.

2. Replace SetState() with constructor initial_state parameter.
   SetState() bypasses the state machine transition logic, which breaks
   encapsulation. Initial state should only be set during construction.

3. Add WaitForReady() to encapsulate initialization wait logic.
   Extracts the ready_for_recovery_ and data_manager_ check into a
   separate method for better readability in RecoveryPipelineMain.

Co-Authored-By: Claude (antchat/GLM-5) <noreply@anthropic.com>

* [Store] Add TODO comment for race window in segment mount logic

Add documentation explaining the race condition between IsDegraded()
check and MountSegment() call, and outline future improvement plan
to decouple storage layer initialization from master interaction.

Co-Authored-By: Claude (antchat/GLM-5) <noreply@anthropic.com>

---------

Co-authored-by: wl177541 <wl177541@antgroup.com>
Co-authored-by: Claude (antchat/GLM-5) <noreply@anthropic.com>
2026-04-22 20:56:56 +08:00
Wan 188bb41121
[Store] Remove read pool and slot of stress test (#1955)
* remove read pool and fix typo

* add parameter check

---------

Co-authored-by: wanyue.wy <wanyue.wy@oceanbase.com>
2026-04-22 19:09:51 +08:00
Wan 29e2c7e873
[Store] optimize stress test (#1953)
Co-authored-by: wanyue.wy <wanyue.wy@oceanbase.com>
2026-04-22 16:32:01 +08:00
Wan 0d96064ee0
[Store] Optimize batch_put with BatchGetWriteRoute RPC (#1947)
* fix write route

* optimize batch_put by adding batch_get_write_route rpc in master

* fix code format

---------

Co-authored-by: wanyue.wy <wanyue.wy@oceanbase.com>
2026-04-21 22:28:25 +08:00
skygragon b21f90d032
[Store] Add HTTP metrics endpoint for Client (#1934)
* [Store] Add HTTP metrics endpoint for Client

Add HTTP server to expose client metrics via Prometheus-compatible endpoints.
This brings Client metrics exposure in line with Master's implementation.

Changes:
- Add metrics_port (default: 9003) and enable_metrics_http config options
- Add coro_http_server in ClientService for metrics HTTP endpoints
- Expose /metrics, /metrics/summary, and /health endpoints
- Add GetMetricsPort() and IsMetricsHttpEnabled() API methods
- Fix port type from int16_t to uint16_t to avoid overflow for ports > 32767
- Add unit tests for config builder and metrics settings
- Fix parameter passing in HA integration test

Co-Authored-By: Claude (antchat/GLM-5) <noreply@anthropic.com>

* [Client] Fix invalid return value in StartMetricsHttpServer

Return 0 instead of -1 on failure, since the function returns uint16_t.
-1 would be implicitly converted to 65535, which is a valid port number
and could cause confusion.

Co-Authored-By: Claude (antchat/GLM-5) <noreply@anthropic.com>

* [Client] Add HA state to health endpoint

Add GetHealthStatus() virtual method to ClientService, returning "OK"
by default. P2PClientService overrides it to return the current HA
state (FULL/DEGRADED/SYNCING) via HARecoveryManager.

Also add toString(HAClientState) helper in types.h for consistent
string conversion.

Co-Authored-By: Claude (antchat/GLM-5) <noreply@anthropic.com>

---------

Co-authored-by: wl177541 <wl177541@antgroup.com>
Co-authored-by: Claude (antchat/GLM-5) <noreply@anthropic.com>
2026-04-21 17:35:20 +08:00
Wan 90f2005b55
[Store] Parallelize Local and Remote Read/Write in Batch Scenarios (#1921)
* tmp commit

* optimize

* fix tests

* tmp commi

* refactor: decouple async memcpy execution into a dedicated TaskHandle-based architecture and remove legacy executor from P2PClientService

* refactor: implement RouteIterator for async read retries and add WriteRetryContinuation for non-blocking remote writes

* optimize format and add ScopedVLogTimer

* optimize stress workload test

* fix some details

* remove async memcpy queue length

* fix compile

* optimize stree workload runner example

* fix format

* fix ha integration test

* fix memory leak when te failed

* Asynchronize the master route query in the retry chain of data reading and writing

* Unify the definition of BatchID

* fix ha test bug for async route sync with master

---------

Co-authored-by: “JiaQi <2963103258@qq.com>
Co-authored-by: wanyue.wy <wanyue.wy@oceanbase.com>
2026-04-20 11:47:43 +08:00
skygragon 6fd613e370
[CI] Improve code format check to only scan changed files (#1922)
Cherry-picked from main branch commits:
- 30ca086 [CI] Improve Code Formatting Workflow (#1368)
- 0267e35 [CI] chore: update clang-format to v20.1.8 and enforce version 20 (#1379)
- e9f2887 [Misc] Fix silent failure in `code_format.sh` when clang-format is missing (#1824)

Why partial cherry-pick:
The original commits include changes to .github/workflows/ci.yml that conflict
with P2P-Mooncake-Store branch (which has additional changes like Ninja build
support, branch name modifications, etc.). Instead of resolving complex merge
conflicts, we manually applied the following changes:

Files changed:
1. scripts/code_format.sh (new) - script for incremental format checking
2. .github/workflows/ci.yml - clang-format job updated to use the script
3. .github/pull_request_template.md - added format checklist item
4. .pre-commit-config.yaml - clang-format v19.1.0 -> v20.1.8

Benefits:
- Reduces CI time by only checking changed files instead of all 500+ files
- Supports --check mode for CI and --all mode for local full scan
- Properly handles PR vs push events with correct base ref detection

Co-authored-by: wl177541 <wl177541@antgroup.com>
2026-04-17 23:04:04 +08:00
skygragon b0214baf0e
[Test] Fix HA integration test flakiness (#1916)
1. MasterRestartRecovery: Add retry logic for client2 reconnection
   - Each client has its own connection pool, so both need to clear stale
     connections independently after master restart
   - Previously client1 had 10 retries but client2 only tried once

2. ForceRecover: Add re-registration on recovery failure
   - If master restarted and lost client registration, recovery would fail
     with CLIENT_NOT_FOUND error and get stuck in SYNCING state
   - Now retries with re-registration if stuck in SYNCING state

Co-authored-by: wl177541 <wl177541@antgroup.com>
Co-authored-by: Claude (antchat/GLM-5) <noreply@anthropic.com>
2026-04-17 18:10:06 +08:00
skygragon 84a823b9eb
[Build] Add memory-aware compile/link parallelism with Ninja support (#1909)
Cherry-picked from main branch commit 79266ffb4d.
Due to significant branch divergence, not all changes were cherry-picked:
- CI workflow files (ci.yml, ci_cu13.yml) were manually adapted
- Dockerfile and ci_ascend.yml were skipped (not present in this branch)

- Add limit_jobs.cmake module for memory-aware build parallelism
- Auto-detect available memory and CPU cores to calculate safe job limits
- With Ninja, create separate job pools for compilation (~1.5GB/job) and linking (~4GB/job)
- Switch CI workflows to use Ninja generator
- Add ninja-build to dependencies.sh

This helps prevent OOM during linking on high-core machines.

Co-authored-by: wl177541 <wl177541@antgroup.com>
Co-authored-by: Claude (antchat/GLM-5) <noreply@anthropic.com>
2026-04-17 10:52:14 +08:00
Wan 6ab0016ed3
[Store ]Implement Master HA recovery based on client (#1876)
* adjust hearbeat init order

* base version

* add test file

* optimize async metadata notifier and test

* rename master_reachable to master_reconnected for clarity

* rewrite HA integration test: two clients, manual heartbeat, fault scenarios

* refactor ha recovery manager test and fix bugs

* modify the comment

* clang format

---------

Co-authored-by: wanyue.wy <wanyue.wy@oceanbase.com>
2026-04-13 19:33:23 +08:00
Wan 5f2a814eb9
[Store] Implement Async Metadata Notifier for P2P Route Sync (#1875) 2026-04-13 02:34:59 +08:00
Yufeng He 9cd7a12bcc
[Store] Fix P2PClientService::Put silently swallowing write errors (#1825)
* fix: propagate PutViaRoute error in P2PClientService::Put

Put() logged errors from PutViaRoute but always returned success (empty
expected), causing callers to believe the write succeeded when the
underlying route write actually failed with SEGMENT_NOT_FOUND or
NO_AVAILABLE_HANDLE.

Return the error for non-idempotent failures while still treating
REPLICA_NUM_EXCEEDED / REPLICA_ALREADY_EXISTS as success (object already
stored).

Fixes #1817

* fix: also capture remote write errors in PutViaRoute result

Address review feedback from Gemini Code Assist:

1. PutViaRoute: assign write_result error to result before continue
   in the remote write path. Previously, if all remote candidates
   failed, result stayed default-initialized (success) because only
   local write failures were captured.

2. Put: simplify return to use result directly instead of
   re-wrapping with tl::unexpected.
2026-04-07 11:43:43 +08:00
Wan 99fb4d4af6
[Store][Bugfix] Fixed the problem of filling in the wrong TE endpoint
Co-authored-by: wanyue.wy <wanyue.wy@oceanbase.com>
2026-04-06 22:46:42 +08:00
Wan 0d8a51f417
[Store] Implement Lock-Free P2P Route Cache (#1793)
* 1. implement route cache
2. optimize some concurrency static check

* remove force parameter

* add EBR mechanism

* fix format

* fix: remove incorrect thread safety annotation

* remove useless test

---------

Co-authored-by: wanyue.wy <wanyue.wy@oceanbase.com>
2026-04-02 14:46:37 +08:00
Wan 46206e777e
[Store] Add orchestration script for matrix sweeps of stress_workload_test and Optimize client read path of P2P Structure (#1779)
* optimize stress workload test

* optimize p2p read path

* fix ci:
1. fix allocate buffer when read failed in get tensor
2. fix format

* trim slice

* update Get and BatchGet interfaces to accept raw buffer pointers and sizes instead of Slice objects

* fix format

* [PG] Remove CPU-only backend tests from CI (#1628)

---------

Co-authored-by: wanyue.wy <wanyue.wy@oceanbase.com>
Co-authored-by: Xun Sun <UNIDY2002@outlook.com>
2026-04-01 17:13:26 +08:00
Wan 4aa6209f8f
[Store] Add ut for P2P-Mooncake-Store (#1715)
* 1. add heartbeat intergration test
2. add QueryClientStatus and GetClientSegment interface in master service

* 1. add client manager test
2. add SyncSegmentMetaResult

* add client_meta test

* add segment manager test

* add concurrency ut for storage tier
2026-03-23 15:08:13 +08:00
EkiRui 62ef96e53b
[Store] Make tiered scheduler incremental and harden SSD tier space management (#1675)
* [Store] Make tiered scheduler incremental

Rework the tiered backend scheduler to avoid a shared global stats lock
and full keyspace scans in the background loop.

Key changes:
- shard stats collection and maintain ordered incremental indexes for
  Simple and LRU snapshots
- split recent heat and recency rank semantics in the scheduler policy
  interface
- build policy input from snapshot candidates plus fast-tier residents
  using scheduler-side metadata cache
- hook scheduler cache maintenance into TieredBackend commit/delete paths
- extend integration tests for concurrent stats, bounded snapshots, and
  wall-clock decay behavior

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Make bucket file eviction async

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Guard bucket offload with physical space

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Unify SSD backend space management

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Preallocate storage tier staging pool

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Make allocation handles own tier lifetime

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Add reclaim planner to tiered scheduler

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Unify tiered backend test init

Fold repeated TieredBackend::Init test setup into a shared helper and guard storage-tier bucket eviction with a safe runtime cast so file-per-key tests no longer crash when capacity is exceeded.

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Surface scheduler policy errors

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Fix tensor API CI mode selection

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Fix bucket offload pre-init capacity check

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Fix storage path build warnings

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Unify tiered storage accounting semantics

Clarify storage-tier live-byte accounting versus backend physical-byte accounting, reclaim per-key physical bytes on delete, and make per-key metadata scans idempotent without holding the accounting lock across the full directory walk.

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Shard scheduler key cache and surface alloc errors

Shard the scheduler key cache to reduce lock contention, preserve allocation error codes across tier fallback, and tighten storage-tier tests around explicit overflow and capacity failures.

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

---------

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2026-03-23 14:01:24 +08:00
Wan 5e746c1f01
[Store] Implement the client service and add adaptation to the P2P architecture for the interface on the client side (#1631)
* 1. update client name to client service
2. clang format

* 1. implement ClientConfig for build client service
2. split client service based on architecture

* 1. add graceful shutdown mechanism for store components
2. fix some ut bugs about param or return value type
3. fix signal ignore in seperatly deployment mode

* modify stress workload test to adapt p2p mode

* 1. rename some class and add some comment
2. move dummy_client_monitor start logic to real_client and add stop logic for it

* add in-flight request check

* 1. optimize GetLocal() function
2. rename some var and refactor some code format

* add stream output for replica descriptor

* fix ut bug

* replace dynamic_cast with static_cast

* fix typo

* fix format

* fix ci

* 1. fix error code typo of p2p ut
2. fix destory bug of centralized client service

---------

Co-authored-by: wanyue.wy <wanyue.wy@oceanbase.com>
2026-03-17 16:17:35 +08:00
EkiRui f036f3d266
[Store] Add SSD tier support with LRU scheduler and thread-safe implementation (#1493)
* [Store]: Add ssd tier support

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store]: add client scheduler support

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store]: Add CAS support for tiered backend

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Add LRU policy and enhance stats collector

- Add LRUPolicy: watermark-based promotion and eviction
  - Promotion limited by capacity budget (up to low_watermark)
  - Eviction triggered when usage exceeds high_watermark
  - Support EVICT (delete replica) and MIGRATE (move to slow tier)

- Add LRUStatsCollector: maintains LRU ordered key list
  - O(1) access recording using list + hashmap
  - MRU keys at front, LRU keys at back

- Enhance StatsCollector interface:
  - Add RemoveKey() for cleanup on key deletion
  - Add decay factor to SimpleStatsCollector (default 0.5)
  - Prevents history loss between scheduler cycles

- Add size_bytes field to KeyContext for capacity calculations

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Integrate LRU scheduler with TieredBackend

- ClientScheduler enhancements:
  - Support JSON config for policy selection (SIMPLE/LRU)
  - Add OnDelete() callback for LRU list cleanup
  - Implement two-phase action execution (EVICT first, then MIGRATE)
  - Handle NO_AVAILABLE_HANDLE error for insufficient space

- TieredBackend integration:
  - Pass config to ClientScheduler constructor
  - Add OnAccess() call in Commit()
  - Add OnDelete() call in Delete()
  - Add capacity pre-check in Transfer()

- Add integration tests:
  - TestLRUCacheThrashing: end-to-end hot/cold data handling
  - TestLRUPromotionBudget: verify capacity-limited promotion
  - TestLRUEviction: verify cold data eviction

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Improve StorageTier capacity tracking and thread safety

- Add capacity parameter to StorageTier constructor
- GetCapacity() now uses configured capacity or falls back to storage backend config
- Add persisted_size_ tracking for data written to disk
- GetUsage() returns pending_batch_size_ + persisted_size_
- Make pending_batch_size_ atomic for thread-safe GetUsage()
- Add IsPersisted() method to StorageBuffer
- Free() now properly updates persisted_size_ when freeing persisted data

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Reorganize tier files into tiers/ subdirectory

- Move all tier-related files to tiered_cache/tiers/ subdirectory:
  - cache_tier.h (base class)
  - dram_tier.h/cpp
  - storage_tier.h/cpp
  - ascend_tier.h/cpp

- Merge disk_buffer.h into storage_tier.h (StorageBuffer class)

- Update all include paths to use new tiers/ location

- Update CMakeLists.txt for new file locations

This improves code organization by grouping all tier implementations
in a dedicated subdirectory.

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Make StorageBuffer::is_on_disk_ atomic for thread safety

Change is_on_disk_ from plain bool to std::atomic<bool> with proper
memory ordering (acquire/release) to prevent data races between
Persist() and concurrent read operations like ReadTo().

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Refactor LRU policy to use global heat ranking

Redesign LRU scheduling algorithm:
- Sort all keys by heat score globally (not per-tier)
- Select hottest keys to fill fast tier up to low_watermark (70%)
- Generate evict/promote actions by comparing current vs target state

This ensures fast tier always contains the globally hottest keys,
and usage stabilizes at low_watermark after scheduling.

Update tests to verify:
- All hot keys are retained in DRAM
- Total keys in DRAM matches expected slots
- Cold keys only fill remaining slots after hot keys

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Fix use-after-free in StorageTier::FlushInternal

Add is_flushing_ flag to StorageBuffer to prevent Free() from
destroying a buffer while FlushInternal() is using it.

Race condition fixed:
1. FlushInternal() snapshots buffers and clears pending_batch_
2. FlushInternal() unlocks and starts IO
3. Free() called - buffer not in pending_batch_, not yet persisted
4. Free() destroys buffer -> FlushInternal() crashes on Persist()

Fix:
- FlushInternal() sets is_flushing_=true before unlock
- Free() calls WaitForFlushComplete() to wait for flush
- FlushInternal() sets is_flushing_=false after Persist()

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Fix data race in StorageBuffer between Persist and ReadTo

Add data_mutex_ to protect data_ vector access. Without this lock,
Persist() and ReadTo() can race:
- ReadTo() checks is_on_disk_=false, starts reading data_
- Persist() clears data_ and sets is_on_disk_=true
- ReadTo() accesses cleared data_ -> undefined behavior

The TieredBackend per-key lock doesn't prevent this because
FlushInternal() operates at StorageTier level without acquiring
TieredBackend locks.

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] fix UAF race, CAS ordering, flush recovery, copier TOCTOU

- StorageTier: replace spin-wait WaitForFlushComplete with
  condition_variable under batch_mutex_, eliminating the TOCTOU
  window between Free() and FlushInternal()
- StorageTier: FlushInternal restores pending_batch_ on IO failure
  instead of silently dropping entries
- TieredBackend::Commit: validate CAS version before tier->Commit()
  and metadata_sync_callback() to prevent side effects on stale writes,
  with re-check under entry write lock for concurrent race safety
- TieredBackend::Get: remove redundant out_version write
- DRAM->NVME copier: snapshot dst.buffer->data() once to avoid
  TOCTOU with concurrent Persist()
- Add regression tests: CASFailureNoSideEffects,
  CASFailureNoCallbackInvoked, ConcurrentFlushDeleteStress

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Add sync eviction mode and strict allocation with single-tier support

Implement configurable eviction modes (SYNC/ASYNC) and strict allocation
parameter to trigger immediate eviction on allocation failure. Fix single-tier
configuration to properly evict keys via DELETE action when no slow tier exists.

Key changes:
- Add EvictionMode enum (SYNC/ASYNC) to ClientScheduler with JSON config support
- Add strict parameter to TieredBackend::Allocate for tier-specific allocation
- Implement TriggerSyncEviction to immediately free space on allocation failure
- Fix LRU policy to generate DELETE actions for single-tier eviction scenarios
- Add comprehensive tests for capacity limits, sync eviction, and single-tier setup

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Add bucket-level eviction with fragmentation tracking

Implement bucket eviction functionality to reclaim space from fragmented
or old buckets in the storage tier. The eviction strategy prioritizes
buckets with >50% fragmentation, falling back to LRU for non-fragmented
buckets.

Key changes:
- Add SelectBucketForEviction() and EvictBucket() methods to BucketStorageBackend
- Track valid key count per bucket for fragmentation calculation
- Implement MarkKeyDeleted() to update fragmentation metrics on key deletion
- Add TriggerBucketEviction() to StorageTier for manual eviction
- Move Storage Tier tests to dedicated storage_tier_test.cpp file
- Add BucketEviction test to verify eviction functionality

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

* [Store] Improve bucket eviction with target-size and auto-eviction

    Enhance bucket eviction to support target-based eviction and automatic
    triggering when storage capacity is exceeded.

    Key improvements:
    - EvictBucket() now returns freed space size instead of void
    - TriggerBucketEviction() accepts target_free_size parameter
    - Loop eviction until target size is met (max 10 attempts)
    - Auto-trigger eviction in Allocate() when capacity exceeded
    - Update persisted_size_ after eviction to reflect freed space
    - Add AutoEvictionOnCapacityExceeded test

    This addresses the issue where single bucket eviction may not free
    enough space, and enables automatic space reclamation when the
    scheduler encounters capacity constraints.

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>

---------

Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2026-03-05 21:28:15 +08:00
Wan 9acd5689a0
[Store] implement p2p master service (#1611)
Co-authored-by: wanyue.wy <wanyue.wy@oceanbase.com>
2026-03-04 22:49:21 +08:00
Wan 48d3a47cdd
[Store]Refactor hierarchy MasterService and heartbeat mechanism 2026-03-04 14:42:59 +08:00
Shelron 80942798a3
[Store][Feature]Implement Remote data transfer in DataManager and PeerClient (#1428)
* Implement TransferDataToRemote and TransferDataFromRemote in DataManager

* resolve reviews

* parallel transfer request submit

* loopback

* [Store] Implement PeerClient async RPC and RDMA performance tests

Implement AsyncReadRemoteData/AsyncWriteRemoteData coroutine interfaces
for PeerClient, enabling concurrent RPC calls via collectAllPara. Add
comprehensive performance benchmarks comparing async vs sync at varying
concurrency levels, plus RDMA-enabled tests that measure real data
transfer throughput across different buffer sizes (4KB-1MB).

Key changes:
- peer_client.h/cpp: Add async single-key RPC interfaces using
  async_simple::coro::Lazy, implement Connect with client pool,
  wire sync/batch methods to delegate to async+syncAwait
- peer_client_perf_test.cpp: RPC-only benchmarks (PeerClientPerfTest)
  and RDMA data transfer benchmarks (PeerClientRdmaPerfTest) with
  MC_RDMA_DEVICE env var for single-NIC loopback filtering
- peer_client_test.cpp: Unit tests for PeerClient interfaces
- CMakeLists.txt: Add new test targets, temporarily exclude
  master_client.cpp due to yalantinglibs v0.5.6 incompatibility

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* [CI/Build] Re-enable master_client.cpp and centralized_master_client.cpp

These files were temporarily excluded due to a compilation error with
GCC 10 where yalantinglibs' util::is_invocable cannot handle abstract
class types (WrappedMasterService). The issue does not occur with
GCC 11+ which is the target build environment.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* [Store] Enhance RDMA perf tests: warmup, windowed concurrency, batch comparison

- Add warmup rounds to all RDMA tests to eliminate cold-start effects
  (QP connection setup, path resolution) that skewed measurements
- Add window parameter to RunRdmaAsyncReads/Writes for windowed
  concurrency via collectAllWindowedPara
- Add RdmaWindowedConcurrency test comparing window sizes {5,10,25,50,ALL}
- Add batch RPC helpers and RdmaSyncAsyncBatchComparison test for
  three-way sync vs async vs batch comparison
- Change minimum test data size from 4KB to 32KB to focus on
  meaningful transfer sizes where async consistently outperforms sync

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* [Store] Use median-of-5 sampling in RDMA perf tests for stability

Add MedianOf helper that runs each benchmark point 5 times and takes
the median, filtering out sporadic outliers caused by RDMA QP
contention, coroutine scheduling jitter, or shared pod noise. This
eliminates the occasional 0.03x-0.27x anomalies that appeared
randomly across different (size, N) combinations.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* add errlog and remove batch api

---------

Co-authored-by: chenwenxiao <chenwenxiaolive@gmail.com>
Co-authored-by: qinwenzh <zhengqinwen4@huawei.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
2026-02-14 15:53:35 +08:00
Wan b363dc9073
[Store] Separate the specific metadata management logic of the centralized Master architecture for P2P architecture implement (#1518)
* move function of master service into different service file according to rpc_service

* refactor master service

* fix metric test

* Update mooncake-store/include/centralized_master_client.h

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update mooncake-store/include/centralized_master_client.h

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update mooncake-store/src/centralized_master_service.cpp

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix clangd format

---------

Co-authored-by: wanyue.wy <wanyue.wy@oceanbase.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-09 16:07:14 +08:00
chenwenxiaolive 034dc124d6 [Store] fix: Move variable declarations into #ifdef to fix unused variable warnings
Move dest_ptr and src_ptr declarations into USE_ASCEND_CACHE_TIER block
to avoid unused variable warnings when the macro is not defined.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive de863db9de [Store] fix: Fix unreachable code in CopyAscendToDram and CopyDramToAscend
Move return statements into #ifdef branch to avoid unreachable code
warning when USE_ASCEND_CACHE_TIER is not defined.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive e4f4128faf [Store] style: Apply clang-format-20 formatting
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive d335215b0b [Store] fix: Move return statement to avoid unreachable code warning
Move is_initialized_ assignment and return into #ifdef branch to avoid
compiler warning about unreachable code after return in #else branch.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive 591bb9edcc [Store] fix: Return error instead of fallback when USE_ASCEND_CACHE_TIER not defined
Remove fallback mode that silently degrades to host memory when
USE_ASCEND_CACHE_TIER is not enabled. Now Init() returns INTERNAL_ERROR
with a clear message asking user to rebuild with the correct flag.

This prevents unexpected behavior when user configures ASCEND_NPU tier
but compiles without enabling the feature.

Changes:
- Init(): Return error instead of initializing in fallback mode
- AllocateDeviceMemory(): Return nullptr instead of using malloc
- ReleaseMemory(): Log error instead of calling free
- CopyAscendToDram/CopyDramToAscend(): Return error instead of memcpy

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
qinwenzh d8918d509e [Store][Fix] Fix ascend_tier_test: use DataCopier for copy verification
The CopyData API was changed to use key-based interface. Update
CopyAscendToDramWithVerification test to use DataCopier.Copy() directly
with a non-owning BufferRef wrapper.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-03 20:38:47 +08:00
chenwenxiaolive 1717e928ae [Store] feat: Add ACL framework cleanup with aclFinalize
- Register std::atexit cleanup function to call aclFinalize
- Ensures proper resource cleanup on program exit
- Prevents resource leak warnings from analysis tools
- Addresses Gemini Code Assist review feedback

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive 2163ab20ed [Store] test: Add CopyAscendToDram verification test
- Add CopyAscendToDramWithVerification test case
- Test validates Ascend->DRAM copy with data verification
- Addresses Gemini Code Assist review feedback

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive 899a46e07a [Store] fix: Correct AclMemcpyWithDevice dst_size and data() return value
- Fix AclMemcpyWithDevice to use dst.buffer->size() for dst_size parameter
  (should be max allocated size, not copy size per ACL API spec)
- Change AscendBuffer::data() to return device_ptr instead of
  AscendUnifiedPointer address for standard BufferBase semantics
- Update documentation to reflect data() behavior change

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive 73a0585bb4 [Store] refactor: Reduce code duplication in copy functions
- Remove redundant null check for backend_ assignment
- Extract common validation logic outside #ifdef blocks in
  CopyAscendToDram and CopyDramToAscend
- Reduce code duplication by 60 lines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive 7d38918fa8 [Store] refactor: Use type-safe cast in copy functions
- Replace reinterpret_cast with dynamic_cast + GetUnifiedPointer()
  in CopyAscendToDram and CopyDramToAscend
- Add explicit type checking for AscendBuffer
- Improve error messages for invalid buffer types

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive b7957ee2f1 [Store] style: Add defensive null check before backend_ assignment
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive 95e36ea5e3 [Store] test: Fix test assertions and comments in ascend_tier_test
- Add conditional assertions for invalid device ID test based on
  USE_ASCEND_CACHE_TIER macro
- Fix misleading comments in DestructorCleansUpAllocations test
- Add usage verification after cleanup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive 39af9953af [Store] fix: Add missing .get() calls for unique_ptr in AscendBuffer
- Fix data() to call unified_ptr_.get() before casting
- Fix GetUnifiedPointer() to return unified_ptr_.get()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive e9c82477d3 [Store] refactor: Use std::unique_ptr for AscendUnifiedPointer ownership
- Change AscendBuffer to accept std::unique_ptr<AscendUnifiedPointer>
- Change unified_ptr_ member from raw pointer to std::unique_ptr
- Update AllocateDeviceMemory to return std::unique_ptr
- Simplify move semantics using std::move
- Use unified_ptr_.reset() instead of manual delete
- Update tests to use std::make_unique

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive dd94ad25e7 [Store] style: Apply clang-format-20 to Ascend tier code
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive 949c9f6bf0 [Store][Fix] Add aclInit() call before using ACL functions
The ascend_tier_test was crashing because ACL functions like
aclrtGetDeviceCount() and aclrtSetDevice() were being called
without first initializing the ACL framework via aclInit().

This fix adds a thread-safe static initialization of ACL in
AscendCacheTier::Init() that:
- Calls aclInit() exactly once across all instances
- Handles ACL_ERROR_REPEAT_INITIALIZE gracefully if ACL is
  already initialized by other components
- Returns an error if ACL initialization fails

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-03 20:38:47 +08:00
chenwenxiaolive 254e51aab9 [Store][Fix] Fix ascend_tier_test: remove non-existent TieredBackend::Read call
TieredBackend doesn't have a Read method. Updated the
CopyBetweenAscendTiersSameDevice test to verify allocation tier
type instead of attempting to read data.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-03 20:38:47 +08:00
chenwenxiaolive 9d9028b467 [Store][Fix] Fix thread safety issues in AscendCacheTier
- Fix race condition in Allocate() using compare-and-swap (CAS) operation
  to atomically check and reserve space before device memory allocation
- Upgrade memory ordering from relaxed to acquire/release for proper
  cross-thread visibility of usage counter updates
- Add rollback logic when device memory allocation fails after space
  reservation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
chenwenxiaolive 7ca1b447ae [Store][Feature] Add Ascend NPU cache tier support
Add AscendCacheTier implementation for Huawei Ascend NPU devices:

- Add ASCEND_NPU to MemoryType enum
- Implement AscendBuffer (RAII wrapper for device memory)
- Implement AscendCacheTier (Allocate/Free API)
- Register CopyAscendToDram and CopyDramToAscend copy functions
- Add TieredBackend integration for ASCEND_NPU tier type
- Add conditional compilation with USE_ASCEND_CACHE_TIER option
- Add comprehensive unit tests

Build with Ascend support:
  cmake .. -DUSE_ASCEND_CACHE_TIER=ON

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 20:38:47 +08:00
Xun Sun d31ebffe95 [CI] Support torch==2.10.0 (#1420) 2026-02-03 20:36:49 +08:00
Shelron ca16744df1
[Store][Feature]Introduce DataManager for unified local + remote data access (#1347)
* client rpc接口定义和data_manager初步实现

* 解决编译问题

* 补充头文件依赖

* 删除多余代码

* client集成改为optional

* add data manager test and adapt to new DataSource

* stub implementation and format change

* clang-formatting

* add lock contention test

* clang-format code style

* implement client rpc service

* Add tests for read lock removal safety verification

Added two test cases to verify that removing read locks from
DataManager::Get and ReadRemoteData is safe:
- ConcurrentGetAndDelete: Tests concurrent Get/Delete operations
- HandleKeepsDataAliveAfterDelete: Verifies handle keeps data alive after deletion

These tests validate that TieredBackend's internal locking and shared_ptr
reference counting provide sufficient thread safety.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* follow clang-format

* follow clang-format

---------

Co-authored-by: ccccccxy <chenxiaoyan32@huawei.com>
Co-authored-by: chenwenxiaolive <chenwenxiaolive@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 17:56:40 +08:00
Wan 3bb98aae29
Merge pull request #1362 from wanyue-wy/uncouple_master
[Store][Feature]Extract common interfaces of rpc_service and master_client for the P2P architecture
2026-01-13 19:43:43 +08:00
wanyue.wy e27a814878 Divide the RPC-related interfaces for P2P and Centralization architectures 2026-01-13 09:23:23 +00:00
Wan bd71efe05a
Merge pull request #1314 from openanolis/xinyi/tiered-backend-next
[Store] Tiered Backend add Dram tier support
2026-01-08 12:50:29 +08:00
Xingrui Yi 751d5f6122 [Store] tiered backend remove FreeInternal func
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2026-01-08 11:54:26 +08:00
Xingrui Yi 90db6b6199 [Store] update tiered backend init return code
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2026-01-08 11:49:20 +08:00
Xingrui Yi 80f5659348 [Store] Add wait handle logic in dram iter exit
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2026-01-08 11:44:11 +08:00
Xingrui Yi 81d673979c [Store] change uuid to CacheTier* in TieredLocation
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2026-01-06 17:11:59 +08:00
Xingrui Yi b40bb82830 [Store] optimize cache tier and backend api
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2026-01-06 15:24:12 +08:00
Xingrui Yi 925fb18b11 [Store] Optimize TempDRAMBuffer with RAII memory management
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2025-12-31 17:28:28 +08:00
Xingrui Yi 6691a9856a [Store]: add unit test for tiered backend
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2025-12-31 17:28:28 +08:00
Xingrui Yi fee0642df6 [Store]: add dram tier support
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2025-12-31 17:28:21 +08:00
wanyue.wy 8e85cd4881 remove segment manager 2025-12-31 06:48:21 +00:00
Wan 6ec3581c3f
Merge pull request #1268 from wanyue-wy/uncoupled_client_segment_meta
[Store] feat Uncouple client segment meta from allocator
2025-12-31 10:58:10 +08:00
Xingrui Yi 6fb4a84e6e add CI for P2P-Mooncake-Store
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2025-12-30 16:52:22 +08:00
Wan 4af9267051
Merge pull request #1279 from openanolis/xinyi/tiered-backend-next
[Store]: Update metadata callback sync strategy for tiered backend
2025-12-30 16:10:57 +08:00
Xingrui Yi 45c5f2c62d [Store]: add tiered backend api return error code support
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2025-12-30 16:08:49 +08:00
wanyue.wy 58110cfe09 fix some details 2025-12-30 07:31:00 +00:00
wanyue.wy 2c8ffdc20e clang format 2025-12-30 07:31:00 +00:00
wanyue.wy b4977b916b fix pure virtual function call in construct function 2025-12-30 07:31:00 +00:00
wanyue.wy 880c5eff3a 1. implement segment manager
2. implement client manager
2025-12-30 07:31:00 +00:00
wanyue.wy 4632f72fe9 rename segment manager 2025-12-30 07:31:00 +00:00
Xingrui Yi 509b554da0 [Store]: Update metadata callback sync strategy for tiered backend
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2025-12-29 11:20:52 +08:00
EkiRui 3beb69812b
[Store] feat: introduce tired backend (#1271)
Signed-off-by: Xingrui Yi <yixingrui@linux.alibaba.com>
2025-12-25 13:54:04 +08:00
167 changed files with 43586 additions and 7837 deletions

View File

@ -25,5 +25,6 @@
## Checklist
- [ ] I have performed a self-review of my own code.
- [ ] I have formatted my own code using `./scripts/code_format.sh` before submitting.
- [ ] I have updated the documentation.
- [ ] I have added tests to prove my changes are effective.

View File

@ -2,9 +2,9 @@ name: 'Build & Test (Linux)'
on:
push:
branches: [ "main" ]
branches: [ "main" , "P2P-Mooncake-Store"]
pull_request:
branches: [ "main" ]
branches: [ "main" , "P2P-Mooncake-Store"]
types: [opened, synchronize, reopened, labeled]
@ -70,17 +70,18 @@ jobs:
- name: Configure project
run: |
sudo apt update -y
sudo apt install -y ninja-build
sudo bash -x dependencies.sh -y
mkdir build
cd build
cmake .. -DUSE_HTTP=ON -DUSE_ETCD=ON -DSTORE_USE_ETCD=ON -DENABLE_ASAN=ON -DENABLE_SCCACHE=ON
cmake -G Ninja .. -DUSE_HTTP=ON -DUSE_CXL=ON -DUSE_ETCD=ON -DSTORE_USE_ETCD=ON -DENABLE_ASAN=ON -DENABLE_SCCACHE=ON -DCMAKE_BUILD_TYPE=Debug
shell: bash
- name: Build project
run: |
cd build
make -j
sudo make install
cmake --build .
sudo cmake --install .
shell: bash
- name: Build nvlink_allocator.so
@ -103,7 +104,7 @@ jobs:
cd build
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
ldconfig -v || echo "always continue"
MC_METADATA_SERVER=http://127.0.0.1:8080/metadata DEFAULT_KV_LEASE_TTL=500 make test -j ARGS="-V"
MC_METADATA_SERVER=http://127.0.0.1:8080/metadata DEFAULT_KV_LEASE_TTL=500 ctest -j --output-on-failure
shell: bash
- name: Generate Python version tag
@ -135,18 +136,19 @@ jobs:
- name: Configure project
run: |
apt update -y
apt install -y ninja-build
bash -x dependencies.sh -y
mkdir build
cd build
cmake .. -DUSE_MUSA=ON -DUSE_MNNVL=ON -DUSE_ETCD=ON -DSTORE_USE_ETCD=ON -DUSE_CXL=ON -DUSE_TCP=ON -DBUILD_UNIT_TESTS=OFF -DBUILD_EXAMPLES=OFF
cmake -G Ninja .. -DUSE_MUSA=ON -DUSE_MNNVL=ON -DUSE_ETCD=ON -DSTORE_USE_ETCD=ON -DUSE_CXL=ON -DUSE_TCP=ON -DBUILD_UNIT_TESTS=OFF -DBUILD_EXAMPLES=OFF
shell: bash
- name: Build project
run: |
cd build
source ~/.bashrc
make -j
make install
cmake --build .
cmake --install .
shell: bash
test-wheel-ubuntu:
@ -238,7 +240,7 @@ jobs:
LOCAL_HOSTNAME: "127.0.0.1"
run: |
source test_env/bin/activate
python scripts/test_tensor_api.py -n 1
python scripts/test_tensor_api.py --mode perf --iterations 1
shell: bash
- name: Run RPC Communicator Bandwidth Test
@ -251,17 +253,6 @@ jobs:
kill $SERVER_PID 2>/dev/null || true
wait $SERVER_PID 2>/dev/null || true
- name: Test Mooncake EP Backend (CPU Only)
env:
MC_FORCE_TCP: "true"
run: |
source test_env/bin/activate
python -m unittest mooncake-wheel.tests.test_mooncake_backend_cpu
# Disable these tests in CI as they fail occasionally.
# python -m unittest mooncake-wheel.tests.test_mooncake_backend_elastic
# python -m unittest mooncake-wheel.tests.test_mooncake_backend_p2p_cpu
shell: bash
test-sglang-integration:
needs: build-flags
runs-on: ubuntu-latest
@ -329,7 +320,7 @@ jobs:
python-version: ['3.10', '3.12']
env:
BUILD_WITH_EP: "1"
EP_TORCH_VERSIONS: "2.8.0;2.9.0;2.9.1"
EP_TORCH_VERSIONS: "2.9.0;2.9.1;2.10.0"
TORCH_CUDA_ARCH_LIST: "8.0;9.0"
SCCACHE_GHA_ENABLED: "true"
@ -375,6 +366,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt update -y
sudo apt install -y ninja-build
sudo bash -x dependencies.sh -y
df -h
shell: bash
@ -386,9 +378,9 @@ jobs:
cd build
export PATH=/usr/local/nvidia/bin:/usr/local/nvidia/lib64:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH
cmake .. -DUSE_ETCD=OFF -DUSE_REDIS=ON -DUSE_HTTP=ON -DWITH_METRICS=ON -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DENABLE_SCCACHE=ON -DUSE_CUDA=OFF -DUSE_MNNVL=OFF -DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs"
make -j4
sudo make install
cmake -G Ninja .. -DUSE_ETCD=OFF -DUSE_CXL=ON -DUSE_REDIS=ON -DUSE_HTTP=ON -DWITH_METRICS=ON -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DENABLE_SCCACHE=ON -DUSE_CUDA=OFF -DUSE_MNNVL=OFF -DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs"
cmake --build .
sudo cmake --install .
df -h
shell: bash
@ -396,7 +388,7 @@ jobs:
run: |
mkdir build
cd build
cmake .. -DUSE_ETCD=ON -DUSE_REDIS=ON -DUSE_HTTP=ON -DWITH_STORE=ON -DWITH_P2P_STORE=ON -DWITH_EP=ON -DWITH_METRICS=ON -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DENABLE_SCCACHE=ON -DUSE_CUDA=ON -DUSE_MNNVL=OFF -DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs"
cmake -G Ninja .. -DUSE_ETCD=ON -DUSE_CXL=ON -DUSE_REDIS=ON -DUSE_HTTP=ON -DWITH_STORE=ON -DWITH_P2P_STORE=ON -DWITH_METRICS=ON -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DENABLE_SCCACHE=ON -DUSE_CUDA=ON -DUSE_MNNVL=OFF -DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs"
shell: bash
# TODO: lack USE_NVMEOF,USE_MNNVL
@ -405,15 +397,15 @@ jobs:
export LD_LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH
export LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LIBRARY_PATH
cd build
make -j4
sudo make install
cmake --build .
sudo cmake --install .
df -h
shell: bash
- name: Configure project with unit tests and examples
run: |
cd build
cmake .. -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DENABLE_SCCACHE=ON
cmake -G Ninja .. -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DENABLE_SCCACHE=ON
shell: bash
# TODO: lack WITH_RUST_EXAMPLE
@ -422,15 +414,15 @@ jobs:
export LD_LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH
export LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LIBRARY_PATH
cd build
make -j4
sudo make install
cmake --build .
sudo cmake --install .
shell: bash
- name: Configure project
run: |
cd build
rm -r */tests
cmake .. -DBUILD_UNIT_TESTS=OFF -DBUILD_EXAMPLES=OFF -DUSE_HTTP=ON -DENABLE_SCCACHE=ON
cmake -G Ninja .. -DBUILD_UNIT_TESTS=OFF -DBUILD_EXAMPLES=OFF -DUSE_HTTP=ON -DENABLE_SCCACHE=ON -DUSE_CXL=ON -DWITH_EP=ON -DEP_TORCH_VERSIONS="2.9.0;2.9.1;2.10.0"
shell: bash
- name: Build project
@ -438,8 +430,8 @@ jobs:
export LD_LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH
export LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LIBRARY_PATH
cd build
make -j4
sudo make install
cmake --build .
sudo cmake --install .
shell: bash
- name: Build nvlink_allocator.so
@ -503,12 +495,16 @@ jobs:
name: Check code format
if: >-
github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
github.event.action == 'opened' ||
contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-22.04
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Need full history for branch comparison
persist-credentials: false
- name: Install clang-format 20
run: |
@ -517,17 +513,31 @@ jobs:
sudo ./llvm.sh 20
sudo apt-get install -y clang-format-20
- name: run clang-format-20
- name: Check code format
run: |
# the old clang-format-14 which is the default version in ubuntu 22.04,
# is inconsistent with clang-format-20.
ls -lh /usr/bin/clang-format*
clang-format --version
clang-format-20 --version
# skip cachelib_memory_allocator
find . -type f \( -name "*.h" -o -name "*.cpp" \) | grep -v cachelib_memory_allocator | xargs clang-format-20 -style=file -i
if ! git diff --exit-code; then
echo "Please follow the .clang-format code style, try clang-format -i FILENAME"
# Check script exists and is executable
if [[ ! -x ./scripts/code_format.sh ]]; then
echo "Error: code_format.sh not found or not executable"
exit 1
fi
# Determine base ref for comparison
if [ "${{ github.event_name }}" == "pull_request" ]; then
# For PRs: compare against the target branch
BASE_REF="origin/${{ github.base_ref }}"
else
# For push events: use github.event.before to handle multi-commit pushes
BEFORE_SHA="${{ github.event.before }}"
if [ "${BEFORE_SHA}" == "0000000000000000000000000000000000000000" ]; then
# New branch push, compare against default branch
BASE_REF="origin/${{ github.event.repository.default_branch }}"
else
# Normal push (single or multiple commits)
BASE_REF="${BEFORE_SHA}"
fi
fi
echo "Comparing against: ${BASE_REF}"
./scripts/code_format.sh --check --base "${BASE_REF}"
shell: bash

128
.github/workflows/ci_cu13.yml vendored Normal file
View File

@ -0,0 +1,128 @@
name: 'Build Wheel (CUDA 13)'
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
types: [opened, synchronize, reopened, labeled]
jobs:
build-wheel-cu13:
if: >-
github.event_name == 'push' ||
github.event.action == 'opened' ||
contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ['3.10', '3.12']
env:
BUILD_WITH_EP: "1"
EP_TORCH_VERSIONS: "2.9.0;2.9.1;2.10.0"
TORCH_CUDA_ARCH_LIST: "8.0;9.0"
SCCACHE_GHA_ENABLED: "true"
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Free up disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo rm -rf /usr/local/lib/android
df -h
- name: Install CUDA Toolkit
uses: Jimver/cuda-toolkit@v0.2.29
with:
cuda: '13.0.2'
linux-local-args: '["--toolkit"]'
method: 'network'
sub-packages: '["nvcc", "nvrtc-dev"]'
non-cuda-sub-packages: '["libcusparse-dev", "libcublas-dev", "libcusolver-dev"]'
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.9
- name: Configure sccache
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Run sccache stat for check
shell: bash
run: ${SCCACHE_PATH} --show-stats
- name: Install dependencies
run: |
sudo apt update -y
sudo apt install -y ninja-build
sudo bash -x dependencies.sh -y
df -h
shell: bash
- name: Configure project
run: |
mkdir build
cd build
cmake -G Ninja .. \
-DUSE_ETCD=ON \
-DUSE_REDIS=ON \
-DUSE_HTTP=ON \
-DWITH_STORE=ON \
-DWITH_P2P_STORE=ON \
-DWITH_EP=ON \
-DWITH_METRICS=ON \
-DBUILD_UNIT_TESTS=OFF \
-DBUILD_EXAMPLES=ON \
-DENABLE_SCCACHE=ON \
-DBUILD_BENCHMARK=ON \
-DUSE_CUDA=ON \
-DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs"
shell: bash
- name: Build project
run: |
export LD_LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH
export LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LIBRARY_PATH
cd build
cmake --build .
sudo cmake --install .
df -h
shell: bash
- name: Build nvlink_allocator.so
run: |
mkdir -p build/mooncake-transfer-engine/nvlink-allocator
cd mooncake-transfer-engine/nvlink-allocator
export PATH=/usr/local/nvidia/bin:/usr/local/nvidia/lib64:$PATH
export LD_LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH
export LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LIBRARY_PATH
bash build.sh ../../build/mooncake-transfer-engine/nvlink-allocator/
shell: bash
- name: Generate Python version tag
id: generate_tag
run: |
echo "python_version_tag=$(echo ${{ matrix.python-version }} | tr -d '.')" >> $GITHUB_OUTPUT
shell: bash
- name: Build Python wheel
run: |
PYTHON_VERSION=${{ matrix.python-version }} OUTPUT_DIR=dist-py${{ steps.generate_tag.outputs.python_version_tag }} ./scripts/build_wheel.sh
shell: bash
- name: Upload Python wheel artifact
uses: actions/upload-artifact@v4
with:
name: mooncake-wheel-cu130-ubuntu-py${{ steps.generate_tag.outputs.python_version_tag }}
path: mooncake-wheel/dist-py${{ steps.generate_tag.outputs.python_version_tag }}/*.whl

View File

@ -17,7 +17,7 @@ jobs:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
env:
BUILD_WITH_EP: "1"
EP_TORCH_VERSIONS: "2.8.0;2.9.0;2.9.1"
EP_TORCH_VERSIONS: "2.9.0;2.9.1;2.10.0"
TORCH_CUDA_ARCH_LIST: "8.0;9.0"
steps:
- name: Checkout source

View File

@ -40,7 +40,7 @@ repos:
args: ['--ignore-words-list=te,mooncake,KVCache']
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v19.1.0
rev: v20.1.8
hooks:
- id: clang-format
files: '\.(c|cc|cpp|cxx|h|hpp)$'

View File

@ -41,6 +41,7 @@ if (STORE_USE_ETCD)
endif()
option(STORE_USE_JEMALLOC "Use jemalloc in mooncake store master" OFF)
option(USE_ASCEND_CACHE_TIER "Enable Ascend NPU cache tier support" OFF)
add_subdirectory(mooncake-common)
include_directories(mooncake-common/etcd)

View File

@ -102,6 +102,7 @@ echo -e "${YELLOW}This may take a few minutes...${NC}"
SYSTEM_PACKAGES="build-essential \
cmake \
ninja-build \
git \
wget \
libibverbs-dev \

View File

@ -40,6 +40,9 @@ add_definitions(-DCONFIG_ERDMA)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Memory-aware build parallelism (compile vs. link job pools)
include(${CMAKE_CURRENT_LIST_DIR}/limit_jobs.cmake)
option(ENABLE_SCCACHE "Whether to open sccache" OFF)
if (ENABLE_SCCACHE)
find_program(SCCACHE sccache REQUIRED)

View File

@ -0,0 +1,86 @@
# limit_jobs.cmake Memory-aware build parallelism
#
# Auto-detects available memory and CPU count, calculates safe parallel job
# limits for compilation and linking separately. With Ninja, creates job pools
# so compilation uses many cores while memory-heavy linking is restricted.
#
# User overrides (cmake -D...):
# PARALLEL_COMPILE_JOBS override compile parallelism
# PARALLEL_LINK_JOBS override link parallelism
# MAX_COMPILER_MEMORY_MB per-compile-job memory estimate (default: 1500)
# MAX_LINKER_MEMORY_MB per-link-job memory estimate (default: 4000)
set(MAX_COMPILER_MEMORY_MB "1500" CACHE STRING
"Estimated peak memory per compile job in MB")
set(MAX_LINKER_MEMORY_MB "4000" CACHE STRING
"Estimated peak memory per link job in MB")
# Guard against invalid user input (division by zero)
if(MAX_COMPILER_MEMORY_MB LESS_EQUAL 0)
message(WARNING "[limit_jobs] MAX_COMPILER_MEMORY_MB=${MAX_COMPILER_MEMORY_MB} "
"invalid, falling back to 1500")
set(MAX_COMPILER_MEMORY_MB 1500 CACHE STRING
"Estimated peak memory per compile job in MB" FORCE)
endif()
if(MAX_LINKER_MEMORY_MB LESS_EQUAL 0)
message(WARNING "[limit_jobs] MAX_LINKER_MEMORY_MB=${MAX_LINKER_MEMORY_MB} "
"invalid, falling back to 4000")
set(MAX_LINKER_MEMORY_MB 4000 CACHE STRING
"Estimated peak memory per link job in MB" FORCE)
endif()
# Detect system resources
cmake_host_system_information(RESULT _available_mem_mb
QUERY AVAILABLE_PHYSICAL_MEMORY)
cmake_host_system_information(RESULT _nproc
QUERY NUMBER_OF_LOGICAL_CORES)
message(STATUS "[limit_jobs] Available memory: ${_available_mem_mb} MB, "
"CPU cores: ${_nproc}")
# Calculate safe parallel jobs from memory
math(EXPR _compile_jobs "${_available_mem_mb} / ${MAX_COMPILER_MEMORY_MB}")
math(EXPR _link_jobs "${_available_mem_mb} / ${MAX_LINKER_MEMORY_MB}")
# Clamp: [1, nproc]
if(_compile_jobs LESS 1)
set(_compile_jobs 1)
endif()
if(_compile_jobs GREATER _nproc)
set(_compile_jobs ${_nproc})
endif()
if(_link_jobs LESS 1)
set(_link_jobs 1)
endif()
if(_link_jobs GREATER _nproc)
set(_link_jobs ${_nproc})
endif()
# Use auto-detected values unless user explicitly overrides with -D
if(NOT DEFINED PARALLEL_COMPILE_JOBS)
set(PARALLEL_COMPILE_JOBS "${_compile_jobs}")
endif()
if(NOT DEFINED PARALLEL_LINK_JOBS)
set(PARALLEL_LINK_JOBS "${_link_jobs}")
endif()
message(STATUS "[limit_jobs] Compile jobs: ${PARALLEL_COMPILE_JOBS} "
"(${MAX_COMPILER_MEMORY_MB} MB/job), "
"Link jobs: ${PARALLEL_LINK_JOBS} (${MAX_LINKER_MEMORY_MB} MB/job)")
# Apply to build system
if(CMAKE_GENERATOR MATCHES "Ninja")
set_property(GLOBAL APPEND PROPERTY JOB_POOLS
compile_pool=${PARALLEL_COMPILE_JOBS}
link_pool=${PARALLEL_LINK_JOBS}
)
set(CMAKE_JOB_POOL_COMPILE "compile_pool" CACHE STRING "" FORCE)
set(CMAKE_JOB_POOL_LINK "link_pool" CACHE STRING "" FORCE)
message(STATUS "[limit_jobs] Ninja job pools: "
"compile=${PARALLEL_COMPILE_JOBS}, link=${PARALLEL_LINK_JOBS}")
else()
message(STATUS "[limit_jobs] Hint: use -G Ninja for automatic "
"compile/link parallelism separation")
message(STATUS "[limit_jobs] With Make, recommend: "
"cmake --build . -j${PARALLEL_LINK_JOBS}")
endif()

File diff suppressed because it is too large Load Diff

View File

@ -20,11 +20,12 @@
#include <random>
#include <thread>
#include <vector>
#include <future>
#include "gflags/gflags.h"
#include "glog/logging.h"
#include "master_client.h"
#include "centralized_master_client.h"
// Size units for better readability
static constexpr size_t KiB = 1024;
@ -56,7 +57,7 @@ class SegmentClient {
public:
SegmentClient(const std::string& name, const std::string& master_server,
uintptr_t segment_base, uint64_t segment_size)
: master_client_(mooncake::generate_uuid()) {
: client_id_(mooncake::generate_uuid()), master_client_(client_id_) {
auto ec = master_client_.Connect(master_server);
if (ec != mooncake::ErrorCode::OK) {
throw std::invalid_argument("Cannot connect to master server at " +
@ -65,9 +66,9 @@ class SegmentClient {
segment_.id = mooncake::generate_uuid();
segment_.name = name;
segment_.base = segment_base;
segment_.size = segment_size;
segment_.te_endpoint = name;
segment_.extra = mooncake::CentralizedSegmentExtraData{
.base = segment_base, .te_endpoint = name};
auto mount_ec = master_client_.MountSegment(segment_);
if (!mount_ec.has_value()) {
throw std::runtime_error("Failed to mount segment " + name +
@ -86,7 +87,7 @@ class SegmentClient {
}
}
void Ping() {
void Heartbeat() {
if (remount_future_.valid() &&
remount_future_.wait_for(std::chrono::seconds(0)) ==
std::future_status::ready) {
@ -94,25 +95,31 @@ class SegmentClient {
remount_future_ = std::future<void>();
}
auto ping_result = master_client_.Ping();
if (!ping_result.has_value()) {
throw std::runtime_error("Failed to ping master server");
mooncake::HeartbeatRequest req;
req.client_id = client_id_;
auto heartbeat_result = master_client_.Heartbeat(req);
if (!heartbeat_result.has_value()) {
throw std::runtime_error("Failed to heartbeat to master server");
}
if (ping_result.value().client_status ==
mooncake::ClientStatus::NEED_REMOUNT &&
if (heartbeat_result.value().status ==
mooncake::ClientStatus::UNDEFINED &&
!remount_future_.valid()) {
remount_future_ = std::async(std::launch::async, [&]() {
auto remount_ec = master_client_.ReMountSegment({segment_});
if (!remount_ec.has_value()) {
throw std::runtime_error("Failed to remount segment");
mooncake::RegisterClientRequest req;
req.client_id = client_id_;
req.segments = {segment_};
auto reg_ec = master_client_.RegisterClient(req);
if (!reg_ec.has_value()) {
throw std::runtime_error("Failed to register client");
}
});
}
}
private:
mooncake::MasterClient master_client_;
mooncake::UUID client_id_;
mooncake::CentralizedMasterClient master_client_;
mooncake::Segment segment_;
std::future<void> remount_future_;
};
@ -349,7 +356,7 @@ class BenchClient {
}
}
mooncake::MasterClient master_client_;
mooncake::CentralizedMasterClient master_client_;
std::atomic<bool> running_;
std::vector<std::thread> threads_;
@ -377,7 +384,7 @@ int main(int argc, char** argv) {
{
std::lock_guard<std::mutex> guard(segment_clients_mutex);
for (auto& segment_client : segment_clients) {
segment_client->Ping();
segment_client->Heartbeat();
}
}
time_elapsed = std::chrono::steady_clock::now() - start_time;
@ -437,12 +444,12 @@ int main(int argc, char** argv) {
}
LOG(INFO) << "Clients stopped";
LOG(INFO) << "Stopping ping thread...";
LOG(INFO) << "Stopping heartbeat thread...";
if (ping_thread.joinable()) {
ping_thread.request_stop();
ping_thread.join();
}
LOG(INFO) << "Ping thread stopped";
LOG(INFO) << "Heartbeat thread stopped";
LOG(INFO) << "Disconnecting from master...";
bench_clients.clear();

View File

@ -30,12 +30,13 @@ class AllocatedBuffer {
struct Descriptor;
AllocatedBuffer(std::shared_ptr<BufferAllocatorBase> allocator,
void* buffer_ptr, std::size_t size,
void* buffer_ptr, std::size_t size, const UUID& segment_id,
std::optional<offset_allocator::OffsetAllocationHandle>&&
offset_handle = std::nullopt)
: allocator_(std::move(allocator)),
buffer_ptr_(buffer_ptr),
size_(size),
segment_id_(segment_id),
offset_handle_(std::move(offset_handle)) {}
~AllocatedBuffer();
@ -58,6 +59,8 @@ class AllocatedBuffer {
[[nodiscard]] std::string getSegmentName() const noexcept;
[[nodiscard]] UUID getSegmentId() const noexcept { return segment_id_; }
// Friend declaration for operator<<
friend std::ostream& operator<<(std::ostream& os,
const AllocatedBuffer& buffer);
@ -74,6 +77,7 @@ class AllocatedBuffer {
std::weak_ptr<BufferAllocatorBase> allocator_;
void* buffer_ptr_{nullptr};
std::size_t size_{0};
UUID segment_id_;
// RAII handle for buffer allocated by offset allocator
std::optional<offset_allocator::OffsetAllocationHandle> offset_handle_{
std::nullopt};
@ -92,6 +96,7 @@ class BufferAllocatorBase {
virtual size_t capacity() const = 0;
virtual size_t size() const = 0;
virtual std::string getSegmentName() const = 0;
virtual UUID getSegmentId() const = 0;
virtual std::string getTransportEndpoint() const = 0;
/**
@ -133,7 +138,8 @@ class CachelibBufferAllocator
public std::enable_shared_from_this<CachelibBufferAllocator> {
public:
CachelibBufferAllocator(std::string segment_name, size_t base, size_t size,
std::string transport_endpoint);
std::string transport_endpoint,
const UUID& segment_id);
~CachelibBufferAllocator() override;
@ -144,6 +150,7 @@ class CachelibBufferAllocator
size_t capacity() const override { return total_size_; }
size_t size() const override { return cur_size_.load(); }
std::string getSegmentName() const override { return segment_name_; }
UUID getSegmentId() const override { return segment_id_; }
std::string getTransportEndpoint() const override {
return transport_endpoint_;
}
@ -164,6 +171,7 @@ class CachelibBufferAllocator
const size_t total_size_;
std::atomic_size_t cur_size_;
const std::string transport_endpoint_;
const UUID segment_id_;
// metrics - removed allocated_bytes_ member
// ylt::metric::gauge_t* allocated_bytes_{nullptr};
@ -184,7 +192,8 @@ class OffsetBufferAllocator
public std::enable_shared_from_this<OffsetBufferAllocator> {
public:
OffsetBufferAllocator(std::string segment_name, size_t base, size_t size,
std::string transport_endpoint);
std::string transport_endpoint,
const UUID& segment_id);
~OffsetBufferAllocator() override;
@ -195,6 +204,7 @@ class OffsetBufferAllocator
size_t capacity() const override { return total_size_; }
size_t size() const override { return cur_size_.load(); }
std::string getSegmentName() const override { return segment_name_; }
UUID getSegmentId() const override { return segment_id_; }
std::string getTransportEndpoint() const override {
return transport_endpoint_;
}
@ -211,6 +221,7 @@ class OffsetBufferAllocator
const size_t total_size_;
std::atomic_size_t cur_size_;
const std::string transport_endpoint_;
const UUID segment_id_;
// offset allocator implementation
std::shared_ptr<offset_allocator::OffsetAllocator> offset_allocator_;

View File

@ -0,0 +1,220 @@
#pragma once
#include <atomic>
#include <condition_variable>
#include <exception>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <queue>
#include <stdexcept>
#include <thread>
#include <type_traits>
#include <vector>
#include <glog/logging.h>
#include "tiered_cache/tiered_backend.h"
#include "types.h"
namespace mooncake {
// ============================================================================
// LocalCopyPlan — describes a local memcpy operation
// ============================================================================
struct LocalCopyPlan {
AllocationHandle source_handle;
const char* source_ptr = nullptr;
size_t source_size = 0;
bool use_single_dest = false;
void* single_dest_ptr = nullptr;
size_t single_dest_size = 0;
std::vector<Slice> dest_slices;
};
ErrorCode ExecuteLocalCopyPlan(const LocalCopyPlan& plan);
// ============================================================================
// AsyncMemcpyExecutor
// ============================================================================
class AsyncMemcpyExecutor {
public:
template <typename ResultType>
struct BatchState {
std::vector<ResultType> results;
std::atomic<size_t> remaining{0};
std::mutex done_mutex;
std::condition_variable done_cv;
bool done = false;
};
template <typename ResultType>
struct BatchHandle {
std::shared_ptr<BatchState<ResultType>> state;
std::vector<ResultType> Wait() const {
if (!state) {
return {};
}
if (state->remaining.load(std::memory_order_acquire) > 0) {
std::unique_lock<std::mutex> lock(state->done_mutex);
auto state_ptr = state;
state->done_cv.wait(lock,
[state_ptr] { return state_ptr->done; });
}
return state->results;
}
};
explicit AsyncMemcpyExecutor(size_t worker_num);
~AsyncMemcpyExecutor();
template <typename ResultType, typename TaskFn, typename ErrorFn>
BatchHandle<ResultType> SubmitBatchTasks(const std::vector<size_t>& indices,
TaskFn&& task_fn,
ErrorFn&& on_error);
template <typename ResultType, typename Fn>
std::future<ResultType> SubmitSingleTask(Fn&& fn);
void Shutdown();
private:
struct QueueTask {
std::function<void()> run;
std::function<void()> cancel;
};
void WorkerMain();
template <typename ResultType>
static void FinishBatchTask(
const std::shared_ptr<BatchState<ResultType>>& state,
size_t batch_index, ResultType result) {
if (!state || batch_index >= state->results.size()) {
return;
}
state->results[batch_index] = std::move(result);
if (state->remaining.fetch_sub(1, std::memory_order_acq_rel) == 1) {
std::lock_guard<std::mutex> lock(state->done_mutex);
state->done = true;
state->done_cv.notify_one();
}
}
bool shutting_down_ = false;
std::mutex mutex_;
std::condition_variable queue_not_empty_cv_;
std::queue<QueueTask> tasks_;
std::vector<std::thread> workers_;
};
// ============================================================================
// Template method implementations (must be in header)
// ============================================================================
template <typename ResultType, typename TaskFn, typename ErrorFn>
AsyncMemcpyExecutor::BatchHandle<ResultType>
AsyncMemcpyExecutor::SubmitBatchTasks(const std::vector<size_t>& indices,
TaskFn&& task_fn, ErrorFn&& on_error) {
auto batch_state = std::make_shared<BatchState<ResultType>>();
batch_state->results.reserve(indices.size());
using TaskFnType = typename std::decay<TaskFn>::type;
using ErrorFnType = typename std::decay<ErrorFn>::type;
auto task_fn_ptr =
std::make_shared<TaskFnType>(std::forward<TaskFn>(task_fn));
auto on_error_ptr =
std::make_shared<ErrorFnType>(std::forward<ErrorFn>(on_error));
for (size_t index : indices) {
batch_state->results.push_back((*on_error_ptr)(index));
}
batch_state->remaining.store(indices.size(), std::memory_order_relaxed);
if (indices.empty()) {
batch_state->done = true;
return BatchHandle<ResultType>{batch_state};
}
{
std::lock_guard<std::mutex> lock(mutex_);
for (size_t enqueued = 0; enqueued < indices.size(); ++enqueued) {
if (shutting_down_) {
// Cancel remaining slots synchronously.
for (size_t slot = enqueued; slot < indices.size(); ++slot) {
FinishBatchTask(batch_state, slot,
(*on_error_ptr)(indices[slot]));
}
return BatchHandle<ResultType>{batch_state};
}
const size_t slot = enqueued;
const size_t index = indices[slot];
QueueTask queue_task;
queue_task.run = [batch_state, task_fn_ptr, on_error_ptr, slot,
index]() mutable {
ResultType result = (*on_error_ptr)(index);
try {
result = (*task_fn_ptr)(index);
} catch (const std::exception& e) {
LOG(ERROR) << "Async batch task threw at index " << index
<< ": " << e.what();
result = (*on_error_ptr)(index);
} catch (...) {
LOG(ERROR) << "Async batch task threw unknown exception "
"at index "
<< index;
result = (*on_error_ptr)(index);
}
FinishBatchTask(batch_state, slot, std::move(result));
};
queue_task.cancel = [batch_state, on_error_ptr, slot, index]() {
FinishBatchTask(batch_state, slot, (*on_error_ptr)(index));
};
tasks_.push(std::move(queue_task));
}
}
queue_not_empty_cv_.notify_all();
return BatchHandle<ResultType>{batch_state};
}
template <typename ResultType, typename Fn>
std::future<ResultType> AsyncMemcpyExecutor::SubmitSingleTask(Fn&& fn) {
auto promise = std::make_shared<std::promise<ResultType>>();
auto future = promise->get_future();
using FnType = typename std::decay<Fn>::type;
auto fn_ptr = std::make_shared<FnType>(std::forward<Fn>(fn));
QueueTask task;
task.run = [promise, fn_ptr]() {
try {
promise->set_value((*fn_ptr)());
} catch (...) {
promise->set_exception(std::current_exception());
}
};
task.cancel = [promise]() {
try {
promise->set_exception(
std::make_exception_ptr(std::runtime_error("task cancelled")));
} catch (...) {
}
};
{
std::lock_guard<std::mutex> lock(mutex_);
if (shutting_down_) {
task.cancel();
return future;
}
tasks_.push(std::move(task));
}
queue_not_empty_cv_.notify_one();
return future;
}
} // namespace mooncake

View File

@ -0,0 +1,261 @@
#pragma once
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
#include <boost/functional/hash.hpp>
#include "p2p_master_client.h"
#include "types.h"
namespace mooncake {
// Called when an async ADD fails with a non-transient error.
// Caller should delete the local replica.
using SyncFailureCallback = std::function<void(
const std::string& key, const UUID& segment_id, ErrorCode error)>;
class AsyncMetadataNotifier {
public:
AsyncMetadataNotifier(P2PMasterClient& master_client, const UUID& client_id,
size_t sender_thread_count, size_t max_batch_size,
size_t queue_capacity,
SyncFailureCallback failure_cb = nullptr);
~AsyncMetadataNotifier();
AsyncMetadataNotifier(const AsyncMetadataNotifier&) = delete;
AsyncMetadataNotifier& operator=(const AsyncMetadataNotifier&) = delete;
// Start/Stop can be called alternately.
// drop_pending=false (default): sender drains remaining ops before exiting.
// drop_pending=true: queued ops are discarded immediately without invoking
// failure_cb.
void Start();
void Stop(bool drop_pending = false);
// --- Normal priority ---
tl::expected<void, ErrorCode> EnqueueAdd(const std::string& key,
const UUID& segment_id,
size_t size);
tl::expected<void, ErrorCode> EnqueueRemove(const std::string& key,
const UUID& segment_id);
// --- Recovery priority (lower send priority) ---
tl::expected<void, ErrorCode> EnqueueRecoveryAdd(const std::string& key,
const UUID& segment_id,
size_t size);
/**
* @brief Wait until all recovery ops have been sent.
* Only valid after the caller has finished enqueueing all recovery ops.
* @return true if drained, false if aborted or timed out.
*/
bool WaitForRecoveryDrain(const std::function<bool()>& abort_fn,
std::chrono::milliseconds timeout);
bool IsPaused() const {
return consecutive_rpc_failures_.load(std::memory_order_acquire) < 0;
}
private:
struct PendingOp {
enum Type : int32_t { ADD = 0, REMOVE = 1 };
Type type = ADD;
std::string key;
UUID segment_id{};
size_t size = 0;
};
// Identifier of a Key. We assume that each key is unique in a segment.
struct CoalesceKey {
std::string key;
UUID segment_id;
bool operator==(const CoalesceKey& o) const {
return key == o.key && segment_id == o.segment_id;
}
};
struct CoalesceKeyHash {
size_t operator()(const CoalesceKey& k) const {
size_t h = std::hash<std::string>{}(k.key);
boost::hash_combine(h, k.segment_id);
return h;
}
};
// Coalescing entry: tracks slot index of a pending op for a key.
// InvalidIdx means no pending op of that type.
static constexpr size_t InvalidIdx = SIZE_MAX;
struct CoalesceEntry {
size_t add_idx = InvalidIdx;
size_t remove_idx = InvalidIdx;
};
// Pool node: PendingOp data + intrusive doubly-linked list pointers.
struct Slot {
PendingOp op;
size_t prev = InvalidIdx;
size_t next = InvalidIdx;
bool is_recovery = false; // which list this slot belongs to
};
struct SenderShard {
public:
SenderShard() = default;
SenderShard(const SenderShard&) = delete;
SenderShard& operator=(const SenderShard&) = delete;
SenderShard(SenderShard&&) = delete;
SenderShard& operator=(SenderShard&&) = delete;
bool IsFull() const { return free_stack.empty(); }
bool IsFullForRecovery() const {
return free_stack.size() <= normal_reserved;
}
bool IsEmpty() const {
return normal_count == 0 && recovery_count == 0;
}
size_t AllocSlot() {
size_t idx = free_stack.back();
free_stack.pop_back();
return idx;
}
void FreeSlot(size_t idx) {
slots[idx].prev = InvalidIdx;
slots[idx].next = InvalidIdx;
slots[idx].is_recovery = false;
free_stack.push_back(idx);
}
// Link to the tail of the appropriate list based on is_recovery flag.
void LinkTail(size_t idx, bool is_recovery) {
auto& head = is_recovery ? recovery_head : normal_head;
auto& tail = is_recovery ? recovery_tail : normal_tail;
auto& count = is_recovery ? recovery_count : normal_count;
slots[idx].prev = tail;
slots[idx].next = InvalidIdx;
slots[idx].is_recovery = is_recovery;
if (tail != InvalidIdx) {
slots[tail].next = idx;
} else {
head = idx;
}
tail = idx;
count++;
}
// Unlink from whichever list the slot belongs to.
void Unlink(size_t idx) {
auto& slot = slots[idx];
auto& head = slot.is_recovery ? recovery_head : normal_head;
auto& tail = slot.is_recovery ? recovery_tail : normal_tail;
auto& count = slot.is_recovery ? recovery_count : normal_count;
if (slot.prev != InvalidIdx) {
slots[slot.prev].next = slot.next;
} else {
head = slot.next;
}
if (slot.next != InvalidIdx) {
slots[slot.next].prev = slot.prev;
} else {
tail = slot.prev;
}
count--;
}
public:
std::mutex mutex;
std::condition_variable sender_cv; // sender waits when both empty
std::condition_variable producer_cv; // normal producer waits when full
std::condition_variable recovery_drain_cv; // WaitForRecoveryDrain
// Pre-allocated slot pool — shared by both queues
std::vector<Slot> slots;
std::vector<size_t> free_stack;
size_t capacity = 0;
size_t normal_reserved = 0; // slots reserved for normal ops
// Normal priority linked list
size_t normal_head = InvalidIdx;
size_t normal_tail = InvalidIdx;
size_t normal_count = 0;
// Recovery (low priority) linked list
size_t recovery_head = InvalidIdx;
size_t recovery_tail = InvalidIdx;
size_t recovery_count = 0;
// Shared coalesce index (covers both queues)
std::unordered_map<CoalesceKey, CoalesceEntry, CoalesceKeyHash>
coalesce_index;
// Recovery ops that have been dequeued by CollectBatch but whose
// SendBatch has not yet completed. WaitForRecoveryDrain waits for
// both recovery_count and recovery_in_flight to reach zero.
size_t recovery_in_flight = 0;
std::thread sender_thread;
};
private:
tl::expected<void, ErrorCode> DoEnqueue(PendingOp&& op, bool is_recovery);
void SenderLoop(size_t shard_idx);
// Returns (total_collected, recovery_collected)
std::pair<size_t, size_t> CollectBatch(SenderShard& shard,
std::vector<PendingOp>& batch_out);
size_t CollectFromList(SenderShard& shard,
std::vector<PendingOp>& batch_out, size_t offset,
size_t max_count, bool from_recovery);
void SendBatch(std::vector<PendingOp>& batch, size_t count);
void RecordSuccess();
void RecordFailure();
void ResetShard(SenderShard& shard);
private:
static constexpr int MaxRetryCount = 3;
static constexpr int CircuitBreakerThreshold = 5;
static constexpr auto BatchTimeout = std::chrono::milliseconds(20);
static constexpr auto EnqueueTimeout = std::chrono::milliseconds(1000);
static constexpr auto CircuitBreakerCooldown = std::chrono::seconds(2);
P2PMasterClient& master_client_;
const UUID client_id_;
const size_t sender_thread_count_;
const size_t max_batch_size_;
std::atomic<bool> running_{false};
// Used to wake sender threads from retry sleep during Stop().
std::mutex stop_mutex_;
std::condition_variable stop_cv_;
std::vector<std::unique_ptr<SenderShard>> shards_;
std::vector<std::vector<PendingOp>> batch_buffers_;
SyncFailureCallback failure_cb_;
// Set to true by Stop(drop_pending=true) before waking senders.
// Tells CollectBatch to return 0 immediately so senders exit after their
// current in-flight SendBatch without processing any further queued ops.
std::atomic<bool> drop_on_stop_{false};
// Circuit breaker via consecutive failure count:
// >=0 : active, value = consecutive failures so far
// <0 : paused (breaker open), abs(value) = failure count at trip
std::atomic<int32_t> consecutive_rpc_failures_{0};
};
} // namespace mooncake

View File

@ -17,6 +17,7 @@
#pragma once
#include <atomic>
#include <array>
#include <mutex>
#include <thread>
#include <unordered_map>

View File

@ -0,0 +1,61 @@
#pragma once
#include "client_manager.h"
#include "allocation_strategy.h"
namespace mooncake {
namespace test {
class MasterServiceTest;
}
class CentralizedClientManager final : public ClientManager {
public:
/**
* @brief CentralizedClientManager support to alloc buf for memory replic
* and support some interfaces about local disk segment
* @param client_live_ttl_sec Timeout for HEALTH -> DISCONNECTION
* @param client_crashed_ttl_sec Timeout for DISCONNECTION -> CRASHED
*/
CentralizedClientManager(const int64_t client_live_ttl_sec,
const int64_t client_crashed_ttl_sec,
const BufferAllocatorType memory_allocator_type,
const ViewVersionId view_version);
auto MountLocalDiskSegment(const UUID& client_id, bool enable_offloading)
-> tl::expected<void, ErrorCode>;
auto OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading)
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>;
auto PushOffloadingQueue(const std::string& key, const int64_t size,
const std::string& segment_name)
-> tl::expected<void, ErrorCode>;
auto Allocate(const uint64_t slice_length, const size_t replica_num,
const std::vector<std::string>& preferred_segments)
-> tl::expected<std::vector<Replica>, ErrorCode>;
protected:
DeploymentMode GetDeploymentMode() const override {
return DeploymentMode::CENTRALIZATION;
}
std::shared_ptr<ClientMeta> CreateClientMeta(
const RegisterClientRequest& req) override;
HeartbeatTaskResult ProcessTask(const UUID& client_id,
const HeartbeatTask& task) override;
private:
BufferAllocatorType memory_allocator_type_;
// Global allocator manager aggregates allocators from all clients.
// Protected by its own mutex, independent of clients_mutex_.
mutable SharedMutex global_allocator_mutex_;
AllocatorManager global_allocator_manager_
GUARDED_BY(global_allocator_mutex_);
std::shared_ptr<AllocationStrategy> allocation_strategy_;
friend class SegmentTest;
friend class test::MasterServiceTest;
};
} // namespace mooncake

View File

@ -0,0 +1,34 @@
#pragma once
#include "client_meta.h"
#include "centralized_segment_manager.h"
namespace mooncake {
class CentralizedClientMeta : public ClientMeta {
public:
CentralizedClientMeta(const UUID& client_id,
BufferAllocatorType allocator_type);
std::shared_ptr<SegmentManager> GetSegmentManager() override;
std::shared_ptr<CentralizedSegmentManager> GetCentralizedSegmentManager();
tl::expected<std::vector<std::string>, ErrorCode> QueryIp(
const UUID& client_id) override;
auto MountLocalDiskSegment(bool enable_offloading)
-> tl::expected<void, ErrorCode>;
auto OffloadObjectHeartbeat(bool enable_offloading)
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>;
auto PushOffloadingQueue(const std::string& key, const int64_t size,
const std::string& segment_name)
-> tl::expected<void, ErrorCode>;
public:
void DoOnDisconnected() override;
void DoOnRecovered() override;
private:
std::shared_ptr<CentralizedSegmentManager> segment_manager_;
};
} // namespace mooncake

View File

@ -0,0 +1,214 @@
#pragma once
#include "client_service.h"
#include "centralized_master_client.h"
#include "storage_backend.h"
#include "file_storage.h"
#include "transfer_task.h"
#include "thread_pool.h"
#include <chrono>
namespace mooncake {
class PutOperation;
class FileStorage;
/**
* @brief Centralized-specific query result with lease timeout information
*/
class CentralizedQueryResult final : public QueryResult {
public:
/** @brief Time point when the lease for this key expires */
const std::chrono::steady_clock::time_point lease_timeout;
CentralizedQueryResult(
std::vector<Replica::Descriptor>&& replicas_param,
std::chrono::steady_clock::time_point lease_timeout_param)
: QueryResult(std::move(replicas_param)),
lease_timeout(lease_timeout_param) {}
bool IsLeaseExpired() const {
return std::chrono::steady_clock::now() >= lease_timeout;
}
bool IsLeaseExpired(std::chrono::steady_clock::time_point& now) const {
return now >= lease_timeout;
}
};
class CentralizedClientService
: public ClientService,
public std::enable_shared_from_this<CentralizedClientService> {
public:
CentralizedClientService(
const std::string& local_ip, uint16_t te_port,
const std::string& metadata_connstring, uint16_t metrics_port = 9003,
bool enable_metrics_http = true,
const std::map<std::string, std::string>& labels = {});
~CentralizedClientService() override;
ErrorCode Init(const CentralizedClientConfig& config);
void Stop() override;
void Destroy() override;
tl::expected<std::unique_ptr<QueryResult>, ErrorCode> Query(
const std::string& object_key,
const ReadRouteConfig& config = {}) override;
std::vector<tl::expected<std::unique_ptr<QueryResult>, ErrorCode>>
BatchQuery(const std::vector<std::string>& object_keys,
const ReadRouteConfig& config = {}) override;
tl::expected<bool, ErrorCode> IsExist(const std::string& key) override;
std::vector<tl::expected<bool, ErrorCode>> BatchIsExist(
const std::vector<std::string>& keys) override;
DeploymentMode deployment_mode() const override {
return DeploymentMode::CENTRALIZATION;
}
tl::expected<std::vector<std::string>, ErrorCode> BatchReplicaClear(
const std::vector<std::string>& object_keys, const UUID& client_id,
const std::string& segment_name);
tl::expected<int64_t, ErrorCode> Get(
const std::string& key, const std::vector<void*>& buffers,
const std::vector<size_t>& sizes,
const ReadRouteConfig& config = {}) override;
std::vector<tl::expected<int64_t, ErrorCode>> BatchGet(
const std::vector<std::string>& keys,
const std::vector<std::vector<void*>>& all_buffers,
const std::vector<std::vector<size_t>>& all_sizes,
const ReadRouteConfig& config = {},
bool aggregate_same_segment_task = false) override;
tl::expected<std::shared_ptr<BufferHandle>, ErrorCode> Get(
const std::string& key,
std::shared_ptr<ClientBufferAllocator> allocator,
const ReadRouteConfig& config = {}) override;
std::vector<tl::expected<std::shared_ptr<BufferHandle>, ErrorCode>>
BatchGet(const std::vector<std::string>& keys,
std::shared_ptr<ClientBufferAllocator> allocator,
const ReadRouteConfig& config = {}) override;
tl::expected<void, ErrorCode> Put(const ObjectKey& key,
std::vector<Slice>& slices,
const WriteConfig& config) override;
std::vector<tl::expected<void, ErrorCode>> BatchPut(
const std::vector<ObjectKey>& keys,
std::vector<std::vector<Slice>>& batched_slices,
const WriteConfig& config) override;
tl::expected<void, ErrorCode> Remove(const ObjectKey& key) override;
tl::expected<long, ErrorCode> RemoveByRegex(const ObjectKey& str) override;
tl::expected<long, ErrorCode> RemoveAll() override;
tl::expected<void, ErrorCode> MountSegment(const void* buffer,
size_t size) override;
tl::expected<void, ErrorCode> UnmountSegment(const void* buffer,
size_t size) override;
tl::expected<void, ErrorCode> MountLocalDiskSegment(bool enable_offloading);
tl::expected<void, ErrorCode> OffloadObjectHeartbeat(
bool enable_offloading,
std::unordered_map<std::string, int64_t>& offloading_objects);
tl::expected<void, ErrorCode> BatchPutOffloadObject(
const std::string& transfer_engine_addr,
const std::vector<std::string>& keys,
const std::vector<uintptr_t>& pointers,
const std::unordered_map<std::string, Slice>& batched_slices);
tl::expected<void, ErrorCode> NotifyOffloadSuccess(
const std::vector<std::string>& keys,
const std::vector<StorageObjectMetadata>& metadatas);
tl::expected<RegisterClientResponse, ErrorCode> RegisterClient() override;
protected:
HeartbeatRequest build_heartbeat_request() override;
MasterClient& GetMasterClient() override { return master_client_; }
private:
void InitTransferSubmitter();
std::vector<tl::expected<void, ErrorCode>> BatchGetWhenPreferSameNode(
const std::vector<std::string>& object_keys,
const std::vector<std::unique_ptr<QueryResult>>& query_results,
std::unordered_map<std::string, std::vector<Slice>>& slices);
tl::expected<void, ErrorCode> InnerGet(const std::string& object_key,
const QueryResult& query_result,
std::vector<Slice>& slices);
std::vector<tl::expected<void, ErrorCode>> InnerBatchGet(
const std::vector<std::string>& object_keys,
const std::vector<std::unique_ptr<QueryResult>>& query_results,
std::unordered_map<std::string, std::vector<Slice>>& slices,
bool prefer_same_node = false);
std::vector<PutOperation> CreatePutOperations(
const std::vector<ObjectKey>& keys,
const std::vector<std::vector<Slice>>& batched_slices);
void StartBatchPut(std::vector<PutOperation>& ops,
const ReplicateConfig& config);
void SubmitTransfers(std::vector<PutOperation>& ops);
void WaitForTransfers(std::vector<PutOperation>& ops);
void FinalizeBatchPut(std::vector<PutOperation>& ops);
std::vector<tl::expected<void, ErrorCode>> CollectResults(
const std::vector<PutOperation>& ops);
std::vector<tl::expected<void, ErrorCode>> BatchPutWhenPreferSameNode(
std::vector<PutOperation>& ops);
void PrepareStorageBackend(const std::string& storage_root_dir,
const std::string& fsdir,
bool enable_eviction = true,
uint64_t quota_bytes = 0);
void PutToLocalFile(const std::string& object_key,
const std::vector<Slice>& slices,
const DiskDescriptor& disk_descriptor);
ErrorCode TransferData(const Replica::Descriptor& replica_descriptor,
std::vector<Slice>& slices,
TransferRequest::OpCode op_code);
ErrorCode TransferWrite(const Replica::Descriptor& replica_descriptor,
std::vector<Slice>& slices);
ErrorCode TransferRead(const Replica::Descriptor& replica_descriptor,
std::vector<Slice>& slices);
tl::expected<void, ErrorCode> InnerUnmountSegment(const void* buffer,
size_t size);
ErrorCode FindFirstCompleteReplica(
const std::vector<Replica::Descriptor>& replica_list,
Replica::Descriptor& replica);
private:
CentralizedMasterClient master_client_;
std::unique_ptr<TransferSubmitter> transfer_submitter_;
// Mutex to protect mounted_segments_
std::mutex mounted_segments_mutex_;
std::unordered_map<UUID, Segment, boost::hash<UUID>> mounted_segments_;
// File storage for offloading
std::shared_ptr<FileStorage> file_storage_;
// Client persistent thread pool for async operations
ThreadPool write_thread_pool_;
std::shared_ptr<StorageBackend> storage_backend_;
};
} // namespace mooncake

View File

@ -0,0 +1,134 @@
#pragma once
#include "master_client.h"
namespace mooncake {
/**
* @brief Client for interacting with the mooncake master service
*/
class CentralizedMasterClient final : public MasterClient {
public:
CentralizedMasterClient(const UUID& client_id,
MasterClientMetric* metrics = nullptr)
: MasterClient(client_id, metrics) {}
CentralizedMasterClient(const CentralizedMasterClient&) = delete;
CentralizedMasterClient& operator=(const CentralizedMasterClient&) = delete;
/**
* @brief Starts a put operation
* @param key Object key
* @param batch_slice_lengths Vector of slice lengths
* @param value_length Total value length
* @param config Replication configuration
* @return tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
* indicating success/failure
*/
[[nodiscard]] tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
PutStart(const std::string& key,
const std::vector<size_t>& batch_slice_lengths,
const ReplicateConfig& config);
/**
* @brief Starts a batch of put operations for N objects
* @param keys Vector of object key
* @param value_lengths Vector of total value lengths
* @param slice_lengths Vector of vectors of slice lengths
* @param config Replication configuration
* @return ErrorCode indicating success/failure
*/
[[nodiscard]] std::vector<
tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
BatchPutStart(const std::vector<std::string>& keys,
const std::vector<std::vector<uint64_t>>& slice_lengths,
const ReplicateConfig& config);
/**
* @brief Ends a put operation
* @param key Object key
* @param replica_type Type of replica (memory or disk)
* @return tl::expected<void, ErrorCode> indicating success/failure
*/
[[nodiscard]] tl::expected<void, ErrorCode> PutEnd(
const std::string& key, ReplicaType replica_type);
/**
* @brief Ends a put operation for a batch of objects
* @param keys Vector of object keys
* @return ErrorCode indicating success/failure
*/
[[nodiscard]] std::vector<tl::expected<void, ErrorCode>> BatchPutEnd(
const std::vector<std::string>& keys);
/**
* @brief Revokes a put operation
* @param key Object key
* @param replica_type Type of replica (memory or disk)
* @return tl::expected<void, ErrorCode> indicating success/failure
*/
[[nodiscard]] tl::expected<void, ErrorCode> PutRevoke(
const std::string& key, ReplicaType replica_type);
/**
* @brief Revokes a put operation for a batch of objects
* @param keys Vector of object keys
* @return ErrorCode indicating success/failure
*/
[[nodiscard]] std::vector<tl::expected<void, ErrorCode>> BatchPutRevoke(
const std::vector<std::string>& keys);
/**
* @brief Batch clear KV cache for specified object keys on a specific
* segment for a given client.
* @param object_keys Vector of object key strings to clear.
* @param client_id The UUID of the client that owns the object keys.
* @param segment_name The name of the segment (storage device) to clear
* from.
* @return An expected object containing a vector of successfully cleared
* object keys on success, or an ErrorCode on failure.
*/
[[nodiscard]] tl::expected<std::vector<std::string>, ErrorCode>
BatchReplicaClear(const std::vector<std::string>& object_keys,
const UUID& client_id, const std::string& segment_name);
/**
* @brief Gets the cluster ID for the current client to use as subdirectory
* name
* @return GetClusterIdResponse containing the cluster ID
*/
[[nodiscard]] tl::expected<std::string, ErrorCode> GetFsdir();
[[nodiscard]] tl::expected<GetStorageConfigResponse, ErrorCode>
GetStorageConfig();
/**
* @brief Mounts a local disk segment into the master.
* @param enable_offloading If true, enables offloading (write-to-file).
*/
[[nodiscard]] tl::expected<void, ErrorCode> MountLocalDiskSegment(
const UUID& client_id, bool enable_offloading);
/**
* @brief Heartbeat call to collect object-level statistics and retrieve the
* set of non-persisted objects.
* @param enable_offloading Indicates whether persistence is enabled for
* this segment.
*/
[[nodiscard]] tl::expected<std::unordered_map<std::string, int64_t>,
ErrorCode>
OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading);
/**
* @brief Adds multiple new objects to a specified client in batch.
* @param keys A list of object keys (names) that were successfully
* offloaded.
* @param metadatas The corresponding metadata for each offloaded object,
* including size, storage location, etc.
*/
[[nodiscard]] tl::expected<void, ErrorCode> NotifyOffloadSuccess(
const UUID& client_id, const std::vector<std::string>& keys,
const std::vector<StorageObjectMetadata>& metadatas);
};
} // namespace mooncake

View File

@ -0,0 +1,446 @@
#pragma once
#include <atomic>
#include <boost/functional/hash.hpp>
#include <boost/lockfree/queue.hpp>
#include <chrono>
#include <cstdint>
#include <list>
#include <memory>
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <ylt/util/expected.hpp>
#include <ylt/util/tl/expected.hpp>
#include "master_metric_manager.h"
#include "master_service.h"
#include "mutex.h"
#include "centralized_client_manager.h"
#include "types.h"
#include "rpc_types.h"
#include "replica.h"
namespace mooncake {
namespace test {
class MasterServiceTest;
}
/**
* @brief The CentralizedMasterService is centralized service implementation for
* master node. It managers all meta of cluster. The main duty of this master
* is:
* 1. Key metadata management (such as routing, allocation, eviction in cluster)
* 2. Cluster state management (such as segment, client, replica)
*/
class CentralizedMasterService final : public MasterService {
struct CentralizedMetadataShard;
public:
CentralizedMasterService();
explicit CentralizedMasterService(const MasterServiceConfig& config);
~CentralizedMasterService() override;
auto GetReplicaList(const std::string& key,
const GetReplicaListRequestConfig& config =
GetReplicaListRequestConfig())
-> tl::expected<GetReplicaListResponse, ErrorCode> override;
/**
* @brief Start a put operation for an object
* @param[out] replica_list Vector to store replica information for the
* slice
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if exists,
* ErrorCode::NO_AVAILABLE_HANDLE if allocation fails,
* ErrorCode::INVALID_PARAMS if slice size is invalid
*/
auto PutStart(const UUID& client_id, const std::string& key,
const uint64_t slice_length, const ReplicateConfig& config)
-> tl::expected<std::vector<Replica::Descriptor>, ErrorCode>;
/**
* @brief Complete a put operation, replica_type indicates the type of
* replica to complete (memory or disk)
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
* found, ErrorCode::INVALID_WRITE if replica status is invalid
*/
auto PutEnd(const UUID& client_id, const std::string& key,
ReplicaType replica_type) -> tl::expected<void, ErrorCode>;
/**
* @brief Complete a batch of put operations
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
* found, ErrorCode::INVALID_WRITE if replica status is invalid
*/
auto BatchPutEnd(const UUID& client_id,
const std::vector<std::string>& keys)
-> std::vector<tl::expected<void, ErrorCode>>;
/**
* @brief Revoke a put operation, replica_type indicates the type of
* replica to revoke (memory or disk)
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
* found, ErrorCode::INVALID_WRITE if replica status is invalid
*/
auto PutRevoke(const UUID& client_id, const std::string& key,
ReplicaType replica_type) -> tl::expected<void, ErrorCode>;
/**
* @brief Revoke a batch of put operations
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
* found, ErrorCode::INVALID_WRITE if replica status is invalid
*/
auto BatchPutRevoke(const UUID& client_id,
const std::vector<std::string>& keys)
-> std::vector<tl::expected<void, ErrorCode>>;
/**
* @brief Batch clear KV cache replicas for specified object keys.
* @param object_keys Vector of object key strings to clear.
* @param client_id The UUID of the client that owns the object keys.
* @param segment_name The name of the segment (storage device) to clear
* from. If empty, clears replicas from all segments for the given
* client_id.
* @return An expected object containing a vector of successfully cleared
* keys on success, or an ErrorCode on failure. Only successfully
* cleared keys are included in the result.
*/
auto BatchReplicaClear(const std::vector<std::string>& object_keys,
const UUID& client_id,
const std::string& segment_name)
-> tl::expected<std::vector<std::string>, ErrorCode>;
/**
* @brief Adds a replica instance associated with the given client and key.
*/
auto AddReplica(const UUID& client_id, const std::string& key,
Replica& replica) -> tl::expected<void, ErrorCode>;
/**
* @brief Get the master service cluster ID to use as subdirectory name
* @return ErrorCode::OK on success, ErrorCode::INTERNAL_ERROR if cluster ID
* is not set
*/
tl::expected<std::string, ErrorCode> GetFsdir() const;
/**
* @brief Get storage backend configuration including eviction settings
* @return GetStorageConfigResponse containing fsdir, enable_disk_eviction,
* and quota_bytes
*/
tl::expected<GetStorageConfigResponse, ErrorCode> GetStorageConfig() const;
/**
* @brief Mounts a file storage segment into the master.
* @param enable_offloading If true, enables offloading (write-to-file).
*/
auto MountLocalDiskSegment(const UUID& client_id, bool enable_offloading)
-> tl::expected<void, ErrorCode>;
/**
* @brief Heartbeat call to collect object-level statistics and retrieve the
* set of non-offloaded objects.
* @param enable_offloading Indicates whether offloading is enabled for this
* segment.
*/
auto OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading)
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>;
/**
* @brief Notifies the master that offloading of specified objects has
* succeeded.
* @param keys A list of object keys (names) that were successfully
* offloaded.
* @param metadatas The corresponding metadata for each offloaded object,
* including size, storage location, etc.
*/
auto NotifyOffloadSuccess(
const UUID& client_id, const std::vector<std::string>& keys,
const std::vector<StorageObjectMetadata>& metadatas)
-> tl::expected<void, ErrorCode>;
public:
ClientManager& GetClientManager() override { return client_manager_; }
const ClientManager& GetClientManager() const override {
return client_manager_;
}
private:
std::vector<Replica::Descriptor> FilterReplicas(
const GetReplicaListRequestConfig& config,
const ObjectMetadata& metadata) override;
// Hooks implementation
void OnObjectAccessed(ObjectMetadata& metadata) override;
void OnObjectHit(const ObjectMetadata& metadata) override;
void OnReplicaRemoved(const Replica& replica) override;
void OnReplicaAdded(const Replica& replica) override;
void OnSegmentRemoved(const UUID& segment_id) override;
private:
// Resolve the key to a sanitized format for storage
std::string SanitizeKey(const std::string& key) const;
std::string ResolvePath(const std::string& key) const;
// BatchEvict evicts objects in a near-LRU way, i.e., prioritizes to evict
// object with smaller lease timeout. It has two passes. The first pass only
// evicts objects without soft pin. The second pass prioritizes objects
// without soft pin, but also allows to evict soft pinned objects if
// allow_evict_soft_pinned_objects_ is true. The first pass tries fulfill
// evict ratio target. If the actual evicted ratio is less than
// evict_ratio_lowerbound, the second pass will be triggered and try to
// fulfill evict ratio lowerbound.
void BatchEvict(double evict_ratio_target, double evict_ratio_lowerbound);
/**
* @brief Helper to discard expired processing keys.
*/
void DiscardExpiredProcessingKeys(
CentralizedMetadataShard& shard,
const std::chrono::steady_clock::time_point& now);
/**
* @brief Helper to release space of expired discarded replicas.
* @return Number of released objects that have memory replicas
*/
uint64_t ReleaseExpiredDiscardedReplicas(
const std::chrono::steady_clock::time_point& now);
// Eviction thread function
void EvictionThreadFunc();
tl::expected<void, ErrorCode> PushOffloadingQueue(const std::string& key,
const Replica& replica);
private:
/**
* @brief CentralizedObjectMetadata extends ObjectMetadata with lease and
* ReplicaStatus management for centralized master service.
*/
struct CentralizedObjectMetadata final : public ObjectMetadata {
public:
CentralizedObjectMetadata(
const UUID& client_id,
const std::chrono::steady_clock::time_point put_start_time,
size_t value_length, std::vector<Replica>&& reps,
bool enable_soft_pin);
~CentralizedObjectMetadata() override;
CentralizedObjectMetadata(const CentralizedObjectMetadata&) = delete;
CentralizedObjectMetadata& operator=(const CentralizedObjectMetadata&) =
delete;
CentralizedObjectMetadata(CentralizedObjectMetadata&&) = delete;
CentralizedObjectMetadata& operator=(CentralizedObjectMetadata&&) =
delete;
// Check if there are some replicas with a different status than the
// given value. If there are, return the status of the first replica
// that is not equal to the given value. Otherwise, return std::nullopt.
std::optional<ReplicaStatus> HasDiffRepStatus(
ReplicaStatus status, ReplicaType replica_type) const;
// Grant a lease with timeout as now() + ttl, only update if the new
// timeout is larger
void GrantLease(const uint64_t ttl, const uint64_t soft_ttl);
// Erase all replicas of the given type
void EraseReplica(ReplicaType type);
// Check if there is a memory replica
bool HasMemReplica() const;
// Get the count of memory replicas
int GetMemReplicaCount() const;
// Check if the lease has expired
bool IsLeaseExpired() const;
// Check if the lease has expired
bool IsLeaseExpired(
const std::chrono::steady_clock::time_point& now) const;
// Check if is in soft pin status
bool IsSoftPinned() const;
// Check if is in soft pin status
bool IsSoftPinned(
const std::chrono::steady_clock::time_point& now) const;
// Check if all replicas are complete
bool IsAllReplicasComplete() const;
// Check if has any completed replicas
bool HasCompletedReplicas() const;
// Discard all processing replicas and return them
std::vector<Replica> DiscardProcessingReplicas();
public:
// Hook functions
tl::expected<void, ErrorCode> IsObjectRemovable() const override;
bool IsReplicaAccessible(const Replica& replica) const override;
tl::expected<void, ErrorCode> IsReplicaRemovable(
const Replica& replica) const override;
public:
// The client that created this object (via PutStart).
const UUID owner_client_id_;
const std::chrono::steady_clock::time_point put_start_time_;
std::chrono::steady_clock::time_point lease_timeout_;
std::optional<std::chrono::steady_clock::time_point> soft_pin_timeout_;
};
private:
// Extended MetadataShard with processing_keys for centralized service
struct CentralizedMetadataShard : public MetadataShard {
// Keys currently being written (PutStart called, but not yet PutEnd)
// Protected by the inherited mutex from MetadataShard
std::unordered_set<std::string> processing_keys GUARDED_BY(mutex);
};
// Override GetShard to return our extended shard type
MetadataShard& GetShard(size_t idx) override {
return metadata_shards_[idx];
}
const MetadataShard& GetShard(size_t idx) const override {
return metadata_shards_[idx];
}
// Helper to get the extended shard with processing_keys
CentralizedMetadataShard& GetCentralizedShard(size_t idx) {
return metadata_shards_[idx];
}
const CentralizedMetadataShard& GetCentralizedShard(size_t idx) const {
return metadata_shards_[idx];
}
static constexpr size_t kNumShards = 1024; // Number of metadata shards
// Helper to get shard index from key
size_t GetShardIndex(const std::string& key) const override {
return std::hash<std::string>{}(key) % kNumShards;
}
size_t GetShardCount() const override { return kNumShards; }
private:
class CentralizedMetadataAccessor final
: public MasterService::MetadataAccessor {
public:
CentralizedMetadataAccessor(CentralizedMasterService* service,
const std::string& key)
: MasterService::MetadataAccessor(service, key),
c_shard_(static_cast<CentralizedMetadataShard&>(shard_)),
processing_it_(c_shard_.processing_keys.find(key)) {}
CentralizedObjectMetadata& Get() NO_THREAD_SAFETY_ANALYSIS {
return static_cast<CentralizedObjectMetadata&>(
MasterService::MetadataAccessor::Get());
}
bool InProcessing() const NO_THREAD_SAFETY_ANALYSIS {
return processing_it_ != c_shard_.processing_keys.end();
}
void EraseFromProcessing() NO_THREAD_SAFETY_ANALYSIS {
c_shard_.processing_keys.erase(processing_it_);
processing_it_ = c_shard_.processing_keys.end();
}
// Access the extended shard directly (for inserting processing keys)
CentralizedMetadataShard& GetCentralizedShard() { return c_shard_; }
private:
CentralizedMetadataShard& c_shard_;
std::unordered_set<std::string>::iterator processing_it_;
};
std::unique_ptr<MetadataAccessor> GetMetadataAccessor(
const std::string& key) override {
return std::make_unique<CentralizedMetadataAccessor>(this, key);
}
private:
class DiscardedReplicas {
public:
DiscardedReplicas() = delete;
DiscardedReplicas(std::vector<Replica>&& replicas,
std::chrono::steady_clock::time_point ttl)
: replicas_(std::move(replicas)), ttl_(ttl), mem_size_(0) {
for (auto& replica : replicas_) {
mem_size_ += replica.get_memory_buffer_size();
}
MasterMetricManager::instance().inc_put_start_discard_cnt(
1, mem_size_);
}
~DiscardedReplicas() {
MasterMetricManager::instance().inc_put_start_release_cnt(
1, mem_size_);
}
uint64_t memSize() const { return mem_size_; }
bool isExpired(const std::chrono::steady_clock::time_point& now) const {
return ttl_ <= now;
}
private:
std::vector<Replica> replicas_;
std::chrono::steady_clock::time_point ttl_;
uint64_t mem_size_;
};
Mutex discarded_replicas_mutex_;
std::list<DiscardedReplicas> discarded_replicas_
GUARDED_BY(discarded_replicas_mutex_);
private:
std::array<CentralizedMetadataShard, kNumShards> metadata_shards_;
// Lease related members
const uint64_t default_kv_lease_ttl_; // in milliseconds
const uint64_t default_kv_soft_pin_ttl_; // in milliseconds
const bool allow_evict_soft_pinned_objects_;
// Eviction related members
std::atomic<bool> need_eviction_{
false}; // Set to trigger eviction when not enough space left
const double eviction_ratio_; // in range [0.0, 1.0]
const double eviction_high_watermark_ratio_; // in range [0.0, 1.0]
// Eviction thread related members
std::thread eviction_thread_;
std::atomic<bool> eviction_running_{false};
static constexpr uint64_t kEvictionThreadSleepMs =
10; // 10 ms sleep between eviction checks
const bool enable_offload_;
// cluster id for persistent sub directory
const std::string cluster_id_;
// root filesystem directory for persistent storage
const std::string root_fs_dir_;
// global 3fs/nfs segment size
int64_t global_file_segment_size_;
// storage backend eviction configuration
const bool enable_disk_eviction_;
const uint64_t quota_bytes_;
bool use_disk_replica_{false};
// Segment management
CentralizedClientManager client_manager_;
BufferAllocatorType memory_allocator_type_;
// Discarded replicas management
const std::chrono::seconds put_start_discard_timeout_sec_;
const std::chrono::seconds put_start_release_timeout_sec_;
friend class CentralizedMetadataAccessor;
friend class test::MasterServiceTest;
};
} // namespace mooncake

View File

@ -0,0 +1,69 @@
#pragma once
#include "rpc_service.h"
#include "centralized_master_service.h"
namespace mooncake {
class WrappedCentralizedMasterService final : public WrappedMasterService {
public:
WrappedCentralizedMasterService(const WrappedMasterServiceConfig& config);
// Initialize centralized-specific HTTP handlers
void init_centralized_http_server();
tl::expected<std::vector<Replica::Descriptor>, ErrorCode> PutStart(
const UUID& client_id, const std::string& key,
const uint64_t slice_length, const ReplicateConfig& config);
tl::expected<void, ErrorCode> PutEnd(const UUID& client_id,
const std::string& key,
ReplicaType replica_type);
tl::expected<void, ErrorCode> PutRevoke(const UUID& client_id,
const std::string& key,
ReplicaType replica_type);
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
BatchPutStart(const UUID& client_id, const std::vector<std::string>& keys,
const std::vector<uint64_t>& slice_lengths,
const ReplicateConfig& config);
std::vector<tl::expected<void, ErrorCode>> BatchPutEnd(
const UUID& client_id, const std::vector<std::string>& keys);
std::vector<tl::expected<void, ErrorCode>> BatchPutRevoke(
const UUID& client_id, const std::vector<std::string>& keys);
tl::expected<std::vector<std::string>, ErrorCode> BatchReplicaClear(
const std::vector<std::string>& object_keys, const UUID& client_id,
const std::string& segment_name);
tl::expected<std::string, ErrorCode> GetFsdir();
tl::expected<GetStorageConfigResponse, ErrorCode> GetStorageConfig();
tl::expected<void, ErrorCode> MountLocalDiskSegment(const UUID& client_id,
bool enable_offloading);
tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>
OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading);
tl::expected<void, ErrorCode> NotifyOffloadSuccess(
const UUID& client_id, const std::vector<std::string>& keys,
const std::vector<StorageObjectMetadata>& metadatas);
protected:
virtual MasterService& GetMasterService() override {
return master_service_;
}
private:
CentralizedMasterService master_service_;
};
void RegisterCentralizedRpcService(
coro_rpc::coro_rpc_server& server,
mooncake::WrappedCentralizedMasterService& wrapped_master_service);
} // namespace mooncake

View File

@ -0,0 +1,96 @@
#pragma once
#include "allocator.h"
#include "segment_manager.h"
#include <boost/functional/hash.hpp>
namespace mooncake {
// Although MountedCentralizedSegments are managed locally,
// buf_allocator of them are also registered to the upper-level
// CentralizedClientManager's global_allocator_manager_ via
// AllocatorChangeCallback, so that the Allocate() of segment can work globally.
struct MountedCentralizedSegment : public Segment {
std::shared_ptr<BufferAllocatorBase> buf_allocator;
};
struct LocalDiskSegment {
mutable Mutex offloading_mutex_;
bool enable_offloading;
std::unordered_map<std::string, int64_t> GUARDED_BY(offloading_mutex_)
offloading_objects;
explicit LocalDiskSegment(bool enable_offloading)
: enable_offloading(enable_offloading) {}
LocalDiskSegment(const LocalDiskSegment&) = delete;
LocalDiskSegment& operator=(const LocalDiskSegment&) = delete;
LocalDiskSegment(LocalDiskSegment&&) = delete;
LocalDiskSegment& operator=(LocalDiskSegment&&) = delete;
};
class CentralizedSegmentManager : public SegmentManager {
public:
/**
* @brief Constructor for CentralizedSegmentManager
* @param memory_allocator Type of buffer allocator to use for new segments
*/
explicit CentralizedSegmentManager(
BufferAllocatorType memory_allocator = BufferAllocatorType::OFFSET)
: memory_allocator_(memory_allocator) {}
/**
* @brief Callback for allocator changes (add/remove).
* Registered by CentralizedClientManager to sync allocators with its
* global_allocator_manager_, enabling global Allocate() without
* iterating per-client segment managers.
* @param segment_name Name of the segment
* @param allocator The allocator being added or removed
* @param is_add true if allocator is being added, false if removed
* @return ErrorCode::OK on success
*/
using AllocatorChangeCallback = std::function<tl::expected<void, ErrorCode>(
const std::string& segment_name,
const std::shared_ptr<BufferAllocatorBase>& allocator, bool is_add)>;
void SetAllocatorChangeCallback(AllocatorChangeCallback cb);
// Set the global visibility of all segments managed by this manager.
// When visible is true, all segments are added to the global allocator
// manager. When visible is false, all segments are removed from the
// global allocator manager.
tl::expected<void, ErrorCode> SetGlobalVisibility(bool visible);
auto QuerySegments(const std::string& segment)
-> tl::expected<std::pair<size_t, size_t>, ErrorCode> override;
auto MountLocalDiskSegment(bool enable_offloading)
-> tl::expected<void, ErrorCode>;
auto OffloadObjectHeartbeat(bool enable_offloading)
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>;
auto PushOffloadingQueue(const std::string& key, const int64_t size,
const std::string& segment_name)
-> tl::expected<void, ErrorCode>;
auto QueryIp() -> tl::expected<std::vector<std::string>, ErrorCode>;
protected:
ErrorCode InnerCheckMountSegment(const Segment& segment);
tl::expected<void, ErrorCode> InnerMountSegment(
const Segment& segment) override;
auto OnUnmountSegment(const std::shared_ptr<Segment>& segment)
-> tl::expected<void, ErrorCode> override;
private:
static constexpr size_t OFFLOADING_QUEUE_LIMIT = 50000;
const BufferAllocatorType
memory_allocator_; // Type of buffer allocator to use
AllocatorChangeCallback allocator_change_cb_;
std::shared_ptr<LocalDiskSegment> local_disk_segment_
GUARDED_BY(segment_mutex_);
friend class SegmentTest; // for unit tests
};
} // namespace mooncake

View File

@ -115,7 +115,17 @@ uint64_t calculate_total_size(const Replica::Descriptor& replica);
* @return 0 on success, non-zero on error
*/
int allocateSlices(std::vector<Slice>& slices,
const Replica::Descriptor& replica,
void* buffer_ptr);
const Replica::Descriptor& replica, void* buffer_ptr);
/**
* @brief Build slices from user-provided buffers, trimmed to total_size.
* @param buffers Vector of buffer pointers.
* @param sizes Vector of buffer sizes (must match buffers.size()).
* @param total_size Maximum total bytes across all slices.
* @return Vector of slices whose combined size equals total_size.
*/
std::vector<Slice> BuildSlicesFromBuffers(const std::vector<void*>& buffers,
const std::vector<size_t>& sizes,
uint64_t total_size);
} // namespace mooncake

View File

@ -0,0 +1,351 @@
#pragma once
#include <string>
#include <optional>
#include <map>
#include <memory>
#include <cstdint>
#include <algorithm>
#include <cctype>
#include <stdexcept>
#include <glog/logging.h>
#include <json/json.h>
#include "common.h"
namespace mooncake {
class TransferEngine;
// ============================================================================
// Config classes
// ============================================================================
/**
* @brief Configuration for a dummy client deployment.
*
* A dummy client communicates with a separately deployed real client via
* shared memory and RPC. It does not directly interact with the master or
* transfer engine.
*/
struct DummyClientConfig {
// Size of the memory pool in bytes.
size_t mem_pool_size = 0;
// Size of the local buffer in bytes.
// The local buffer will be registered as shm and shared to real client.
size_t local_buffer_size = 0;
// RPC connection string to real client ("ip:port").
std::string real_client_addr;
// The IPC socket path between dummy and real client.
std::string ipc_socket_path;
};
/**
* @brief Common base configuration shared by centralized and P2P real clients.
*
* Contains all fields needed for transfer engine initialization, master
* connection, segment mounting, and local buffer registration.
*/
struct RealClientConfigBase {
// Local IP address
std::string local_ip;
// Transfer engine port (0 means randomly assigned)
uint16_t te_port = 0;
/**
* @brief Returns the "ip:port" endpoint string.
*/
std::string local_endpoint() const {
return local_ip + ":" + std::to_string(te_port);
}
// Connection string for metadata service
std::string metadata_connstring;
// Transport protocol (e.g., "tcp", "rdma", "ascend").
std::string protocol = "tcp";
// Comma-separated RDMA device names.
// Optional with default auto-discovery.
// Only required when auto-discovery is disabled
// (set env `MC_MS_AUTO_DISC=0`).
std::optional<std::string> rdma_devices = std::nullopt;
// The entry of master server:
// 1. "IP:Port" for non-HA mode
// 2. "etcd://IP:Port;...;IP:Port" for HA mode
std::string master_server_entry = "127.0.0.1:50051";
// Size of the local buffer (0 to skip).
// For the case which separately deploys real client and dummy client,
// the `local_buffer_size` could be 0, which means the local buffer is
// shared by dummy client.
// For the case which integrates real client,
// if the `local_buffer_size` is 0, some interfaces might fail to work.
uint64_t local_buffer_size = 0;
// Optional metric labels for the client
std::map<std::string, std::string> labels = {};
// Optional TransferEngine instance.
// If not provided, it will be created by client_service.
std::shared_ptr<TransferEngine> transfer_engine = nullptr;
// The IPC socket path between dummy and real clients.
// If use integrated deployment, this could be empty.
std::string ipc_socket_path;
// Port for metrics HTTP server.
// Only used when enable_metrics_http is true.
uint16_t metrics_port = 9003;
// Whether to enable metrics HTTP server.
// The metrics server exposes /metrics, /metrics/summary, and /health
// endpoints.
bool enable_metrics_http = true;
};
/**
* @brief Configuration for a centralized real client.
*
* Inherits all common real client fields and adds centralized-specific options.
*/
struct CentralizedClientConfig : RealClientConfigBase {
// Size of global segment to mount (0 to skip)
uint64_t global_segment_size = 0;
// Whether to enable file storage offloading.
bool enable_offload = false;
};
enum class LocalTransferMode {
MEMCPY = 0,
TE = 1,
};
/**
* @brief Configuration for a P2P real client.
*
* Inherits all common real client fields and adds P2P-specific options.
*/
struct P2PClientConfig : RealClientConfigBase {
// Port for P2P RPC service.
uint16_t client_rpc_port = 12345;
// Num threads for P2P RPC service.
uint32_t rpc_thread_num = 2;
// Parsed custom tiered backend configuration
Json::Value tiered_backend_config;
// Number of key lock shards for DataManager.
// Higher values reduce contention of key.
size_t lock_shard_count = 1024;
// RouteCache configuration
// each size of route entry is about 240B:
// Aligned Node(64B) + hash_bucket(8B) + Key(assume 64B)
// + P2PRouteData(each item is 96B and count is 8B)
size_t route_cache_max_memory_bytes = 300 * 1024 * 1024; // 300MB
uint64_t route_cache_ttl_ms = 5 * 60 * 1000; // 5min
// Async route notification.
// async_sender_thread_count > 0 enables async notifier.
// async_route_queue_size controls queue capacity
// (minimum async_max_batch_size * async_sender_thread_count).
size_t async_sender_thread_count = 0;
size_t async_max_batch_size = 2000;
size_t async_route_queue_size = 0;
// Local transfer mode for P2P local Get/Put path.
// - MEMCPY: copy through local CPU memory path
// - TE: transfer through local TransferEngine path
LocalTransferMode local_transfer_mode = LocalTransferMode::TE;
// When local_transfer_mode == MEMCPY, the following parameter is used:
// 0 means forbid async memcpy (fall back to synchronous).
size_t local_memcpy_async_worker_num = 32;
};
// ============================================================================
// Factory class
// ============================================================================
/**
* @brief Factory class for building typed client configurations.
*/
class ClientConfigBuilder {
public:
static DummyClientConfig build_dummy(size_t mem_pool_size,
size_t local_buffer_size,
const std::string& real_client_addr,
const std::string& ipc_socket_path) {
DummyClientConfig config;
config.mem_pool_size = mem_pool_size;
config.local_buffer_size = local_buffer_size;
config.real_client_addr = real_client_addr;
config.ipc_socket_path = ipc_socket_path;
return config;
}
static CentralizedClientConfig build_centralized_real_client(
const std::string& local_hostname,
const std::string& metadata_connstring,
const std::string& protocol = "tcp",
const std::optional<std::string>& rdma_devices = std::nullopt,
const std::string& master_server_entry = "127.0.0.1:50051",
uint64_t global_segment_size = 0, uint64_t local_buffer_size = 0,
const std::shared_ptr<TransferEngine>& transfer_engine = nullptr,
const std::string& ipc_socket_path = "", bool enable_offload = false,
uint16_t metrics_port = 9003, bool enable_metrics_http = true,
const std::map<std::string, std::string>& labels = {}) {
CentralizedClientConfig config;
fill_real_client_config_base(
config, local_hostname, metadata_connstring, protocol, rdma_devices,
master_server_entry, local_buffer_size, transfer_engine,
ipc_socket_path, metrics_port, enable_metrics_http, labels);
config.global_segment_size = global_segment_size;
config.enable_offload = enable_offload;
return config;
}
static P2PClientConfig build_p2p_real_client(
const std::string& local_hostname,
const std::string& metadata_connstring,
const std::string& protocol = "tcp",
const std::optional<std::string>& rdma_devices = std::nullopt,
const std::string& master_server_entry = "127.0.0.1:50051",
const std::string& tiered_backend_config_json = "",
uint64_t local_buffer_size = 0,
const std::shared_ptr<TransferEngine>& transfer_engine = nullptr,
const std::string& ipc_socket_path = "",
uint16_t client_rpc_port = 12345, uint32_t rpc_thread_num = 2,
size_t lock_shard_count = 1024,
size_t route_cache_max_memory_bytes = 300 * 1024 * 1024,
uint64_t route_cache_ttl_ms = 5 * 60 * 1000,
const std::string& local_transfer_mode = "te",
size_t local_memcpy_async_worker_num = 32, uint16_t metrics_port = 9003,
bool enable_metrics_http = true,
const std::map<std::string, std::string>& labels = {},
size_t async_sender_thread_count = 0,
size_t async_max_batch_size = 2000, size_t async_route_queue_size = 0) {
P2PClientConfig config;
fill_real_client_config_base(
config, local_hostname, metadata_connstring, protocol, rdma_devices,
master_server_entry, local_buffer_size, transfer_engine,
ipc_socket_path, metrics_port, enable_metrics_http, labels);
config.client_rpc_port = client_rpc_port;
config.rpc_thread_num = rpc_thread_num;
config.lock_shard_count = lock_shard_count;
config.route_cache_max_memory_bytes = route_cache_max_memory_bytes;
config.route_cache_ttl_ms = route_cache_ttl_ms;
config.local_transfer_mode =
parse_p2p_local_transfer_mode(local_transfer_mode);
if (config.local_transfer_mode == LocalTransferMode::MEMCPY) {
config.local_memcpy_async_worker_num =
local_memcpy_async_worker_num;
}
config.async_sender_thread_count = async_sender_thread_count;
config.async_max_batch_size = async_max_batch_size;
config.async_route_queue_size = async_route_queue_size;
Json::Value tiered_config;
std::string actual_json = tiered_backend_config_json;
if (actual_json.empty()) {
if (const char* env_p = std::getenv("MOONCAKE_TIERED_CONFIG")) {
actual_json = env_p;
}
}
if (!actual_json.empty()) {
Json::CharReaderBuilder builder;
auto reader =
std::unique_ptr<Json::CharReader>(builder.newCharReader());
std::string errors;
if (!reader->parse(actual_json.data(),
actual_json.data() + actual_json.length(),
&tiered_config, &errors)) {
LOG(ERROR) << "Failed to parse tiered config: " << errors;
}
}
if (tiered_config.isNull() || !tiered_config.isMember("tiers") ||
tiered_config["tiers"].empty()) {
throw std::runtime_error(
"Tiered backend configuration is missing. Please provide "
"tiered_backend_config_json or set MOONCAKE_TIERED_CONFIG "
"environment variable.");
}
config.tiered_backend_config = tiered_config;
return config;
}
private:
static void fill_real_client_config_base(
RealClientConfigBase& config, const std::string& local_hostname,
const std::string& metadata_connstring, const std::string& protocol,
const std::optional<std::string>& rdma_devices,
const std::string& master_server_entry, uint64_t local_buffer_size,
const std::shared_ptr<TransferEngine>& transfer_engine,
const std::string& ipc_socket_path, uint16_t metrics_port = 9003,
bool enable_metrics_http = true,
const std::map<std::string, std::string>& labels = {}) {
// Parse local_hostname into IP and optional port.
// Only set te_port when the user explicitly provides a port;
// otherwise keep the default value (0 = randomly assigned).
auto bracket_pos = local_hostname.find(']');
if (bracket_pos != std::string::npos) {
// Bracketed IPv6, e.g. "[2001:db8::1]" or "[2001:db8::1]:1234"
config.local_ip = local_hostname.substr(1, bracket_pos - 1);
auto colon_after = local_hostname.find(':', bracket_pos);
if (colon_after != std::string::npos) {
config.te_port = getPortFromString(
local_hostname.substr(colon_after + 1), 0);
}
} else if (isValidIpV6(local_hostname)) {
// Raw IPv6 without brackets, no way to specify port
config.local_ip = local_hostname;
} else {
// IPv4 or hostname, optionally with port
auto colon_pos = local_hostname.rfind(':');
if (colon_pos != std::string::npos) {
config.local_ip = local_hostname.substr(0, colon_pos);
config.te_port =
getPortFromString(local_hostname.substr(colon_pos + 1), 0);
} else {
config.local_ip = local_hostname;
}
}
config.metadata_connstring = metadata_connstring;
config.protocol = protocol;
config.rdma_devices = rdma_devices;
config.master_server_entry = master_server_entry;
config.local_buffer_size = local_buffer_size;
config.transfer_engine = transfer_engine;
config.ipc_socket_path = ipc_socket_path;
config.metrics_port = metrics_port;
config.enable_metrics_http = enable_metrics_http;
config.labels = labels;
}
static LocalTransferMode parse_p2p_local_transfer_mode(std::string mode) {
std::transform(
mode.begin(), mode.end(), mode.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (mode == "memcpy") {
return LocalTransferMode::MEMCPY;
}
if (mode == "te") {
return LocalTransferMode::TE;
}
throw std::runtime_error(
"Invalid p2p local transfer mode. Expected 'memcpy' or 'te'.");
}
};
} // namespace mooncake

View File

@ -0,0 +1,195 @@
#pragma once
#include <atomic>
#include <boost/functional/hash.hpp>
#include <memory>
#include <string>
#include <thread>
#include <ylt/util/expected.hpp>
#include <ylt/util/tl/expected.hpp>
#include "heartbeat_type.h"
#include "client_meta.h"
#include "mutex.h"
#include "rpc_types.h"
#include "types.h"
#include <random>
namespace mooncake {
class ClientIterator {
public:
virtual ~ClientIterator() = default;
std::shared_ptr<ClientMeta> Next() {
if (index_ < clients_.size()) {
return clients_[index_++];
}
return nullptr;
}
protected:
ClientIterator() = default;
std::vector<std::shared_ptr<ClientMeta>> clients_;
size_t index_ = 0;
};
class OrderedClientIterator : public ClientIterator {
public:
OrderedClientIterator(
const std::unordered_map<UUID, std::shared_ptr<ClientMeta>,
boost::hash<UUID>>& client_metas) {
clients_.reserve(client_metas.size());
for (const auto& [id, meta] : client_metas) {
clients_.emplace_back(meta);
}
}
};
class RandomClientIterator : public ClientIterator {
public:
RandomClientIterator(
const std::unordered_map<UUID, std::shared_ptr<ClientMeta>,
boost::hash<UUID>>& client_metas) {
clients_.reserve(client_metas.size());
for (const auto& [id, meta] : client_metas) {
clients_.emplace_back(meta);
}
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(clients_.begin(), clients_.end(), g);
}
};
/**
* @brief ClientManager is a base class for managing clients' lifecycle and
* heartbeat with a three-state state machine (HEALTH/DISCONNECTION/CRASHED).
*/
class ClientManager {
public:
ClientManager(const int64_t disconnect_timeout_sec,
const int64_t crash_timeout_sec,
const ViewVersionId view_version);
virtual ~ClientManager();
void Start();
void Stop();
void StartClientMonitor();
void StopClientMonitor();
/**
* @brief Register a client with its segments.
* Writes ClientMeta to client_metas_ and batch-mounts segments.
* Must be called before any other client/segment operations.
* @return RegisterClientResponse containing master's view_version
*/
auto RegisterClient(const RegisterClientRequest& req)
-> tl::expected<RegisterClientResponse, ErrorCode>;
/**
* @brief Process a heartbeat from a client.
* 1. maintain client healthy status machine:
* - If client not in client_metas_: returns UNDEFINED + view_version,
* client should register it again.
* - If CRASHED: returns CRASHED:
* master is cleaning up the client meta, client should retry until cleaning
* over and register it again
* - If DISCONNECTION: recovers to HEALTH
* 2. Processes lightweight sync tasks
*/
auto Heartbeat(const HeartbeatRequest& req)
-> tl::expected<HeartbeatResponse, ErrorCode>;
auto QueryClientStatus(const QueryClientStatusRequest& req)
-> tl::expected<QueryClientStatusResponse, ErrorCode>;
auto GetAllSegments() -> tl::expected<std::vector<std::string>, ErrorCode>;
auto GetClientSegments(const UUID& client_id)
-> tl::expected<std::vector<std::string>, ErrorCode>;
auto QuerySegments(const std::string& segment)
-> tl::expected<std::pair<size_t, size_t>, ErrorCode>;
auto QuerySegment(const UUID& client_id, const UUID& segment_id)
-> tl::expected<std::shared_ptr<Segment>, ErrorCode>;
auto QueryIp(const UUID& client_id)
-> tl::expected<std::vector<std::string>, ErrorCode>;
auto GetClient(const UUID& client_id) -> std::shared_ptr<ClientMeta>;
auto GetAllClients() -> std::vector<std::shared_ptr<ClientMeta>>;
/**
* @brief Iterate clients in the order determined by strategy.
* @param strategy Client iteration strategy
* @param visitor Callback invoked for each client, return
* <is_continue, error_reason>:
* - if visitor occurs error, just return the `error_code`.
* - otherwise, return bool value to indicate whether
* the iteration is over.
* @return if clients iteration correctly, just return nothing,
* otherwise return the first non-OK ErrorCode from the visitor.
*/
using ClientVisitor = std::function<tl::expected<bool, ErrorCode>(
const std::shared_ptr<ClientMeta>& client)>;
auto ForEachClient(ObjectIterateStrategy strategy,
const ClientVisitor& visitor)
-> tl::expected<void, ErrorCode>;
using SegmentRemovalCallback = std::function<void(const UUID& segment_id)>;
void SetSegmentRemovalCallback(SegmentRemovalCallback cb);
protected:
/**
* @brief Client monitor implementation with three-state machine.
*/
void ClientMonitorFunc();
/**
* @brief simple heartbeat task dispatcher
*/
virtual HeartbeatTaskResult ProcessTask(const UUID& client_id,
const HeartbeatTask& task) = 0;
virtual std::unique_ptr<ClientIterator> InnerBuildClientIterator(
ObjectIterateStrategy strategy);
protected:
/**
* @brief Create architecture-specific ClientMeta
*/
virtual std::shared_ptr<ClientMeta> CreateClientMeta(
const RegisterClientRequest& req) = 0;
/**
* @brief Hook called after a client is registered.
* Subclasses can override to perform post-registration logic
* (e.g., setting is_syncing flag for HA recovery).
*/
virtual void OnClientRegistered(
const std::shared_ptr<ClientMeta>& /*meta*/) {}
/**
* @brief Get the deployment mode of this ClientManager.
* Used for architecture validation during client registration.
*/
virtual DeploymentMode GetDeploymentMode() const = 0;
protected:
static constexpr uint64_t kClientMonitorSleepMs =
1000; // 1000 ms sleep between client monitor checks
protected:
mutable SharedMutex clients_mutex_;
// Client metadata: client_id -> metadata (including health state)
std::unordered_map<UUID, std::shared_ptr<ClientMeta>, boost::hash<UUID>>
client_metas_ GUARDED_BY(clients_mutex_);
std::thread client_monitor_thread_;
std::atomic<bool> client_monitor_running_{false};
const ViewVersionId view_version_; // Passed from MasterService
SegmentRemovalCallback segment_removal_cb_;
};
} // namespace mooncake

View File

@ -0,0 +1,106 @@
#pragma once
#include <boost/functional/hash.hpp>
#include "segment_manager.h"
#include <chrono>
#include <memory>
#include <ylt/util/expected.hpp>
#include <ylt/util/tl/expected.hpp>
#include "mutex.h"
#include "types.h"
namespace mooncake {
struct ClientHealthState {
ClientStatus status = ClientStatus::UNDEFINED;
std::chrono::steady_clock::time_point last_heartbeat;
};
/**
* @brief ClientMeta records the meta data of a client including health status
* and segment information.
*/
class ClientMeta {
public:
virtual ~ClientMeta() = default;
ClientMeta(const UUID& client_id);
tl::expected<void, ErrorCode> MountSegment(const Segment& segment);
tl::expected<void, ErrorCode> UnmountSegment(const UUID& segment_id);
tl::expected<std::vector<Segment>, ErrorCode> GetSegments();
tl::expected<std::pair<size_t, size_t>, ErrorCode> QuerySegments(
const std::string& segment_name);
tl::expected<std::shared_ptr<Segment>, ErrorCode> QuerySegment(
const UUID& segment_id);
virtual tl::expected<std::vector<std::string>, ErrorCode> QueryIp(
const UUID& client_id) = 0;
using SegmentRemovalCallback = std::function<void(const UUID& segment_id)>;
void SetSegmentRemovalCallback(SegmentRemovalCallback cb);
public:
static void SetTimeouts(int64_t disconnect_sec, int64_t crash_sec);
/**
* @brief Update heartbeat timestamp and health status.
* Attention: if client is CRASHED, the heartbeat will not be updated.
* @return std::pair<ClientStatus, ClientStatus> {old_status, new_status}
*/
std::pair<ClientStatus, ClientStatus> Heartbeat();
/**
* @brief Based on last heartbeat timestamp, update health status
*
* States machine:
* - HEALTH:
* -> DISCONNECTION: If (now - last_heartbeat) > disconnect_timeout_sec.
*
* - DISCONNECTION:
* -> HEALTH: If (now - last_heartbeat) <= disconnect_timeout_sec.
* (Implies a Heartbeat() call updated the timestamp).
* -> CRASHED: If (now - last_heartbeat) > crash_timeout_sec.
*
* - CRASHED: Final state.
*
* @return std::pair<ClientStatus, ClientStatus> {old_status, new_status}
*/
std::pair<ClientStatus, ClientStatus> CheckHealth();
public:
// Hooks for health status changes
void OnDisconnected();
virtual void DoOnDisconnected() = 0;
void OnRecovered();
virtual void DoOnRecovered() = 0;
void OnCrashed();
public:
UUID get_client_id() const { return client_id_; }
ClientHealthState get_health_state() const;
bool is_health() const;
protected:
tl::expected<void, ErrorCode> InnerStatusCheck() const;
void InnerUpdateHeartbeat();
std::pair<ClientStatus, ClientStatus> InnerUpdateHealthStatus();
std::string HealthToString(ClientStatus status) const;
protected:
virtual std::shared_ptr<SegmentManager> GetSegmentManager() = 0;
protected:
static int64_t disconnect_timeout_sec_;
static int64_t crash_timeout_sec_;
mutable SharedMutex client_mutex_;
UUID client_id_;
ClientHealthState health_state_ GUARDED_BY(client_mutex_);
};
} // namespace mooncake

View File

@ -0,0 +1,66 @@
#pragma once
#include <memory>
#include <ylt/util/tl/expected.hpp>
#include "client_rpc_types.h"
#include "data_manager.h"
#include "types.h"
#include <ylt/coro_rpc/coro_rpc_server.hpp>
namespace mooncake {
/**
* @class ClientRpcService
* @brief RPC service for handling remote data read/write requests from peer
* clients
*
* This service receives RPC requests from other clients (e.g., Client A) to
* read/write data stored locally (on Client B). It uses DataManager to access
* TieredBackend and TransferEngine to perform zero-copy RDMA transfers.
*/
class ClientRpcService {
public:
/**
* @brief Constructor
* @param data_manager Reference to DataManager instance (must outlive this
* object)
*/
explicit ClientRpcService(DataManager& data_manager);
/**
* @brief Read remote data: Client A requests Client B to read data and
* transfer to A
* @param request RemoteReadRequest containing key and destination buffers
* @return ErrorCode indicating success or failure
*
* Flow:
* 1. DataManager.ReadRemoteData(key, dest_buffers)
* 2. TieredBackend.Get(key) handle
* 3. TransferEngine.submitTransfer(WRITE) to transfer data from B to A
*/
tl::expected<void, ErrorCode> ReadRemoteData(
const RemoteReadRequest& request);
/**
* @brief Write remote data: Client A requests Client B to write data from A
* @param request RemoteWriteRequest containing key, source buffers, and
* target_tier_id
* @return UUID containing the route descriptor of the
* written replica, or ErrorCode
*/
tl::expected<UUID, ErrorCode> WriteRemoteData(
const RemoteWriteRequest& request);
private:
DataManager& data_manager_; // Reference: owned by Client, same lifetime
};
/**
* @brief Register ClientRpcService methods with coro_rpc_server
* @param server coro_rpc_server instance
* @param service ClientRpcService instance
*/
void RegisterClientRpcService(coro_rpc::coro_rpc_server& server,
ClientRpcService& service);
} // namespace mooncake

View File

@ -0,0 +1,74 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include "types.h"
#include "ylt/struct_json/json_reader.h"
#include "ylt/struct_json/json_writer.h"
namespace mooncake {
/**
* @struct RemoteBufferDesc
* @brief Describes a remote buffer location for RDMA transfer
*/
struct RemoteBufferDesc {
std::string segment_endpoint; // Target segment endpoint
uintptr_t addr; // Buffer address
uint64_t size; // Buffer size in bytes
};
YLT_REFL(RemoteBufferDesc, segment_endpoint, addr, size);
/**
* @struct RemoteReadRequest
* @brief RPC request for reading remote data
*/
struct RemoteReadRequest {
std::string key; // Object key to read
std::vector<RemoteBufferDesc>
dest_buffers; // Destination buffers on remote client
};
YLT_REFL(RemoteReadRequest, key, dest_buffers);
/**
* @struct RemoteWriteRequest
* @brief RPC request for writing remote data
*/
struct RemoteWriteRequest {
std::string key;
std::vector<RemoteBufferDesc> src_buffers;
std::optional<UUID> target_tier_id;
};
YLT_REFL(RemoteWriteRequest, key, src_buffers, target_tier_id);
/**
* @struct BatchRemoteReadRequest
* @brief Batch RPC request for reading multiple remote data objects
*/
struct BatchRemoteReadRequest {
std::vector<std::string> keys; // Object keys to read
std::vector<std::vector<RemoteBufferDesc>>
dest_buffers_list; // Destination buffers for each key
};
YLT_REFL(BatchRemoteReadRequest, keys, dest_buffers_list);
/**
* @struct BatchRemoteWriteRequest
* @brief Batch RPC request for writing multiple remote data objects
*/
struct BatchRemoteWriteRequest {
std::vector<std::string> keys; // Object keys to write
std::vector<std::vector<RemoteBufferDesc>>
src_buffers_list; // Source buffers for each key
std::vector<std::optional<UUID>>
target_tier_ids; // Target tier IDs for each key
};
YLT_REFL(BatchRemoteWriteRequest, keys, src_buffers_list, target_tier_ids);
} // namespace mooncake

View File

@ -1,101 +1,92 @@
#pragma once
#include <boost/functional/hash.hpp>
#include <condition_variable>
#include <functional>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <optional>
#include <string>
#include <thread>
#include <vector>
#include <ylt/util/tl/expected.hpp>
#include <chrono>
#include "mutex.h"
#include "client_metric.h"
#include "ha_helper.h"
#include "master_client.h"
#include "storage_backend.h"
#include "thread_pool.h"
#include "transfer_engine.h"
#include "transfer_task.h"
#include "types.h"
#include "p2p_rpc_types.h"
#include "replica.h"
#include "master_metric_manager.h"
#include "master_client.h"
#include <ylt/coro_rpc/coro_rpc_server.hpp>
#include <ylt/coro_http/coro_http_server.hpp>
#include "client_config_builder.h"
#include "client_buffer.hpp"
namespace mooncake {
class PutOperation;
using WriteConfig = std::variant<ReplicateConfig, WriteRouteRequestConfig>;
/**
* @brief Result of a query operation containing replica information and lease
* timeout
* @brief Result of a query operation containing replica information
*/
class QueryResult {
public:
/** @brief List of available replicas for the queried key */
const std::vector<Replica::Descriptor> replicas;
/** @brief Time point when the lease for this key expires */
const std::chrono::steady_clock::time_point lease_timeout;
QueryResult(std::vector<Replica::Descriptor>&& replicas_param,
std::chrono::steady_clock::time_point lease_timeout_param)
: replicas(std::move(replicas_param)),
lease_timeout(lease_timeout_param) {}
explicit QueryResult(std::vector<Replica::Descriptor>&& replicas_param)
: replicas(std::move(replicas_param)) {}
bool IsLeaseExpired() const {
return std::chrono::steady_clock::now() >= lease_timeout;
}
virtual ~QueryResult() = default;
bool IsLeaseExpired(std::chrono::steady_clock::time_point& now) const {
return now >= lease_timeout;
}
// Disable copy to prevent slicing; allow move
QueryResult(const QueryResult&) = delete;
QueryResult& operator=(const QueryResult&) = delete;
QueryResult(QueryResult&&) = default;
QueryResult& operator=(QueryResult&&) = default;
};
/**
* @brief Client for interacting with the mooncake distributed object store
*/
class Client {
class ClientService {
public:
~Client();
virtual ~ClientService();
/**
* @brief Creates and initializes a new Client instance
* @param local_hostname Local host address (IP:Port)
* @param metadata_connstring Connection string for metadata service
* @param protocol Transfer protocol ("rdma" or "tcp")
* @param device_names Comma-separated RDMA device names.
* Optional with default auto-discovery. Only required when
* auto-discovery is disabled (set env `MC_MS_AUTO_DISC=0`).
* @param master_server_entry The entry of master server (IP:Port of master
* address for non-HA mode, etcd://IP:Port;IP:Port;...;IP:Port for
* HA mode)
* @return std::optional containing a shared_ptr to Client if successful,
* std::nullopt otherwise
* @brief stops background threads
*/
static std::optional<std::shared_ptr<Client>> Create(
const std::string& local_hostname,
const std::string& metadata_connstring, const std::string& protocol,
const std::optional<std::string>& device_names = std::nullopt,
const std::string& master_server_entry = kDefaultMasterAddress,
const std::shared_ptr<TransferEngine>& transfer_engine = nullptr,
std::map<std::string, std::string> labels = {});
virtual void Stop();
/**
* @brief Retrieves data for a given key
* @param object_key Key to retrieve
* @param slices Vector of slices to store the retrieved data
* @return ErrorCode indicating success/failure
* @brief stops heartbeat thread
*/
tl::expected<void, ErrorCode> Get(const std::string& object_key,
std::vector<Slice>& slices);
virtual void StopHeartbeat();
/**
* @brief Batch retrieve data for multiple keys
* @param object_keys Keys to query
* @param slices Map of object keys to their data slices
* @brief Release internal resources. Should be called after Stop()
*/
std::vector<tl::expected<void, ErrorCode>> BatchGet(
const std::vector<std::string>& object_keys,
std::unordered_map<std::string, std::vector<Slice>>& slices);
virtual void Destroy();
/**
* @brief Creates and initializes a new ClientService instance
* @param config The start up configuration for the client service.
* @return std::optional containing a shared_ptr to ClientService if
* successful, std::nullopt otherwise
*/
static std::optional<std::shared_ptr<ClientService>> Create(
const CentralizedClientConfig& config);
static std::optional<std::shared_ptr<ClientService>> Create(
const P2PClientConfig& config);
/**
* @brief Returns the deployment mode of the client service.
* @return DeploymentMode (CENTRALIZATION or P2P).
*/
virtual DeploymentMode deployment_mode() const = 0;
/**
* @brief Batch query IP addresses for multiple client IDs.
@ -103,76 +94,89 @@ class Client {
* @return An expected object containing a map from client_id to their IP
* address lists on success, or an ErrorCode on failure.
*/
tl::expected<
virtual tl::expected<
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>,
ErrorCode>
BatchQueryIp(const std::vector<UUID>& client_ids);
/**
* @brief Gets object metadata without transferring data
* @param object_key Key to query
* @return QueryResult containing replicas and lease timeout, or ErrorCode
* indicating failure
*/
tl::expected<QueryResult, ErrorCode> Query(const std::string& object_key);
/**
* @brief Queries replica lists for object keys that match a regex pattern.
* @param str The regular expression string to match against object keys.
* @return An expected object containing a map from object keys to their
* replica descriptors on success, or an ErrorCode on failure.
*/
tl::expected<
virtual tl::expected<
std::unordered_map<std::string, std::vector<Replica::Descriptor>>,
ErrorCode>
QueryByRegex(const std::string& str);
/**
* @brief Gets object metadata without transferring data
* @param object_key Key to query
* @return QueryResult (or its subclass) containing replicas, or ErrorCode
* indicating failure
*/
virtual tl::expected<std::unique_ptr<QueryResult>, ErrorCode> Query(
const std::string& object_key, const ReadRouteConfig& config = {}) = 0;
/**
* @brief Batch query object metadata without transferring data
* @param object_keys Keys to query
* @return Vector of QueryResult objects containing replicas and lease
* timeouts
* @return Vector of QueryResult (or its subclass) containing replicas
*/
std::vector<tl::expected<QueryResult, ErrorCode>> BatchQuery(
const std::vector<std::string>& object_keys);
virtual std::vector<tl::expected<std::unique_ptr<QueryResult>, ErrorCode>>
BatchQuery(const std::vector<std::string>& object_keys,
const ReadRouteConfig& config = {}) = 0;
/**
* @brief Batch clear KV cache for specified object keys on a specific
* segment for a given client.
* @param object_keys Vector of object key strings to clear.
* @param client_id The UUID of the client that owns the object keys.
* @param segment_name The name of the segment (storage device) to clear
* from.
* @return An expected object containing a vector of successfully cleared
* object keys on success, or an ErrorCode on failure.
* @brief Gets data with memory allocation
* @param key Object key
* @param allocator Read buffer allocator
* @param config Read route config
* @return BufferHandle allocated by `allocator` on success.
* ErrorCode on failure.
*/
tl::expected<std::vector<std::string>, ErrorCode> BatchReplicaClear(
const std::vector<std::string>& object_keys, const UUID& client_id,
const std::string& segment_name);
virtual tl::expected<std::shared_ptr<BufferHandle>, ErrorCode> Get(
const std::string& key,
std::shared_ptr<ClientBufferAllocator> allocator,
const ReadRouteConfig& config = {}) = 0;
virtual std::vector<tl::expected<std::shared_ptr<BufferHandle>, ErrorCode>>
BatchGet(const std::vector<std::string>& keys,
std::shared_ptr<ClientBufferAllocator> allocator,
const ReadRouteConfig& config = {}) = 0;
/**
* @brief Transfers data using pre-queried object information
* @param object_key Key of the object
* @param query_result Previously queried object metadata containing
* replicas and lease timeout
* @param slices Vector of slices to store the data
* @return ErrorCode indicating success/failure
* @brief Gets data into user-provided buffers without memory allocation
* @param key Object key
* @param buffers Vector of destination buffer pointers
* @param sizes Vector of buffer sizes (must match buffers.size())
* @param config Read route config
* @return Number of bytes read on success. ErrorCode on failure.
*/
tl::expected<void, ErrorCode> Get(const std::string& object_key,
const QueryResult& query_result,
std::vector<Slice>& slices);
virtual tl::expected<int64_t, ErrorCode> Get(
const std::string& key, const std::vector<void*>& buffers,
const std::vector<size_t>& sizes,
const ReadRouteConfig& config = {}) = 0;
/**
* @brief Transfers data using pre-queried object information
* @param object_keys Keys of the objects
* @param query_results Previously queried object metadata for each key
* @param slices Map of object keys to their data slices
* @return Vector of ErrorCode results for each object
* @brief Batch get data into user-provided buffers
* @param keys Object keys
* @param all_buffers Vector of buffer pointer vectors (one per key)
* @param all_sizes Vector of buffer size vectors (one per key)
* @param config Read route config
* @param aggregate_same_segment_task
* Whether to aggregate read tasks on the same segment.
* If false, each key will be generated as a independent task.
* Otherwise, the tasks will be aggregated on the same segment.
* @return Vector of bytes read on success. ErrorCode on failure.
*/
std::vector<tl::expected<void, ErrorCode>> BatchGet(
const std::vector<std::string>& object_keys,
const std::vector<QueryResult>& query_results,
std::unordered_map<std::string, std::vector<Slice>>& slices,
bool prefer_same_node = false);
virtual std::vector<tl::expected<int64_t, ErrorCode>> BatchGet(
const std::vector<std::string>& keys,
const std::vector<std::vector<void*>>& all_buffers,
const std::vector<std::vector<size_t>>& all_sizes,
const ReadRouteConfig& config = {},
bool aggregate_same_segment_task = false) = 0;
/**
* @brief Stores data with replication
@ -181,9 +185,9 @@ class Client {
* @param config Replication configuration
* @return ErrorCode indicating success/failure
*/
tl::expected<void, ErrorCode> Put(const ObjectKey& key,
std::vector<Slice>& slices,
const ReplicateConfig& config);
virtual tl::expected<void, ErrorCode> Put(const ObjectKey& key,
std::vector<Slice>& slices,
const WriteConfig& config) = 0;
/**
* @brief Batch put data with replication
@ -192,17 +196,17 @@ class Client {
* to match keys)
* @param config Replication configuration
*/
std::vector<tl::expected<void, ErrorCode>> BatchPut(
virtual std::vector<tl::expected<void, ErrorCode>> BatchPut(
const std::vector<ObjectKey>& keys,
std::vector<std::vector<Slice>>& batched_slices,
const ReplicateConfig& config);
const WriteConfig& config) = 0;
/**
* @brief Removes an object and all its replicas
* @param key Key to remove
* @return ErrorCode indicating success/failure
*/
tl::expected<void, ErrorCode> Remove(const ObjectKey& key);
virtual tl::expected<void, ErrorCode> Remove(const ObjectKey& key) = 0;
/**
* @brief Removes objects from the store whose keys match a regex pattern.
@ -210,13 +214,14 @@ class Client {
* @return An expected object containing the number of removed objects on
* success, or an ErrorCode on failure.
*/
tl::expected<long, ErrorCode> RemoveByRegex(const ObjectKey& str);
virtual tl::expected<long, ErrorCode> RemoveByRegex(
const ObjectKey& str) = 0;
/**
* @brief Removes all objects and all its replicas
* @return tl::expected<long, ErrorCode> number of removed objects or error
*/
tl::expected<long, ErrorCode> RemoveAll();
virtual tl::expected<long, ErrorCode> RemoveAll() = 0;
/**
* @brief Registers a memory segment to master for allocation
@ -224,7 +229,8 @@ class Client {
* @param size Size of the buffer in bytes
* @return ErrorCode indicating success/failure
*/
tl::expected<void, ErrorCode> MountSegment(const void* buffer, size_t size);
virtual tl::expected<void, ErrorCode> MountSegment(const void* buffer,
size_t size) = 0;
/**
* @brief Unregisters a memory segment from master
@ -232,8 +238,8 @@ class Client {
* @param size Size of the buffer in bytes
* @return ErrorCode indicating success/failure
*/
tl::expected<void, ErrorCode> UnmountSegment(const void* buffer,
size_t size);
virtual tl::expected<void, ErrorCode> UnmountSegment(const void* buffer,
size_t size) = 0;
/**
* @brief Registers memory buffer with TransferEngine for data transfer
@ -260,65 +266,17 @@ class Client {
/**
* @brief Checks if an object exists
* @param key Key to check
* @return ErrorCode::OK if exists, ErrorCode::OBJECT_NOT_FOUND if not
* exists, other ErrorCode for errors
* @return True if exists, false if not, or ErrorCode for unexpected errors.
*/
tl::expected<bool, ErrorCode> IsExist(const std::string& key);
virtual tl::expected<bool, ErrorCode> IsExist(const std::string& key) = 0;
/**
* @brief Checks if multiple objects exist
* @param keys Vector of keys to check
* @return Vector of existence results for each key
*/
std::vector<tl::expected<bool, ErrorCode>> BatchIsExist(
const std::vector<std::string>& keys);
/**
* @brief Mounts a local disk segment into the master.
* @param enable_offloading If true, enables offloading (write-to-file).
*/
tl::expected<void, ErrorCode> MountLocalDiskSegment(bool enable_offloading);
/**
* @brief Heartbeat call to collect object-level statistics and retrieve the
* set of non-offloaded objects.
* @param enable_offloading Indicates whether offloading is enabled for this
* segment.
* @param offloading_objects On return, contains a map from object key to
* size (in bytes) for all objects that require offload.
*/
tl::expected<void, ErrorCode> OffloadObjectHeartbeat(
bool enable_offloading,
std::unordered_map<std::string, int64_t>& offloading_objects);
/**
* @brief Performs a batched write of multiple objects using a
* high-throughput Transfer Engine.
* @param transfer_engine_addr Address of the Transfer Engine service (e.g.,
* "ip:port").
* @param keys List of keys identifying the data objects to be transferred
* @param pointers Array of destination memory addresses on the remote node
* where data will be written (one per key)
* @param batched_slices Map from object key to its data slice
* (`mooncake::Slice`), containing raw bytes to be written.
*/
tl::expected<void, ErrorCode> BatchPutOffloadObject(
const std::string& transfer_engine_addr,
const std::vector<std::string>& keys,
const std::vector<uintptr_t>& pointers,
const std::unordered_map<std::string, Slice>& batched_slices);
/**
* @brief Notifies the master that offloading of specified objects has
* succeeded.
* @param keys A list of object keys (names) that were successfully
* offloaded.
* @param metadatas The corresponding metadata for each offloaded object,
* including size, storage location, etc.
*/
tl::expected<void, ErrorCode> NotifyOffloadSuccess(
const std::vector<std::string>& keys,
const std::vector<StorageObjectMetadata>& metadatas);
virtual std::vector<tl::expected<bool, ErrorCode>> BatchIsExist(
const std::vector<std::string>& keys) = 0;
// For human-readable metrics
tl::expected<std::string, ErrorCode> GetSummaryMetrics() {
@ -330,7 +288,12 @@ class Client {
tl::expected<MasterMetricManager::CacheHitStatDict, ErrorCode>
CalcCacheStats() {
return master_client_.CalcCacheStats();
auto guard = AcquireInflightGuard();
if (!guard.is_valid()) {
LOG(ERROR) << "client is shutting down";
return tl::unexpected(ErrorCode::SHUTTING_DOWN);
}
return GetMasterClient().CalcCacheStats();
}
// For Prometheus-style metrics
@ -343,79 +306,223 @@ class Client {
return str;
}
/**
* @brief Gets the metrics HTTP server port.
* @return The port number, or 0 if metrics server is disabled.
*/
uint16_t GetMetricsPort() const { return metrics_port_; }
/**
* @brief Checks if metrics HTTP server is enabled.
* @return True if enabled, false otherwise.
*/
bool IsMetricsHttpEnabled() const { return enable_metrics_http_; }
/**
* @brief Gets the health status for the /health endpoint.
* @return A string representing the health status.
*/
virtual std::string GetHealthStatus() const { return "OK"; }
public:
/**
* @brief Gets the local transport endpoint (IP and port).
* @return The transport endpoint string.
*/
[[nodiscard]] std::string GetTransportEndpoint() {
return transfer_engine_->getLocalIpAndPort();
}
UUID GetClientID() const { return client_id_; }
ViewVersionId GetViewVersion() const { return view_version_.load(); }
private:
public:
/**
* @brief Checks if memory registration parameters are valid
* @param addr Memory address to check
* @param length Size of the memory region
* @return ErrorCode indicating success or failure
*/
static tl::expected<void, ErrorCode> CheckRegisterMemoryParams(
const void* addr, size_t length);
/**
* @brief Calculate the total size of a list of slices.
* @param slices Vector of slices.
* @return Total size in bytes.
*/
[[nodiscard]] static size_t CalculateSliceSize(
const std::vector<Slice>& slices);
/**
* @brief Calculate the total size of a list of slices.
* @param slices Span of slices.
* @return Total size in bytes.
*/
[[nodiscard]] static size_t CalculateSliceSize(
std::span<const Slice> slices);
protected:
/**
* @brief Private constructor to enforce creation through Create() method
*/
Client(const std::string& local_hostname,
const std::string& metadata_connstring,
const std::map<std::string, std::string>& labels = {});
ClientService(const std::string& local_ip, uint16_t te_port,
const std::string& metadata_connstring,
uint16_t metrics_port = 9003, bool enable_metrics_http = true,
const std::map<std::string, std::string>& labels = {});
/**
* @brief Internal helper functions for initialization and data transfer
* @brief Get the RPC Client for Master service calls
* @return Reference to MasterClient
*/
virtual MasterClient& GetMasterClient() = 0;
/**
* @brief Connects to the master server.
* @param master_server_entry Entry point of the master server.
* @return ErrorCode indicating success or failure.
*/
ErrorCode ConnectToMaster(const std::string& master_server_entry);
/**
* @brief Initializes the Transfer Engine.
* @param local_hostname Local hostname or IP.
* @param metadata_connstring Connection string for metadata service.
* @param protocol Transport protocol (e.g., "tcp", "rdma").
* @param device_names Optional RDMA device names.
* @return ErrorCode indicating success or failure.
*/
ErrorCode InitTransferEngine(
const std::string& local_hostname,
const std::string& metadata_connstring, const std::string& protocol,
const std::string& endpoint, const std::string& metadata_connstring,
const std::string& protocol,
const std::optional<std::string>& device_names);
void InitTransferSubmitter();
ErrorCode TransferData(const Replica::Descriptor& replica_descriptor,
std::vector<Slice>& slices,
TransferRequest::OpCode op_code);
ErrorCode TransferWrite(const Replica::Descriptor& replica_descriptor,
std::vector<Slice>& slices);
ErrorCode TransferRead(const Replica::Descriptor& replica_descriptor,
std::vector<Slice>& slices);
protected:
// Heartbeat-related function
/**
* @brief Prepare and use the storage backend for persisting data
* @brief Starts the heartbeat thread.
* @param master_server_entry Entry point of the master server.
*/
void PrepareStorageBackend(const std::string& storage_root_dir,
const std::string& fsdir,
bool enable_eviction = true,
uint64_t quota_bytes = 0);
void StartHeartbeat(const std::string& master_server_entry);
void PutToLocalFile(const std::string& object_key,
const std::vector<Slice>& slices,
const DiskDescriptor& disk_descriptor);
void HeartbeatThreadMain(bool is_ha_mode,
std::string current_master_address,
const std::string& master_server_entry);
/**
* @brief Find the first complete replica from a replica list
* @param replica_list List of replicas to search through
* @param replica the first complete replica (file or memory)
* @return ErrorCode::OK if found, ErrorCode::INVALID_REPLICA if no complete
* replica
* @brief Handles a successful heartbeat response.
* Triggers async RegisterClient if master reports UNDEFINED status.
* Fires MASTER_RECONNECTED if connection_interrupted_ was set.
* @return true if heartbeat was successfully processed.
*/
ErrorCode FindFirstCompleteReplica(
const std::vector<Replica::Descriptor>& replica_list,
Replica::Descriptor& replica);
bool HandleHeartbeatResponse(const HeartbeatResponse& response,
const std::string& current_master_address,
const std::function<void()>& register_client,
std::future<void>& register_client_future);
/**
* @brief Batch put helper methods for structured approach
* @brief Handles the result of a task received in a heartbeat response.
* @param task_result The result of the task.
*/
std::vector<PutOperation> CreatePutOperations(
const std::vector<ObjectKey>& keys,
const std::vector<std::vector<Slice>>& batched_slices);
void StartBatchPut(std::vector<PutOperation>& ops,
const ReplicateConfig& config);
void SubmitTransfers(std::vector<PutOperation>& ops);
void WaitForTransfers(std::vector<PutOperation>& ops);
void FinalizeBatchPut(std::vector<PutOperation>& ops);
std::vector<tl::expected<void, ErrorCode>> CollectResults(
const std::vector<PutOperation>& ops);
void HandleHeartbeatTaskResult(const HeartbeatTaskResult& task_result);
std::vector<tl::expected<void, ErrorCode>> BatchPutWhenPreferSameNode(
std::vector<PutOperation>& ops);
std::vector<tl::expected<void, ErrorCode>> BatchGetWhenPreferSameNode(
const std::vector<std::string>& object_keys,
const std::vector<QueryResult>& query_results,
std::unordered_map<std::string, std::vector<Slice>>& slices);
/**
* @brief Attempts to reconnect to master after heartbeat failures.
* For HA mode, fetches the latest master address from etcd.
* For non-HA mode, reconnects to the current_master_address.
* @param is_ha_mode Whether HA mode is enabled.
* @param current_master_address Current master address, may be updated
* after successful reconnection in HA mode.
* @return true if reconnect succeeded, false otherwise.
*/
bool ReconnectToMaster(bool is_ha_mode,
std::string& current_master_address);
/**
* @brief Waits for the next heartbeat interval using condition variable.
* @param interval_ms Milliseconds to wait.
*/
void WaitForNextHeartbeat(int interval_ms);
virtual HeartbeatRequest build_heartbeat_request() = 0;
/**
* @brief Starts the metrics HTTP server.
* @param enable_metrics_http Whether to enable the HTTP server.
* @param metrics_port Port to use.
* @return The actual port number, or 0 if disabled.
*/
uint16_t StartMetricsHttpServer(bool enable_metrics_http,
uint16_t metrics_port);
/**
* @brief Stops the metrics HTTP server.
*/
void StopMetricsHttpServer();
/**
* @brief Registers the client into the master server.
* @return An ErrorCode indicating success or failure.
*/
virtual tl::expected<RegisterClientResponse, ErrorCode>
RegisterClient() = 0;
/**
* @brief Single hook for all HA-related events from the heartbeat loop.
* Subclasses override to handle state transitions.
*/
virtual void OnHAEvent(HAEvent event) { (void)event; }
protected:
/**
* @brief RAII guard for managing in-flight requests during service
* shutdown.
*/
class InflightRequestGuard {
public:
explicit InflightRequestGuard(ClientService* client)
: client_(client),
valid_(false),
lock_(&client_->running_rw_mtx_, shared_lock) {
valid_ = client_->is_running_;
}
~InflightRequestGuard() = default;
InflightRequestGuard(const InflightRequestGuard&) = delete;
InflightRequestGuard& operator=(const InflightRequestGuard&) = delete;
InflightRequestGuard(InflightRequestGuard&& other) = delete;
InflightRequestGuard& operator=(InflightRequestGuard&& other) = delete;
bool is_valid() const { return valid_; }
private:
ClientService* client_;
bool valid_;
SharedMutexLocker lock_;
};
/**
* @brief Acquires an inflight request guard.
* @return An InflightRequestGuard. If shutting down, is_valid() will be
* false.
*/
InflightRequestGuard AcquireInflightGuard() {
return InflightRequestGuard(this);
}
/**
* @brief Marks the service as shutting down.
* @return true if successfully marked, false if already shutting down.
*/
bool MarkShuttingDown() {
SharedMutexLocker lock(&running_rw_mtx_);
if (!is_running_) return false;
is_running_ = false;
return true;
}
friend class InflightRequestGuard;
protected:
// Client identification
const UUID client_id_;
@ -424,26 +531,61 @@ class Client {
// Core components
std::shared_ptr<TransferEngine> transfer_engine_;
MasterClient master_client_;
std::unique_ptr<TransferSubmitter> transfer_submitter_;
// Global segment pointers
struct SegmentDeleter {
void operator()(void* ptr) {
if (ptr) {
free(ptr);
}
}
};
// Mutex to protect mounted_segments_
std::mutex mounted_segments_mutex_;
std::unordered_map<UUID, Segment, boost::hash<UUID>> mounted_segments_;
struct AscendSegmentDeleter {
void operator()(void* ptr) {
if (ptr) {
free_memory("ascend", ptr);
}
}
};
std::vector<std::unique_ptr<void, SegmentDeleter>> segment_ptrs_;
std::vector<std::unique_ptr<void, AscendSegmentDeleter>>
ascend_segment_ptrs_;
// Configuration
const std::string local_hostname_;
const std::string local_ip_;
const uint16_t te_port_;
std::string local_endpoint() const {
return local_ip_ + ":" + std::to_string(te_port_);
}
// The segment endpoint that the transfer engine registered with the
// metadata backend.
std::string te_endpoint_;
void initTeEndpoint();
const std::string& get_te_endpoint() const { return te_endpoint_; }
const std::string metadata_connstring_;
// Client persistent thread pool for async operations
ThreadPool write_thread_pool_;
std::shared_ptr<StorageBackend> storage_backend_;
// For high availability
MasterViewHelper master_view_helper_;
std::thread ping_thread_;
std::atomic<bool> ping_running_{false};
void PingThreadMain(bool is_ha_mode, std::string current_master_address);
std::thread heartbeat_thread_;
std::atomic<bool> heartbeat_running_{false};
std::condition_variable heartbeat_cv_;
std::mutex heartbeat_mtx_;
/// View version from master. Updated by async registration thread,
/// read by heartbeat thread.
std::atomic<ViewVersionId> view_version_{0};
/// True after MASTER_UNREACHABLE fires; cleared when MASTER_RECONNECTED
/// fires. Only accessed from the heartbeat thread — no locking required.
bool connection_interrupted_ = false;
// Shutdown protection
SharedMutex running_rw_mtx_;
bool is_running_ GUARDED_BY(running_rw_mtx_) = false;
// Metrics HTTP server
std::unique_ptr<coro_http::coro_http_server> metrics_http_server_;
uint16_t metrics_port_ = 0; // 0 means disabled
bool enable_metrics_http_ = true;
};
} // namespace mooncake

View File

@ -0,0 +1,386 @@
#pragma once
#include <shared_mutex>
#include <vector>
#include <string>
#include <memory>
#include <optional>
#include <ylt/util/tl/expected.hpp>
#include "async_memcpy_executor.h"
#include "client_buffer.hpp"
#include "client_config_builder.h"
#include "task_handle.h"
#include "tiered_cache/tiered_backend.h"
#include "transfer_engine.h"
#include "types.h"
#include "client_rpc_types.h"
namespace mooncake {
/**
* @struct ReadTaskHandle
* @brief Handle for a read operation
*/
struct ReadTaskHandle {
std::unique_ptr<TaskHandle<void>> task_handle;
int64_t data_size;
// if user use zero-copy get(), the var is useless;
// if user provides allocator, the var is the buffer allocated by allocator;
std::shared_ptr<BufferHandle> read_buf;
};
/**
* @struct LocalTransferConfig
* @brief Configuration for local data transfer operations
*/
struct LocalTransferConfig {
LocalTransferMode mode = LocalTransferMode::TE;
// When mode == TE, the following parameters are used:
std::string te_endpoint;
// When mode == MEMCPY, the following parameters are used:
// 0 means forbid async memcpy (fall back to synchronous).
size_t local_memcpy_async_worker_num = 32;
};
/**
* @class DataManager
* @brief Manages data access operations using TieredBackend and TransferEngine
*
* Provides unified interface for local and remote data operations, handling
* tiered storage access and zero-copy transfers.
*/
class DataManager {
// Allow test class to access private methods for testing
friend class DataManagerTest;
public:
/**
* @brief Constructor
* @param tiered_backend Unique pointer to TieredBackend instance (takes
* ownership)
* @param transfer_engine Shared pointer to TransferEngine instance (shared
* with Client)
*/
DataManager(std::unique_ptr<TieredBackend> tiered_backend,
std::shared_ptr<TransferEngine> transfer_engine,
size_t lock_shard_count = 1024,
const LocalTransferConfig& local_transfer_config = {});
void Stop() {
if (async_memcpy_executor_) {
async_memcpy_executor_->Shutdown();
}
if (tiered_backend_) {
tiered_backend_->Stop();
}
}
/**
* @brief Cleanup: delegates to TieredBackend::Destroy().
*/
void Destroy() {
if (tiered_backend_) {
tiered_backend_->Destroy();
}
}
// ================================================================
// Public local read/write interface
// Internally selects TE or Memcpy path based on config.
// ================================================================
// The Put operation consists of three phases:
// 1. Allocation: allocate memory from the tiered backend for the data
// 2. Write: write the data to the allocated memory
// 3. Commit: commit the data to the tiered backend
//
// IMPORTANT: The caller must keep the memory referenced by `slices` alive
// from the time Put() returns until TaskHandle::Wait() completes. The
// returned TaskHandle may capture raw pointers from the slices for
// asynchronous transfer.
tl::expected<std::unique_ptr<TaskHandle<void>>, ErrorCode> Put(
const std::string& key, std::vector<Slice>& slices);
// Attention!!!
// Get() method run without key lock.
// It works based on two assumptions:
// 1. We assume that each key will not be updated after they are created.
// 2. The key is acquired by handle which protect the data accessibility
// based on ref count. Once the method acquire handle successfully, the
// accessor can safely access the data until the handle is released.
//
// IMPORTANT: The caller must keep the memory referenced by `slices` alive
// from the time Get() returns until TaskHandle::Wait() completes. The
// returned TaskHandle may capture raw pointers from the slices for
// asynchronous data copy.
tl::expected<ReadTaskHandle, ErrorCode> Get(
const std::string& key, const std::vector<Slice>& slices);
tl::expected<ReadTaskHandle, ErrorCode> Get(
const std::string& key,
std::shared_ptr<ClientBufferAllocator> allocator);
/**
* @brief Query the size of an object.
* @param key Object key
* @return Object size in bytes, or ErrorCode on failure
*/
tl::expected<size_t, ErrorCode> QueryObjectSize(const std::string& key);
tl::expected<void, ErrorCode> Delete(
const std::string& key, std::optional<UUID> tier_id = std::nullopt);
/**
* @brief Get tier views from underlying tiered storage
*/
std::vector<TierView> GetTierViews() const;
// ================================================================
// Remote data transfer — called by RPC service layer
// ================================================================
/**
* @brief Iterate all keys in batches.
* Delegates to TieredBackend::ForEachKeyBatch().
*/
void ForEachKeyBatch(
const std::function<bool(std::vector<ReplicaLocation>&&)>& callback)
const;
/**
* @brief Get hot key statistics
*/
AccessStats GetHotKeyStats() const;
/**
* @brief Get all tier IDs where a key has replicas.
*/
std::vector<UUID> GetReplicaTierIds(const std::string& key) const;
/**
* @brief Read data and transfer to remote destination buffers
*
* This is the core method for remote data access:
* 1. Get data handle from TieredBackend
* 2. Use TransferEngine to transfer data via RDMA to destination buffers
*
* @param key Object key to read
* @param dest_buffers Destination buffers on remote client (Client A)
* @return ErrorCode indicating success or failure
*/
tl::expected<void, ErrorCode> ReadRemoteData(
const std::string& key,
const std::vector<RemoteBufferDesc>& dest_buffers);
/**
* @brief Write data from remote source buffers
* @param key Object key to write
* @param src_buffers Source buffers on remote client (Client A)
* @param tier_id Optional tier ID (nullopt = use default tier selection)
* @return UUID of the tier (segment) where data was written, or ErrorCode
*/
tl::expected<UUID, ErrorCode> WriteRemoteData(
const std::string& key,
const std::vector<RemoteBufferDesc>& src_buffers,
std::optional<UUID> tier_id = std::nullopt);
// ================================================================
// Utilities
// ================================================================
/**
* @brief Rectify stale read route by checking local key existence
* and removing replica from master if key is not found locally.
*
* @param key Object key to rectify
* @param tier_id Optional tier ID. If specified, only checks the given
* tier; if nullopt, checks all tiers.
*/
void RectifyReadRoute(const std::string& key,
std::optional<UUID> tier_id = std::nullopt);
/**
* @brief Set the callback for rectifying read routes in Master.
* @param fn Callback invoked when key not found locally.
*/
void SetRectifyCallback(
std::function<void(const std::string&, std::optional<UUID>)> fn);
bool Exist(const std::string& key,
std::optional<UUID> tier_id = std::nullopt) const;
private:
std::shared_mutex& GetKeyLock(const std::string& key) {
size_t hash = std::hash<std::string>{}(key);
return lock_shards_[hash % lock_shard_count_];
}
/**
* @brief Transfer data from local source to remote destination buffers
* @param handle Local allocation handle (source)
* @param dest_buffers Remote destination buffers
* @return ErrorCode indicating success or failure
*/
tl::expected<void, ErrorCode> TransferDataToRemote(
AllocationHandle handle,
const std::vector<RemoteBufferDesc>& dest_buffers);
/**
* @brief Transfer data from remote source buffers to local allocated space
* @param handle Local allocation handle (destination)
* @param src_buffers Remote source buffers
* @return ErrorCode indicating success or failure. Any segment failure
* will result in error (no partial success).
*/
tl::expected<void, ErrorCode> TransferDataFromRemote(
AllocationHandle handle,
const std::vector<RemoteBufferDesc>& src_buffers);
tl::expected<ReadTaskHandle, ErrorCode> BuildDataCopier(
const AllocationHandle& handle, const std::string& key,
const std::vector<Slice>& slices);
tl::expected<ReadTaskHandle, ErrorCode> BuildDataCopierViaTe(
const AllocationHandle& handle, const std::vector<Slice>& slices);
tl::expected<ReadTaskHandle, ErrorCode> BuildDataCopierViaMemcpy(
const AllocationHandle& handle, const std::string& key,
const std::vector<Slice>& slices);
tl::expected<LocalCopyPlan, ErrorCode> BuildLocalCopyPlan(
const std::string& key, const AllocationHandle& handle,
const std::vector<Slice>& slices) const;
// --- Put dispatch by transfer mode ---
tl::expected<std::unique_ptr<TaskHandle<void>>, ErrorCode> PutViaTe(
const std::string& key, std::vector<Slice>& slices);
tl::expected<std::unique_ptr<TaskHandle<void>>, ErrorCode> PutViaMemcpy(
const std::string& key, std::vector<Slice>& slices);
// --- Conversion helpers ---
std::vector<RemoteBufferDesc> SlicesToRemoteBufferDescs(
const std::vector<Slice>& slices) const;
// --- TE transfer helpers ---
struct TeSubmitResult {
std::vector<std::tuple<Transport::BatchID, size_t, std::string>>
transfer_batches;
// Temp DRAM buffer used when source or destination is non-DRAM.
// Non-null means a post-copy (for Write) or pre-copy (for Read) is
// required.
std::shared_ptr<void> temp_buffer;
AllocationHandle handle; // Ensure local memory is not released
};
tl::expected<TeSubmitResult, ErrorCode> SubmitTeTransferInternal(
const AllocationHandle& handle,
const std::vector<RemoteBufferDesc>& remote_buffers,
Transport::TransferRequest::OpCode opcode);
/**
* @brief Helper to wait for a transfer batch to complete
* @param batch_id Batch ID to poll
* @param num_tasks Number of tasks in the batch
* @param segment_name Name of the segment for logging
* @return ErrorCode indicating success or failure
*/
tl::expected<void, ErrorCode> WaitTransferBatch(
Transport::BatchID batch_id, size_t num_tasks,
const std::string& segment_endpoint);
/**
* @brief Validate remote buffer descriptors
* @param buffers Buffer descriptors to validate
* @return ErrorCode if validation fails, otherwise OK
*/
tl::expected<void, ErrorCode> ValidateRemoteBuffers(
const std::vector<RemoteBufferDesc>& buffers);
/**
* @brief Prepare DRAM buffer for transfer from non-DRAM source
* @param source_ptr Source data pointer
* @param source_type Source memory type
* @param total_size Total data size
* @param backend TieredBackend for DataCopier access
* @return Pair of (transfer_source_ptr, temp_buffer_owner) or error
*/
tl::expected<std::pair<void*, std::unique_ptr<void, void (*)(void*)>>,
ErrorCode>
PrepareDRAMTransferBuffer(void* source_ptr, MemoryType source_type,
size_t total_size, TieredBackend* backend);
/**
* @brief Prepare DRAM buffer for receiving data to non-DRAM destination
* @param dest_ptr Destination data pointer
* @param dest_type Destination memory type
* @param total_size Total data size
* @return Pair of (transfer_dest_ptr, temp_buffer_owner) or error
*/
tl::expected<std::pair<void*, std::unique_ptr<void, void (*)(void*)>>,
ErrorCode>
PrepareDRAMReceiveBuffer(void* dest_ptr, MemoryType dest_type,
size_t total_size);
/**
* @brief Copy data from DRAM buffer to non-DRAM tier
* @param temp_buffer Temp DRAM buffer pointer
* @param dest_ptr Destination pointer
* @param dest_type Destination memory type
* @param total_size Total data size
* @param backend TieredBackend for DataCopier access
* @return ErrorCode indicating success or failure
*/
tl::expected<void, ErrorCode> CopyFromDRAMBuffer(void* temp_buffer,
void* dest_ptr,
MemoryType dest_type,
size_t total_size,
TieredBackend* backend);
/**
* @brief Submit transfer requests for a segment (without waiting)
* @param segment_name Segment name (for logging)
* @param seg Segment handle (already opened)
* @param requests Transfer requests to submit
* @return BatchID if successful, or error
*/
tl::expected<Transport::BatchID, ErrorCode> SubmitTransferRequests(
const std::string& segment_endpoint, Transport::SegmentHandle seg,
const std::vector<Transport::TransferRequest>& requests);
/**
* @brief Wait for multiple transfer batches to complete
* @param batches Vector of (batch_id, num_tasks, segment_endpoint) tuples
* @return ErrorCode indicating success or failure. If any batch fails,
* remaining batch IDs are freed and error is returned immediately.
*/
tl::expected<void, ErrorCode> WaitAllTransferBatches(
const std::vector<std::tuple<Transport::BatchID, size_t, std::string>>&
batches);
// Wait for all tasks to reach a terminal state, then free the batch.
void CancelBatchTETask(Transport::BatchID batch_id, size_t num_tasks);
private:
std::unique_ptr<TieredBackend> tiered_backend_; // Owned by DataManager
std::shared_ptr<TransferEngine> transfer_engine_; // Shared with Client
// Sharded locks for concurrent access
// Configurable via MOONCAKE_DM_LOCK_SHARD_COUNT environment variable
// (default: 1024)
size_t lock_shard_count_;
std::vector<std::shared_mutex> lock_shards_;
// Callback for rectifying stale read routes
std::function<void(const std::string&, std::optional<UUID>)>
rectify_wrong_route_fn_;
LocalTransferConfig local_transfer_config_;
std::unique_ptr<AsyncMemcpyExecutor> async_memcpy_executor_;
};
} // namespace mooncake

View File

@ -1,9 +1,10 @@
#pragma once
#include "client_config_builder.h"
#include <ylt/coro_rpc/coro_rpc_client.hpp>
#include "pyclient.h"
#include "real_client.h"
#include <memory>
namespace mooncake {
@ -12,30 +13,30 @@ class ShmHelper {
public:
struct ShmSegment {
int fd = -1;
void *base_addr = nullptr;
void* base_addr = nullptr;
size_t size = 0;
std::string name;
bool registered = false;
bool is_local = false;
};
static ShmHelper *getInstance();
static ShmHelper* getInstance();
void *allocate(size_t size);
int free(void *addr);
void* allocate(size_t size);
int free(void* addr);
bool cleanup();
// Get the shm that contains the given address
// Returns a shared_ptr to ensure the segment remains valid
std::shared_ptr<ShmSegment> get_shm(void *addr);
std::shared_ptr<ShmSegment> get_shm(void* addr);
const std::vector<std::shared_ptr<ShmSegment>> &get_shms() const {
const std::vector<std::shared_ptr<ShmSegment>>& get_shms() const {
return shms_;
}
ShmHelper(const ShmHelper &) = delete;
ShmHelper &operator=(const ShmHelper &) = delete;
ShmHelper(const ShmHelper&) = delete;
ShmHelper& operator=(const ShmHelper&) = delete;
private:
ShmHelper();
@ -52,106 +53,104 @@ class DummyClient : public PyClient {
int64_t unregister_shm();
int setup_real(const std::string &local_hostname,
const std::string &metadata_server,
size_t global_segment_size, size_t local_buffer_size,
const std::string &protocol, const std::string &rdma_devices,
const std::string &master_server_addr,
const std::shared_ptr<TransferEngine> &transfer_engine,
const std::string &ipc_socket_path) {
// Dummy client does not support real setup
return -1;
};
int setup(DummyClientConfig& config);
int setup_dummy(size_t mem_pool_size, size_t local_buffer_size,
const std::string &server_address,
const std::string &ipc_socket_path);
int initAll(const std::string &protocol, const std::string &device_name,
size_t mount_segment_size) {
int initAll(const std::string& protocol, const std::string& device_name,
size_t mount_segment_size) override {
// Dummy client does not support real setup
return -1;
}
uint64_t alloc_from_mem_pool(size_t size);
uint64_t alloc_from_mem_pool(size_t size) override;
int put(const std::string &key, std::span<const char> value,
const ReplicateConfig &config = ReplicateConfig{});
// if a dummy client has connected to a real client,
// return the mode of real client
DeploymentMode deployment_mode() const override { return deployment_mode_; }
int register_buffer(void *buffer, size_t size);
int put(const std::string& key, std::span<const char> value,
const WriteConfig& config) override;
int unregister_buffer(void *buffer);
int register_buffer(void* buffer, size_t size) override;
int64_t get_into(const std::string &key, void *buffer, size_t size);
int unregister_buffer(void* buffer) override;
std::vector<int64_t> batch_get_into(const std::vector<std::string> &keys,
const std::vector<void *> &buffers,
const std::vector<size_t> &sizes);
int64_t get_into(const std::string& key, void* buffer, size_t size,
const ReadRouteConfig& config = {}) override;
std::vector<int64_t> batch_get_into(
const std::vector<std::string>& keys, const std::vector<void*>& buffers,
const std::vector<size_t>& sizes,
const ReadRouteConfig& config = {}) override;
std::vector<int> batch_get_into_multi_buffers(
const std::vector<std::string> &keys,
const std::vector<std::vector<void *>> &all_buffers,
const std::vector<std::vector<size_t>> &all_sizes,
bool prefer_same_node);
const std::vector<std::string>& keys,
const std::vector<std::vector<void*>>& all_buffers,
const std::vector<std::vector<size_t>>& all_sizes,
bool aggregate_same_segment_task,
const ReadRouteConfig& config = {}) override;
int put_from(const std::string &key, void *buffer, size_t size,
const ReplicateConfig &config = ReplicateConfig{});
int put_from(const std::string& key, void* buffer, size_t size,
const WriteConfig& config) override;
int put_from_with_metadata(
const std::string &key, void *buffer, void *metadata_buffer,
size_t size, size_t metadata_size,
const ReplicateConfig &config = ReplicateConfig{});
int put_from_with_metadata(const std::string& key, void* buffer,
void* metadata_buffer, size_t size,
size_t metadata_size,
const WriteConfig& config) override;
std::vector<int> batch_put_from(
const std::vector<std::string> &keys,
const std::vector<void *> &buffers, const std::vector<size_t> &sizes,
const ReplicateConfig &config = ReplicateConfig{});
std::vector<int> batch_put_from(const std::vector<std::string>& keys,
const std::vector<void*>& buffers,
const std::vector<size_t>& sizes,
const WriteConfig& config) override;
std::vector<int> batch_put_from_multi_buffers(
const std::vector<std::string> &keys,
const std::vector<std::vector<void *>> &all_buffers,
const std::vector<std::vector<size_t>> &all_sizes,
const ReplicateConfig &config = ReplicateConfig{});
const std::vector<std::string>& keys,
const std::vector<std::vector<void*>>& all_buffers,
const std::vector<std::vector<size_t>>& all_sizes,
const WriteConfig& config) override;
std::shared_ptr<BufferHandle> get_buffer(const std::string &key);
std::shared_ptr<BufferHandle> get_buffer(
const std::string& key, const ReadRouteConfig& config = {}) override;
std::tuple<uint64_t, size_t> get_buffer_info(const std::string &key);
std::tuple<uint64_t, size_t> get_buffer_info(
const std::string& key, const ReadRouteConfig& config = {}) override;
std::vector<std::shared_ptr<BufferHandle>> batch_get_buffer(
const std::vector<std::string> &keys);
const std::vector<std::string>& keys,
const ReadRouteConfig& config = {}) override;
int put_parts(const std::string &key,
int put_parts(const std::string& key,
std::vector<std::span<const char>> values,
const ReplicateConfig &config = ReplicateConfig{});
const WriteConfig& config) override;
int put_batch(const std::vector<std::string> &keys,
const std::vector<std::span<const char>> &values,
const ReplicateConfig &config = ReplicateConfig{});
int put_batch(const std::vector<std::string>& keys,
const std::vector<std::span<const char>>& values,
const WriteConfig& config) override;
[[nodiscard]] std::string get_hostname() const;
[[nodiscard]] std::string get_hostname() const override;
int remove(const std::string &key);
int remove(const std::string& key) override;
long removeByRegex(const std::string &str);
long removeByRegex(const std::string& str) override;
long removeAll();
long removeAll() override;
int isExist(const std::string &key);
int isExist(const std::string& key) override;
std::vector<int> batchIsExist(const std::vector<std::string> &keys);
std::vector<int> batchIsExist(
const std::vector<std::string>& keys) override;
int64_t getSize(const std::string &key);
int64_t getSize(const std::string& key) override;
std::map<std::string, std::vector<Replica::Descriptor>>
batch_get_replica_desc(const std::vector<std::string> &keys);
std::vector<Replica::Descriptor> get_replica_desc(const std::string &key);
batch_get_replica_desc(const std::vector<std::string>& keys);
std::vector<Replica::Descriptor> get_replica_desc(const std::string& key);
int tearDownAll();
int tearDownAll() override;
private:
ErrorCode connect(const std::string &server_address);
ErrorCode connect(const std::string& server_address);
int register_shm_via_ipc(const ShmHelper::ShmSegment *shm,
int register_shm_via_ipc(const ShmHelper::ShmSegment* shm,
bool is_local = false);
/**
@ -164,7 +163,7 @@ class DummyClient : public PyClient {
*/
template <auto ServiceMethod, typename ReturnType, typename... Args>
[[nodiscard]] tl::expected<ReturnType, ErrorCode> invoke_rpc(
Args &&...args);
Args&&... args);
/**
* @brief Generic RPC invocation helper for batch operations
@ -177,7 +176,7 @@ class DummyClient : public PyClient {
*/
template <auto ServiceMethod, typename ResultType, typename... Args>
[[nodiscard]] std::vector<tl::expected<ResultType, ErrorCode>>
invoke_batch_rpc(size_t input_size, Args &&...args);
invoke_batch_rpc(size_t input_size, Args&&... args);
/**
* @brief Accessor for the coro_rpc_client pool. Since coro_rpc_client
@ -217,8 +216,10 @@ class DummyClient : public PyClient {
// The address which is passed to the coro_rpc_client
std::string client_addr_param_ GUARDED_BY(connect_mutex_);
DeploymentMode deployment_mode_ = DeploymentMode::UNKNOWN;
// For shared memory management
ShmHelper *shm_helper_ = nullptr;
ShmHelper* shm_helper_ = nullptr;
std::string ipc_socket_path_;
// For high availability

View File

@ -1,14 +1,16 @@
#pragma once
#include "client_service.h"
#include "centralized_client_service.h"
#include "client_buffer.hpp"
#include "storage_backend.h"
namespace mooncake {
class CentralizedClientService;
class FileStorage {
public:
FileStorage(std::shared_ptr<Client> client,
FileStorage(std::shared_ptr<CentralizedClientService> client,
const std::string& local_rpc_addr,
const FileStorageConfig& config);
~FileStorage();
@ -80,7 +82,7 @@ class FileStorage {
const std::vector<std::string>& keys,
const std::vector<int64_t>& sizes);
std::shared_ptr<Client> client_;
std::shared_ptr<CentralizedClientService> client_;
std::string local_rpc_addr_;
FileStorageConfig config_;
std::shared_ptr<StorageBackendInterface> storage_backend_;

View File

@ -0,0 +1,118 @@
#pragma once
#include <atomic>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <boost/functional/hash.hpp>
#include "async_metadata_notifier.h"
#include "data_manager.h"
#include "p2p_master_client.h"
#include "types.h"
namespace mooncake {
/**
* @class HARecoveryManager
* @brief Manages client-side HA state machine and multi-phase recovery pipeline
* for Master crash recovery.
*
* State machine (2 events: MASTER_UNREACHABLE, MASTER_REACHABLE):
* FULL MASTER_UNREACHABLE> DEGRADED
* FULL MASTER_REACHABLE> SYNCING (Master restarted)
* DEGRADED MASTER_REACHABLE> SYNCING (always full re-sync)
* SYNCING recovery complete> FULL
* SYNCING MASTER_UNREACHABLE> DEGRADED
* SYNCING MASTER_REACHABLE> SYNCING (restart pipeline)
*
* Thread safety:
* state_ is atomic for lock-free reads on data-path hot path.
* mutex_ protects transitions and recovery thread lifecycle.
* need_abort_ (shared atomic bool) signals the recovery thread to stop
* without requiring mutex_, enabling safe join from HandleEvent.
*/
class HARecoveryManager {
public:
HARecoveryManager(const UUID& client_id, P2PMasterClient& master_client,
std::optional<DataManager>& data_manager,
std::unique_ptr<AsyncMetadataNotifier>& notifier,
std::atomic<ViewVersionId>& view_version,
HAClientState initial_state = HAClientState::FULL);
~HARecoveryManager();
HARecoveryManager(const HARecoveryManager&) = delete;
HARecoveryManager& operator=(const HARecoveryManager&) = delete;
void Stop();
bool IsDegraded() const {
return state_.load(std::memory_order_acquire) ==
HAClientState::DEGRADED;
}
HAClientState GetState() const {
return state_.load(std::memory_order_acquire);
}
/**
* @brief Mark that P2PClientService::Init has completed. Called at the end
* of Init(). Recovery thread will wait for this before accessing
* data_manager_.
*/
void SetReadyForRecovery() {
ready_for_recovery_.store(true, std::memory_order_release);
}
void HandleEvent(HAEvent event);
tl::expected<void, ErrorCode> SetSyncCompleted();
private:
using AbortToken = std::shared_ptr<std::atomic<bool>>;
void TransitionState(HAClientState to, const std::string& reason);
void StartRecoveryThread();
void RecoveryPipelineMain(AbortToken need_abort);
/**
* @brief Retry enqueue until success or abort is signalled.
* Hot keys use the normal (high-priority) queue; recovery keys use
* the recovery queue. Sleeps 10 ms between attempts so normal
* writes keep priority.
* @return true on success, false if aborted.
*/
bool EnqueueWithRetry(const std::string& key, const UUID& tier_id,
size_t size, bool is_hot,
const AbortToken& need_abort);
/**
* @brief Wait for P2PClientService::Init to complete.
* Checks ready_for_recovery_ and data_manager_ availability.
* @return true if ready, false if aborted or data_manager not initialized.
*/
bool WaitForReady(const AbortToken& need_abort);
const UUID& client_id_;
P2PMasterClient& master_client_;
std::optional<DataManager>& data_manager_;
std::unique_ptr<AsyncMetadataNotifier>& notifier_;
std::atomic<ViewVersionId>& view_version_;
std::atomic<HAClientState> state_;
std::atomic<bool> ready_for_recovery_{
false}; // Set true when P2PClientService::Init completes
std::mutex mutex_; // protects transitions + recovery thread lifecycle
std::thread recovery_thread_;
AbortToken need_abort_; // signals recovery thread to exit
// Separate mutex/CV for interruptible sleeps inside the recovery thread.
// Must NOT be mutex_ (HandleEvent holds mutex_ while joining the thread).
std::mutex abort_mutex_;
std::condition_variable abort_cv_;
};
} // namespace mooncake

View File

@ -0,0 +1,86 @@
#pragma once
#include <variant>
#include <vector>
#include "types.h"
namespace mooncake {
// =====================================================================
// Heartbeat Task Types
// =====================================================================
/**
* @brief Types of tasks that can be carried in a heartbeat request.
* Only lightweight info-sync tasks; heavy operations like RegisterClient
* should use specifical RPC.
*/
enum class HeartbeatTaskType {
SYNC_SEGMENT_META, // Sync segment usage metadata (for P2P structure)
};
// =====================================================================
// Heartbeat Task Params
// =====================================================================
/**
* @brief Usage info for a single tier (e.g. segment).
*/
struct TierUsageInfo {
UUID segment_id;
size_t usage = 0;
};
YLT_REFL(TierUsageInfo, segment_id, usage);
/**
* @brief Param for SYNC_SEGMENT_META task.
*/
struct SyncSegmentMetaParam {
std::vector<TierUsageInfo> tier_usages;
};
YLT_REFL(SyncSegmentMetaParam, tier_usages);
// =====================================================================
// HeartbeatTask
// =====================================================================
/**
* @brief A single task carried in a heartbeat request.
*/
struct HeartbeatTask {
using ParamVariant = std::variant<SyncSegmentMetaParam>;
HeartbeatTask() = default;
HeartbeatTask(HeartbeatTaskType type, ParamVariant param)
: type_(type), param_(std::move(param)) {}
HeartbeatTaskType type_;
ParamVariant param_;
};
YLT_REFL(HeartbeatTask, type_, param_);
/**
* @brief Detailed result for SYNC_SEGMENT_META task.
*/
struct SyncSegmentMetaResult {
struct SubResult {
UUID segment_id;
ErrorCode error = ErrorCode::OK;
};
std::vector<SubResult> sub_results;
};
YLT_REFL(SyncSegmentMetaResult::SubResult, segment_id, error);
YLT_REFL(SyncSegmentMetaResult, sub_results);
struct HeartbeatTaskResult {
using DetailVariant = std::variant<std::monostate, SyncSegmentMetaResult>;
HeartbeatTaskType type;
ErrorCode error = ErrorCode::OK;
DetailVariant detail;
};
YLT_REFL(HeartbeatTaskResult, type, error, detail);
} // namespace mooncake

View File

@ -1,5 +1,11 @@
#pragma once
#include <coroutine>
#include <async_simple/coro/FutureAwaiter.h>
#include <async_simple/coro/Lazy.h>
#include <async_simple/coro/SyncAwait.h>
#include <glog/logging.h>
#include <memory>
#include <string>
#include <vector>
@ -9,13 +15,14 @@
#include <ylt/coro_io/client_pool.hpp>
#include "client_metric.h"
#include "replica.h"
#include "types.h"
#include "rpc_types.h"
#include "master_metric_manager.h"
namespace mooncake {
template <auto Method>
struct RpcNameTraits;
static const std::string kDefaultMasterAddress = "localhost:50051";
/**
@ -23,20 +30,7 @@ static const std::string kDefaultMasterAddress = "localhost:50051";
*/
class MasterClient {
public:
MasterClient(const UUID& client_id, MasterClientMetric* metrics = nullptr)
: client_id_(client_id), metrics_(metrics) {
coro_io::client_pool<coro_rpc::coro_rpc_client>::pool_config
pool_conf{};
const char* value = std::getenv("MC_RPC_PROTOCOL");
if (value && std::string_view(value) == "rdma") {
pool_conf.client_config.socket_config =
coro_io::ib_socket_t::config_t{};
}
client_pools_ =
std::make_shared<coro_io::client_pools<coro_rpc::coro_rpc_client>>(
pool_conf);
}
~MasterClient();
virtual ~MasterClient() = default;
MasterClient(const MasterClient&) = delete;
MasterClient& operator=(const MasterClient&) = delete;
@ -64,6 +58,30 @@ class MasterClient {
*/
[[nodiscard]] std::vector<tl::expected<bool, ErrorCode>> BatchExistKey(
const std::vector<std::string>& object_keys);
/**
* @brief Gets replica list for an object
* @param object_key Key to query
* @param config Filter configuration for getting replica list
* @return ErrorCode indicating success/failure
*/
[[nodiscard]] tl::expected<GetReplicaListResponse, ErrorCode>
GetReplicaList(const std::string& key,
const GetReplicaListRequestConfig& config =
GetReplicaListRequestConfig());
[[nodiscard]] async_simple::coro::Lazy<
tl::expected<GetReplicaListResponse, ErrorCode>>
AsyncGetReplicaList(const std::string& key,
const GetReplicaListRequestConfig& config =
GetReplicaListRequestConfig());
/**
* @brief Batch query read routes
*/
[[nodiscard]] std::vector<tl::expected<GetReplicaListResponse, ErrorCode>>
BatchGetReplicaList(const std::vector<std::string>& keys,
const GetReplicaListRequestConfig& config =
GetReplicaListRequestConfig());
/**
* @brief Calculate cache hit rate metrics
@ -84,29 +102,6 @@ class MasterClient {
ErrorCode>
BatchQueryIp(const std::vector<UUID>& client_ids);
/**
* @brief Batch clear KV cache for specified object keys on a specific
* segment for a given client.
* @param object_keys Vector of object key strings to clear.
* @param client_id The UUID of the client that owns the object keys.
* @param segment_name The name of the segment (storage device) to clear
* from.
* @return An expected object containing a vector of successfully cleared
* object keys on success, or an ErrorCode on failure.
*/
[[nodiscard]] tl::expected<std::vector<std::string>, ErrorCode>
BatchReplicaClear(const std::vector<std::string>& object_keys,
const UUID& client_id, const std::string& segment_name);
/**
* @brief Gets object metadata without transferring data
* @param object_key Key to query
* @param object_info Output parameter for object metadata
* @return ErrorCode indicating success/failure
*/
[[nodiscard]] tl::expected<GetReplicaListResponse, ErrorCode>
GetReplicaList(const std::string& object_key);
/**
* @brief Retrieves replica lists for object keys that match a regex
* pattern.
@ -119,77 +114,6 @@ class MasterClient {
ErrorCode>
GetReplicaListByRegex(const std::string& str);
/**
* @brief Gets object metadata without transferring data
* @param object_keys Keys to query
* @param object_infos Output parameter for object metadata
* @return ErrorCode indicating success/failure
*/
[[nodiscard]]
std::vector<tl::expected<GetReplicaListResponse, ErrorCode>>
BatchGetReplicaList(const std::vector<std::string>& object_keys);
/**
* @brief Starts a put operation
* @param key Object key
* @param slice_lengths Vector of slice lengths
* @param value_length Total value length
* @param config Replication configuration
* @return tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
* indicating success/failure
*/
[[nodiscard]] tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
PutStart(const std::string& key, const std::vector<size_t>& slice_lengths,
const ReplicateConfig& config);
/**
* @brief Starts a batch of put operations for N objects
* @param keys Vector of object key
* @param value_lengths Vector of total value lengths
* @param slice_lengths Vector of vectors of slice lengths
* @param config Replication configuration
* @return ErrorCode indicating success/failure
*/
[[nodiscard]] std::vector<
tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
BatchPutStart(const std::vector<std::string>& keys,
const std::vector<std::vector<uint64_t>>& slice_lengths,
const ReplicateConfig& config);
/**
* @brief Ends a put operation
* @param key Object key
* @param replica_type Type of replica (memory or disk)
* @return tl::expected<void, ErrorCode> indicating success/failure
*/
[[nodiscard]] tl::expected<void, ErrorCode> PutEnd(
const std::string& key, ReplicaType replica_type);
/**
* @brief Ends a put operation for a batch of objects
* @param keys Vector of object keys
* @return ErrorCode indicating success/failure
*/
[[nodiscard]] std::vector<tl::expected<void, ErrorCode>> BatchPutEnd(
const std::vector<std::string>& keys);
/**
* @brief Revokes a put operation
* @param key Object key
* @param replica_type Type of replica (memory or disk)
* @return tl::expected<void, ErrorCode> indicating success/failure
*/
[[nodiscard]] tl::expected<void, ErrorCode> PutRevoke(
const std::string& key, ReplicaType replica_type);
/**
* @brief Revokes a put operation for a batch of objects
* @param keys Vector of object keys
* @return ErrorCode indicating success/failure
*/
[[nodiscard]] std::vector<tl::expected<void, ErrorCode>> BatchPutRevoke(
const std::vector<std::string>& keys);
/**
* @brief Removes an object and all its replicas
* @param key Key to remove
@ -212,25 +136,6 @@ class MasterClient {
*/
[[nodiscard]] tl::expected<long, ErrorCode> RemoveAll();
/**
* @brief Registers a segment to master for allocation
* @param segment Segment to register
* @return tl::expected<void, ErrorCode> indicating success/failure
*/
[[nodiscard]] tl::expected<void, ErrorCode> MountSegment(
const Segment& segment);
/**
* @brief Re-mount segments, invoked when the client is the first time to
* connect to the master or the client Ping TTL is expired and need
* to remount. This function is idempotent. Client should retry if the
* return code is not ErrorCode::OK.
* @param segments Segments to remount
* @return tl::expected<void, ErrorCode> indicating success/failure
*/
[[nodiscard]] tl::expected<void, ErrorCode> ReMountSegment(
const std::vector<Segment>& segments);
/**
* @brief Unregisters a memory segment from master
* @param segment_id ID of the segment to unmount
@ -240,51 +145,51 @@ class MasterClient {
const UUID& segment_id);
/**
* @brief Gets the cluster ID for the current client to use as subdirectory
* name
* @return GetClusterIdResponse containing the cluster ID
* @brief Queries the status of a client.
* @param client_id The UUID of the client to query.
* @return tl::expected<QueryClientStatusResponse, ErrorCode>
*/
[[nodiscard]] tl::expected<std::string, ErrorCode> GetFsdir();
[[nodiscard]] tl::expected<GetStorageConfigResponse, ErrorCode>
GetStorageConfig();
[[nodiscard]] tl::expected<QueryClientStatusResponse, ErrorCode>
QueryClientStatus(const UUID& client_id);
/**
* @brief Pings master to check its availability
* @return tl::expected<PingResponse, ErrorCode>
* @brief Sends heartbeat to master to maintain client liveness
* @return tl::expected<HeartbeatResponse, ErrorCode>
* containing view version and client status
*/
[[nodiscard]] tl::expected<PingResponse, ErrorCode> Ping();
[[nodiscard]] tl::expected<HeartbeatResponse, ErrorCode> Heartbeat(
const HeartbeatRequest& req);
/**
* @brief Mounts a local disk segment into the master.
* @param enable_offloading If true, enables offloading (write-to-file).
* @brief Registers a segment to master for allocation
* @param segment Segment to register
* @return tl::expected<void, ErrorCode> indicating success/failure
*/
[[nodiscard]] tl::expected<void, ErrorCode> MountLocalDiskSegment(
const UUID& client_id, bool enable_offloading);
[[nodiscard]] tl::expected<void, ErrorCode> MountSegment(
const Segment& segment);
/**
* @brief Heartbeat call to collect object-level statistics and retrieve the
* set of non-persisted objects.
* @param enable_offloading Indicates whether persistence is enabled for
* this segment.
* @brief Register client with the master on startup.
* @param req request with registration information
* @return tl::expected<RegisterClientResponse, ErrorCode>
*/
[[nodiscard]] tl::expected<std::unordered_map<std::string, int64_t>,
ErrorCode>
OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading);
[[nodiscard]] tl::expected<RegisterClientResponse, ErrorCode>
RegisterClient(const RegisterClientRequest& req);
/**
* @brief Adds multiple new objects to a specified client in batch.
* @param keys A list of object keys (names) that were successfully
* offloaded.
* @param metadatas The corresponding metadata for each offloaded object,
* including size, storage location, etc.
*/
[[nodiscard]] tl::expected<void, ErrorCode> NotifyOffloadSuccess(
const UUID& client_id, const std::vector<std::string>& keys,
const std::vector<StorageObjectMetadata>& metadatas);
private:
protected:
MasterClient(const UUID& client_id, MasterClientMetric* metrics = nullptr)
: client_id_(client_id), metrics_(metrics) {
coro_io::client_pool<coro_rpc::coro_rpc_client>::pool_config
pool_conf{};
const char* value = std::getenv("MC_RPC_PROTOCOL");
if (value && std::string_view(value) == "rdma") {
pool_conf.client_config.socket_config =
coro_io::ib_socket_t::config_t{};
}
client_pools_ =
std::make_shared<coro_io::client_pools<coro_rpc::coro_rpc_client>>(
pool_conf);
}
/**
* @brief Generic RPC invocation helper for single-result operations
* @tparam ServiceMethod Pointer to WrappedMasterService member function
@ -293,9 +198,49 @@ class MasterClient {
* @param args Arguments to pass to the RPC call
* @return The result of the RPC call
*/
template <auto ServiceMethod, typename ReturnType, typename... Args>
[[nodiscard]] async_simple::coro::Lazy<tl::expected<ReturnType, ErrorCode>>
invoke_rpc_async(Args&&... args) {
auto pool = client_accessor_.GetClientPool();
// Increment RPC counter
if (metrics_) {
metrics_->rpc_count.inc({RpcNameTraits<ServiceMethod>::value});
}
auto start_time = std::chrono::steady_clock::now();
auto ret = co_await pool->send_request(
[&](coro_io::client_reuse_hint, coro_rpc::coro_rpc_client& client) {
return client.send_request<ServiceMethod>(
std::forward<Args>(args)...);
});
if (!ret.has_value()) {
LOG(ERROR) << "Client not available";
co_return tl::make_unexpected(ErrorCode::RPC_FAIL);
}
auto result = co_await std::move(ret.value());
if (!result) {
LOG(ERROR) << "RPC call failed: " << result.error().msg;
co_return tl::make_unexpected(ErrorCode::RPC_FAIL);
}
if (metrics_) {
auto end_time = std::chrono::steady_clock::now();
auto latency =
std::chrono::duration_cast<std::chrono::microseconds>(
end_time - start_time);
metrics_->rpc_latency.observe({RpcNameTraits<ServiceMethod>::value},
latency.count());
}
co_return result->result();
}
template <auto ServiceMethod, typename ReturnType, typename... Args>
[[nodiscard]] tl::expected<ReturnType, ErrorCode> invoke_rpc(
Args&&... args);
Args&&... args) {
return async_simple::coro::syncAwait(
invoke_rpc_async<ServiceMethod, ReturnType>(
std::forward<Args>(args)...));
}
/**
* @brief Generic RPC invocation helper for batch operations
@ -308,13 +253,60 @@ class MasterClient {
*/
template <auto ServiceMethod, typename ResultType, typename... Args>
[[nodiscard]] std::vector<tl::expected<ResultType, ErrorCode>>
invoke_batch_rpc(size_t input_size, Args&&... args);
invoke_batch_rpc(size_t input_size, Args&&... args) {
auto pool = client_accessor_.GetClientPool();
// Increment RPC counter
if (metrics_) {
metrics_->rpc_count.inc({RpcNameTraits<ServiceMethod>::value});
}
auto start_time = std::chrono::steady_clock::now();
return async_simple::coro::syncAwait(
[&]() -> async_simple::coro::Lazy<
std::vector<tl::expected<ResultType, ErrorCode>>> {
auto ret = co_await pool->send_request(
[&](coro_io::client_reuse_hint,
coro_rpc::coro_rpc_client& client) {
return client.send_request<ServiceMethod>(
std::forward<Args>(args)...);
});
if (!ret.has_value()) {
LOG(ERROR) << "Client not available";
co_return std::vector<tl::expected<ResultType, ErrorCode>>(
input_size, tl::make_unexpected(ErrorCode::RPC_FAIL));
}
auto result = co_await std::move(ret.value());
if (!result) {
LOG(ERROR)
<< "Batch RPC call failed: " << result.error().msg;
std::vector<tl::expected<ResultType, ErrorCode>>
error_results;
error_results.reserve(input_size);
for (size_t i = 0; i < input_size; ++i) {
error_results.emplace_back(
tl::make_unexpected(ErrorCode::RPC_FAIL));
}
co_return error_results;
}
if (metrics_) {
auto end_time = std::chrono::steady_clock::now();
auto latency =
std::chrono::duration_cast<std::chrono::microseconds>(
end_time - start_time);
metrics_->rpc_latency.observe(
{RpcNameTraits<ServiceMethod>::value}, latency.count());
}
co_return result->result();
}());
}
/**
* @brief Accessor for the coro_rpc_client pool. Since coro_rpc_client pool
* cannot reconnect to a different address, a new coro_rpc_client pool is
* created if the address is different from the current one.
*/
protected:
class RpcClientAccessor {
public:
void SetClientPool(
@ -335,6 +327,8 @@ class MasterClient {
std::shared_ptr<coro_io::client_pool<coro_rpc::coro_rpc_client>>
client_pool_;
};
protected:
RpcClientAccessor client_accessor_;
// The client identification.

View File

@ -24,6 +24,7 @@ struct MasterConfig {
double eviction_ratio;
double eviction_high_watermark_ratio;
int64_t client_live_ttl_sec;
int64_t client_crashed_ttl_sec = -1;
bool enable_ha;
bool enable_offload;
@ -45,6 +46,9 @@ struct MasterConfig {
// Storage backend eviction configuration
bool enable_disk_eviction;
uint64_t quota_bytes;
uint64_t max_replicas_per_key;
std::string deployment_mode;
};
class MasterServiceSupervisorConfig {
@ -60,6 +64,7 @@ class MasterServiceSupervisorConfig {
RequiredParam<double> eviction_high_watermark_ratio{
"eviction_high_watermark_ratio"};
RequiredParam<int64_t> client_live_ttl_sec{"client_live_ttl_sec"};
RequiredParam<int64_t> client_crashed_ttl_sec{"client_crashed_ttl_sec"};
RequiredParam<bool> enable_offload{"enable_offload"};
RequiredParam<int> rpc_port{"rpc_port"};
RequiredParam<size_t> rpc_thread_num{"rpc_thread_num"};
@ -79,6 +84,8 @@ class MasterServiceSupervisorConfig {
uint64_t put_start_release_timeout_sec = DEFAULT_PUT_START_RELEASE_TIMEOUT;
bool enable_disk_eviction = true;
uint64_t quota_bytes = 0;
uint64_t max_replicas_per_key = 1;
DeploymentMode deployment_mode = DeploymentMode::CENTRALIZATION;
MasterServiceSupervisorConfig() = default;
@ -94,6 +101,7 @@ class MasterServiceSupervisorConfig {
eviction_ratio = config.eviction_ratio;
eviction_high_watermark_ratio = config.eviction_high_watermark_ratio;
client_live_ttl_sec = config.client_live_ttl_sec;
client_crashed_ttl_sec = config.client_crashed_ttl_sec;
enable_offload = config.enable_offload;
rpc_port = static_cast<int>(config.rpc_port);
rpc_thread_num = static_cast<size_t>(config.rpc_thread_num);
@ -120,6 +128,12 @@ class MasterServiceSupervisorConfig {
put_start_release_timeout_sec = config.put_start_release_timeout_sec;
enable_disk_eviction = config.enable_disk_eviction;
quota_bytes = config.quota_bytes;
max_replicas_per_key = config.max_replicas_per_key;
if (config.deployment_mode == "Centralization") {
deployment_mode = DeploymentMode::CENTRALIZATION;
} else {
deployment_mode = DeploymentMode::P2P;
}
validate();
}
@ -155,6 +169,9 @@ class MasterServiceSupervisorConfig {
if (!client_live_ttl_sec.IsSet()) {
throw std::runtime_error("client_live_ttl_sec is not set");
}
if (!client_crashed_ttl_sec.IsSet()) {
throw std::runtime_error("client_crashed_ttl_sec is not set");
}
if (!rpc_port.IsSet()) {
throw std::runtime_error("rpc_port is not set");
}
@ -180,6 +197,7 @@ class WrappedMasterServiceConfig {
DEFAULT_EVICTION_HIGH_WATERMARK_RATIO;
ViewVersionId view_version = 0;
int64_t client_live_ttl_sec = DEFAULT_CLIENT_LIVE_TTL_SEC;
int64_t client_crashed_ttl_sec = DEFAULT_CLIENT_CRASHED_TTL_SEC;
bool enable_ha = false;
bool enable_offload = false;
std::string cluster_id = DEFAULT_CLUSTER_ID;
@ -190,6 +208,7 @@ class WrappedMasterServiceConfig {
uint64_t put_start_release_timeout_sec = DEFAULT_PUT_START_RELEASE_TIMEOUT;
bool enable_disk_eviction = true;
uint64_t quota_bytes = 0;
uint64_t max_replicas_per_key = 1;
WrappedMasterServiceConfig() = default;
@ -209,6 +228,7 @@ class WrappedMasterServiceConfig {
eviction_high_watermark_ratio = config.eviction_high_watermark_ratio;
view_version = view_version_param;
client_live_ttl_sec = config.client_live_ttl_sec;
client_crashed_ttl_sec = config.client_crashed_ttl_sec;
enable_ha = config.enable_ha;
enable_offload = config.enable_offload;
cluster_id = config.cluster_id;
@ -216,6 +236,7 @@ class WrappedMasterServiceConfig {
global_file_segment_size = config.global_file_segment_size;
enable_disk_eviction = config.enable_disk_eviction;
quota_bytes = config.quota_bytes;
max_replicas_per_key = config.max_replicas_per_key;
// Convert string memory_allocator to BufferAllocatorType enum
if (config.memory_allocator == "cachelib") {
@ -254,6 +275,7 @@ class WrappedMasterServiceConfig {
memory_allocator = config.memory_allocator;
enable_disk_eviction = config.enable_disk_eviction;
quota_bytes = config.quota_bytes;
max_replicas_per_key = config.max_replicas_per_key;
put_start_discard_timeout_sec = config.put_start_discard_timeout_sec;
put_start_release_timeout_sec = config.put_start_release_timeout_sec;
}
@ -274,6 +296,10 @@ class MasterServiceConfigBuilder {
DEFAULT_EVICTION_HIGH_WATERMARK_RATIO;
ViewVersionId view_version_ = 0;
int64_t client_live_ttl_sec_ = DEFAULT_CLIENT_LIVE_TTL_SEC;
// We use a separate flag to track if crashed ttl is explicitly set
bool client_crashed_ttl_sec_set_ = false;
int64_t client_crashed_ttl_sec_ = DEFAULT_CLIENT_CRASHED_TTL_SEC;
bool enable_ha_ = false;
bool enable_offload_ = false;
std::string cluster_id_ = DEFAULT_CLUSTER_ID;
@ -282,6 +308,7 @@ class MasterServiceConfigBuilder {
BufferAllocatorType memory_allocator_ = BufferAllocatorType::OFFSET;
bool enable_disk_eviction_ = true;
uint64_t quota_bytes_ = 0;
uint64_t max_replicas_per_key_ = 1;
uint64_t put_start_discard_timeout_sec_ = DEFAULT_PUT_START_DISCARD_TIMEOUT;
uint64_t put_start_release_timeout_sec_ = DEFAULT_PUT_START_RELEASE_TIMEOUT;
@ -325,6 +352,12 @@ class MasterServiceConfigBuilder {
return *this;
}
MasterServiceConfigBuilder& set_client_crashed_ttl_sec(int64_t ttl) {
client_crashed_ttl_sec_ = ttl;
client_crashed_ttl_sec_set_ = true;
return *this;
}
MasterServiceConfigBuilder& set_enable_ha(bool enable) {
enable_ha_ = enable;
return *this;
@ -357,6 +390,11 @@ class MasterServiceConfigBuilder {
return *this;
}
MasterServiceConfigBuilder& set_max_replicas_per_key(uint64_t limit) {
max_replicas_per_key_ = limit;
return *this;
}
MasterServiceConfigBuilder& set_put_start_discard_timeout_sec(
uint64_t put_start_discard_timeout_sec) {
put_start_discard_timeout_sec_ = put_start_discard_timeout_sec;
@ -383,6 +421,7 @@ class MasterServiceConfig {
DEFAULT_EVICTION_HIGH_WATERMARK_RATIO;
ViewVersionId view_version = 0;
int64_t client_live_ttl_sec = DEFAULT_CLIENT_LIVE_TTL_SEC;
int64_t client_crashed_ttl_sec = DEFAULT_CLIENT_CRASHED_TTL_SEC;
bool enable_ha = false;
bool enable_offload = false;
std::string cluster_id = DEFAULT_CLUSTER_ID;
@ -393,6 +432,7 @@ class MasterServiceConfig {
uint64_t put_start_release_timeout_sec = DEFAULT_PUT_START_RELEASE_TIMEOUT;
bool enable_disk_eviction = true;
uint64_t quota_bytes = 0;
uint64_t max_replicas_per_key = 1;
MasterServiceConfig() = default;
@ -406,6 +446,7 @@ class MasterServiceConfig {
eviction_high_watermark_ratio = config.eviction_high_watermark_ratio;
view_version = config.view_version;
client_live_ttl_sec = config.client_live_ttl_sec;
client_crashed_ttl_sec = config.client_crashed_ttl_sec;
enable_ha = config.enable_ha;
enable_offload = config.enable_offload;
cluster_id = config.cluster_id;
@ -414,6 +455,7 @@ class MasterServiceConfig {
memory_allocator = config.memory_allocator;
enable_disk_eviction = config.enable_disk_eviction;
quota_bytes = config.quota_bytes;
max_replicas_per_key = config.max_replicas_per_key;
put_start_discard_timeout_sec = config.put_start_discard_timeout_sec;
put_start_release_timeout_sec = config.put_start_release_timeout_sec;
}
@ -432,6 +474,7 @@ inline MasterServiceConfig MasterServiceConfigBuilder::build() const {
config.eviction_high_watermark_ratio = eviction_high_watermark_ratio_;
config.view_version = view_version_;
config.client_live_ttl_sec = client_live_ttl_sec_;
config.client_crashed_ttl_sec = client_crashed_ttl_sec_;
config.enable_ha = enable_ha_;
config.enable_offload = enable_offload_;
config.cluster_id = cluster_id_;
@ -442,6 +485,20 @@ inline MasterServiceConfig MasterServiceConfigBuilder::build() const {
config.put_start_release_timeout_sec = put_start_release_timeout_sec_;
config.enable_disk_eviction = enable_disk_eviction_;
config.quota_bytes = quota_bytes_;
config.max_replicas_per_key = max_replicas_per_key_;
// Logic for client_crashed_ttl_sec
if (client_crashed_ttl_sec_set_) {
// User explicitly set it, so we must validate it is >= live ttl
if (config.client_crashed_ttl_sec < config.client_live_ttl_sec) {
throw std::invalid_argument(
"client_crashed_ttl_sec must be >= client_live_ttl_sec");
}
} else {
// User did not set it, defaults to 3 * live ttl
config.client_crashed_ttl_sec = config.client_live_ttl_sec * 3;
}
return config;
}
@ -456,6 +513,8 @@ struct InProcMasterConfig {
std::optional<int> http_metrics_port;
std::optional<int> http_metadata_port;
std::optional<uint64_t> default_kv_lease_ttl;
std::optional<int64_t> client_live_ttl_sec;
std::optional<int64_t> client_crashed_ttl_sec;
};
// Builder class for InProcMasterConfig
@ -465,6 +524,8 @@ class InProcMasterConfigBuilder {
std::optional<int> http_metrics_port_ = std::nullopt;
std::optional<int> http_metadata_port_ = std::nullopt;
std::optional<uint64_t> default_kv_lease_ttl_ = std::nullopt;
std::optional<int64_t> client_live_ttl_sec_ = std::nullopt;
std::optional<int64_t> client_crashed_ttl_sec_ = std::nullopt;
public:
InProcMasterConfigBuilder() = default;
@ -489,6 +550,16 @@ class InProcMasterConfigBuilder {
return *this;
}
InProcMasterConfigBuilder& set_client_live_ttl_sec(int64_t ttl) {
client_live_ttl_sec_ = ttl;
return *this;
}
InProcMasterConfigBuilder& set_client_crashed_ttl_sec(int64_t ttl) {
client_crashed_ttl_sec_ = ttl;
return *this;
}
InProcMasterConfig build() const;
};
@ -499,6 +570,8 @@ inline InProcMasterConfig InProcMasterConfigBuilder::build() const {
config.http_metrics_port = http_metrics_port_;
config.http_metadata_port = http_metadata_port_;
config.default_kv_lease_ttl = default_kv_lease_ttl_;
config.client_live_ttl_sec = client_live_ttl_sec_;
config.client_crashed_ttl_sec = client_crashed_ttl_sec_;
return config;
}

View File

@ -1,6 +1,5 @@
#pragma once
#include <mutex>
#include <string>
#include "ylt/metric/counter.hpp"
@ -13,6 +12,7 @@ class MasterMetricManager {
public:
// --- Singleton Access ---
static MasterMetricManager& instance();
void reset_all_metrics();
MasterMetricManager(const MasterMetricManager&) = delete;
MasterMetricManager& operator=(const MasterMetricManager&) = delete;
@ -55,8 +55,6 @@ class MasterMetricManager {
// Memory Storage Metrics
void inc_allocated_mem_size(int64_t val = 1);
void dec_allocated_mem_size(int64_t val = 1);
void inc_total_mem_capacity(int64_t val = 1);
void dec_total_mem_capacity(int64_t val = 1);
int64_t get_allocated_mem_size();
int64_t get_total_mem_capacity();
double get_segment_mem_used_ratio(const std::string& segment);
@ -111,8 +109,14 @@ class MasterMetricManager {
void inc_unmount_segment_failures(int64_t val = 1);
void inc_remount_segment_requests(int64_t val = 1);
void inc_remount_segment_failures(int64_t val = 1);
void inc_ping_requests(int64_t val = 1);
void inc_ping_failures(int64_t val = 1);
void inc_heartbeat_requests(int64_t val = 1);
void inc_heartbeat_failures(int64_t val = 1);
void inc_get_write_route_requests(int64_t val = 1);
void inc_get_write_route_failures(int64_t val = 1);
void inc_add_replica_requests(int64_t val = 1);
void inc_add_replica_failures(int64_t val = 1);
void inc_remove_replica_requests(int64_t val = 1);
void inc_remove_replica_failures(int64_t val = 1);
// Batch Operation Statistics (Counters)
void inc_batch_exist_key_requests(int64_t items);
@ -136,6 +140,12 @@ class MasterMetricManager {
void inc_batch_put_revoke_requests(int64_t items);
void inc_batch_put_revoke_failures(int64_t failed_items);
void inc_batch_put_revoke_partial_success(int64_t failed_items);
void inc_batch_remove_replica_requests(int64_t items);
void inc_batch_remove_replica_failures(int64_t failed_items);
void inc_batch_remove_replica_partial_success(int64_t failed_items);
void inc_batch_get_write_route_requests(int64_t items);
void inc_batch_get_write_route_failures(int64_t failed_items);
void inc_batch_get_write_route_partial_success(int64_t failed_items);
// Operation Statistics Getters
int64_t get_put_start_requests();
@ -162,8 +172,14 @@ class MasterMetricManager {
int64_t get_unmount_segment_failures();
int64_t get_remount_segment_requests();
int64_t get_remount_segment_failures();
int64_t get_ping_requests();
int64_t get_ping_failures();
int64_t get_heartbeat_requests();
int64_t get_heartbeat_failures();
int64_t get_get_write_route_requests();
int64_t get_get_write_route_failures();
int64_t get_add_replica_requests();
int64_t get_add_replica_failures();
int64_t get_remove_replica_requests();
int64_t get_remove_replica_failures();
// Batch Operation Statistics Getters
int64_t get_batch_exist_key_requests();
@ -201,6 +217,14 @@ class MasterMetricManager {
int64_t get_batch_put_revoke_partial_successes();
int64_t get_batch_put_revoke_items();
int64_t get_batch_put_revoke_failed_items();
int64_t get_batch_remove_replica_requests();
int64_t get_batch_remove_replica_failures();
int64_t get_batch_remove_replica_partial_successes();
int64_t get_batch_remove_replica_items();
int64_t get_batch_remove_replica_failed_items();
int64_t get_batch_get_write_route_requests();
int64_t get_batch_get_write_route_failures();
int64_t get_batch_get_write_route_partial_successes();
// Eviction Metrics
void inc_eviction_success(int64_t key_count, int64_t size);
@ -293,8 +317,14 @@ class MasterMetricManager {
ylt::metric::counter_t unmount_segment_failures_;
ylt::metric::counter_t remount_segment_requests_;
ylt::metric::counter_t remount_segment_failures_;
ylt::metric::counter_t ping_requests_;
ylt::metric::counter_t ping_failures_;
ylt::metric::counter_t heartbeat_requests_;
ylt::metric::counter_t heartbeat_failures_;
ylt::metric::counter_t get_write_route_requests_;
ylt::metric::counter_t get_write_route_failures_;
ylt::metric::counter_t add_replica_requests_;
ylt::metric::counter_t add_replica_failures_;
ylt::metric::counter_t remove_replica_requests_;
ylt::metric::counter_t remove_replica_failures_;
// Batch Operation Statistics
ylt::metric::counter_t batch_exist_key_requests_;
@ -332,6 +362,14 @@ class MasterMetricManager {
ylt::metric::counter_t batch_put_revoke_partial_successes_;
ylt::metric::counter_t batch_put_revoke_items_;
ylt::metric::counter_t batch_put_revoke_failed_items_;
ylt::metric::counter_t batch_remove_replica_requests_;
ylt::metric::counter_t batch_remove_replica_failures_;
ylt::metric::counter_t batch_remove_replica_partial_successes_;
ylt::metric::counter_t batch_remove_replica_items_;
ylt::metric::counter_t batch_remove_replica_failed_items_;
ylt::metric::counter_t batch_get_write_route_requests_;
ylt::metric::counter_t batch_get_write_route_failures_;
ylt::metric::counter_t batch_get_write_route_partial_successes_;
// cache hit Statistics
ylt::metric::counter_t mem_cache_hit_nums_;

View File

@ -1,80 +1,80 @@
#pragma once
#include <atomic>
#include <boost/functional/hash.hpp>
#include <boost/lockfree/queue.hpp>
#include <chrono>
#include <cstdint>
#include <list>
#include <memory>
#include <optional>
#include <shared_mutex>
#include <string>
#include <thread>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <ylt/util/expected.hpp>
#include <ylt/util/tl/expected.hpp>
#include "allocation_strategy.h"
#include "master_metric_manager.h"
#include "mutex.h"
#include "segment.h"
#include "types.h"
#include "master_config.h"
#include "rpc_types.h"
#include "replica.h"
#include "master_config.h"
namespace mooncake {
// Forward declarations
class AllocationStrategy;
class EvictionStrategy;
class ClientManager;
/*
* @brief MasterService is the main class for the master server.
* Lock order: To avoid deadlocks, the following lock order should be followed:
* 1. client_mutex_
* 2. metadata_shards_[shard_idx_].mutex
* 3. segment_mutex_
/**
* @brief
* 1. MasterService is a abstract base class for master server.
* This class defines common rpc interfaces that correspond to
* WrappedMasterService.
*
* 2. The multiple metadata of Master are hierarchical, MasterService manages
* metadata of key (ObjectMeta), and its ClientManager manages metadata of
* client (ClientMeta), each ClientMeta uses SegmentManager to manage its
* segments. The relationship between metadata:
* a. Client (1) (0..*) Segment
* b. Key (1) (1..*) Replica
* c. Replica (1) (1) Segment
*
* 3. The lock order of MasterService is:
* a. MetadataShard's mutex
* b. ClientManager's client_mutex_
* c. SegmentManager's segment_mutex_
* For avoiding deadlock, each metadata managers should follow this order.
*/
class MasterService {
public:
MasterService();
MasterService(const MasterServiceConfig& config);
~MasterService();
virtual ~MasterService() = default;
virtual void InitializeClientManager();
/**
* @brief Mount a memory segment for buffer allocation. This function is
* idempotent.
* @return ErrorCode::OK on success,
* ErrorCode::INVALID_PARAMS on invalid parameters,
* ErrorCode::UNAVAILABLE_IN_CURRENT_STATUS if the segment cannot
* be mounted temporarily,
* ErrorCode::INTERNAL_ERROR on internal errors.
* @brief Register a client with its segments.
*/
auto RegisterClient(const RegisterClientRequest& req)
-> tl::expected<RegisterClientResponse, ErrorCode>;
/**
* @brief heartbeat interface for client to sync its status
* @param req HeartbeatRequest containing client_id and tasks
* @return HeartbeatResponse containing client status, view_version,
* and task results
*/
auto Heartbeat(const HeartbeatRequest& req)
-> tl::expected<HeartbeatResponse, ErrorCode>;
/**
* @brief Queries the status of a client
*/
auto QueryClientStatus(const QueryClientStatusRequest& req)
-> tl::expected<QueryClientStatusResponse, ErrorCode>;
/**
* @brief Mount a memory segment.
* @return ErrorCode::SEGMENT_ALREADY_EXISTS if it is already mounted.
* ErrorCode::CLIENT_UNHEALTHY if the client is unhealthy.
*/
auto MountSegment(const Segment& segment, const UUID& client_id)
-> tl::expected<void, ErrorCode>;
/**
* @brief Re-mount segments, invoked when the client is the first time to
* connect to the master or the client Ping TTL is expired and need
* to remount. This function is idempotent. Client should retry if the
* return code is not ErrorCode::OK.
* @return ErrorCode::OK means either all segments are remounted
* successfully or the fail is not solvable by a new remount request.
* ErrorCode::UNAVAILABLE_IN_CURRENT_STATUS if the segment cannot
* be mounted temporarily.
* ErrorCode::INTERNAL_ERROR if something temporary error happens.
*/
auto ReMountSegment(const std::vector<Segment>& segments,
const UUID& client_id) -> tl::expected<void, ErrorCode>;
/**
* @brief Unmount a memory segment. This function is idempotent.
* @brief Unmount a memory segment.
* @return ErrorCode::OK on success,
* ErrorCode::UNAVAILABLE_IN_CURRENT_STATUS if the segment is
* currently unmounting.
* ErrorCode::SEGMENT_NOT_FOUND if the segment doesn't exist
* ErrorCode::CLIENT_UNHEALTHY if the client is unhealthy
*/
auto UnmountSegment(const UUID& segment_id, const UUID& client_id)
-> tl::expected<void, ErrorCode>;
@ -102,6 +102,15 @@ class MasterService {
*/
auto GetAllSegments() -> tl::expected<std::vector<std::string>, ErrorCode>;
/**
* @brief Get all segments belonging to a specific client.
* @param client_id The UUID of the client.
* @return An expected object containing a vector of segment names on
* success, or ErrorCode on failure.
*/
auto GetClientSegments(const UUID& client_id)
-> tl::expected<std::vector<std::string>, ErrorCode>;
/**
* @brief Query a segment's capacity and used size in bytes.
* Conductor should use these information to schedule new requests.
@ -132,22 +141,6 @@ class MasterService {
std::unordered_map<UUID, std::vector<std::string>, boost::hash<UUID>>,
ErrorCode>;
/**
* @brief Batch clear KV cache replicas for specified object keys.
* @param object_keys Vector of object key strings to clear.
* @param client_id The UUID of the client that owns the object keys.
* @param segment_name The name of the segment (storage device) to clear
* from. If empty, clears replicas from all segments for the given
* client_id.
* @return An expected object containing a vector of successfully cleared
* keys on success, or an ErrorCode on failure. Only successfully
* cleared keys are included in the result.
*/
auto BatchReplicaClear(const std::vector<std::string>& object_keys,
const UUID& client_id,
const std::string& segment_name)
-> tl::expected<std::vector<std::string>, ErrorCode>;
/**
* @brief Retrieves replica lists for object keys that match a regex
* pattern.
@ -162,65 +155,16 @@ class MasterService {
/**
* @brief Get list of replicas for an object
* @param[out] replica_list Vector to store replica information
* @return ErrorCode::OK on success, ErrorCode::REPLICA_IS_NOT_READY if not
* ready
* @param key The key of the object
* @param config The filter configuration for the replica list
* @return An expected object containing the replica list on success, or an
* ErrorCode on failure.
*/
auto GetReplicaList(std::string_view key)
virtual auto GetReplicaList(const std::string& key,
const GetReplicaListRequestConfig& config =
GetReplicaListRequestConfig())
-> tl::expected<GetReplicaListResponse, ErrorCode>;
/**
* @brief Start a put operation for an object
* @param[out] replica_list Vector to store replica information for the
* slice
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if exists,
* ErrorCode::NO_AVAILABLE_HANDLE if allocation fails,
* ErrorCode::INVALID_PARAMS if slice size is invalid
*/
auto PutStart(const UUID& client_id, const std::string& key,
const uint64_t slice_length, const ReplicateConfig& config)
-> tl::expected<std::vector<Replica::Descriptor>, ErrorCode>;
/**
* @brief Complete a put operation, replica_type indicates the type of
* replica to complete (memory or disk)
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
* found, ErrorCode::INVALID_WRITE if replica status is invalid
*/
auto PutEnd(const UUID& client_id, const std::string& key,
ReplicaType replica_type) -> tl::expected<void, ErrorCode>;
/**
* @brief Adds a replica instance associated with the given client and key.
*/
auto AddReplica(const UUID& client_id, const std::string& key,
Replica& replica) -> tl::expected<void, ErrorCode>;
/**
* @brief Revoke a put operation, replica_type indicates the type of
* replica to revoke (memory or disk)
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
* found, ErrorCode::INVALID_WRITE if replica status is invalid
*/
auto PutRevoke(const UUID& client_id, const std::string& key,
ReplicaType replica_type) -> tl::expected<void, ErrorCode>;
/**
* @brief Complete a batch of put operations
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
* found, ErrorCode::INVALID_WRITE if replica status is invalid
*/
std::vector<tl::expected<void, ErrorCode>> BatchPutEnd(
const UUID& client_id, const std::vector<std::string>& keys);
/**
* @brief Revoke a batch of put operations
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
* found, ErrorCode::INVALID_WRITE if replica status is invalid
*/
std::vector<tl::expected<void, ErrorCode>> BatchPutRevoke(
const UUID& client_id, const std::vector<std::string>& keys);
/**
* @brief Remove an object and its replicas
* @return ErrorCode::OK on success, ErrorCode::OBJECT_NOT_FOUND if not
@ -248,428 +192,204 @@ class MasterService {
*/
size_t GetKeyCount() const;
/**
* @brief Heartbeat from client
* @param client_id The uuid of the client
* @return PingResponse containing view version and client status
* @return ErrorCode::OK on success, ErrorCode::INTERNAL_ERROR if the client
* ping queue is full
*/
auto Ping(const UUID& client_id) -> tl::expected<PingResponse, ErrorCode>;
/**
* @brief Get the master service cluster ID to use as subdirectory name
* @return ErrorCode::OK on success, ErrorCode::INTERNAL_ERROR if cluster ID
* is not set
*/
tl::expected<std::string, ErrorCode> GetFsdir() const;
/**
* @brief Get storage backend configuration including eviction settings
* @return GetStorageConfigResponse containing fsdir, enable_disk_eviction,
* and quota_bytes
*/
tl::expected<GetStorageConfigResponse, ErrorCode> GetStorageConfig() const;
/**
* @brief Mounts a file storage segment into the master.
* @param enable_offloading If true, enables offloading (write-to-file).
*/
auto MountLocalDiskSegment(const UUID& client_id, bool enable_offloading)
-> tl::expected<void, ErrorCode>;
/**
* @brief Heartbeat call to collect object-level statistics and retrieve the
* set of non-offloaded objects.
* @param enable_offloading Indicates whether offloading is enabled for this
* segment.
*/
auto OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading)
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>;
/**
* @brief Notifies the master that offloading of specified objects has
* succeeded.
* @param keys A list of object keys (names) that were successfully
* offloaded.
* @param metadatas The corresponding metadata for each offloaded object,
* including size, storage location, etc.
*/
auto NotifyOffloadSuccess(
const UUID& client_id, const std::vector<std::string>& keys,
const std::vector<StorageObjectMetadata>& metadatas)
-> tl::expected<void, ErrorCode>;
private:
// Resolve the key to a sanitized format for storage
std::string SanitizeKey(const std::string& key) const;
std::string ResolvePath(const std::string& key) const;
// BatchEvict evicts objects in a near-LRU way, i.e., prioritizes to evict
// object with smaller lease timeout. It has two passes. The first pass only
// evicts objects without soft pin. The second pass prioritizes objects
// without soft pin, but also allows to evict soft pinned objects if
// allow_evict_soft_pinned_objects_ is true. The first pass tries fulfill
// evict ratio target. If the actual evicted ratio is less than
// evict_ratio_lowerbound, the second pass will be triggered and try to
// fulfill evict ratio lowerbound.
void BatchEvict(double evict_ratio_target, double evict_ratio_lowerbound);
// Clear invalid handles in all shards
void ClearInvalidHandles();
// Internal data structures
protected:
struct ObjectMetadata {
// RAII-style metric management
~ObjectMetadata() {
MasterMetricManager::instance().dec_key_count(1);
if (soft_pin_timeout) {
MasterMetricManager::instance().dec_soft_pin_key_count(1);
}
}
public:
virtual ~ObjectMetadata();
ObjectMetadata(size_t value_length, std::vector<Replica>&& reps);
ObjectMetadata() = delete;
ObjectMetadata(
const UUID& client_id_,
const std::chrono::steady_clock::time_point put_start_time_,
size_t value_length, std::vector<Replica>&& reps,
bool enable_soft_pin)
: client_id(client_id_),
put_start_time(put_start_time_),
replicas(std::move(reps)),
size(value_length),
lease_timeout(),
soft_pin_timeout(std::nullopt) {
MasterMetricManager::instance().inc_key_count(1);
if (enable_soft_pin) {
soft_pin_timeout.emplace();
MasterMetricManager::instance().inc_soft_pin_key_count(1);
}
MasterMetricManager::instance().observe_value_size(value_length);
}
ObjectMetadata(const ObjectMetadata&) = delete;
ObjectMetadata& operator=(const ObjectMetadata&) = delete;
ObjectMetadata(ObjectMetadata&&) = delete;
ObjectMetadata& operator=(ObjectMetadata&&) = delete;
const UUID client_id;
const std::chrono::steady_clock::time_point put_start_time;
// Check if the metadata is valid
// Valid means it has at least one replica and size is greater than 0
bool IsValid() const { return !replicas_.empty() && size_ > 0; }
std::vector<Replica> replicas;
size_t size;
// Default constructor, creates a time_point representing
// the Clock's epoch (i.e., time_since_epoch() is zero).
std::chrono::steady_clock::time_point lease_timeout; // hard lease
std::optional<std::chrono::steady_clock::time_point>
soft_pin_timeout; // optional soft pin, only set for vip objects
public:
// Attention:
// The MasterService instance will call hook functions based on
// following status functions. Each subclass of ObjectMetadata should
// define its own status by overriding these functions.
// Check if there are some replicas with a different status than the
// given value. If there are, return the status of the first replica
// that is not equal to the given value. Otherwise, return false.
std::optional<ReplicaStatus> HasDiffRepStatus(
ReplicaStatus status, ReplicaType replica_type) const {
for (const auto& replica : replicas) {
if (replica.status() != status &&
replica.type() == replica_type) {
return replica.status();
/**
* @brief Whether the object is readable
* @return true if the object is readable, false otherwise
*/
virtual bool IsObjectAccessible() const {
for (const auto& replica : replicas_) {
if (IsReplicaAccessible(replica)) {
return true;
}
}
return false;
}
/**
* @brief Whether the object is removable
* @return ErrorCode::OK if removable, otherwise return error specific
* to the reason
*/
virtual tl::expected<void, ErrorCode> IsObjectRemovable() const {
return {};
}
// Grant a lease with timeout as now() + ttl, only update if the new
// timeout is larger
void GrantLease(const uint64_t ttl, const uint64_t soft_ttl) {
std::chrono::steady_clock::time_point now =
std::chrono::steady_clock::now();
lease_timeout =
std::max(lease_timeout, now + std::chrono::milliseconds(ttl));
if (soft_pin_timeout) {
soft_pin_timeout =
std::max(*soft_pin_timeout,
now + std::chrono::milliseconds(soft_ttl));
}
/**
* @brief Whether the replica is readable
* @return true if the replica is readable, false otherwise
*/
virtual bool IsReplicaAccessible(const Replica& replica) const {
return true;
};
/**
* @brief Whether the replica is removable
* @return ErrorCode::OK if removable, otherwise return error specific
* to the reason
*/
virtual tl::expected<void, ErrorCode> IsReplicaRemovable(
const Replica& replica) const {
return {};
}
// Erase all replicas of the given type
void EraseReplica(ReplicaType replica_type) {
replicas.erase(
std::remove_if(replicas.begin(), replicas.end(),
[replica_type](const Replica& replica) {
return replica.type() == replica_type;
}),
replicas.end());
}
// Check if there is a memory replica
bool HasMemReplica() const {
return std::any_of(replicas.begin(), replicas.end(),
[](const Replica& replica) {
return replica.type() == ReplicaType::MEMORY;
});
}
// Get the count of memory replicas
int GetMemReplicaCount() const {
return std::count_if(
replicas.begin(), replicas.end(), [](const Replica& replica) {
return replica.type() == ReplicaType::MEMORY;
});
}
// Check if the lease has expired
bool IsLeaseExpired() const {
return std::chrono::steady_clock::now() >= lease_timeout;
}
// Check if the lease has expired
bool IsLeaseExpired(std::chrono::steady_clock::time_point& now) const {
return now >= lease_timeout;
}
// Check if is in soft pin status
bool IsSoftPinned() const {
return soft_pin_timeout &&
std::chrono::steady_clock::now() < *soft_pin_timeout;
}
// Check if is in soft pin status
bool IsSoftPinned(std::chrono::steady_clock::time_point& now) const {
return soft_pin_timeout && now < *soft_pin_timeout;
}
// Check if the metadata is valid
// Valid means it has at least one replica and size is greater than 0
bool IsValid() const { return !replicas.empty() && size > 0; }
bool IsAllReplicasComplete() const {
return std::all_of(
replicas.begin(), replicas.end(), [](const Replica& replica) {
return replica.status() == ReplicaStatus::COMPLETE;
});
}
bool HasCompletedReplicas() const {
return std::any_of(
replicas.begin(), replicas.end(), [](const Replica& replica) {
return replica.status() == ReplicaStatus::COMPLETE;
});
}
std::vector<Replica> DiscardProcessingReplicas() {
auto partition_point = std::partition(
replicas.begin(), replicas.end(), [](const Replica& replica) {
return replica.status() != ReplicaStatus::PROCESSING;
});
std::vector<Replica> discarded_replicas;
if (partition_point != replicas.end()) {
discarded_replicas.reserve(
std::distance(partition_point, replicas.end()));
std::move(partition_point, replicas.end(),
std::back_inserter(discarded_replicas));
replicas.erase(partition_point, replicas.end());
}
return discarded_replicas;
}
public:
std::vector<Replica> replicas_;
size_t size_;
};
static constexpr size_t kNumShards = 1024; // Number of metadata shards
// Sharded metadata maps and their mutexes
protected:
// Sharded metadata maps and their mutexes.
// Attention:
// 1. Each subclass of MasterService should define its own shard and provide
// the accessor functions.
// 2. `segment_key_index` is a reverse index for `metadata` and segment.
// Due to the object key in `segment_key_index` is a string_view acquired
// from `metadata`, when removing an entry from `metadata`, you MUST
// first remove the corresponding key from `segment_key_index`.
struct MetadataShard {
mutable Mutex mutex;
std::unordered_map<std::string, ObjectMetadata> metadata
GUARDED_BY(mutex);
std::unordered_set<std::string> processing_keys GUARDED_BY(mutex);
std::unordered_map<std::string, std::unique_ptr<ObjectMetadata>>
metadata GUARDED_BY(mutex);
// segment_id -> { key -> replica_reference_count }.
std::unordered_map<UUID, std::unordered_map<std::string_view, size_t>,
boost::hash<UUID>>
segment_key_index GUARDED_BY(mutex);
};
std::array<MetadataShard, kNumShards> metadata_shards_;
// Virtual function to access shards
virtual MetadataShard& GetShard(size_t idx) = 0;
virtual const MetadataShard& GetShard(size_t idx) const = 0;
virtual size_t GetShardIndex(const std::string& key) const = 0;
virtual size_t GetShardCount() const = 0;
// Helper to get shard index from key
size_t getShardIndex(const std::string& key) const {
return std::hash<std::string>{}(key) % kNumShards;
}
// Helpers for maintaining per-shard segment_key_index.
// 1. Must be called while holding shard.mutex.
// 2. When add or remove a replica, must call the following functions to
// update the segment_key_index.
void AddReplicaToSegmentIndex(MetadataShard& shard, const std::string& key,
const Replica& replica)
NO_THREAD_SAFETY_ANALYSIS;
void RemoveReplicaFromSegmentIndex(
MetadataShard& shard, const std::string& key,
const std::vector<Replica>& replicas) NO_THREAD_SAFETY_ANALYSIS;
void RemoveReplicaFromSegmentIndex(
MetadataShard& shard, const std::string& key,
const Replica& replica) NO_THREAD_SAFETY_ANALYSIS;
// Helper to clean up stale handles pointing to unmounted segments
bool CleanupStaleHandles(ObjectMetadata& metadata);
/**
* @brief Helper to discard expired processing keys.
*/
void DiscardExpiredProcessingKeys(
MetadataShard& shard, const std::chrono::steady_clock::time_point& now);
/**
* @brief Helper to release space of expired discarded replicas.
* @return Number of released objects that have memory replicas
*/
uint64_t ReleaseExpiredDiscardedReplicas(
const std::chrono::steady_clock::time_point& now);
// Eviction thread function
void EvictionThreadFunc();
tl::expected<void, ErrorCode> PushOffloadingQueue(const std::string& key,
const Replica& replica);
// Lease related members
const uint64_t default_kv_lease_ttl_; // in milliseconds
const uint64_t default_kv_soft_pin_ttl_; // in milliseconds
const bool allow_evict_soft_pinned_objects_;
// Eviction related members
std::atomic<bool> need_eviction_{
false}; // Set to trigger eviction when not enough space left
const double eviction_ratio_; // in range [0.0, 1.0]
const double eviction_high_watermark_ratio_; // in range [0.0, 1.0]
// Eviction thread related members
std::thread eviction_thread_;
std::atomic<bool> eviction_running_{false};
static constexpr uint64_t kEvictionThreadSleepMs =
10; // 10 ms sleep between eviction checks
// Helper class for accessing metadata with automatic locking and cleanup
protected:
// Helper class for accessing metadata with automatic locking
class MetadataAccessor {
public:
MetadataAccessor(MasterService* service, const std::string& key)
: service_(service),
key_(key),
shard_idx_(service_->getShardIndex(key)),
shard_(service_->metadata_shards_[shard_idx_]),
shard_idx_(service_->GetShardIndex(key)),
shard_(service_->GetShard(shard_idx_)),
lock_(&shard_.mutex),
it_(shard_.metadata.find(key)),
processing_it_(shard_.processing_keys.find(key)) {
// Automatically clean up invalid handles
if (it_ != shard_.metadata.end()) {
if (service_->CleanupStaleHandles(it_->second)) {
this->Erase();
it_(shard_.metadata.find(key)) {}
if (processing_it_ != shard_.processing_keys.end()) {
this->EraseFromProcessing();
}
}
}
}
virtual ~MetadataAccessor() = default;
// Check if metadata exists
bool Exists() const NO_THREAD_SAFETY_ANALYSIS {
return it_ != shard_.metadata.end();
}
bool InProcessing() const NO_THREAD_SAFETY_ANALYSIS {
return processing_it_ != shard_.processing_keys.end();
MetadataShard& GetShard() NO_THREAD_SAFETY_ANALYSIS { return shard_; }
const std::string& GetKey() const NO_THREAD_SAFETY_ANALYSIS {
return it_->first;
}
// Get metadata (only call when Exists() is true)
ObjectMetadata& Get() NO_THREAD_SAFETY_ANALYSIS { return it_->second; }
ObjectMetadata& Get() NO_THREAD_SAFETY_ANALYSIS { return *it_->second; }
// Delete current metadata (for PutRevoke or Remove operations)
// Delete current metadata.
// To prevent dangling string_views in segment_key_index, segment index
// should be cleaned up before erasing the metadata entry.
void Erase() NO_THREAD_SAFETY_ANALYSIS {
shard_.metadata.erase(it_);
it_ = shard_.metadata.end();
if (it_ != shard_.metadata.end()) {
service_->RemoveReplicaFromSegmentIndex(shard_, it_->first,
it_->second->replicas_);
shard_.metadata.erase(it_);
it_ = shard_.metadata.end();
}
}
void EraseFromProcessing() NO_THREAD_SAFETY_ANALYSIS {
shard_.processing_keys.erase(processing_it_);
processing_it_ = shard_.processing_keys.end();
}
private:
protected:
MasterService* service_;
std::string key_;
size_t shard_idx_;
MetadataShard& shard_;
MutexLocker lock_;
std::unordered_map<std::string, ObjectMetadata>::iterator it_;
std::unordered_set<std::string>::iterator processing_it_;
std::unordered_map<std::string,
std::unique_ptr<ObjectMetadata>>::iterator it_;
};
friend class MetadataAccessor;
virtual std::unique_ptr<MetadataAccessor> GetMetadataAccessor(
const std::string& key) {
return std::make_unique<MetadataAccessor>(this, key);
}
ViewVersionId view_version_;
protected:
virtual ClientManager& GetClientManager() = 0;
virtual const ClientManager& GetClientManager() const = 0;
// Client related members
mutable std::shared_mutex client_mutex_;
std::unordered_set<UUID, boost::hash<UUID>>
ok_client_; // client with ok status
void ClientMonitorFunc();
std::thread client_monitor_thread_;
std::atomic<bool> client_monitor_running_{false};
static constexpr uint64_t kClientMonitorSleepMs =
1000; // 1000 ms sleep between client monitor checks
// boost lockfree queue requires trivial assignment operator
struct PodUUID {
uint64_t first;
uint64_t second;
};
static constexpr size_t kClientPingQueueSize =
128 * 1024; // Size of the client ping queue
boost::lockfree::queue<PodUUID> client_ping_queue_{kClientPingQueueSize};
const int64_t client_live_ttl_sec_;
protected:
virtual std::vector<Replica::Descriptor> FilterReplicas(
const GetReplicaListRequestConfig& config,
const ObjectMetadata& metadata) = 0;
// The following methods are hooks function to handle special events
// Triggered when the metadata of an object is accessed (e.g. Get or Exist)
virtual void OnObjectAccessed(ObjectMetadata& metadata) = 0;
// Triggered when the object is removed
virtual void OnObjectRemoved(ObjectMetadata& metadata);
// Triggered when the object is hit (e.g. Get)
virtual void OnObjectHit(const ObjectMetadata& metadata) = 0;
// Triggered when the replica is removed
virtual void OnReplicaRemoved(const Replica& replica) = 0;
// Triggered when the replica is added
virtual void OnReplicaAdded(const Replica& replica) = 0;
// Callback for segment removal (triggered by ClientManager via
// SegmentManager)
virtual void OnSegmentRemoved(const UUID& segment_id);
protected:
// if high availability features enabled
const bool enable_ha_;
ViewVersionId view_version_;
const bool enable_offload_;
// cluster id for persistent sub directory
const std::string cluster_id_;
// root filesystem directory for persistent storage
const std::string root_fs_dir_;
// global 3fs/nfs segment size
int64_t global_file_segment_size_;
// storage backend eviction configuration
const bool enable_disk_eviction_;
const uint64_t quota_bytes_;
bool use_disk_replica_{false};
// Segment management
SegmentManager segment_manager_;
BufferAllocatorType memory_allocator_type_;
std::shared_ptr<AllocationStrategy> allocation_strategy_;
// Discarded replicas management
const std::chrono::seconds put_start_discard_timeout_sec_;
const std::chrono::seconds put_start_release_timeout_sec_;
class DiscardedReplicas {
public:
DiscardedReplicas() = delete;
DiscardedReplicas(std::vector<Replica>&& replicas,
std::chrono::steady_clock::time_point ttl)
: replicas_(std::move(replicas)), ttl_(ttl), mem_size_(0) {
for (auto& replica : replicas_) {
mem_size_ += replica.get_memory_buffer_size();
}
MasterMetricManager::instance().inc_put_start_discard_cnt(
1, mem_size_);
}
~DiscardedReplicas() {
MasterMetricManager::instance().inc_put_start_release_cnt(
1, mem_size_);
}
uint64_t memSize() const { return mem_size_; }
bool isExpired(const std::chrono::steady_clock::time_point& now) const {
return ttl_ <= now;
}
private:
std::vector<Replica> replicas_;
std::chrono::steady_clock::time_point ttl_;
uint64_t mem_size_;
};
std::mutex discarded_replicas_mutex_;
std::list<DiscardedReplicas> discarded_replicas_
GUARDED_BY(discarded_replicas_mutex_);
size_t offloading_queue_limit_ = 50000;
friend class MetadataAccessor;
};
} // namespace mooncake

View File

@ -3,6 +3,7 @@
#include <mutex>
#include <shared_mutex>
#include <atomic>
// Enable thread safety attributes only with clang.
// The attributes can be safely erased when compiling with other compilers.
@ -56,6 +57,15 @@
#define NO_THREAD_SAFETY_ANALYSIS \
THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis)
#if defined(__x86_64__) || defined(__i386__)
#define MOONCAKE_CPU_RELAX() asm volatile("pause" ::: "memory")
#elif defined(__aarch64__)
#define MOONCAKE_CPU_RELAX() asm volatile("yield" ::: "memory")
#else
#include <thread>
#define MOONCAKE_CPU_RELAX() std::this_thread::yield()
#endif
// Simple mutex implementation using std::mutex for exclusive locking only.
class CAPABILITY("mutex") Mutex {
private:
@ -106,6 +116,84 @@ class CAPABILITY("shared_mutex") SharedMutex {
const SharedMutex& operator!() const { return *this; }
};
// Simple spinlock implementation using std::atomic<bool>.
class CAPABILITY("mutex") SpinLock {
private:
std::atomic<bool> flag_{false};
public:
void lock() ACQUIRE() {
while (flag_.exchange(true, std::memory_order_acquire)) {
while (flag_.load(std::memory_order_relaxed)) {
MOONCAKE_CPU_RELAX();
}
}
}
void unlock() RELEASE() { flag_.store(false, std::memory_order_release); }
bool try_lock() TRY_ACQUIRE(true) {
return !flag_.exchange(true, std::memory_order_acquire);
}
const SpinLock& operator!() const { return *this; }
};
// Simple spin-read-write lock implementation.
// state_ > 0: number of readers
// state_ == -1: writer
class CAPABILITY("shared_mutex") SpinRWLock {
public:
void lock() ACQUIRE() {
int32_t expected = 0;
while (!state_.compare_exchange_weak(expected, -1,
std::memory_order_acquire)) {
while (state_.load(std::memory_order_relaxed) != 0) {
MOONCAKE_CPU_RELAX();
}
expected = 0;
}
}
void lock_shared() ACQUIRE_SHARED() {
while (true) {
int32_t current = state_.load(std::memory_order_relaxed);
if (current >= 0) {
if (state_.compare_exchange_weak(current, current + 1,
std::memory_order_acquire)) {
break;
}
} else {
MOONCAKE_CPU_RELAX();
}
}
}
void unlock() RELEASE() { state_.store(0, std::memory_order_release); }
void unlock_shared() RELEASE_SHARED() {
state_.fetch_sub(1, std::memory_order_release);
}
bool try_lock() TRY_ACQUIRE(true) {
int32_t expected = 0;
return state_.compare_exchange_strong(expected, -1,
std::memory_order_acquire);
}
bool try_lock_shared() TRY_ACQUIRE_SHARED(true) {
int32_t current = state_.load(std::memory_order_relaxed);
if (current < 0) return false;
return state_.compare_exchange_strong(current, current + 1,
std::memory_order_acquire);
}
const SpinRWLock& operator!() const { return *this; }
private:
std::atomic<int32_t> state_{0};
};
// MutexLocker is an RAII class that acquires a mutex in its constructor, and
// releases it in its destructor.
class SCOPED_CAPABILITY MutexLocker {
@ -117,6 +205,14 @@ class SCOPED_CAPABILITY MutexLocker {
// Acquire mu, implicitly acquire *this and associate it with mu.
MutexLocker(Mutex* mu) ACQUIRE(mu) : mut(mu), locked(true) { mu->lock(); }
// Constructor without immediate locking.
MutexLocker(Mutex* mu, bool lock_now) : mut(mu), locked(false) {
if (lock_now) {
mut->lock();
locked = true;
}
}
// Release *this and all associated mutexes, if they are still held.
~MutexLocker() RELEASE() {
if (locked) {
@ -228,4 +324,61 @@ class SCOPED_CAPABILITY SharedMutexLocker {
}
};
// RAII class for SpinLock
class SCOPED_CAPABILITY SpinLockLocker {
private:
SpinLock* mut;
bool locked;
public:
explicit SpinLockLocker(SpinLock* mu) ACQUIRE(mu) : mut(mu), locked(true) {
mu->lock();
}
~SpinLockLocker() RELEASE() {
if (locked) mut->unlock();
}
// Prevent copying and assignment
SpinLockLocker(const SpinLockLocker&) = delete;
SpinLockLocker& operator=(const SpinLockLocker&) = delete;
void unlock() RELEASE() {
if (!locked) return;
mut->unlock();
locked = false;
}
};
// RAII class for SpinRWLock
class SCOPED_CAPABILITY SpinRWLockLocker {
private:
SpinRWLock* mut;
bool is_exclusive;
bool locked;
public:
explicit SpinRWLockLocker(SpinRWLock* mu) ACQUIRE(mu)
: mut(mu), is_exclusive(true), locked(true) {
mu->lock();
}
SpinRWLockLocker(SpinRWLock* mu, const shared_lock_t&) ACQUIRE_SHARED(mu)
: mut(mu), is_exclusive(false), locked(true) {
mu->lock_shared();
}
~SpinRWLockLocker() RELEASE() { unlock(); }
// Prevent copying and assignment
SpinRWLockLocker(const SpinRWLockLocker&) = delete;
SpinRWLockLocker& operator=(const SpinRWLockLocker&) = delete;
void unlock() RELEASE() {
if (!locked) return;
if (is_exclusive)
mut->unlock();
else
mut->unlock_shared();
locked = false;
}
};
#endif // THREAD_SAFETY_ANALYSIS_MUTEX_H

View File

@ -0,0 +1,34 @@
#pragma once
#include "client_manager.h"
#include "p2p_client_meta.h"
namespace mooncake {
class P2PClientManager final : public ClientManager {
public:
P2PClientManager(const int64_t disconnect_timeout_sec,
const int64_t crash_timeout_sec,
const ViewVersionId view_version);
protected:
DeploymentMode GetDeploymentMode() const override {
return DeploymentMode::P2P;
}
std::unique_ptr<ClientIterator> InnerBuildClientIterator(
ObjectIterateStrategy strategy) override;
std::shared_ptr<ClientMeta> CreateClientMeta(
const RegisterClientRequest& req) override;
void OnClientRegistered(const std::shared_ptr<ClientMeta>& meta) override {
auto p2p_meta = std::dynamic_pointer_cast<P2PClientMeta>(meta);
if (p2p_meta) {
p2p_meta->SetSyncing(true);
}
}
HeartbeatTaskResult ProcessTask(const UUID& client_id,
const HeartbeatTask& task) override;
};
} // namespace mooncake

View File

@ -0,0 +1,66 @@
#pragma once
#include "client_meta.h"
#include "p2p_segment_manager.h"
#include "p2p_rpc_types.h"
#include "heartbeat_type.h"
namespace mooncake {
class P2PClientMeta final : public ClientMeta {
public:
P2PClientMeta(const UUID& client_id, const std::string& ip_address,
uint16_t rpc_port);
std::shared_ptr<SegmentManager> GetSegmentManager() override;
auto QueryIp(const UUID& client_id)
-> tl::expected<std::vector<std::string>, ErrorCode> override;
auto UpdateSegmentUsages(const std::vector<TierUsageInfo>& usages)
-> SyncSegmentMetaResult;
size_t GetAvailableCapacity() const;
const std::string& get_ip_address() const { return ip_address_; }
uint16_t get_rpc_port() const { return rpc_port_; }
public:
/**
* @brief A wrapper function of collecting write route candidates from this
* client in ForEachClient
* @param req The write route request containing filter config
* @param candidates Output: collected candidates
* @return On error, returns an unexpected ErrorCode.
* Otherwise, returns bool value to indicate whether collected
* candidates are enough. If return `true`, ForEachClient will stop
*/
auto CollectWriteRouteCandidates(const WriteRouteRequest& req,
std::vector<WriteCandidate>& candidates)
-> tl::expected<bool, ErrorCode>;
public:
void DoOnDisconnected() override {}
void DoOnRecovered() override {}
// HA sync tracking
void SetSyncing(bool syncing) {
is_syncing_.store(syncing, std::memory_order_release);
}
bool IsSyncing() const {
return is_syncing_.load(std::memory_order_acquire);
}
private:
static constexpr size_t INF_PRIORITY = 10000;
std::string ip_address_;
uint16_t rpc_port_ = 0;
std::shared_ptr<P2PSegmentManager> segment_manager_;
mutable SpinRWLock capacity_mutex_;
size_t client_capacity_ GUARDED_BY(capacity_mutex_) = 0;
size_t client_usage_ GUARDED_BY(capacity_mutex_) = 0;
std::atomic<bool> is_syncing_{false};
};
} // namespace mooncake

View File

@ -0,0 +1,357 @@
#pragma once
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <thread>
#include <utility>
#include <coroutine>
#include <async_simple/Try.h>
#include <async_simple/coro/Lazy.h>
#include "async_metadata_notifier.h"
#include "client_service.h"
#include "data_manager.h"
#include "client_rpc_service.h"
#include "ha_recovery_manager.h"
#include "peer_client.h"
#include "p2p_master_client.h"
#include "route_cache.h"
#include "task_handle.h"
namespace mooncake {
class P2PClientService final : public ClientService {
public:
/**
* @brief Constructor for P2PClientService.
* @param local_ip IP address of the local node.
* @param te_port TE port of the local node.
* @param metadata_connstring Connection string for metadata server.
* @param metrics_port Port for metrics HTTP server.
* @param enable_metrics_http Whether to enable metrics HTTP server.
* @param labels Optional labels for client metrics.
*/
P2PClientService(const std::string& local_ip, uint16_t te_port,
const std::string& metadata_connstring,
uint16_t metrics_port = 9003,
bool enable_metrics_http = true,
const std::map<std::string, std::string>& labels = {});
virtual ~P2PClientService();
ErrorCode Init(const P2PClientConfig& config);
/**
* @brief
* 1. Stops heartbeat, RPC server, and all background threads of submodules.
* 2. Rejects all incoming requests.
*/
void Stop() override;
/**
* @brief Release internal resources.
*/
void Destroy() override;
/**
* @brief Single put data for a key.
* @param key The object key.
* @param slices Data slices.
* @param config Replicate configuration.
* @return An ErrorCode indicating the status.
*/
tl::expected<void, ErrorCode> Put(const ObjectKey& key,
std::vector<Slice>& slices,
const WriteConfig& config) override;
/**
* @brief Batch put data for multiple keys.
* currently.
* @param keys The list of object keys.
* @param batched_slices The list of data slices for each key.
* @param config Replicate configuration.
* @return A vector of ErrorCode results for each key.
*/
std::vector<tl::expected<void, ErrorCode>> BatchPut(
const std::vector<ObjectKey>& keys,
std::vector<std::vector<Slice>>& batched_slices,
const WriteConfig& config) override;
/**
* @brief Gets object metadata without transferring data
* @param object_key Key to query
* @return QueryResult containing replicas, or ErrorCode
* indicating failure
*/
tl::expected<std::unique_ptr<QueryResult>, ErrorCode> Query(
const std::string& object_key,
const ReadRouteConfig& config = {}) override;
/**
* @brief Batch query object metadata without transferring data
* @param object_keys Keys to query
* @return Vector of QueryResult objects containing replicas
*/
std::vector<tl::expected<std::unique_ptr<QueryResult>, ErrorCode>>
BatchQuery(const std::vector<std::string>& object_keys,
const ReadRouteConfig& config = {}) override;
tl::expected<bool, ErrorCode> IsExist(const std::string& key) override;
std::vector<tl::expected<bool, ErrorCode>> BatchIsExist(
const std::vector<std::string>& keys) override;
DeploymentMode deployment_mode() const override {
return DeploymentMode::P2P;
}
tl::expected<std::shared_ptr<BufferHandle>, ErrorCode> Get(
const std::string& key,
std::shared_ptr<ClientBufferAllocator> allocator,
const ReadRouteConfig& config = {}) override;
std::vector<tl::expected<std::shared_ptr<BufferHandle>, ErrorCode>>
BatchGet(const std::vector<std::string>& keys,
std::shared_ptr<ClientBufferAllocator> allocator,
const ReadRouteConfig& config = {}) override;
tl::expected<int64_t, ErrorCode> Get(
const std::string& key, const std::vector<void*>& buffers,
const std::vector<size_t>& sizes,
const ReadRouteConfig& config = {}) override;
std::vector<tl::expected<int64_t, ErrorCode>> BatchGet(
const std::vector<std::string>& keys,
const std::vector<std::vector<void*>>& all_buffers,
const std::vector<std::vector<size_t>>& all_sizes,
const ReadRouteConfig& config = {},
bool aggregate_same_segment_task = false) override;
/**
* @brief Mount a memory segment in P2P mode.
* @param buffer Start address of the buffer.
* @param size Size of the buffer in bytes.
* @return An ErrorCode indicating success or failure.
*/
tl::expected<void, ErrorCode> MountSegment(const void* buffer,
size_t size) override;
/**
* @brief Unmount a memory segment in P2P mode.
* @param buffer Start address of the buffer.
* @param size Size of the buffer in bytes.
* @return An ErrorCode indicating success or failure.
*/
tl::expected<void, ErrorCode> UnmountSegment(const void* buffer,
size_t size) override;
/**
* @brief Removes an object and all its replicas
* @param key Key to remove
* @return ErrorCode indicating success/failure
*/
tl::expected<void, ErrorCode> Remove(const ObjectKey& key) override;
/**
* @brief Removes objects from the store whose keys match a regex pattern.
* @param str The regular expression string to match against object keys.
* @return An expected object containing the number of removed objects on
* success, or an ErrorCode on failure.
*/
tl::expected<long, ErrorCode> RemoveByRegex(const ObjectKey& str) override;
/**
* @brief Removes all objects and all its replicas
* @return tl::expected<long, ErrorCode> number of removed objects or error
*/
tl::expected<long, ErrorCode> RemoveAll() override;
MasterClient& GetMasterClient() override { return master_client_; }
std::string GetHealthStatus() const override;
private:
/**
* @brief init TieredBackend and DataManager
* 1. build metadata and segment sync callback
* 2. build tiered config
* 3. init tiered backend and data manager
*/
ErrorCode InitStorage(const P2PClientConfig& config);
/**
* @brief build add replica callback.
* when tier add replica, call master to update metadata
*/
AddReplicaCallback BuildAddReplicaCallback();
/**
* @brief build remove replica callback.
* when tier remove replica, call master to update metadata
*/
RemoveReplicaCallback BuildRemoveReplicaCallback();
/**
* @brief build segment sync callback.
* when tier add/remove segment, call master to mount/unmount segment
*/
SegmentSyncCallback BuildSegmentSyncCallback();
/**
* @brief handle COMMIT type callback: notify master to add new replica
*/
tl::expected<void, ErrorCode> SyncAddReplica(const std::string& key,
const UUID& tier_id,
size_t size);
/**
* @brief handle DELETE type callback: notify master to remove replica
*/
tl::expected<void, ErrorCode> SyncRemoveReplica(const std::string& key,
const UUID& tier_id);
/**
* @brief handle batch DELETE: notify master to remove replicas from
* multiple segments in one RPC call
* @param key Key to remove
* @param segment_ids Vector of segment IDs to remove (it will be moved)
* @return Vector of ErrorCode results for each segment
*/
std::vector<tl::expected<void, ErrorCode>> SyncBatchRemoveReplica(
const std::string& key, std::vector<UUID> segment_ids);
/**
* @brief Collect tier info from DataManager and build P2P Segments.
*/
std::vector<Segment> CollectTierSegments() const;
/**
* @brief Register the P2P client with the master server.
* Collects segments from mounted_segments_ and registers them.
* @return An ErrorCode indicating success or failure.
*/
tl::expected<RegisterClientResponse, ErrorCode> RegisterClient() override;
HeartbeatRequest build_heartbeat_request() override;
private:
struct ResolvedRoute {
PeerClient* peer = nullptr;
uint64_t object_size = 0;
bool is_cached = false;
P2PProxyDescriptor proxy; // for RemoveReplica on stale-cache eviction
};
// Yields ResolvedRoute candidates from cache first, then a one-shot lazy
// master fallback. Call Prime() to pre-load before accessing object_size().
class RouteIterator {
public:
using MasterFetch = std::function<
async_simple::coro::Lazy<std::vector<ResolvedRoute>>()>;
RouteIterator(std::string key, std::vector<ResolvedRoute> initial,
uint64_t object_size, RouteCache* route_cache,
MasterFetch master_fetch);
uint64_t object_size() const { return object_size_; }
bool empty() const { return routes_.empty() && master_queried_; }
void Prime();
async_simple::coro::Lazy<std::optional<ResolvedRoute>> AsyncNext();
void Evict(const ResolvedRoute& route);
private:
void UpsertToCache(const std::vector<ResolvedRoute>& routes);
std::string key_;
std::vector<ResolvedRoute> routes_;
size_t idx_ = 0;
bool master_queried_ = false;
uint64_t object_size_ = 0;
RouteCache* route_cache_ = nullptr;
MasterFetch master_fetch_;
};
tl::expected<RouteIterator, ErrorCode> BuildRouteIter(
const std::string& key, const ReadRouteConfig& config);
async_simple::coro::Lazy<std::vector<ResolvedRoute>>
AsyncResolveRoutesFromMaster(const std::string& key,
const ReadRouteConfig& config);
static async_simple::coro::Lazy<void> RunReadRetry(
RouteIterator iter, std::shared_ptr<RemoteReadRequest> req,
std::shared_ptr<std::promise<tl::expected<void, ErrorCode>>> promise);
private:
// Fetch write routes for all keys in one master RPC.
tl::expected<BatchGetWriteRouteResponse, ErrorCode> BatchFetchWriteRoutes(
const std::vector<ObjectKey>& keys,
const std::vector<std::vector<Slice>>& batched_slices,
const WriteRouteRequestConfig& config);
tl::expected<std::unique_ptr<TaskHandle<void>>, ErrorCode> CreatePutHandle(
const std::string& key, std::vector<Slice>& slices,
const WriteRouteRequestConfig& config);
tl::expected<std::unique_ptr<TaskHandle<void>>, ErrorCode>
CreateLocalPutHandle(const std::string& key, std::vector<Slice>& slices);
tl::expected<std::unique_ptr<TaskHandle<void>>, ErrorCode>
InnerCreatePutHandle(const std::string& key, std::vector<Slice>& slices,
const WriteRouteRequestConfig& config,
std::vector<WriteCandidate> candidates);
tl::expected<ReadTaskHandle, ErrorCode> CreateGetHandle(
const std::string& key,
std::shared_ptr<ClientBufferAllocator> allocator,
const ReadRouteConfig& config);
tl::expected<ReadTaskHandle, ErrorCode> CreateGetHandle(
const std::string& key, std::vector<Slice>& slices,
const ReadRouteConfig& config);
/**
* @brief Launch async reads driven by a RouteIterator.
*
* Creates a ReadRetryContinuation that fires the first RPC immediately
* and chains subsequent candidates on failure (no stack recursion).
*/
tl::expected<ReadTaskHandle, ErrorCode> InnerGetViaRoute(
const std::string& key, std::vector<Slice>& slices, RouteIterator iter);
/**
* @brief Get or create a PeerClient for the given endpoint.
* Thread-safe via peer_clients_mutex_.
*/
PeerClient& GetOrCreatePeerClient(const std::string& endpoint);
private:
void OnHAEvent(HAEvent event) override;
private:
P2PMasterClient master_client_;
uint16_t client_rpc_port_ = 12345;
std::unique_ptr<coro_rpc::coro_rpc_server> client_rpc_server_;
std::thread client_rpc_server_thread_;
std::optional<DataManager> data_manager_;
std::optional<ClientRpcService> client_rpc_service_;
// Each PeerClient instance maintains its own fixed-size connection pool.
std::mutex peer_clients_mutex_;
std::map<std::string, std::unique_ptr<PeerClient>> peer_clients_;
// Route cache for reducing Master query pressure
std::optional<RouteCache> route_cache_;
// Async route notifier (nullptr when disabled)
std::unique_ptr<AsyncMetadataNotifier> async_route_notifier_;
// HA recovery manager
std::unique_ptr<HARecoveryManager> ha_manager_;
};
} // namespace mooncake

View File

@ -0,0 +1,63 @@
#pragma once
#include "master_client.h"
#include "p2p_rpc_types.h"
namespace mooncake {
/**
* @brief Client for interacting with the mooncake P2P master service
*/
class P2PMasterClient final : public MasterClient {
public:
P2PMasterClient(const UUID& client_id,
MasterClientMetric* metrics = nullptr)
: MasterClient(client_id, metrics) {}
P2PMasterClient(const P2PMasterClient&) = delete;
P2PMasterClient& operator=(const P2PMasterClient&) = delete;
/**
* @brief Gets write candidate route for a segment
*/
[[nodiscard]] tl::expected<WriteRouteResponse, ErrorCode> GetWriteRoute(
const WriteRouteRequest& req);
/**
* @brief Batch gets write candidate routes for multiple keys in one RPC
*/
[[nodiscard]] tl::expected<BatchGetWriteRouteResponse, ErrorCode>
BatchGetWriteRoute(const BatchGetWriteRouteRequest& req);
/**
* @brief Adds a replica to master
*/
[[nodiscard]] tl::expected<void, ErrorCode> AddReplica(
const AddReplicaRequest& req);
/**
* @brief Removes a replica from master
*/
[[nodiscard]] tl::expected<void, ErrorCode> RemoveReplica(
const RemoveReplicaRequest& req);
/**
* @brief Removes replicas from multiple segments in one call
*/
[[nodiscard]] std::vector<tl::expected<void, ErrorCode>> BatchRemoveReplica(
const BatchRemoveReplicaRequest& req);
/**
* @brief Batch sync replicas with mixed ADD and REMOVE ops
*/
[[nodiscard]] tl::expected<BatchSyncReplicaResponse, ErrorCode>
BatchSyncReplica(const BatchSyncReplicaRequest& req);
/**
* @brief Notify Master that this client has finished syncing metadata
*/
[[nodiscard]] tl::expected<void, ErrorCode> SetSyncCompleted(
UUID client_id);
};
} // namespace mooncake

View File

@ -0,0 +1,109 @@
#pragma once
#include "master_service.h"
#include "p2p_client_manager.h"
#include "p2p_rpc_types.h"
namespace mooncake {
class P2PMasterService : public MasterService {
public:
explicit P2PMasterService(const MasterServiceConfig& config);
~P2PMasterService() override = default;
ClientManager& GetClientManager() override { return *client_manager_; }
const ClientManager& GetClientManager() const override {
return *client_manager_;
}
/**
* @brief Get write route based on the config in the request
*/
auto GetWriteRoute(const WriteRouteRequest& req)
-> tl::expected<WriteRouteResponse, ErrorCode>;
/**
* @brief Batch get write routes for multiple keys.
* Reuses GetWriteRoute logic per key.
*/
auto BatchGetWriteRoute(const BatchGetWriteRouteRequest& req)
-> BatchGetWriteRouteResponse;
/**
* @brief Add a route replica to master
*/
auto AddReplica(const AddReplicaRequest& req)
-> tl::expected<void, ErrorCode>;
/**
* @brief Remove a route replica from master
*/
auto RemoveReplica(const RemoveReplicaRequest& req)
-> tl::expected<void, ErrorCode>;
/**
* @brief Remove replicas from multiple segments in one call
*/
auto BatchRemoveReplica(const BatchRemoveReplicaRequest& req)
-> std::vector<tl::expected<void, ErrorCode>>;
/**
* @brief Batch sync replicas with mixed ADD and REMOVE ops
*/
auto BatchSyncReplica(const BatchSyncReplicaRequest& req)
-> BatchSyncReplicaResponse;
/**
* @brief Client notifies Master that metadata sync is complete
*/
auto SetSyncCompleted(UUID client_id) -> tl::expected<void, ErrorCode>;
std::vector<Replica::Descriptor> FilterReplicas(
const GetReplicaListRequestConfig& config,
const ObjectMetadata& metadata) override;
protected:
typedef MetadataShard P2PMetadataShard;
MetadataShard& GetShard(size_t idx) override {
return metadata_shards_[idx];
}
const MetadataShard& GetShard(size_t idx) const override {
return metadata_shards_[idx];
}
P2PMetadataShard& GetP2PShard(size_t idx) { return metadata_shards_[idx]; }
const P2PMetadataShard& GetP2PShard(size_t idx) const {
return metadata_shards_[idx];
}
static constexpr size_t kNumShards = 1024; // Number of metadata shards
// Helper to get shard index from key
size_t GetShardIndex(const std::string& key) const override {
return std::hash<std::string>{}(key) % kNumShards;
}
size_t GetShardCount() const override { return kNumShards; }
protected:
// Hooks
void OnObjectAccessed(ObjectMetadata& metadata) override;
void OnObjectHit(const ObjectMetadata& metadata) override;
void OnReplicaRemoved(const Replica& replica) override;
void OnReplicaAdded(const Replica& replica) override;
private:
tl::expected<void, ErrorCode> InnerAddReplica(
MetadataShard& shard, const std::string& key, const UUID& client_id,
const UUID& segment_id, size_t size,
const std::shared_ptr<P2PClientMeta>& client) NO_THREAD_SAFETY_ANALYSIS;
tl::expected<void, ErrorCode> InnerRemoveReplica(
MetadataShard& shard, const std::string& key, const UUID& client_id,
const UUID& segment_id) NO_THREAD_SAFETY_ANALYSIS;
std::shared_ptr<P2PClientManager> client_manager_;
std::array<P2PMetadataShard, kNumShards> metadata_shards_;
// for the number of replicas of a key:
// 1. max_replicas_per_key_ == 0 means no limitation
// 2. max_replicas_per_key_ > 0 means the max replica number of a key
uint64_t max_replicas_per_key_;
};
} // namespace mooncake

View File

@ -0,0 +1,45 @@
#pragma once
#include "p2p_master_service.h"
#include "rpc_service.h"
#include "p2p_rpc_types.h"
namespace mooncake {
class WrappedP2PMasterService final : public WrappedMasterService {
public:
WrappedP2PMasterService(const WrappedMasterServiceConfig& config);
~WrappedP2PMasterService() override = default;
MasterService& GetMasterService() override { return master_service_; }
tl::expected<WriteRouteResponse, ErrorCode> GetWriteRoute(
const WriteRouteRequest& req);
BatchGetWriteRouteResponse BatchGetWriteRoute(
const BatchGetWriteRouteRequest& req);
tl::expected<void, ErrorCode> AddReplica(const AddReplicaRequest& req);
tl::expected<void, ErrorCode> RemoveReplica(
const RemoveReplicaRequest& req);
std::vector<tl::expected<void, ErrorCode>> BatchRemoveReplica(
const BatchRemoveReplicaRequest& req);
BatchSyncReplicaResponse BatchSyncReplica(
const BatchSyncReplicaRequest& req);
tl::expected<void, ErrorCode> SetSyncCompleted(UUID client_id);
private:
P2PMasterService master_service_;
};
void RegisterP2PRpcService(
coro_rpc::coro_rpc_server& server,
mooncake::WrappedP2PMasterService& wrapped_master_service);
} // namespace mooncake

View File

@ -0,0 +1,153 @@
#pragma once
#include <string>
#include <vector>
#include "replica.h"
#include "types.h"
#include <ylt/reflection/user_reflect_macro.hpp>
namespace mooncake {
/**
* @brief Request config for write route
*/
struct WriteRouteRequestConfig {
static constexpr size_t RETURN_ALL_CANDIDATES = 0;
size_t max_candidates = RETURN_ALL_CANDIDATES;
ObjectIterateStrategy strategy = ObjectIterateStrategy::CAPACITY_PRIORITY;
bool allow_local = true; // whether to filter local client
bool prefer_local = true; // enhance the priority of local client
// works only when allow_local==true
bool early_return = true; // whether to return immediately once candidates
// meet conditions of config
// segment level (TODO)
// filter the segment with tag
std::vector<std::string> tag_filters;
// filter the segments whose priority is lower than priority_limit
int priority_limit = 0;
};
YLT_REFL(WriteRouteRequestConfig, max_candidates, strategy, allow_local,
prefer_local, early_return, tag_filters, priority_limit);
inline std::ostream& operator<<(std::ostream& os,
const WriteRouteRequestConfig& config) {
os << "WriteRouteRequestConfig: { max_candidates: " << config.max_candidates
<< ", strategy: " << config.strategy
<< ", allow_local: " << (config.allow_local ? "true" : "false")
<< ", prefer_local: " << (config.prefer_local ? "true" : "false")
<< ", early_return: " << (config.early_return ? "true" : "false")
<< ", priority_limit: " << config.priority_limit << " }";
return os;
}
/**
* @brief Request structure for getting write route.
*/
struct WriteRouteRequest {
std::string key; // used for pre-filter with limitation of replica number
UUID client_id;
size_t size = 0;
WriteRouteRequestConfig config;
};
YLT_REFL(WriteRouteRequest, key, client_id, size, config);
/**
* @brief Candidate node for writing route
*/
struct WriteCandidate {
P2PProxyDescriptor replica;
size_t available_capacity = 0;
int priority = 0;
};
YLT_REFL(WriteCandidate, replica, available_capacity, priority);
/**
* @brief Response structure for getting write route.
*/
struct WriteRouteResponse {
std::vector<WriteCandidate> candidates;
};
YLT_REFL(WriteRouteResponse, candidates);
/**
* @brief Request for batch write route lookup.
*/
struct BatchGetWriteRouteRequest {
UUID client_id;
std::vector<std::string> keys;
std::vector<size_t> sizes;
WriteRouteRequestConfig config; // shared config for all keys
};
YLT_REFL(BatchGetWriteRouteRequest, client_id, keys, sizes, config);
/**
* @brief Response for batch write route lookup.
* responses[i] and error_codes[i] correspond to keys[i] in the request.
*/
struct BatchGetWriteRouteResponse {
std::vector<WriteRouteResponse> responses; // valid when error_codes[i]==OK
std::vector<ErrorCode> error_codes;
};
YLT_REFL(BatchGetWriteRouteResponse, responses, error_codes);
/**
* @brief Request to add a replica.
* Master resolves ip_address/rpc_port from registered client info.
*/
struct AddReplicaRequest {
std::string key;
size_t size;
UUID client_id;
UUID segment_id;
};
YLT_REFL(AddReplicaRequest, key, size, client_id, segment_id);
/**
* @brief Request to remove a replica
*/
struct RemoveReplicaRequest {
std::string key;
UUID client_id;
UUID segment_id;
};
YLT_REFL(RemoveReplicaRequest, key, client_id, segment_id);
/**
* @brief Request to remove replicas from multiple segments in one call
*/
struct BatchRemoveReplicaRequest {
std::string key;
UUID client_id;
std::vector<UUID> segment_ids;
};
YLT_REFL(BatchRemoveReplicaRequest, key, client_id, segment_ids);
/**
* @brief Request to batch sync replicas (mixed ADD and REMOVE ops).
* Master only needs client_id + segment_id to identify replicas
*/
struct BatchSyncReplicaRequest {
UUID client_id;
// ADD operations
std::vector<std::string> add_keys;
std::vector<size_t> add_sizes;
std::vector<UUID> add_segment_ids;
// REMOVE operations
std::vector<std::string> remove_keys;
std::vector<UUID> remove_segment_ids;
};
YLT_REFL(BatchSyncReplicaRequest, client_id, add_keys, add_sizes,
add_segment_ids, remove_keys, remove_segment_ids);
/**
* @brief Response for batch sync replicas.
*/
struct BatchSyncReplicaResponse {
std::vector<ErrorCode> add_results;
std::vector<ErrorCode> remove_results;
};
YLT_REFL(BatchSyncReplicaResponse, add_results, remove_results);
} // namespace mooncake

View File

@ -0,0 +1,52 @@
#pragma once
#include <boost/functional/hash.hpp>
#include "segment_manager.h"
#include "types.h"
namespace mooncake {
class P2PSegmentManager : public SegmentManager {
public:
auto QuerySegments(const std::string& segment)
-> tl::expected<std::pair<size_t, size_t>, ErrorCode> override;
using OnSegmentAddedCallback = std::function<void(const Segment& segment)>;
using OnSegmentRemovedCallback =
std::function<void(const Segment& segment)>;
void SetSegmentChangeCallbacks(OnSegmentAddedCallback on_add,
OnSegmentRemovedCallback on_remove) {
on_segment_added_ = std::move(on_add);
on_segment_removed_ = std::move(on_remove);
}
/**
* @brief update segment usage and return old usage
*/
tl::expected<size_t, ErrorCode> UpdateSegmentUsage(const UUID& segment_id,
size_t usage);
/**
* @brief get segment usage
*/
size_t GetSegmentUsage(const UUID& segment_id) const;
/**
* @brief Iterate over all mounted P2P segments under a single read lock.
* Visitor returns true to stop early.
*/
using SegmentVisitor = std::function<bool(const Segment& segment)>;
void ForEachSegment(const SegmentVisitor& visitor) const;
protected:
tl::expected<void, ErrorCode> InnerMountSegment(
const Segment& segment) override;
tl::expected<void, ErrorCode> OnUnmountSegment(
const std::shared_ptr<Segment>& segment) override;
private:
OnSegmentAddedCallback on_segment_added_;
OnSegmentRemovedCallback on_segment_removed_;
};
} // namespace mooncake

View File

@ -0,0 +1,40 @@
#pragma once
#include "client_rpc_types.h"
#include "types.h"
#include <vector>
#include <string>
#include <ylt/coro_rpc/coro_rpc_client.hpp>
#include <ylt/coro_io/client_pool.hpp>
#include <ylt/util/tl/expected.hpp>
#include <async_simple/coro/Lazy.h>
#include <async_simple/coro/SyncAwait.h>
namespace mooncake {
class PeerClient {
public:
tl::expected<void, ErrorCode> Connect(const std::string& endpoint);
// --- Async single-key interfaces ---
async_simple::coro::Lazy<tl::expected<void, ErrorCode>> AsyncReadRemoteData(
const RemoteReadRequest& request);
async_simple::coro::Lazy<tl::expected<UUID, ErrorCode>>
AsyncWriteRemoteData(const RemoteWriteRequest& request);
// --- Sync single-key interfaces ---
tl::expected<void, ErrorCode> ReadRemoteData(
const RemoteReadRequest& request);
tl::expected<UUID, ErrorCode> WriteRemoteData(
const RemoteWriteRequest& request);
private:
std::shared_ptr<coro_io::client_pools<coro_rpc::coro_rpc_client>>
client_pools_;
std::shared_ptr<coro_io::client_pool<coro_rpc::coro_rpc_client>>
client_pool_;
std::string endpoint_;
};
} // namespace mooncake

View File

@ -1,17 +1,11 @@
#pragma once
#include <csignal>
#include <atomic>
#include <thread>
#include <string>
#include <memory>
#include <vector>
#include "client_service.h"
#include "client_buffer.hpp"
#include "mutex.h"
#include "utils.h"
#include "file_storage.h"
namespace mooncake {
@ -29,106 +23,96 @@ struct ShmRegisterRequest {
class PyClient {
public:
virtual ~PyClient() = 0;
virtual int setup_real(
const std::string &local_hostname, const std::string &metadata_server,
size_t global_segment_size, size_t local_buffer_size,
const std::string &protocol, const std::string &rdma_devices,
const std::string &master_server_addr,
const std::shared_ptr<TransferEngine> &transfer_engine,
const std::string &ipc_socket_path) = 0;
virtual int setup_dummy(size_t mem_pool_size, size_t local_buffer_size,
const std::string &server_address,
const std::string &ipc_socket_path) = 0;
virtual int initAll(const std::string &protocol,
const std::string &device_name,
virtual int initAll(const std::string& protocol,
const std::string& device_name,
size_t mount_segment_size) = 0;
virtual uint64_t alloc_from_mem_pool(size_t size) = 0;
virtual int put(const std::string &key, std::span<const char> value,
const ReplicateConfig &config = ReplicateConfig{}) = 0;
virtual DeploymentMode deployment_mode() const = 0;
virtual int register_buffer(void *buffer, size_t size) = 0;
virtual int put(const std::string& key, std::span<const char> value,
const WriteConfig& config) = 0;
virtual int unregister_buffer(void *buffer) = 0;
virtual int register_buffer(void* buffer, size_t size) = 0;
virtual int64_t get_into(const std::string &key, void *buffer,
size_t size) = 0;
virtual int unregister_buffer(void* buffer) = 0;
virtual int64_t get_into(const std::string& key, void* buffer, size_t size,
const ReadRouteConfig& config = {}) = 0;
virtual std::vector<int64_t> batch_get_into(
const std::vector<std::string> &keys,
const std::vector<void *> &buffers,
const std::vector<size_t> &sizes) = 0;
const std::vector<std::string>& keys, const std::vector<void*>& buffers,
const std::vector<size_t>& sizes,
const ReadRouteConfig& config = {}) = 0;
virtual std::vector<int> batch_get_into_multi_buffers(
const std::vector<std::string> &keys,
const std::vector<std::vector<void *>> &all_buffers,
const std::vector<std::vector<size_t>> &all_sizes,
bool prefer_same_node) = 0;
const std::vector<std::string>& keys,
const std::vector<std::vector<void*>>& all_buffers,
const std::vector<std::vector<size_t>>& all_sizes,
bool aggregate_same_segment_task,
const ReadRouteConfig& config = {}) = 0;
virtual int put_from(const std::string &key, void *buffer, size_t size,
const ReplicateConfig &config = ReplicateConfig{}) = 0;
virtual int put_from(const std::string& key, void* buffer, size_t size,
const WriteConfig& config) = 0;
virtual int put_from_with_metadata(
const std::string &key, void *buffer, void *metadata_buffer,
size_t size, size_t metadata_size,
const ReplicateConfig &config = ReplicateConfig{}) = 0;
virtual int put_from_with_metadata(const std::string& key, void* buffer,
void* metadata_buffer, size_t size,
size_t metadata_size,
const WriteConfig& config) = 0;
virtual std::vector<int> batch_put_from(
const std::vector<std::string> &keys,
const std::vector<void *> &buffers, const std::vector<size_t> &sizes,
const ReplicateConfig &config = ReplicateConfig{}) = 0;
const std::vector<std::string>& keys, const std::vector<void*>& buffers,
const std::vector<size_t>& sizes, const WriteConfig& config) = 0;
virtual std::vector<int> batch_put_from_multi_buffers(
const std::vector<std::string> &keys,
const std::vector<std::vector<void *>> &all_buffers,
const std::vector<std::vector<size_t>> &all_sizes,
const ReplicateConfig &config = ReplicateConfig{}) = 0;
const std::vector<std::string>& keys,
const std::vector<std::vector<void*>>& all_buffers,
const std::vector<std::vector<size_t>>& all_sizes,
const WriteConfig& config) = 0;
virtual std::shared_ptr<BufferHandle> get_buffer(
const std::string &key) = 0;
const std::string& key, const ReadRouteConfig& config = {}) = 0;
virtual std::tuple<uint64_t, size_t> get_buffer_info(
const std::string &key) = 0;
const std::string& key, const ReadRouteConfig& config = {}) = 0;
virtual std::vector<std::shared_ptr<BufferHandle>> batch_get_buffer(
const std::vector<std::string> &keys) = 0;
const std::vector<std::string>& keys,
const ReadRouteConfig& config = {}) = 0;
virtual int put_parts(
const std::string &key, std::vector<std::span<const char>> values,
const ReplicateConfig &config = ReplicateConfig{}) = 0;
virtual int put_parts(const std::string& key,
std::vector<std::span<const char>> values,
const WriteConfig& config) = 0;
virtual int put_batch(
const std::vector<std::string> &keys,
const std::vector<std::span<const char>> &values,
const ReplicateConfig &config = ReplicateConfig{}) = 0;
virtual int put_batch(const std::vector<std::string>& keys,
const std::vector<std::span<const char>>& values,
const WriteConfig& config) = 0;
[[nodiscard]] virtual std::string get_hostname() const = 0;
virtual int remove(const std::string &key) = 0;
virtual int remove(const std::string& key) = 0;
virtual long removeByRegex(const std::string &str) = 0;
virtual long removeByRegex(const std::string& str) = 0;
virtual long removeAll() = 0;
virtual int isExist(const std::string &key) = 0;
virtual int isExist(const std::string& key) = 0;
virtual std::vector<int> batchIsExist(
const std::vector<std::string> &keys) = 0;
const std::vector<std::string>& keys) = 0;
virtual int64_t getSize(const std::string &key) = 0;
virtual int64_t getSize(const std::string& key) = 0;
virtual std::map<std::string, std::vector<Replica::Descriptor>>
batch_get_replica_desc(const std::vector<std::string> &keys) = 0;
batch_get_replica_desc(const std::vector<std::string>& keys) = 0;
virtual std::vector<Replica::Descriptor> get_replica_desc(
const std::string &key) = 0;
const std::string& key) = 0;
virtual int tearDownAll() = 0;
std::shared_ptr<mooncake::Client> client_ = nullptr;
std::shared_ptr<mooncake::FileStorage> file_storage_ = nullptr;
std::shared_ptr<mooncake::ClientService> client_service_ = nullptr;
std::shared_ptr<ClientBufferAllocator> client_buffer_allocator_ = nullptr;
};

View File

@ -1,16 +1,17 @@
#pragma once
#include "client_config_builder.h"
#include <atomic>
#include <boost/lockfree/queue.hpp>
#include <csignal>
#include <memory>
#include <string>
#include <thread>
#include <unordered_set>
#include <vector>
#include <condition_variable>
#include <mutex>
#include "pyclient.h"
#include "client_service.h"
#include "client_buffer.hpp"
#include "mutex.h"
#include "utils.h"
@ -24,18 +25,18 @@ class RealClient;
class ResourceTracker {
public:
// Get the singleton instance
static ResourceTracker &getInstance();
static ResourceTracker& getInstance();
// Register a DistributedObjectStore instance for cleanup
void registerInstance(const std::shared_ptr<PyClient> &instance);
void registerInstance(const std::shared_ptr<PyClient>& instance);
private:
ResourceTracker();
~ResourceTracker();
// Prevent copying
ResourceTracker(const ResourceTracker &) = delete;
ResourceTracker &operator=(const ResourceTracker &) = delete;
ResourceTracker(const ResourceTracker&) = delete;
ResourceTracker& operator=(const ResourceTracker&) = delete;
// Cleanup all registered resources
void cleanupAllResources();
@ -66,34 +67,25 @@ class RealClient : public PyClient {
// Factory to create shared instances and auto-register to ResourceTracker
static std::shared_ptr<RealClient> create();
int setup_real(
const std::string &local_hostname, const std::string &metadata_server,
size_t global_segment_size = 1024 * 1024 * 16,
size_t local_buffer_size = 1024 * 1024 * 16,
const std::string &protocol = "tcp",
const std::string &rdma_devices = "",
const std::string &master_server_addr = "127.0.0.1:50051",
const std::shared_ptr<TransferEngine> &transfer_engine = nullptr,
const std::string &ipc_socket_path = "");
template <typename ConfigT>
int setup(ConfigT& config);
int setup_dummy(size_t mem_pool_size, size_t local_buffer_size,
const std::string &server_address,
const std::string &ipc_socket_path) {
// Real client does not support dummy setup
return -1;
};
int initAll(const std::string& protocol, const std::string& device_name,
size_t mount_segment_size = 1024 * 1024 * 16) override;
int initAll(const std::string &protocol, const std::string &device_name,
size_t mount_segment_size = 1024 * 1024 * 16); // Default 16MB
uint64_t alloc_from_mem_pool(size_t size) override { return 0; };
uint64_t alloc_from_mem_pool(size_t size) { return 0; };
DeploymentMode deployment_mode() const override {
return client_service_ ? client_service_->deployment_mode()
: DeploymentMode::UNKNOWN;
}
int put(const std::string &key, std::span<const char> value,
const ReplicateConfig &config = ReplicateConfig{});
int put(const std::string& key, std::span<const char> value,
const WriteConfig& config) override;
int register_buffer(void *buffer, size_t size);
int register_buffer(void* buffer, size_t size) override;
int unregister_buffer(void *buffer);
int unregister_buffer(void* buffer) override;
/**
* @brief Get object data directly into a pre-allocated buffer
@ -105,7 +97,8 @@ class RealClient : public PyClient {
* @note The buffer address must be previously registered with
* register_buffer() for zero-copy operations
*/
int64_t get_into(const std::string &key, void *buffer, size_t size);
int64_t get_into(const std::string& key, void* buffer, size_t size,
const ReadRouteConfig& config = {}) override;
/**
* @brief Get object data directly into pre-allocated buffers for multiple
@ -118,9 +111,10 @@ class RealClient : public PyClient {
* @note The buffer addresses must be previously registered with
* register_buffer() for zero-copy operations
*/
std::vector<int64_t> batch_get_into(const std::vector<std::string> &keys,
const std::vector<void *> &buffers,
const std::vector<size_t> &sizes);
std::vector<int64_t> batch_get_into(
const std::vector<std::string>& keys, const std::vector<void*>& buffers,
const std::vector<size_t>& sizes,
const ReadRouteConfig& config = {}) override;
/**
* @brief Get object data directly into pre-allocated buffers for multiple
@ -135,10 +129,11 @@ class RealClient : public PyClient {
* register_buffer() for zero-copy operations
*/
std::vector<int> batch_get_into_multi_buffers(
const std::vector<std::string> &keys,
const std::vector<std::vector<void *>> &all_buffers,
const std::vector<std::vector<size_t>> &all_sizes,
bool prefer_same_node);
const std::vector<std::string>& keys,
const std::vector<std::vector<void*>>& all_buffers,
const std::vector<std::vector<size_t>>& all_sizes,
bool aggregate_same_segment_task,
const ReadRouteConfig& config = {}) override;
/**
* @brief Put object data directly from a pre-allocated buffer
@ -150,8 +145,8 @@ class RealClient : public PyClient {
* @note The buffer address must be previously registered with
* register_buffer() for zero-copy operations
*/
int put_from(const std::string &key, void *buffer, size_t size,
const ReplicateConfig &config = ReplicateConfig{});
int put_from(const std::string& key, void* buffer, size_t size,
const WriteConfig& config) override;
/**
* @brief Put object data directly from pre-allocated buffers for multiple
@ -168,10 +163,10 @@ class RealClient : public PyClient {
* @note The buffer addresses must be previously registered with
* register_buffer() for zero-copy operations
*/
int put_from_with_metadata(
const std::string &key, void *buffer, void *metadata_buffer,
size_t size, size_t metadata_size,
const ReplicateConfig &config = ReplicateConfig{});
int put_from_with_metadata(const std::string& key, void* buffer,
void* metadata_buffer, size_t size,
size_t metadata_size,
const WriteConfig& config) override;
/**
* @brief Put object data directly from pre-allocated buffers for multiple
@ -186,10 +181,10 @@ class RealClient : public PyClient {
* register_buffer() for zero-copy operations
*/
std::vector<int> batch_put_from(
const std::vector<std::string> &keys,
const std::vector<void *> &buffers, const std::vector<size_t> &sizes,
const ReplicateConfig &config = ReplicateConfig{});
std::vector<int> batch_put_from(const std::vector<std::string>& keys,
const std::vector<void*>& buffers,
const std::vector<size_t>& sizes,
const WriteConfig& config) override;
/**
* @brief Put object data directly from multiple pre-allocated buffers for
@ -205,20 +200,20 @@ class RealClient : public PyClient {
* register_buffer() for zero-copy operations
*/
std::vector<int> batch_put_from_multi_buffers(
const std::vector<std::string> &keys,
const std::vector<std::vector<void *>> &all_buffers,
const std::vector<std::vector<size_t>> &all_sizes,
const ReplicateConfig &config = ReplicateConfig{});
const std::vector<std::string>& keys,
const std::vector<std::vector<void*>>& all_buffers,
const std::vector<std::vector<size_t>>& all_sizes,
const WriteConfig& config) override;
int put_parts(const std::string &key,
int put_parts(const std::string& key,
std::vector<std::span<const char>> values,
const ReplicateConfig &config = ReplicateConfig{});
const WriteConfig& config) override;
int put_batch(const std::vector<std::string> &keys,
const std::vector<std::span<const char>> &values,
const ReplicateConfig &config = ReplicateConfig{});
int put_batch(const std::vector<std::string>& keys,
const std::vector<std::span<const char>>& values,
const WriteConfig& config) override;
[[nodiscard]] std::string get_hostname() const;
[[nodiscard]] std::string get_hostname() const override;
/**
* @brief Get a buffer containing the data for a key
@ -226,14 +221,16 @@ class RealClient : public PyClient {
* @return std::shared_ptr<BufferHandle> Buffer containing the data, or
* nullptr if error
*/
std::shared_ptr<BufferHandle> get_buffer(const std::string &key);
std::shared_ptr<BufferHandle> get_buffer(
const std::string& key, const ReadRouteConfig& config = {}) override;
/**
* @brief Get buffer information (address and size) for a key
* @param key Key to get buffer information for
* @return Tuple containing buffer address and size, or (0, 0) if error
*/
std::tuple<uint64_t, size_t> get_buffer_info(const std::string &key);
std::tuple<uint64_t, size_t> get_buffer_info(
const std::string& key, const ReadRouteConfig& config = {}) override;
/**
* @brief Get buffers containing the data for multiple keys (batch version)
@ -242,22 +239,23 @@ class RealClient : public PyClient {
* data, or nullptr for each key if error
*/
std::vector<std::shared_ptr<BufferHandle>> batch_get_buffer(
const std::vector<std::string> &keys);
const std::vector<std::string>& keys,
const ReadRouteConfig& config = {}) override;
int remove(const std::string &key);
int remove(const std::string& key) override;
long removeByRegex(const std::string &str);
long removeByRegex(const std::string& str) override;
long removeAll();
long removeAll() override;
int tearDownAll();
int tearDownAll() override;
/**
* @brief Check if an object exists
* @param key Key to check
* @return 1 if exists, 0 if not exists, -1 if error
*/
int isExist(const std::string &key);
int isExist(const std::string& key) override;
/**
* @brief Check if multiple objects exist
@ -265,7 +263,7 @@ class RealClient : public PyClient {
* @return Vector of existence results: 1 if exists, 0 if not exists, -1 if
* error
*/
std::vector<int> batchIsExist(const std::vector<std::string> &keys);
std::vector<int> batchIsExist(const std::vector<std::string>& keys);
/**
* @brief Get the size of an object
@ -273,35 +271,38 @@ class RealClient : public PyClient {
* @return Size of the object in bytes, or -1 if error or object doesn't
* exist
*/
int64_t getSize(const std::string &key);
int64_t getSize(const std::string& key) override;
// Dummy client helper functions that return tl::expected
tl::expected<std::tuple<uint64_t, size_t>, ErrorCode>
get_buffer_info_dummy_helper(const std::string &key, const UUID &client_id);
get_buffer_info_dummy_helper(const std::string& key,
const ReadRouteConfig& config,
const UUID& client_id);
tl::expected<void, ErrorCode> put_dummy_helper(
const std::string &key, std::span<const char> value,
const ReplicateConfig &config, const UUID &client_id);
tl::expected<void, ErrorCode> put_dummy_helper(const std::string& key,
std::span<const char> value,
const WriteConfig& config,
const UUID& client_id);
tl::expected<void, ErrorCode> put_batch_dummy_helper(
const std::vector<std::string> &keys,
const std::vector<std::span<const char>> &values,
const ReplicateConfig &config, const UUID &client_id);
const std::vector<std::string>& keys,
const std::vector<std::span<const char>>& values,
const WriteConfig& config, const UUID& client_id);
tl::expected<void, ErrorCode> put_parts_dummy_helper(
const std::string &key, std::vector<std::span<const char>> values,
const ReplicateConfig &config, const UUID &client_id);
const std::string& key, std::vector<std::span<const char>> values,
const WriteConfig& config, const UUID& client_id);
std::vector<tl::expected<int64_t, ErrorCode>> batch_get_into_dummy_helper(
const std::vector<std::string> &keys,
const std::vector<uint64_t> &buffers, const std::vector<size_t> &sizes,
const UUID &client_id);
const std::vector<std::string>& keys,
const std::vector<uint64_t>& buffers, const std::vector<size_t>& sizes,
const ReadRouteConfig& config, const UUID& client_id);
std::vector<tl::expected<void, ErrorCode>> batch_put_from_dummy_helper(
const std::vector<std::string> &keys,
const std::vector<uint64_t> &dummy_buffers,
const std::vector<size_t> &sizes, const ReplicateConfig &config,
const UUID &client_id);
const std::vector<std::string>& keys,
const std::vector<uint64_t>& dummy_buffers,
const std::vector<size_t>& sizes, const WriteConfig& config,
const UUID& client_id);
// Share mem management for dummy client
// Modified: map_shm_internal now takes fd instead of just name
@ -309,145 +310,123 @@ class RealClient : public PyClient {
uint64_t shm_base_addr,
size_t shm_size,
bool is_local_buffer,
const UUID &client_id);
const UUID& client_id);
tl::expected<void, ErrorCode> unmap_shm_internal(const UUID &client_id);
tl::expected<void, ErrorCode> unmap_shm_internal(const UUID& client_id);
tl::expected<void, ErrorCode> unregister_shm_buffer_internal(
uint64_t dummy_base_addr, const UUID &client_id);
uint64_t dummy_base_addr, const UUID& client_id);
// Internal versions that return tl::expected
tl::expected<void, ErrorCode> service_ready_internal() { return {}; }
tl::expected<DeploymentMode, ErrorCode> service_ready_internal() {
return deployment_mode();
}
tl::expected<void, ErrorCode> setup_internal(
const std::string &local_hostname, const std::string &metadata_server,
size_t global_segment_size = 1024 * 1024 * 16,
size_t local_buffer_size = 1024 * 1024 * 16,
const std::string &protocol = "tcp",
const std::string &rdma_devices = "",
const std::string &master_server_addr = "127.0.0.1:50051",
const std::shared_ptr<TransferEngine> &transfer_engine = nullptr,
const std::string &ipc_socket_path = "", bool enable_offload = false);
template <typename ConfigT>
tl::expected<void, ErrorCode> setup_internal(ConfigT& config);
tl::expected<void, ErrorCode> initAll_internal(
const std::string &protocol, const std::string &device_name,
const std::string& protocol, const std::string& device_name,
size_t mount_segment_size = 1024 * 1024 * 16);
tl::expected<void, ErrorCode> unregister_buffer_internal(void *buffer);
tl::expected<void, ErrorCode> unregister_buffer_internal(void* buffer);
tl::expected<void, ErrorCode> put_internal(
const std::string &key, std::span<const char> value,
const ReplicateConfig &config = ReplicateConfig{},
const std::string& key, std::span<const char> value,
const WriteConfig& config,
std::shared_ptr<ClientBufferAllocator> client_buffer_allocator =
nullptr);
tl::expected<void, ErrorCode> register_buffer_internal(void *buffer,
tl::expected<void, ErrorCode> register_buffer_internal(void* buffer,
size_t size);
tl::expected<int64_t, ErrorCode> get_into_internal(const std::string &key,
void *buffer,
size_t size);
tl::expected<int64_t, ErrorCode> get_into_internal(
const std::string& key, void* buffer, size_t size,
const ReadRouteConfig& config = {});
std::vector<tl::expected<int64_t, ErrorCode>> batch_get_into_internal(
const std::vector<std::string> &keys,
const std::vector<void *> &buffers, const std::vector<size_t> &sizes);
const std::vector<std::string>& keys, const std::vector<void*>& buffers,
const std::vector<size_t>& sizes, const ReadRouteConfig& config = {});
std::vector<tl::expected<int64_t, ErrorCode>>
batch_get_into_multi_buffers_internal(
const std::vector<std::string> &keys,
const std::vector<std::vector<void *>> &all_buffers,
const std::vector<std::vector<size_t>> &all_sizes,
bool prefer_same_node);
const std::vector<std::string>& keys,
const std::vector<std::vector<void*>>& all_buffers,
const std::vector<std::vector<size_t>>& all_sizes,
bool aggregate_same_segment_task, const ReadRouteConfig& config = {});
tl::expected<void, ErrorCode> put_from_internal(
const std::string &key, void *buffer, size_t size,
const ReplicateConfig &config = ReplicateConfig{});
tl::expected<void, ErrorCode> put_from_internal(const std::string& key,
void* buffer, size_t size,
const WriteConfig& config);
std::vector<tl::expected<void, ErrorCode>> batch_put_from_internal(
const std::vector<std::string> &keys,
const std::vector<void *> &buffers, const std::vector<size_t> &sizes,
const ReplicateConfig &config = ReplicateConfig{});
const std::vector<std::string>& keys, const std::vector<void*>& buffers,
const std::vector<size_t>& sizes, const WriteConfig& config);
std::vector<tl::expected<void, ErrorCode>>
batch_put_from_multi_buffers_internal(
const std::vector<std::string> &keys,
const std::vector<std::vector<void *>> &all_buffers,
const std::vector<std::vector<size_t>> &all_sizes,
const ReplicateConfig &config = ReplicateConfig{});
const std::vector<std::string>& keys,
const std::vector<std::vector<void*>>& all_buffers,
const std::vector<std::vector<size_t>>& all_sizes,
const WriteConfig& config);
tl::expected<void, ErrorCode> put_parts_internal(
const std::string &key, std::vector<std::span<const char>> values,
const ReplicateConfig &config = ReplicateConfig{},
const std::string& key, std::vector<std::span<const char>> values,
const WriteConfig& config,
std::shared_ptr<ClientBufferAllocator> client_buffer_allocator =
nullptr);
tl::expected<void, ErrorCode> put_batch_internal(
const std::vector<std::string> &keys,
const std::vector<std::span<const char>> &values,
const ReplicateConfig &config = ReplicateConfig{},
const std::vector<std::string>& keys,
const std::vector<std::span<const char>>& values,
const WriteConfig& config,
std::shared_ptr<ClientBufferAllocator> client_buffer_allocator =
nullptr);
tl::expected<void, ErrorCode> remove_internal(const std::string &key);
tl::expected<void, ErrorCode> remove_internal(const std::string& key);
tl::expected<long, ErrorCode> removeByRegex_internal(
const std::string &str);
const std::string& str);
tl::expected<int64_t, ErrorCode> removeAll_internal();
tl::expected<void, ErrorCode> tearDownAll_internal();
tl::expected<bool, ErrorCode> isExist_internal(const std::string &key);
tl::expected<bool, ErrorCode> isExist_internal(const std::string& key);
std::vector<tl::expected<bool, ErrorCode>> batchIsExist_internal(
const std::vector<std::string> &keys);
const std::vector<std::string>& keys);
tl::expected<int64_t, ErrorCode> getSize_internal(const std::string &key);
tl::expected<int64_t, ErrorCode> getSize_internal(const std::string& key);
std::shared_ptr<BufferHandle> get_buffer_internal(
const std::string &key,
const std::string& key,
std::shared_ptr<ClientBufferAllocator> client_buffer_allocator =
nullptr);
nullptr,
const ReadRouteConfig& config = {});
std::vector<std::shared_ptr<BufferHandle>> batch_get_buffer_internal(
const std::vector<std::string> &keys);
const std::vector<std::string>& keys,
const ReadRouteConfig& config = {});
std::map<std::string, std::vector<Replica::Descriptor>>
batch_get_replica_desc(const std::vector<std::string> &keys);
std::vector<Replica::Descriptor> get_replica_desc(const std::string &key);
batch_get_replica_desc(const std::vector<std::string>& keys);
std::vector<Replica::Descriptor> get_replica_desc(const std::string& key);
tl::expected<PingResponse, ErrorCode> ping(const UUID &client_id);
tl::expected<HeartbeatResponse, ErrorCode> ping(const UUID& client_id);
std::unique_ptr<AutoPortBinder> port_binder_ = nullptr;
struct SegmentDeleter {
void operator()(void *ptr) {
if (ptr) {
free(ptr);
}
}
};
struct AscendSegmentDeleter {
void operator()(void *ptr) {
if (ptr) {
free_memory("ascend", ptr);
}
}
};
std::vector<std::unique_ptr<void, SegmentDeleter>> segment_ptrs_;
std::vector<std::unique_ptr<void, AscendSegmentDeleter>>
ascend_segment_ptrs_;
std::string protocol;
std::string device_name;
std::string local_hostname;
std::string local_ip;
uint16_t te_port = 0;
struct MappedShm {
std::string shm_name;
// Offset = real_base - dummy_base
uintptr_t shm_addr_offset = 0;
void *shm_buffer = nullptr;
void* shm_buffer = nullptr;
size_t shm_size = 0;
uintptr_t dummy_base_addr = 0;
};
@ -467,8 +446,11 @@ class RealClient : public PyClient {
// Dummy Client manage related members
void dummy_client_monitor_func();
int start_dummy_client_monitor();
int stop_dummy_client_monitor();
std::thread dummy_client_monitor_thread_;
std::atomic<bool> dummy_client_monitor_running_{false};
std::condition_variable dummy_client_monitor_cv_;
std::mutex dummy_client_monitor_cv_mutex_;
static constexpr uint64_t kDummyClientMonitorSleepMs =
1000; // 1000 ms sleep between client monitor checks
// boost lockfree queue requires trivial assignment operator

View File

@ -18,13 +18,16 @@
namespace mooncake {
class P2PClientMeta;
/**
* @brief Type of buffer allocator used in the system
*/
enum class ReplicaType {
MEMORY, // Memory replica
DISK, // Disk replica
LOCAL_DISK // Local disk replica
MEMORY, // Memory replica
DISK, // Disk replica
LOCAL_DISK, // Local disk replica
P2P_PROXY, // routing replica (only for P2P structure)
};
/**
@ -34,7 +37,9 @@ inline std::ostream& operator<<(std::ostream& os,
const ReplicaType& replicaType) noexcept {
static const std::unordered_map<ReplicaType, std::string_view>
replica_type_strings{{ReplicaType::MEMORY, "MEMORY"},
{ReplicaType::DISK, "DISK"}};
{ReplicaType::DISK, "DISK"},
{ReplicaType::LOCAL_DISK, "LOCAL_DISK"},
{ReplicaType::P2P_PROXY, "P2P_PROXY"}};
os << (replica_type_strings.count(replicaType)
? replica_type_strings.at(replicaType)
@ -119,6 +124,19 @@ struct LocalDiskReplicaData {
std::string transport_endpoint;
};
struct P2PProxyReplicaData {
P2PProxyReplicaData() = default;
P2PProxyReplicaData(std::shared_ptr<P2PClientMeta> client,
std::shared_ptr<Segment> segment, uint64_t object_size)
: client(std::move(client)),
segment(std::move(segment)),
object_size(object_size) {}
std::shared_ptr<const P2PClientMeta> client;
std::shared_ptr<const Segment> segment;
uint64_t object_size = 0;
};
struct MemoryDescriptor {
AllocatedBuffer::Descriptor buffer_descriptor;
YLT_REFL(MemoryDescriptor, buffer_descriptor);
@ -137,6 +155,16 @@ struct LocalDiskDescriptor {
YLT_REFL(LocalDiskDescriptor, client_id, object_size, transport_endpoint);
};
struct P2PProxyDescriptor {
UUID client_id;
UUID segment_id;
std::string ip_address;
uint16_t rpc_port = 0;
uint64_t object_size = 0;
YLT_REFL(P2PProxyDescriptor, client_id, segment_id, ip_address, rpc_port,
object_size);
};
class Replica {
public:
struct Descriptor;
@ -159,6 +187,9 @@ class Replica {
std::move(transport_endpoint)}),
status_(status) {}
Replica(P2PProxyReplicaData proxy_data, ReplicaStatus status)
: data_(std::move(proxy_data)), status_(status) {}
~Replica() {
if (status_ != ReplicaStatus::UNDEFINED && is_disk_replica()) {
const auto& disk_data = std::get<DiskReplicaData>(data_);
@ -220,6 +251,10 @@ class Replica {
return std::holds_alternative<LocalDiskReplicaData>(data_);
}
[[nodiscard]] bool is_p2p_proxy_replica() const {
return std::holds_alternative<P2PProxyReplicaData>(data_);
}
[[nodiscard]] bool has_invalid_mem_handle() const {
if (is_memory_replica()) {
const auto& mem_data = std::get<MemoryReplicaData>(data_);
@ -241,6 +276,9 @@ class Replica {
[[nodiscard]] std::vector<std::optional<std::string>> get_segment_names()
const;
// only memory replica and p2p proxy replica have segment id
[[nodiscard]] std::optional<UUID> get_segment_id() const;
void mark_complete() {
if (status_ == ReplicaStatus::PROCESSING) {
status_ = ReplicaStatus::COMPLETE;
@ -251,6 +289,44 @@ class Replica {
}
}
const std::vector<std::string>& get_p2p_tags() const {
static const std::vector<std::string> empty_tags;
auto segment = get_p2p_segment();
if (segment && segment->IsP2PSegment()) {
return segment->GetP2PExtra().tags;
}
return empty_tags;
}
std::optional<int> get_p2p_priority() const {
auto segment = get_p2p_segment();
if (segment && segment->IsP2PSegment()) {
return segment->GetP2PExtra().priority;
}
return std::nullopt;
}
std::optional<MemoryType> get_p2p_memory_type() const {
auto segment = get_p2p_segment();
if (segment && segment->IsP2PSegment()) {
return segment->GetP2PExtra().memory_type;
}
return std::nullopt;
}
std::optional<UUID> get_p2p_client_id() const;
std::shared_ptr<const Segment> get_p2p_segment() const {
if (!is_p2p_proxy_replica()) return nullptr;
return std::get<P2PProxyReplicaData>(data_).segment;
}
std::shared_ptr<const P2PClientMeta> get_p2p_client() const {
if (!is_p2p_proxy_replica()) return nullptr;
return std::get<P2PProxyReplicaData>(data_).client;
}
public:
friend std::ostream& operator<<(std::ostream& os, const Replica& replica);
struct ReplicaTypeVisitor {
@ -263,15 +339,34 @@ class Replica {
ReplicaType operator()(const LocalDiskReplicaData&) const {
return ReplicaType::LOCAL_DISK;
}
ReplicaType operator()(const P2PProxyReplicaData&) const {
return ReplicaType::P2P_PROXY;
}
};
struct Descriptor {
std::variant<MemoryDescriptor, DiskDescriptor, LocalDiskDescriptor>
std::variant<MemoryDescriptor, DiskDescriptor, LocalDiskDescriptor,
P2PProxyDescriptor>
descriptor_variant;
ReplicaStatus status;
YLT_REFL(Descriptor, descriptor_variant, status);
// Helper functions
ReplicaType type() const {
return std::visit(
[](const auto& desc) -> ReplicaType {
using T = std::decay_t<decltype(desc)>;
if constexpr (std::is_same_v<T, MemoryDescriptor>)
return ReplicaType::MEMORY;
else if constexpr (std::is_same_v<T, DiskDescriptor>)
return ReplicaType::DISK;
else if constexpr (std::is_same_v<T, LocalDiskDescriptor>)
return ReplicaType::LOCAL_DISK;
else
return ReplicaType::P2P_PROXY;
},
descriptor_variant);
}
bool is_memory_replica() noexcept {
return std::holds_alternative<MemoryDescriptor>(descriptor_variant);
}
@ -298,6 +393,16 @@ class Replica {
descriptor_variant);
}
bool is_p2p_proxy_replica() noexcept {
return std::holds_alternative<P2PProxyDescriptor>(
descriptor_variant);
}
bool is_p2p_proxy_replica() const noexcept {
return std::holds_alternative<P2PProxyDescriptor>(
descriptor_variant);
}
MemoryDescriptor& get_memory_descriptor() {
if (auto* desc =
std::get_if<MemoryDescriptor>(&descriptor_variant)) {
@ -343,48 +448,34 @@ class Replica {
}
throw std::runtime_error("Expected LocalDiskDescriptor");
}
P2PProxyDescriptor& get_p2p_proxy_descriptor() {
if (auto* desc =
std::get_if<P2PProxyDescriptor>(&descriptor_variant)) {
return *desc;
}
throw std::runtime_error("Expected P2PProxyDescriptor");
}
const P2PProxyDescriptor& get_p2p_proxy_descriptor() const {
if (auto* desc =
std::get_if<P2PProxyDescriptor>(&descriptor_variant)) {
return *desc;
}
throw std::runtime_error("Expected P2PProxyDescriptor");
}
friend std::ostream& operator<<(std::ostream& os,
const Descriptor& desc);
};
private:
std::variant<MemoryReplicaData, DiskReplicaData, LocalDiskReplicaData>
std::variant<MemoryReplicaData, DiskReplicaData, LocalDiskReplicaData,
P2PProxyReplicaData>
data_;
ReplicaStatus status_{ReplicaStatus::UNDEFINED};
};
inline Replica::Descriptor Replica::get_descriptor() const {
Replica::Descriptor desc;
desc.status = status_;
if (is_memory_replica()) {
const auto& mem_data = std::get<MemoryReplicaData>(data_);
MemoryDescriptor mem_desc;
if (mem_data.buffer) {
mem_desc.buffer_descriptor = mem_data.buffer->get_descriptor();
} else {
mem_desc.buffer_descriptor.size_ = 0;
mem_desc.buffer_descriptor.buffer_address_ = 0;
mem_desc.buffer_descriptor.transport_endpoint_ = "";
LOG(ERROR) << "Trying to get invalid memory replica descriptor";
}
desc.descriptor_variant = std::move(mem_desc);
} else if (is_disk_replica()) {
const auto& disk_data = std::get<DiskReplicaData>(data_);
DiskDescriptor disk_desc;
disk_desc.file_path = disk_data.file_path;
disk_desc.object_size = disk_data.object_size;
desc.descriptor_variant = std::move(disk_desc);
} else if (is_local_disk_replica()) {
const auto& disk_data = std::get<LocalDiskReplicaData>(data_);
LocalDiskDescriptor local_disk_desc;
local_disk_desc.client_id = disk_data.client_id;
local_disk_desc.object_size = disk_data.object_size;
local_disk_desc.transport_endpoint = disk_data.transport_endpoint;
desc.descriptor_variant = std::move(local_disk_desc);
}
return desc;
}
inline std::vector<std::optional<std::string>> Replica::get_segment_names()
const {
if (is_memory_replica()) {
@ -400,24 +491,21 @@ inline std::vector<std::optional<std::string>> Replica::get_segment_names()
return std::vector<std::optional<std::string>>();
}
inline std::ostream& operator<<(std::ostream& os, const Replica& replica) {
os << "Replica: { status: " << replica.status_ << ", ";
if (replica.is_memory_replica()) {
const auto& mem_data = std::get<MemoryReplicaData>(replica.data_);
os << "type: MEMORY, buffers: [";
inline std::optional<UUID> Replica::get_segment_id() const {
if (is_memory_replica()) {
const auto& mem_data = std::get<MemoryReplicaData>(data_);
if (mem_data.buffer) {
os << *mem_data.buffer;
return mem_data.buffer->getSegmentId();
}
} else if (is_p2p_proxy_replica()) {
const auto& proxy_data = std::get<P2PProxyReplicaData>(data_);
if (proxy_data.segment) {
return proxy_data.segment->id;
}
os << "]";
} else if (replica.is_disk_replica()) {
const auto& disk_data = std::get<DiskReplicaData>(replica.data_);
os << "type: DISK, file_path: " << disk_data.file_path
<< ", object_size: " << disk_data.object_size;
}
os << " }";
return os;
return std::nullopt;
}
std::ostream& operator<<(std::ostream& os, const Replica& replica);
} // namespace mooncake

View File

@ -0,0 +1,379 @@
#pragma once
#include <atomic>
#include <chrono>
#include <cstddef>
#include <deque>
#include <mutex>
#include <string>
#include <thread>
#include <span>
#include <vector>
#include <memory>
#include <optional>
#include <utility>
#include "replica.h"
#include "offset_allocator/offset_allocator.hpp"
#include "mutex.h"
namespace mooncake {
/**
* @brief Memory-efficient, flat storage for P2P replica routes.
*/
struct P2PRouteData {
struct Item {
UUID client_id;
UUID segment_id;
char ip_address[48];
uint16_t rpc_port;
uint64_t object_size;
};
size_t count;
// items follow count in memory
[[nodiscard]] const Item* data() const {
return reinterpret_cast<const Item*>(
reinterpret_cast<const char*>(this) + sizeof(size_t));
}
[[nodiscard]] Item* data() {
return reinterpret_cast<Item*>(reinterpret_cast<char*>(this) +
sizeof(size_t));
}
static size_t CalculateSize(size_t replica_count) {
// Ensure 8-byte alignment for the entire record
return (sizeof(size_t) + replica_count * sizeof(Item) + 7) & ~7;
}
/**
* @brief Serialize replica data and key into a continuous memory block.
* @return Total bytes written.
*/
static size_t Serialize(void* dest, const std::string& key,
const std::vector<P2PProxyDescriptor>& replicas);
};
/**
* @brief High-performance read-only handle for RouteCache entries.
*
* Holds a shared_ptr to the underlying OffsetAllocationHandle, ensuring the
* data memory remains valid as long as this handle exists even after the
* Node is recycled by the epoch-based reclamation system.
*/
class P2PRouteHandle {
public:
P2PRouteHandle() : record_(nullptr) {}
P2PRouteHandle(
const P2PRouteData* record,
std::shared_ptr<offset_allocator::OffsetAllocationHandle> handle)
: record_(record), handle_(std::move(handle)) {}
[[nodiscard]] std::span<const P2PRouteData::Item> items() const {
if (!record_ || record_->count == 0) return {};
return {record_->data(), record_->count};
}
private:
const P2PRouteData* record_;
std::shared_ptr<offset_allocator::OffsetAllocationHandle> handle_;
};
/**
* @brief Client-side route cache for P2P read operations.
*
* This implementation uses an Atomic Bucket Array with Epoch-Based Reclamation
* (EBR) to achieve lock-free reads while maintaining consistency under
* concurrent writes. It uses a Clock algorithm (Second Chance) for O(1)
* eviction.
*
* PROTECTION MODEL:
* 1. [Node Metadata]: Protected by EBR. Readers enter an epoch guard before
* traversing bucket chains. Nodes are only recycled after all readers that
* observed the node have left their epoch providing a deterministic
* safety guarantee
* 2. [Data Memory]: Protected by shared_ptr within P2PRouteHandle. Even after
* the Node is recycled, the underlying P2PRouteData remains valid as long
* as the caller holds the handle.
*/
class RouteCache {
public:
RouteCache(size_t max_memory_bytes, uint64_t ttl_ms);
~RouteCache();
/**
* @brief Lock-free lookup protected by EBR epoch guard.
*/
P2PRouteHandle Get(const std::string& key);
/**
* @brief overwrite
*/
void Replace(const std::string& key,
const std::vector<P2PProxyDescriptor>& replicas);
/**
* @brief update if key exists, otherwise insert
*/
void Upsert(const std::string& key,
const std::vector<P2PProxyDescriptor>& replicas);
void RemoveReplica(const std::string& key,
const std::vector<P2PProxyDescriptor>& remove_replicas);
struct Metrics {
size_t free_node_count;
size_t total_node_count;
size_t free_memory_bytes;
size_t total_memory_bytes;
};
Metrics GetMetrics() const;
private:
// ========================================================================
// Epoch-Based Reclamation (EBR) Infrastructure
// ========================================================================
static constexpr uint64_t EPOCH_INACTIVE = UINT64_MAX;
static constexpr size_t MAX_READER_SLOTS = 256;
struct alignas(64) ReaderSlot {
std::atomic<uint64_t> epoch{EPOCH_INACTIVE};
};
/**
* @brief RAII guard that pins the current epoch for the calling thread.
*
* While the guard is alive, SyncGC will not recycle any Node that was
* retired in or after the pinned epoch. This ensures safe lock-free
* traversal of bucket chains in Get() and RemoveReplica().
*/
class EpochGuard {
public:
explicit EpochGuard(RouteCache* cache) : cache_(cache) {
slot_ = GetReaderSlot();
cache_->reader_slots_[slot_].epoch.store(
cache_->global_epoch_.load(std::memory_order_acquire),
std::memory_order_release);
}
~EpochGuard() {
cache_->reader_slots_[slot_].epoch.store(EPOCH_INACTIVE,
std::memory_order_release);
}
EpochGuard(const EpochGuard&) = delete;
EpochGuard& operator=(const EpochGuard&) = delete;
private:
static size_t GetReaderSlot() {
static thread_local size_t slot =
std::hash<std::thread::id>{}(std::this_thread::get_id()) %
MAX_READER_SLOTS;
return slot;
}
RouteCache* cache_;
size_t slot_;
};
/**
* @brief Compute the safe epoch for reclamation.
*
* Returns the maximum epoch E such that no active reader is in epoch <= E.
* Nodes retired at epoch <= E can be safely recycled.
*/
uint64_t ComputeSafeEpoch() const;
std::atomic<uint64_t> global_epoch_{0};
std::unique_ptr<ReaderSlot[]> reader_slots_;
// ========================================================================
// Node & Shard Structures
// ========================================================================
struct alignas(64) Node {
const char* key_;
uint32_t key_len_;
std::atomic<bool> accessed_{false};
std::atomic<bool> is_deleted_{false};
std::shared_ptr<offset_allocator::OffsetAllocationHandle> handle_;
std::atomic<int64_t> deadline_;
std::atomic<Node*> next_;
// Hash of the full key for fast bucket location during eviction
uint32_t key_hash_{0};
Node() : key_(nullptr), key_len_(0), next_(nullptr) {}
void Init(
const char* key, uint32_t key_len, size_t key_hash,
std::shared_ptr<offset_allocator::OffsetAllocationHandle> handle,
int64_t deadline_count) {
key_ = key;
key_len_ = key_len;
key_hash_ = (uint32_t)key_hash;
handle_ = std::move(handle);
deadline_.store(deadline_count, std::memory_order_relaxed);
next_.store(nullptr, std::memory_order_relaxed);
accessed_.store(false, std::memory_order_relaxed);
is_deleted_.store(false, std::memory_order_release);
}
/**
* @brief Read handle_.
* Caller MUST be within an EpochGuard or holding the shard mutex.
*/
std::shared_ptr<offset_allocator::OffsetAllocationHandle> GetHandle()
const {
return handle_;
}
void MarkDeleted() {
is_deleted_.store(true, std::memory_order_release);
}
bool IsDeleted() const {
return is_deleted_.load(std::memory_order_acquire);
}
bool IsExpired(int64_t now_count) const {
return now_count >= deadline_.load(std::memory_order_relaxed);
}
bool IsActive(int64_t now) { return !IsDeleted() && !IsExpired(now); }
};
struct alignas(64) Shard {
mutable Mutex mtx_;
std::unique_ptr<std::atomic<Node*>[]> buckets_ GUARDED_BY(mtx_);
// Clock Eviction scanning cursor
std::atomic<size_t> evict_cursor_{0};
// Shard-local Node pool manager
struct NodeResourcePool {
std::unique_ptr<Node[]> storage_;
Node* free_head_{nullptr};
size_t free_count_{0};
void Init(size_t count) {
storage_ = std::make_unique<Node[]>(count);
Node* prev = nullptr;
for (size_t i = 0; i < count; ++i) {
storage_[i].next_.store(prev, std::memory_order_relaxed);
prev = &storage_[i];
}
free_head_ = prev;
free_count_ = count;
}
Node* Pop() {
if (!free_head_) return nullptr;
Node* node = free_head_;
free_head_ = node->next_.load(std::memory_order_relaxed);
free_count_--;
return node;
}
void Push(Node* node) {
if (!node) return;
node->next_.store(free_head_, std::memory_order_relaxed);
free_head_ = node;
free_count_++;
}
} nodes_ GUARDED_BY(mtx_);
// EBR Garbage Collection Queue
struct PendingDelete {
Node* node_;
uint64_t retire_epoch_;
};
std::vector<PendingDelete> pending_deletes_ GUARDED_BY(mtx_);
std::shared_ptr<offset_allocator::OffsetAllocator> allocator_;
void* base_addr_ = nullptr;
size_t gc_skip_count_ = 0;
Shard() = default;
// Non-copyable & Non-movable (due to mutex and arrays)
Shard(const Shard&) = delete;
Shard& operator=(const Shard&) = delete;
Shard(Shard&&) = delete;
Shard& operator=(Shard&&) = delete;
};
private:
void InnerPut(Shard& shard, size_t bucket_idx, size_t hash_val,
const std::string& key,
const std::vector<P2PProxyDescriptor>& replicas, bool merge);
void BuildReplicaList(
Node* old_node,
const std::vector<P2PProxyDescriptor>& increment_replicas,
const std::vector<P2PProxyDescriptor>& remove_replicas,
std::vector<P2PProxyDescriptor>& out);
size_t Evict(Shard& shard, size_t goal_free_count) REQUIRES(shard.mtx_);
void GCLoop();
void SyncGC(Shard& shard) REQUIRES(shard.mtx_);
// Internal Helpers
// Returns {prev, node} for a given key in the bucket.
std::pair<Node*, Node*> findNodeInBucket(Shard& shard, size_t bucket_idx,
const char* key_ptr,
uint32_t key_len);
void retireNode(Shard& shard, Node* prev, Node* node, size_t bucket_idx)
REQUIRES(shard.mtx_);
bool acquireResource(
Shard& shard, size_t total_size, Node** out_node,
std::optional<offset_allocator::OffsetAllocationHandle>& out_handle)
REQUIRES(shard.mtx_);
private:
// Estimation of memory cost for dynamic resource allocation
static constexpr size_t AVG_REPLICA_COUNT = 1;
static constexpr size_t AVG_KEY_LEN = 64;
static constexpr size_t ENTRY_METADATA_COST =
sizeof(Node) + sizeof(std::atomic<Node*>);
static constexpr size_t ENTRY_DATA_COST =
sizeof(P2PRouteData) +
(AVG_REPLICA_COUNT * sizeof(P2PRouteData::Item)) + AVG_KEY_LEN;
static constexpr size_t TOTAL_AVG_COST_PER_ENTRY =
ENTRY_METADATA_COST + ENTRY_DATA_COST;
const size_t max_memory_bytes_;
const uint64_t ttl_ms_;
size_t shard_count_;
size_t nodes_per_shard_;
size_t buckets_per_shard_;
static constexpr int MAX_TRY_LOCK_RETRIES = 5;
// GC Control
static constexpr size_t MAX_GC_SKIP_COUNT = 5;
static constexpr auto GC_IDLE_SLEEP = std::chrono::milliseconds(2000);
static constexpr auto GC_LOW_PRESSURE_SLEEP =
std::chrono::milliseconds(300);
static constexpr auto GC_HIGH_PRESSURE_SLEEP =
std::chrono::milliseconds(50);
static constexpr double LOW_WATERMARK = 0.7;
static constexpr double HIGH_WATERMARK = 0.9;
static constexpr double ASYNC_EVICT_PROPORTION = 0.05;
static constexpr size_t SYNC_EVICT_BATCH_SIZE = 5;
std::atomic<bool> stop_gc_{false};
std::thread gc_thread_;
void* base_all_ = nullptr;
std::vector<std::unique_ptr<Shard>> shards_;
};
} // namespace mooncake

View File

@ -15,13 +15,13 @@
namespace mooncake {
extern const uint64_t kMetricReportIntervalSeconds;
static const uint64_t kMetricReportIntervalSeconds = 10;
class WrappedMasterService {
public:
WrappedMasterService(const WrappedMasterServiceConfig& config);
~WrappedMasterService();
virtual ~WrappedMasterService();
void init_http_server();
@ -38,43 +38,19 @@ class WrappedMasterService {
ErrorCode>
BatchQueryIp(const std::vector<UUID>& client_ids);
tl::expected<std::vector<std::string>, ErrorCode> BatchReplicaClear(
const std::vector<std::string>& object_keys, const UUID& client_id,
const std::string& segment_name);
tl::expected<
std::unordered_map<std::string, std::vector<Replica::Descriptor>>,
ErrorCode>
GetReplicaListByRegex(const std::string& str);
tl::expected<GetReplicaListResponse, ErrorCode> GetReplicaList(
const std::string& key);
const std::string& key, const GetReplicaListRequestConfig& config =
GetReplicaListRequestConfig());
std::vector<tl::expected<GetReplicaListResponse, ErrorCode>>
BatchGetReplicaList(const std::vector<std::string>& keys);
tl::expected<std::vector<Replica::Descriptor>, ErrorCode> PutStart(
const UUID& client_id, const std::string& key,
const uint64_t slice_length, const ReplicateConfig& config);
tl::expected<void, ErrorCode> PutEnd(const UUID& client_id,
const std::string& key,
ReplicaType replica_type);
tl::expected<void, ErrorCode> PutRevoke(const UUID& client_id,
const std::string& key,
ReplicaType replica_type);
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
BatchPutStart(const UUID& client_id, const std::vector<std::string>& keys,
const std::vector<uint64_t>& slice_lengths,
const ReplicateConfig& config);
std::vector<tl::expected<void, ErrorCode>> BatchPutEnd(
const UUID& client_id, const std::vector<std::string>& keys);
std::vector<tl::expected<void, ErrorCode>> BatchPutRevoke(
const UUID& client_id, const std::vector<std::string>& keys);
BatchGetReplicaList(const std::vector<std::string>& keys,
const GetReplicaListRequestConfig& config =
GetReplicaListRequestConfig());
tl::expected<void, ErrorCode> Remove(const std::string& key);
@ -82,35 +58,27 @@ class WrappedMasterService {
long RemoveAll();
tl::expected<void, ErrorCode> MountSegment(const Segment& segment,
const UUID& client_id);
tl::expected<void, ErrorCode> ReMountSegment(
const std::vector<Segment>& segments, const UUID& client_id);
tl::expected<void, ErrorCode> UnmountSegment(const UUID& segment_id,
const UUID& client_id);
tl::expected<std::string, ErrorCode> GetFsdir();
tl::expected<void, ErrorCode> MountSegment(const Segment& segment,
const UUID& client_id);
tl::expected<GetStorageConfigResponse, ErrorCode> GetStorageConfig();
tl::expected<HeartbeatResponse, ErrorCode> Heartbeat(
const HeartbeatRequest& req);
tl::expected<PingResponse, ErrorCode> Ping(const UUID& client_id);
tl::expected<QueryClientStatusResponse, ErrorCode> QueryClientStatus(
const QueryClientStatusRequest& req);
tl::expected<RegisterClientResponse, ErrorCode> RegisterClient(
const RegisterClientRequest& req);
tl::expected<std::string, ErrorCode> ServiceReady();
tl::expected<void, ErrorCode> MountLocalDiskSegment(const UUID& client_id,
bool enable_offloading);
protected:
virtual MasterService& GetMasterService() = 0;
tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>
OffloadObjectHeartbeat(const UUID& client_id, bool enable_offloading);
tl::expected<void, ErrorCode> NotifyOffloadSuccess(
const UUID& client_id, const std::vector<std::string>& keys,
const std::vector<StorageObjectMetadata>& metadatas);
private:
MasterService master_service_;
protected:
std::thread metric_report_thread_;
coro_http::coro_http_server http_server_;
std::atomic<bool> metric_report_running_;

View File

@ -2,43 +2,65 @@
#include "types.h"
#include "replica.h"
#include "heartbeat_type.h"
namespace mooncake {
/**
* @brief Response structure for Ping operation
* @brief P2P specific configuration for read route
*/
struct PingResponse {
ViewVersionId view_version_id;
ClientStatus client_status;
PingResponse() = default;
PingResponse(ViewVersionId view_version, ClientStatus status)
: view_version_id(view_version), client_status(status) {}
friend std::ostream& operator<<(std::ostream& os,
const PingResponse& response) noexcept {
return os << "PingResponse: { view_version_id: "
<< response.view_version_id
<< ", client_status: " << response.client_status << " }";
}
struct P2PGetReplicaListConfigExtra {
// exclude replicas whose segment contains any tag in tag_filters
std::vector<std::string> tag_filters;
// filter replicas whose segment priority is lower than priority_limit
int priority_limit = 0;
};
YLT_REFL(PingResponse, view_version_id, client_status);
YLT_REFL(P2PGetReplicaListConfigExtra, tag_filters, priority_limit);
/**
* @brief Request config for getting replica list
*/
struct GetReplicaListRequestConfig {
GetReplicaListRequestConfig() = default;
GetReplicaListRequestConfig(size_t max_c) : max_candidates(max_c) {}
// 0 means return all viable replica candidates;
// otherwise, return at most max_candidates candidates
static const size_t RETURN_ALL_CANDIDATES = 0;
size_t max_candidates = RETURN_ALL_CANDIDATES;
std::optional<P2PGetReplicaListConfigExtra> p2p_config;
};
YLT_REFL(GetReplicaListRequestConfig, max_candidates, p2p_config);
// config for filter replicas in read route
typedef GetReplicaListRequestConfig ReadRouteConfig;
typedef P2PGetReplicaListConfigExtra P2PReadRouteConfigExtra;
/**
* @brief Extra info for centralized read route response (Internal use)
*/
struct CentralizedGetReplicaListResponseExtra {
CentralizedGetReplicaListResponseExtra() = default;
CentralizedGetReplicaListResponseExtra(uint64_t lease_ttl_ms_param)
: lease_ttl_ms(lease_ttl_ms_param) {}
uint64_t lease_ttl_ms = 0;
};
YLT_REFL(CentralizedGetReplicaListResponseExtra, lease_ttl_ms);
/**
* @brief Response structure for GetReplicaList operation
*/
struct GetReplicaListResponse {
std::vector<Replica::Descriptor> replicas;
uint64_t lease_ttl_ms;
GetReplicaListResponse() : lease_ttl_ms(0) {}
GetReplicaListResponse() = default;
GetReplicaListResponse(std::vector<Replica::Descriptor>&& replicas_param,
uint64_t lease_ttl_ms_param)
: replicas(std::move(replicas_param)),
lease_ttl_ms(lease_ttl_ms_param) {}
centralized_extra(lease_ttl_ms_param) {}
std::vector<Replica::Descriptor> replicas;
std::optional<CentralizedGetReplicaListResponseExtra> centralized_extra;
};
YLT_REFL(GetReplicaListResponse, replicas, lease_ttl_ms);
YLT_REFL(GetReplicaListResponse, replicas, centralized_extra);
/**
* @brief Response structure for GetStorageConfig operation
@ -56,4 +78,69 @@ struct GetStorageConfigResponse {
quota_bytes(quota) {}
};
YLT_REFL(GetStorageConfigResponse, fsdir, enable_disk_eviction, quota_bytes);
/**
* @brief Request structure for Heartbeat operation.
* Client could set HeartbeatTasks for Master to run
*/
struct HeartbeatRequest {
UUID client_id;
std::vector<HeartbeatTask> tasks;
};
YLT_REFL(HeartbeatRequest, client_id, tasks);
/**
* @brief Response structure for Heartbeat operation.
* Always returns view_version; client uses it under UNDEFINED status
* for crash-recovery decisions, other statuses for defensive checks.
*/
struct HeartbeatResponse {
ClientStatus status;
ViewVersionId view_version = 0;
std::vector<HeartbeatTaskResult> task_results;
};
YLT_REFL(HeartbeatResponse, status, view_version, task_results);
/**
* @brief Request structure for RegisterClient operation.
* Client calls this on startup to register its UUID and local segments.
* P2P clients additionally provide ip_address and rpc_port.
*/
struct RegisterClientRequest {
UUID client_id;
std::vector<Segment> segments;
DeploymentMode deployment_mode = DeploymentMode::CENTRALIZATION;
// P2P only: network endpoint info
std::optional<std::string> ip_address;
std::optional<uint16_t> rpc_port;
};
YLT_REFL(RegisterClientRequest, client_id, segments, deployment_mode,
ip_address, rpc_port);
/**
* @brief Response structure for RegisterClient operation.
* Returns the master's view_version to client for crash checking.
*/
struct RegisterClientResponse {
ViewVersionId view_version = 0;
};
YLT_REFL(RegisterClientResponse, view_version);
/**
* @brief Request structure for QueryClientStatus operation.
*/
struct QueryClientStatusRequest {
UUID client_id;
};
YLT_REFL(QueryClientStatusRequest, client_id);
/**
* @brief Response structure for QueryClientStatus operation.
*/
struct QueryClientStatusResponse {
ClientStatus status = ClientStatus::UNDEFINED;
};
YLT_REFL(QueryClientStatusResponse, status);
} // namespace mooncake

View File

@ -1,232 +0,0 @@
#pragma once
#include <boost/functional/hash.hpp>
#include <ostream>
#include <shared_mutex>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include "allocation_strategy.h"
#include "allocator.h"
#include "types.h"
namespace mooncake {
/**
* @brief Status of a mounted segment in master
*/
enum class SegmentStatus {
UNDEFINED = 0, // Uninitialized
OK, // Segment is mounted and available for allocation
UNMOUNTING, // Segment is under unmounting
};
/**
* @brief Stream operator for SegmentStatus
*/
inline std::ostream& operator<<(std::ostream& os,
const SegmentStatus& status) noexcept {
static const std::unordered_map<SegmentStatus, std::string_view>
status_strings{{SegmentStatus::UNDEFINED, "UNDEFINED"},
{SegmentStatus::OK, "OK"},
{SegmentStatus::UNMOUNTING, "UNMOUNTING"}};
os << (status_strings.count(status) ? status_strings.at(status)
: "UNKNOWN");
return os;
}
struct MountedSegment {
Segment segment;
SegmentStatus status;
std::shared_ptr<BufferAllocatorBase> buf_allocator;
};
struct LocalDiskSegment {
mutable Mutex offloading_mutex_;
bool enable_offloading;
std::unordered_map<std::string, int64_t> GUARDED_BY(offloading_mutex_)
offloading_objects;
explicit LocalDiskSegment(bool enable_offloading)
: enable_offloading(enable_offloading) {}
LocalDiskSegment(const LocalDiskSegment&) = delete;
LocalDiskSegment& operator=(const LocalDiskSegment&) = delete;
LocalDiskSegment(LocalDiskSegment&&) = delete;
LocalDiskSegment& operator=(LocalDiskSegment&&) = delete;
};
// Forward declarations
class SegmentManager;
/**
* @brief RAII-style access to segment mutex for thread-safe segment operations
*/
class ScopedSegmentAccess {
public:
/**
* @brief Acquires a lock on the segment mutex
* @param mutex Reference to the segment mutex
*/
explicit ScopedSegmentAccess(SegmentManager* segment_manager,
std::shared_mutex& mutex)
: segment_manager_(segment_manager), lock_(mutex) {}
/**
* @brief Mount a segment
*/
ErrorCode MountSegment(const Segment& segment, const UUID& client_id);
ErrorCode MountLocalDiskSegment(const UUID& client_id,
bool enable_offloading);
/**
* @brief Re-mount a segment. To avoid infinite remount trying, only the
* errors that may be solved by subsequent remount tryings are considered as
* errors. When encounters unsolvable errors, the segment will not be
* mounted while the return value will be OK.
*/
ErrorCode ReMountSegment(const std::vector<Segment>& segments,
const UUID& client_id);
/**
* @brief Prepare to unmount a segment by deleting its allocator
*/
ErrorCode PrepareUnmountSegment(const UUID& segment_id,
size_t& metrics_dec_capacity);
/**
* @brief Deleting the segment to complete the unmounting operation
*/
ErrorCode CommitUnmountSegment(const UUID& segment_id,
const UUID& client_id,
const size_t& metrics_dec_capacity);
/**
* @brief Get all the segments of a client
*/
ErrorCode GetClientSegments(const UUID& client_id,
std::vector<Segment>& segments) const;
/**
* @brief Get the names of all the segments
*/
ErrorCode GetAllSegments(std::vector<std::string>& all_segments);
/**
* @brief Get the segment by name. If there are multiple segments with the
* same name, return the first one.
*/
ErrorCode QuerySegments(const std::string& segment, size_t& used,
size_t& capacity);
private:
SegmentManager* segment_manager_;
std::unique_lock<std::shared_mutex> lock_;
};
/**
* @brief RAII-style access to allocators for thread-safe allocator usage
*/
class ScopedAllocatorAccess {
public:
explicit ScopedAllocatorAccess(const AllocatorManager& allocator_manager,
std::shared_mutex& mutex)
: allocator_manager_(allocator_manager), lock_(mutex) {}
const AllocatorManager& getAllocatorManager() { return allocator_manager_; }
private:
const AllocatorManager& allocator_manager_;
std::shared_lock<std::shared_mutex> lock_;
};
/**
* @brief RAII-style access to LocalDiskOffloadingQueues for thread-safe
* LocalDiskOffloadingQueue usage
*/
class ScopedLocalDiskSegmentAccess {
public:
explicit ScopedLocalDiskSegmentAccess(
std::unordered_map<std::string, UUID>& client_by_name,
std::unordered_map<UUID, std::shared_ptr<LocalDiskSegment>,
boost::hash<UUID>>& client_local_disk_segment,
std::shared_mutex& mutex)
: client_by_name_(client_by_name),
client_local_disk_segment_(client_local_disk_segment),
lock_(mutex) {}
const std::unordered_map<std::string, UUID>& getClientByName() {
return client_by_name_;
}
std::unordered_map<UUID, std::shared_ptr<LocalDiskSegment>,
boost::hash<UUID>>&
getClientLocalDiskSegment() {
return client_local_disk_segment_;
}
private:
const std::unordered_map<std::string, UUID>&
client_by_name_; // segment name -> client_id
std::unordered_map<UUID, std::shared_ptr<LocalDiskSegment>,
boost::hash<UUID>>& client_local_disk_segment_;
std::shared_lock<std::shared_mutex> lock_;
};
class SegmentManager {
public:
/**
* @brief Constructor for SegmentManager
* @param memory_allocator Type of buffer allocator to use for new segments
*/
explicit SegmentManager(
BufferAllocatorType memory_allocator = BufferAllocatorType::CACHELIB)
: memory_allocator_(memory_allocator) {}
/**
* @brief Get RAII-style access to segment management operations
* @return ScopedSegmentAccess object that holds the lock
*/
ScopedSegmentAccess getSegmentAccess() {
return ScopedSegmentAccess(this, segment_mutex_);
}
/**
* @brief Get RAII-style access to use allocators
* @return ScopedAllocatorAccess object that holds the lock
*/
ScopedAllocatorAccess getAllocatorAccess() {
return ScopedAllocatorAccess(allocator_manager_, segment_mutex_);
}
ScopedLocalDiskSegmentAccess getLocalDiskSegmentAccess() {
return ScopedLocalDiskSegmentAccess(
client_by_name_, client_local_disk_segment_, segment_mutex_);
}
private:
mutable std::shared_mutex segment_mutex_;
std::shared_ptr<AllocationStrategy> allocation_strategy_;
const BufferAllocatorType
memory_allocator_; // Type of buffer allocator to use
// allocator_manager_ only contains allocators whose segment status is OK.
AllocatorManager allocator_manager_;
std::unordered_map<UUID, MountedSegment, boost::hash<UUID>>
mounted_segments_; // segment_id -> mounted segment
std::unordered_map<UUID, std::vector<UUID>, boost::hash<UUID>>
client_segments_; // client_id -> segment_ids
std::unordered_map<std::string, UUID>
client_by_name_; // segment name -> client_id
std::unordered_map<UUID, std::shared_ptr<LocalDiskSegment>,
boost::hash<UUID>>
client_local_disk_segment_; // client_id -> local_disk_segment
friend class ScopedSegmentAccess;
friend class SegmentTest; // for unit tests
};
} // namespace mooncake

View File

@ -0,0 +1,55 @@
#pragma once
#include <boost/functional/hash.hpp>
#include <memory>
#include <string>
#include <vector>
#include "mutex.h"
#include "types.h"
#include "ylt/util/tl/expected.hpp"
namespace mooncake {
class SegmentManager {
public:
virtual ~SegmentManager() = default;
auto MountSegment(const Segment& segment) -> tl::expected<void, ErrorCode>;
auto UnmountSegment(const UUID& segment_id)
-> tl::expected<void, ErrorCode>;
// TODO: wanyue-wy
// There is currently no mechanism to guarantee `segment_name`'s uniqueness
// within the cluster.
// For backward compatibility during refactoring, we temporarily maintain a
// weak assumption of `segment_name`'s uniqueness.
// However, before merging to main, we need to discuss whether name
// uniqueness is necessary and whether the query interface definition should
// be modified.
virtual auto QuerySegments(const std::string& segment)
-> tl::expected<std::pair<size_t, size_t>, ErrorCode> = 0;
auto QuerySegment(const UUID& segment_id)
-> tl::expected<std::shared_ptr<Segment>, ErrorCode>;
auto GetSegments() -> tl::expected<std::vector<Segment>, ErrorCode>;
using SegmentRemovalCallback = std::function<void(const UUID& segment_id)>;
void SetSegmentRemovalCallback(SegmentRemovalCallback cb);
protected:
// Pure virtual inner methods (Implementation specific, No locking required)
virtual auto InnerMountSegment(const Segment& segment)
-> tl::expected<void, ErrorCode> = 0;
virtual auto OnUnmountSegment(const std::shared_ptr<Segment>& segment)
-> tl::expected<void, ErrorCode> = 0;
protected:
mutable SharedMutex segment_mutex_;
SegmentRemovalCallback segment_removal_cb_;
std::unordered_map<UUID, std::shared_ptr<Segment>, boost::hash<UUID>>
mounted_segments_
GUARDED_BY(segment_mutex_); // segment_id -> mounted segment
};
} // namespace mooncake

View File

@ -2,9 +2,14 @@
#include <glog/logging.h>
#include <atomic>
#include <condition_variable>
#include <deque>
#include <filesystem>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <vector>
#include "file_interface.h"
@ -33,6 +38,8 @@ struct BucketMetadata {
YLT_REFL(BucketMetadata, data_size, keys, metadatas);
struct OffloadMetadata {
// `total_keys` tracks live keys reachable through the backend.
// `total_size` tracks backend-accounted used bytes.
int64_t total_keys;
int64_t total_size;
OffloadMetadata(std::size_t keys, int64_t size)
@ -131,9 +138,62 @@ class StorageBackendInterface {
const std::vector<std::string>& keys,
std::vector<StorageObjectMetadata>& metadatas)>& handler) = 0;
/**
* @brief Mark a key as deleted.
* @param key The object key that was deleted
* @return tl::expected<void, ErrorCode> indicating operation status
* @note FilePerKey physically removes the file and updates physical
* accounting. Bucket backend removes the live mapping but defers physical
* reclaim to bucket eviction.
*/
virtual tl::expected<void, ErrorCode> MarkKeyDeleted(
const std::string& key) {
// Default implementation: no-op
return {};
}
FileStorageConfig file_storage_config_;
};
class LocalStorageSpaceManager {
public:
explicit LocalStorageSpaceManager(
std::filesystem::path storage_root = std::filesystem::path());
void SetStorageRoot(std::filesystem::path storage_root);
tl::expected<void, ErrorCode> Init(uint64_t used_space_bytes,
uint64_t quota_bytes = 0);
tl::expected<bool, ErrorCode> HasPhysicalSpace(
uint64_t required_size) const;
bool IsInitialized() const;
bool TryReserve(uint64_t required_size);
void Release(uint64_t size_to_release);
uint64_t TotalSpace() const;
uint64_t UsedSpace() const;
uint64_t AvailableSpace() const;
bool IsOverQuota() const;
private:
void RecalculateAvailableSpaceLocked();
private:
std::filesystem::path storage_root_;
mutable std::shared_mutex mutex_;
uint64_t total_space_ = 0;
uint64_t used_space_ = 0;
uint64_t available_space_ = 0;
std::atomic<bool> initialized_{false};
};
/**
* @class StorageBackend
* @brief Implementation of StorageBackend interface using local filesystem
@ -295,7 +355,7 @@ class StorageBackend {
* @brief Deletes the physical file associated with the given object key
* @param path Path to the file to remove
*/
void RemoveFile(const std::string& path);
tl::expected<void, ErrorCode> RemoveFile(const std::string& path);
/**
* @brief Removes objects from the storage backend whose keys match a regex
@ -334,14 +394,8 @@ class StorageBackend {
mutable std::shared_mutex
file_queue_mutex_; // Mutex to protect file queue operations
// Storage space tracking variables
mutable std::shared_mutex
space_mutex_; // Mutex to protect space tracking variables
uint64_t total_space_ = 0; // Total storage space in bytes
uint64_t used_space_ = 0; // Used storage space in bytes
uint64_t available_space_ = 0; // Available storage space in bytes
std::atomic<bool> initialized_{false};
LocalStorageSpaceManager space_manager_;
/**
* @brief Make sure the path is valid and create necessary directories
@ -409,12 +463,6 @@ class StorageBackend {
*/
void ReleaseSpace(uint64_t size_to_release);
/**
* @brief Recalculates available_space_ based on total_space_ and
* used_space_. Must be called with space_mutex_ locked.
*/
void RecalculateAvailableSpace();
/**
* @brief Gets the actual filesystem directory name by removing "moon_"
* prefix if present.
@ -506,6 +554,9 @@ class StorageBackendAdaptor : public StorageBackendInterface {
const std::vector<std::string>& keys,
std::vector<StorageObjectMetadata>& metadatas)>& handler) override;
tl::expected<void, ErrorCode> MarkKeyDeleted(
const std::string& key) override;
private:
const FilePerKeyConfig file_per_key_config_;
@ -519,6 +570,7 @@ class StorageBackendAdaptor : public StorageBackendInterface {
static std::string ConcatSlicesToString(const std::vector<Slice>& slices);
mutable Mutex scan_mutex_;
mutable Mutex mutex_;
int64_t total_keys GUARDED_BY(mutex_);
@ -542,6 +594,7 @@ class BucketStorageBackend : public StorageBackendInterface {
public:
BucketStorageBackend(const FileStorageConfig& file_storage_config_,
const BucketBackendConfig& bucket_backend_config_);
~BucketStorageBackend();
/**
* @brief Offload objects in batches
@ -658,11 +711,36 @@ class BucketStorageBackend : public StorageBackendInterface {
/**
* @brief Retrieves the global metadata of the store.
* @return On success: `tl::expected` containing a `StoreMetadata`
* object. On failure: an error code.
* @return On success: `tl::expected` containing live key count and
* backend-accounted used bytes. On failure: an error code.
*/
tl::expected<OffloadMetadata, ErrorCode> GetStoreMetadata();
/**
* @brief Select a bucket for eviction based on fragmentation and age.
* @return tl::expected<int64_t, ErrorCode>
* - On success: the bucket ID to evict
* - On failure: error code (e.g., OBJECT_NOT_FOUND if no buckets exist)
*/
tl::expected<int64_t, ErrorCode> SelectBucketForEviction() const;
/**
* @brief Evict an entire bucket by removing its data and metadata files.
* @param bucket_id The ID of the bucket to evict
* @return tl::expected<size_t, ErrorCode>
* - On success: the amount of space freed (in bytes)
* - On failure: error code
*/
tl::expected<size_t, ErrorCode> EvictBucket(int64_t bucket_id);
/**
* @brief Mark a key as deleted (for fragmentation tracking).
* @param key The object key that was deleted
* @return tl::expected<void, ErrorCode> indicating operation status
*/
tl::expected<void, ErrorCode> MarkKeyDeleted(
const std::string& key) override;
private:
tl::expected<std::shared_ptr<BucketMetadata>, ErrorCode> BuildBucket(
int64_t bucket_id,
@ -675,7 +753,11 @@ class BucketStorageBackend : public StorageBackendInterface {
std::vector<iovec>& iovs);
tl::expected<void, ErrorCode> StoreBucketMetadata(
int64_t bucket_id, std::shared_ptr<BucketMetadata> bucket_metadata);
const std::string& metadata_path,
const std::string& serialized_metadata);
tl::expected<std::string, ErrorCode> SerializeBucketMetadata(
const std::shared_ptr<BucketMetadata>& bucket_metadata);
tl::expected<void, ErrorCode> LoadBucketMetadata(
int64_t bucket_id, std::shared_ptr<BucketMetadata> bucket_metadata);
@ -706,6 +788,27 @@ class BucketStorageBackend : public StorageBackendInterface {
tl::expected<bool, ErrorCode> HasNext();
struct PendingBucketDeletion {
int64_t bucket_id;
uint64_t data_bytes;
uint64_t meta_bytes;
uint64_t queued_bytes;
std::string data_path;
std::string meta_path;
};
tl::expected<bool, ErrorCode> CanAcceptAnotherBucket() const;
uint64_t MaxPendingDeletionBytes() const;
void StartDeletionWorker();
void StopDeletionWorker();
void EnqueueBucketDeletion(PendingBucketDeletion task);
void BucketDeletionWorker();
private:
std::atomic<bool> initialized_{false};
std::optional<BucketIdGenerator> bucket_id_generator_;
@ -718,25 +821,37 @@ class BucketStorageBackend : public StorageBackendInterface {
* metadata members:
* - object_bucket_map_: maps object keys to bucket IDs
* - buckets_: ordered map of bucket ID to bucket metadata
* - total_size_: cumulative data size of all stored objects
* - physical_used_bytes_: physical bytes still present on local storage
*/
mutable SharedMutex mutex_;
mutable Mutex iterator_mutex_;
std::string storage_path_;
int64_t total_size_ GUARDED_BY(mutex_) = 0;
int64_t physical_used_bytes_ GUARDED_BY(mutex_) = 0;
std::unordered_map<std::string, StorageObjectMetadata> GUARDED_BY(mutex_)
object_bucket_map_;
std::map<int64_t, std::shared_ptr<BucketMetadata>> GUARDED_BY(
mutex_) buckets_;
int64_t GUARDED_BY(mutex_) next_bucket_ = -1;
BucketBackendConfig bucket_backend_config_;
LocalStorageSpaceManager space_manager_;
mutable Mutex offloading_mutex_;
std::unordered_map<std::string, int64_t> GUARDED_BY(offloading_mutex_)
ungrouped_offloading_objects_;
// Track valid key count per bucket for fragmentation calculation
std::unordered_map<int64_t, int> GUARDED_BY(mutex_) bucket_valid_keys_;
std::mutex deletion_mutex_;
std::condition_variable deletion_cv_;
std::deque<PendingBucketDeletion> pending_bucket_deletions_;
std::thread deletion_thread_;
bool stop_deletion_worker_ = false;
std::atomic<uint64_t> pending_deletion_bytes_{0};
std::atomic<uint64_t> pending_deletion_count_{0};
};
tl::expected<std::shared_ptr<StorageBackendInterface>, ErrorCode>
CreateStorageBackend(const FileStorageConfig& config);
} // namespace mooncake
} // namespace mooncake

View File

@ -0,0 +1,91 @@
#pragma once
#include <future>
#include <memory>
#include <ylt/util/tl/expected.hpp>
#include "types.h"
namespace mooncake {
// ============================================================================
// TaskHandle<V> — abstract base for pending operations.
// Wait() returns tl::expected<V, ErrorCode>, where ErrorCode is the error type
// and V is the value type on success.
// ============================================================================
template <typename V>
class TaskHandle {
public:
virtual ~TaskHandle() = default;
virtual tl::expected<V, ErrorCode> Wait() = 0;
};
template <typename V>
class ImmediateHandle : public TaskHandle<V> {
public:
tl::expected<V, ErrorCode> Wait() override { return {}; }
static std::unique_ptr<ImmediateHandle<V>> Create() {
return std::make_unique<ImmediateHandle<V>>();
}
};
template <typename V>
class CallableTaskHandle : public TaskHandle<V> {
public:
template <typename F>
explicit CallableTaskHandle(F&& fn)
: impl_(
std::make_unique<Wrapper<std::decay_t<F>>>(std::forward<F>(fn))) {
}
tl::expected<V, ErrorCode> Wait() override { return impl_->invoke(); }
template <typename F>
static std::unique_ptr<CallableTaskHandle<V>> Create(F&& fn) {
return std::make_unique<CallableTaskHandle<V>>(std::forward<F>(fn));
}
private:
struct Impl {
virtual ~Impl() = default;
virtual tl::expected<V, ErrorCode> invoke() = 0;
};
template <typename F>
struct Wrapper final : Impl {
explicit Wrapper(F&& f) : fn(std::move(f)) {}
tl::expected<V, ErrorCode> invoke() override { return fn(); }
F fn;
};
std::unique_ptr<Impl> impl_;
};
template <typename V>
class RemoteRpcHandle : public TaskHandle<V> {
public:
RemoteRpcHandle(std::shared_ptr<void> request_storage,
std::future<tl::expected<V, ErrorCode>> future)
: request_storage_(std::move(request_storage)),
future_(std::move(future)) {}
tl::expected<V, ErrorCode> Wait() override { return future_.get(); }
template <typename T>
static std::unique_ptr<RemoteRpcHandle<V>> Create(
std::shared_ptr<T> request_storage,
std::future<tl::expected<V, ErrorCode>> future) {
return std::make_unique<RemoteRpcHandle<V>>(std::move(request_storage),
std::move(future));
}
private:
std::shared_ptr<void> request_storage_;
std::future<tl::expected<V, ErrorCode>> future_;
};
} // namespace mooncake

View File

@ -1,4 +1,5 @@
// ThreadPool.h
#pragma once
#include <vector>
#include <queue>

View File

@ -0,0 +1,86 @@
#pragma once
#include "tiered_cache/tiers/cache_tier.h"
#include "tiered_cache/data_copier.h"
#include <functional>
#include <map>
#include <string>
#include <vector>
namespace mooncake {
// Forward declaration from data_copier.h to avoid circular dependency
class DataCopierBuilder;
// Holds the registration information for a memory type.
struct MemoryTypeRegistration {
MemoryType type;
CopyFunction to_dram_func;
CopyFunction from_dram_func;
};
// Holds the registration for an optimized direct path.
struct DirectPathRegistration {
MemoryType src_type;
MemoryType dest_type;
CopyFunction func;
};
/**
* @brief A singleton registry for data copier functions.
*
* Modules can register their copy functions here during static initialization.
* The DataCopierBuilder will then use this registry to construct a DataCopier.
*/
class CopierRegistry {
public:
/**
* @brief Get the singleton instance of the registry.
*/
static CopierRegistry& GetInstance();
/**
* @brief Registers the to/from DRAM copy functions for a memory type.
*/
void RegisterMemoryType(MemoryType type, CopyFunction to_dram,
CopyFunction from_dram);
/**
* @brief Registers an optional, optimized direct copy path.
*/
void RegisterDirectPath(MemoryType src, MemoryType dest, CopyFunction func);
// These methods are used by the DataCopierBuilder to collect all
// registrations.
const std::vector<MemoryTypeRegistration>& GetMemoryTypeRegistrations()
const;
const std::vector<DirectPathRegistration>& GetDirectPathRegistrations()
const;
private:
friend class DataCopierBuilder;
CopierRegistry() = default;
~CopierRegistry() = default;
CopierRegistry(const CopierRegistry&) = delete;
CopierRegistry& operator=(const CopierRegistry&) = delete;
std::vector<MemoryTypeRegistration> memory_type_regs_;
std::vector<DirectPathRegistration> direct_path_regs_;
};
/**
* @brief A helper class to automatically register copiers at static
* initialization time.
*
* To register a new memory type, simply declare a static instance of this class
* in the corresponding .cpp file, providing the type and its to/from DRAM
* copiers.
*/
class CopierRegistrar {
public:
CopierRegistrar(MemoryType type, CopyFunction to_dram,
CopyFunction from_dram);
};
} // namespace mooncake

View File

@ -0,0 +1,92 @@
#pragma once
#include <functional>
#include <map>
#include <memory>
#include <glog/logging.h>
#include <stdexcept>
#include <vector>
#include "rpc_types.h"
#include "tiered_cache/tiers/cache_tier.h"
namespace mooncake {
using CopyFunction = std::function<tl::expected<void, ErrorCode>(
const DataSource& src, const DataSource& dst)>;
class DataCopier;
/**
* @brief A helper class to build a valid DataCopier.
*
* This builder enforces the rule that for any new memory type added,
* its copy functions to and from DRAM *must* be provided via the
* CopierRegistry.
*/
class DataCopierBuilder {
public:
/**
* @brief Constructs a builder. It automatically pulls all existing
* registrations from the global CopierRegistry.
*/
DataCopierBuilder();
/**
* @brief (Optional) Registers a highly optimized direct copy path.
* This will be used instead of the DRAM fallback. Can be used for testing
* or for paths that are not self-registered.
* @return A reference to the builder for chaining.
*/
DataCopierBuilder& AddDirectPath(MemoryType src_type, MemoryType dest_type,
CopyFunction func);
/**
* @brief Builds the final, immutable DataCopier object.
* It verifies that all memory types defined in the MemoryType enum
* have been registered via the registry before creating the object.
* @return A unique_ptr to the new DataCopier.
* @throws std::logic_error if a required to/from DRAM copier is missing.
*/
std::unique_ptr<DataCopier> Build() const;
private:
std::map<std::pair<MemoryType, MemoryType>, CopyFunction> copy_matrix_;
};
/**
* @brief A central utility for copying data between different memory types.
* It supports a fallback mechanism via DRAM for any copy paths that are not
* explicitly registered as a direct path.
*/
class DataCopier {
public:
// The constructor is private. Use DataCopierBuilder to create an instance.
~DataCopier() = default;
DataCopier(const DataCopier&) = delete;
DataCopier& operator=(const DataCopier&) = delete;
/**
* @brief Executes a copy from a source to a destination.
* It first attempts to find a direct copy function (e.g., VRAM -> VRAM).
* If not found, it automatically falls back to a two-step copy via a
* temporary DRAM buffer (e.g., VRAM -> DRAM -> SSD).
* @param src The data source descriptor.
* @param dest_type The memory type of the destination.
* @param dest_ptr A pointer to the destination (memory address, handle,
* etc.).
*/
tl::expected<void, ErrorCode> Copy(const DataSource& src,
const DataSource& dst) const;
private:
friend class DataCopierBuilder; // Allow builder to access the constructor.
DataCopier(
std::map<std::pair<MemoryType, MemoryType>, CopyFunction> copy_matrix);
CopyFunction FindCopier(MemoryType src_type, MemoryType dest_type) const;
const std::map<std::pair<MemoryType, MemoryType>, CopyFunction>
copy_matrix_;
};
} // namespace mooncake

View File

@ -0,0 +1,168 @@
#pragma once
#include <array>
#include <memory>
#include <thread>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <optional>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <string_view>
#include "mutex.h"
#include "tiered_cache/scheduler/scheduler_policy.h"
#include "tiered_cache/scheduler/stats_collector.h"
#include "types.h"
#include <json/value.h>
namespace mooncake {
class TieredBackend; // Forward declaration
class CacheTier;
/**
* @class ClientScheduler
* @brief Coordinates statistics collection, policy execution, and action
* application.
*/
class ClientScheduler {
public:
ClientScheduler(TieredBackend* backend, const Json::Value& config);
~ClientScheduler();
// Lifecycle management
void Start();
void Stop();
// Register a managed tier
void RegisterTier(CacheTier* tier);
// Incoming event hook (thread-safe)
void OnAccess(const std::string& key);
/**
* @brief Get current hot key statistics for HA recovery prioritization.
*/
AccessStats GetHotKeyStats() const;
// Called when a replica is committed or updated
void OnCommit(const std::string& key, UUID tier_id, size_t size_bytes);
// Called when a key or a replica is deleted
void OnDelete(const std::string& key,
std::optional<UUID> tier_id = std::nullopt);
// Called when allocation fails due to insufficient space
// Returns true if reclaim freed enough space for an immediate retry
bool OnAllocationFailure(UUID tier_id, size_t required_bytes);
private:
struct PlannedReclaim {
struct Step {
SchedAction action;
size_t size_bytes = 0;
};
std::vector<Step> steps;
size_t target_reclaim_bytes = 0;
};
// Background worker loop
void WorkerLoop();
// Execute generated actions
void ExecuteActions(const std::vector<SchedAction>& actions);
// Trigger immediate eviction for a tier (sync mode)
bool TriggerSyncEviction(UUID tier_id, size_t required_bytes);
// Reclaim pre-replicated cold replicas without copying data on the
// allocation failure path.
bool TryFastReclaim(UUID tier_id, size_t required_bytes);
PlannedReclaim BuildReclaimPlan(
UUID tier_id, const std::unordered_map<UUID, TierStats>& tier_stats,
const std::vector<KeyContext>& active_keys,
bool require_existing_replica, size_t required_bytes) const;
size_t ExecuteReclaimPlan(const PlannedReclaim& plan);
bool HasAvailableBytes(UUID tier_id, size_t required_bytes) const;
std::optional<UUID> SelectDemotionTier(UUID source_tier_id) const;
// Build policy input from the latest stats snapshot and scheduler cache
std::vector<KeyContext> BuildActiveKeys(
const AccessStats& access_stats,
std::optional<UUID> pinned_tier_id = std::nullopt);
// Build a fresh tier stats map for policy execution
std::unordered_map<UUID, TierStats> CollectTierStats() const;
struct CachedKeyState {
size_t size_bytes = 0;
std::vector<UUID> current_locations;
};
struct KeyCacheShard {
mutable Mutex mutex;
std::unordered_map<std::string, CachedKeyState> key_cache
GUARDED_BY(mutex);
std::unordered_map<UUID, std::unordered_set<std::string>>
tier_resident_keys GUARDED_BY(mutex);
};
static constexpr size_t kKeyCacheShardCount = 16;
static size_t KeyCacheShardIndex(std::string_view key);
KeyCacheShard& GetKeyCacheShard(std::string_view key);
const KeyCacheShard& GetKeyCacheShard(std::string_view key) const;
size_t EstimateActiveKeyReserve(const AccessStats& access_stats,
std::optional<UUID> pinned_tier_id) const;
void AppendHotKeys(const AccessStats& access_stats,
std::vector<KeyContext>& active_keys,
std::unordered_set<std::string>& seen_keys) const;
void AppendPinnedTierKeys(UUID pinned_tier_id,
std::vector<KeyContext>& active_keys,
std::unordered_set<std::string>& seen_keys) const;
std::optional<KeyContext> BuildKeyContextLocked(
const std::string& key, const CachedKeyState& state,
const AccessStats& access_stats,
const AccessStatEntry* stat_entry = nullptr) const;
size_t GetCachedKeySize(const std::string& key) const;
void TrackReplicaLocked(KeyCacheShard& shard, const std::string& key,
UUID tier_id, size_t size_bytes)
REQUIRES(shard.mutex);
bool RemoveReplicaLocked(KeyCacheShard& shard, const std::string& key,
std::optional<UUID> tier_id) REQUIRES(shard.mutex);
private:
TieredBackend* backend_;
std::unique_ptr<SchedulerPolicy> policy_;
std::unique_ptr<StatsCollector> stats_collector_;
std::atomic<bool> running_{false};
std::thread worker_thread_;
// Local view of tiers for policy input
std::unordered_map<UUID, CacheTier*> tiers_;
// Scheduler-side metadata cache to avoid full backend scans each cycle
std::array<KeyCacheShard, kKeyCacheShardCount> key_cache_shards_;
std::optional<UUID> fast_tier_id_;
// Configuration
int loop_interval_ms_ = 1000;
size_t stats_snapshot_limit_ = detail::DefaultSnapshotLimit();
enum class EvictionMode { SYNC, ASYNC };
EvictionMode eviction_mode_ = EvictionMode::ASYNC;
// Used to wake the worker thread immediately on Stop().
std::mutex cv_mutex_;
std::condition_variable cv_;
};
} // namespace mooncake

View File

@ -0,0 +1,36 @@
#pragma once
#include "tiered_cache/scheduler/scheduler_policy.h"
namespace mooncake {
/**
* @class LRUPolicy
* @brief Recency-based promotion/eviction policy with watermark control.
*
* All keys are sorted by recency. The most recently used keys (up to target
* capacity) should be in fast tier, the rest in slow tier.
*/
class LRUPolicy : public SchedulerPolicy {
public:
struct Config {
double high_watermark = 0.90; // Trigger scheduling when above this
double low_watermark = 0.70; // Target usage after scheduling
};
explicit LRUPolicy(Config config);
void SetFastTier(UUID id) override;
bool IsFastTier(UUID id) const;
tl::expected<std::vector<SchedAction>, ErrorCode> Decide(
const std::unordered_map<UUID, TierStats>& tier_stats,
const std::vector<KeyContext>& active_keys) override;
private:
Config config_;
std::optional<UUID> fast_tier_id_;
};
} // namespace mooncake

View File

@ -0,0 +1,143 @@
#pragma once
#include <atomic>
#include <cstdint>
#include <mutex>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
#include "tiered_cache/scheduler/stats_collector.h"
namespace mooncake {
/**
* @class LRUStatsCollector
* @brief Maintains an LRU list of accessed keys.
* Used for LRU-based promotion and eviction policies.
*/
class LRUStatsCollector : public StatsCollector {
public:
explicit LRUStatsCollector(
size_t shard_count = detail::DefaultStatsShardCount(),
size_t max_snapshot_keys = detail::DefaultSnapshotLimit())
: shards_(detail::NormalizeShardCount(shard_count)),
shard_mask_(shards_.size() - 1),
max_snapshot_keys_(
detail::NormalizeSnapshotLimit(max_snapshot_keys)) {}
void RecordAccess(const std::string& key) override {
const uint64_t sequence =
next_sequence_.fetch_add(1, std::memory_order_relaxed);
auto& shard = GetShard(key);
std::lock_guard<std::mutex> lock(shard.mutex);
shard.pending_updates[key] = sequence;
}
AccessStats GetSnapshot() override {
std::lock_guard<std::mutex> snapshot_lock(snapshot_mutex_);
ApplyPendingChanges();
return BuildSnapshot();
}
void RemoveKey(const std::string& key) override {
auto& shard = GetShard(key);
std::lock_guard<std::mutex> lock(shard.mutex);
shard.pending_updates.erase(key);
shard.pending_deletes.push_back(key);
}
private:
struct OrderedKey {
uint64_t sequence;
std::string key;
};
struct OrderedKeyCompare {
bool operator()(const OrderedKey& lhs, const OrderedKey& rhs) const {
if (lhs.sequence != rhs.sequence) {
return lhs.sequence > rhs.sequence;
}
return lhs.key < rhs.key;
}
};
struct alignas(64) Shard {
std::mutex mutex;
std::unordered_map<std::string, uint64_t> pending_updates;
std::vector<std::string> pending_deletes;
};
Shard& GetShard(const std::string& key) {
const auto shard_index = std::hash<std::string>{}(key)&shard_mask_;
return shards_[shard_index];
}
void ApplyPendingChanges() {
for (auto& shard : shards_) {
std::unordered_map<std::string, uint64_t> pending_updates;
std::vector<std::string> pending_deletes;
{
std::lock_guard<std::mutex> lock(shard.mutex);
pending_updates.swap(shard.pending_updates);
pending_deletes.swap(shard.pending_deletes);
}
for (const auto& key : pending_deletes) {
RemoveTrackedKey(key);
}
for (const auto& [key, sequence] : pending_updates) {
auto it = latest_access_.find(key);
if (it != latest_access_.end()) {
ordered_keys_.erase(OrderedKey{it->second, key});
it->second = sequence;
} else {
latest_access_[key] = sequence;
}
ordered_keys_.insert(OrderedKey{sequence, key});
}
}
}
AccessStats BuildSnapshot() const {
AccessStats stats;
stats.metric = AccessStatMetric::kRecencyRank;
stats.hot_keys.reserve(
std::min(max_snapshot_keys_, ordered_keys_.size()));
size_t recency_rank = 1;
size_t emitted = 0;
for (const auto& ordered_key : ordered_keys_) {
stats.hot_keys.push_back(
AccessStatEntry{ordered_key.key, 0.0, recency_rank++});
emitted++;
if (emitted >= max_snapshot_keys_) {
break;
}
}
return stats;
}
void RemoveTrackedKey(const std::string& key) {
auto it = latest_access_.find(key);
if (it == latest_access_.end()) {
return;
}
ordered_keys_.erase(OrderedKey{it->second, key});
latest_access_.erase(it);
}
std::atomic<uint64_t> next_sequence_{1};
std::mutex snapshot_mutex_;
std::vector<Shard> shards_;
size_t shard_mask_;
size_t max_snapshot_keys_;
std::unordered_map<std::string, uint64_t> latest_access_;
std::set<OrderedKey, OrderedKeyCompare> ordered_keys_;
};
} // namespace mooncake

View File

@ -0,0 +1,81 @@
#pragma once
#include <string>
#include <vector>
#include <unordered_map>
#include <optional>
#include <ylt/util/tl/expected.hpp>
#include "types.h"
namespace mooncake {
/**
* @struct TierStats
* @brief Runtime statistics for a specific storage tier
*/
struct TierStats {
size_t total_capacity_bytes = 0;
size_t used_capacity_bytes = 0;
};
/**
* @struct KeyContext
* @brief Context for a key, including access metadata and current location.
*/
struct KeyContext {
std::string key;
double recent_heat_score = 0.0;
size_t recency_rank = 0;
std::vector<UUID> current_locations; // Which tiers currently hold this key
size_t size_bytes = 0; // Size of the key's data in bytes
};
/**
* @struct SchedAction
* @brief A single scheduling action decision
*/
struct SchedAction {
enum class Type {
REPLICATE, // Copy data from Source to Target and keep the source copy
MIGRATE, // Move data from Source to Target (Promote/Offload)
EVICT, // Delete data from Source
};
Type type;
std::string key;
// For MIGRATE: Source and Target must be specified
// For EVICT: Only Source is required
std::optional<UUID> source_tier_id;
std::optional<UUID> target_tier_id;
};
/**
* @class SchedulerPolicy
* @brief Abstract interface for scheduling algorithms
*/
class SchedulerPolicy {
public:
virtual ~SchedulerPolicy() = default;
/**
* @brief Core decision function
* @param tier_stats Current status of all managed tiers
* @param active_keys List of active/hot keys with their context
* @return List of recommended actions or an error when the policy cannot
* produce a valid plan
*/
virtual tl::expected<std::vector<SchedAction>, ErrorCode> Decide(
const std::unordered_map<UUID, TierStats>& tier_stats,
const std::vector<KeyContext>& active_keys) = 0;
/**
* @brief Set the fast tier ID for the policy.
* @param id The UUID of the fast tier (e.g., DRAM).
*/
virtual void SetFastTier(UUID id) {}
};
} // namespace mooncake

View File

@ -0,0 +1,32 @@
#pragma once
#include "tiered_cache/scheduler/scheduler_policy.h"
namespace mooncake {
/**
* @class SimplePolicy
* @brief MVP Policy: Promote hot data to a designated "Fast Tier"
*/
class SimplePolicy : public SchedulerPolicy {
public:
struct Config {
double promotion_threshold =
10.0; // Min access count/score to trigger promotion
};
explicit SimplePolicy(Config config);
// Configure which tier is considered "Fast" (Target for promotion)
void SetFastTier(UUID id) override;
tl::expected<std::vector<SchedAction>, ErrorCode> Decide(
const std::unordered_map<UUID, TierStats>& tier_stats,
const std::vector<KeyContext>& active_keys) override;
private:
Config config_;
std::optional<UUID> fast_tier_id_;
};
} // namespace mooncake

View File

@ -0,0 +1,317 @@
#pragma once
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cmath>
#include <functional>
#include <limits>
#include <mutex>
#include <set>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
#include "tiered_cache/scheduler/scheduler_policy.h"
namespace mooncake {
namespace detail {
inline size_t NormalizeShardCount(size_t shard_count) {
size_t normalized = 1;
while (normalized < std::max<size_t>(1, shard_count)) {
normalized <<= 1;
}
return normalized;
}
inline size_t DefaultStatsShardCount() {
const auto hardware_threads =
std::max<unsigned int>(1, std::thread::hardware_concurrency());
return NormalizeShardCount(static_cast<size_t>(hardware_threads) * 4);
}
inline size_t NormalizeSnapshotLimit(size_t limit) {
return limit == 0 ? std::numeric_limits<size_t>::max() : limit;
}
inline size_t DefaultSnapshotLimit() { return 4096; }
} // namespace detail
/**
* @enum AccessStatMetric
* @brief Semantic meaning carried by a stats snapshot.
*/
enum class AccessStatMetric {
kRecentHeat,
kRecencyRank,
};
/**
* @struct AccessStatEntry
* @brief Per-key access metadata emitted by a stats collector.
*/
struct AccessStatEntry {
std::string key;
double recent_heat_score = 0.0;
size_t recency_rank = 0;
};
/**
* @struct AccessStats
* @brief Snapshot of access statistics
*/
struct AccessStats {
AccessStatMetric metric = AccessStatMetric::kRecentHeat;
std::vector<AccessStatEntry> hot_keys;
};
/**
* @class StatsCollector
* @brief Interface for collecting runtime statistics
*/
class StatsCollector {
public:
virtual ~StatsCollector() = default;
// Record an access event for a key
virtual void RecordAccess(const std::string& key) = 0;
// Get a snapshot of current stats for policy decision
virtual AccessStats GetSnapshot() = 0;
// Remove a key from tracking (called when key is deleted)
virtual void RemoveKey(const std::string& key) = 0;
};
/**
* @class SimpleStatsCollector
* @brief MVP implementation using thread-safe map counter with decay
*/
class SimpleStatsCollector : public StatsCollector {
public:
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
using NowFn = std::function<TimePoint()>;
// Decay factor per second: 0.5 means scores are halved every second.
// This preserves history while giving more weight to recent accesses.
explicit SimpleStatsCollector(
double decay_factor_per_second = 0.5,
size_t shard_count = detail::DefaultStatsShardCount(),
size_t max_snapshot_keys = detail::DefaultSnapshotLimit(),
NowFn now_fn = []() { return Clock::now(); })
: shards_(detail::NormalizeShardCount(shard_count)),
shard_mask_(shards_.size() - 1),
max_snapshot_keys_(detail::NormalizeSnapshotLimit(max_snapshot_keys)),
decay_factor_per_second_(
NormalizeDecayFactor(decay_factor_per_second)),
log_decay_factor_per_second_(std::log(decay_factor_per_second_)),
now_fn_(std::move(now_fn)) {
last_aggregate_update_time_ = now_fn_();
}
void RecordAccess(const std::string& key) override {
auto& shard = GetShard(key);
std::lock_guard<std::mutex> lock(shard.mutex);
shard.pending_counts[key]++;
}
AccessStats GetSnapshot() override {
const auto now = now_fn_();
std::lock_guard<std::mutex> snapshot_lock(snapshot_mutex_);
AdvanceAggregateTo(now);
RebaseScoresIfNeeded();
ApplyPendingChanges();
AccessStats stats = BuildSnapshot();
PruneColdKeys();
return stats;
}
void RemoveKey(const std::string& key) override {
auto& shard = GetShard(key);
std::lock_guard<std::mutex> lock(shard.mutex);
shard.pending_counts.erase(key);
shard.pending_deletes.push_back(key);
}
private:
struct OrderedKey {
double raw_score;
std::string key;
};
struct OrderedKeyCompare {
bool operator()(const OrderedKey& lhs, const OrderedKey& rhs) const {
if (lhs.raw_score != rhs.raw_score) {
return lhs.raw_score > rhs.raw_score;
}
return lhs.key < rhs.key;
}
};
struct alignas(64) Shard {
std::mutex mutex;
std::unordered_map<std::string, uint64_t> pending_counts;
std::vector<std::string> pending_deletes;
};
Shard& GetShard(const std::string& key) {
const auto shard_index = std::hash<std::string>{}(key)&shard_mask_;
return shards_[shard_index];
}
void ApplyPendingChanges() {
for (auto& shard : shards_) {
std::unordered_map<std::string, uint64_t> pending_counts;
std::vector<std::string> pending_deletes;
{
std::lock_guard<std::mutex> lock(shard.mutex);
pending_counts.swap(shard.pending_counts);
pending_deletes.swap(shard.pending_deletes);
}
for (const auto& key : pending_deletes) {
RemoveAggregateKey(key);
}
for (const auto& [key, count] : pending_counts) {
AddCount(key, static_cast<double>(count));
}
}
}
void AddCount(const std::string& key, double actual_score_delta) {
if (actual_score_delta <= 0.0) {
return;
}
const double delta = actual_score_delta / global_scale_;
auto it = raw_counts_.find(key);
if (it != raw_counts_.end()) {
ordered_keys_.erase(OrderedKey{it->second, key});
it->second += delta;
ordered_keys_.insert(OrderedKey{it->second, key});
return;
}
raw_counts_[key] = delta;
ordered_keys_.insert(OrderedKey{delta, key});
}
void RemoveAggregateKey(const std::string& key) {
auto it = raw_counts_.find(key);
if (it == raw_counts_.end()) {
return;
}
ordered_keys_.erase(OrderedKey{it->second, key});
raw_counts_.erase(it);
}
AccessStats BuildSnapshot() const {
AccessStats stats;
stats.metric = AccessStatMetric::kRecentHeat;
stats.hot_keys.reserve(
std::min(max_snapshot_keys_, ordered_keys_.size()));
size_t emitted = 0;
for (const auto& ordered_key : ordered_keys_) {
const double score = ordered_key.raw_score * global_scale_;
if (score < kMinTrackedScore) {
break;
}
stats.hot_keys.push_back(
AccessStatEntry{ordered_key.key, score, 0});
emitted++;
if (emitted >= max_snapshot_keys_) {
break;
}
}
return stats;
}
void AdvanceAggregateTo(TimePoint now) {
global_scale_ =
AdvanceScale(global_scale_, last_aggregate_update_time_, now);
}
double AdvanceScale(double scale, TimePoint& last_update_time,
TimePoint now) const {
if (now <= last_update_time) {
return scale;
}
const double elapsed_seconds =
std::chrono::duration<double>(now - last_update_time).count();
last_update_time = now;
if (log_decay_factor_per_second_ == 0.0) {
return scale;
}
return scale * std::exp(log_decay_factor_per_second_ * elapsed_seconds);
}
void RebaseScoresIfNeeded() {
if (global_scale_ >= kMinScaleBeforeRebase) {
return;
}
if (raw_counts_.empty()) {
global_scale_ = 1.0;
return;
}
std::set<OrderedKey, OrderedKeyCompare> rebased_order;
for (auto& [key, raw_score] : raw_counts_) {
raw_score *= global_scale_;
rebased_order.insert(OrderedKey{raw_score, key});
}
ordered_keys_.swap(rebased_order);
global_scale_ = 1.0;
}
void PruneColdKeys() {
while (!ordered_keys_.empty()) {
auto coldest_it = std::prev(ordered_keys_.end());
if (coldest_it->raw_score * global_scale_ >= kMinTrackedScore) {
break;
}
raw_counts_.erase(coldest_it->key);
ordered_keys_.erase(coldest_it);
}
}
static constexpr double kMinTrackedScore = 1.0;
static constexpr double kMinScaleBeforeRebase = 1e-100;
static double NormalizeDecayFactor(double decay_factor_per_second) {
if (!std::isfinite(decay_factor_per_second) ||
decay_factor_per_second <= 0.0) {
return std::numeric_limits<double>::min();
}
return std::min(decay_factor_per_second, 1.0);
}
std::mutex snapshot_mutex_;
std::vector<Shard> shards_;
size_t shard_mask_;
std::unordered_map<std::string, double> raw_counts_;
std::set<OrderedKey, OrderedKeyCompare> ordered_keys_;
size_t max_snapshot_keys_;
TimePoint last_aggregate_update_time_{};
double global_scale_ = 1.0;
double decay_factor_per_second_;
double log_decay_factor_per_second_;
NowFn now_fn_;
};
} // namespace mooncake

View File

@ -0,0 +1,305 @@
#pragma once
#include <array>
#include <atomic>
#include <string>
#include <vector>
#include <memory>
#include <unordered_map>
#include <shared_mutex>
#include <optional>
#include <functional>
#include <json/value.h>
#include "tiered_cache/tiers/cache_tier.h"
#include "tiered_cache/data_copier.h"
#include "tiered_cache/scheduler/stats_collector.h"
#include "rpc_types.h"
namespace mooncake {
class TieredBackend; // Forward declaration
class ClientScheduler; // Forward declaration
/**
* @struct TieredLocation
* @brief Describes the physical location of a segment within the tiered
* storage.
*/
struct TieredLocation {
std::shared_ptr<CacheTier> tier;
struct DataSource data;
};
/**
* @struct TierView
* @brief A snapshot of a tier's status, used for reporting topology to the
* Master.
*/
struct TierView {
UUID id;
MemoryType type;
size_t capacity;
size_t usage;
size_t free_space;
int priority;
std::vector<std::string> tags;
};
/**
* @enum REMOVE_CALLBACK_TYPE
* @brief The type of metadata synchronization callback.
*/
enum REMOVE_CALLBACK_TYPE { DELETE = 0, DELETE_ALL = 1 };
/**
* @struct AllocationEntry
* @brief The internal state of an allocation.
* acts as the "Control Block" for the resource.
* When the last shared_ptr pointing to this entry dies, the destructor
* releases the resource through the owning tier.
*/
struct AllocationEntry {
TieredBackend* backend;
TieredLocation loc;
AllocationEntry(TieredBackend* b, TieredLocation&& l)
: backend(b), loc(std::move(l)) {}
AllocationEntry(const AllocationEntry&) = delete;
AllocationEntry& operator=(const AllocationEntry&) = delete;
// Destructor: Automatically releases the resource if valid
~AllocationEntry();
};
/**
* @typedef AllocationHandle
* @brief A reference-counted handle to a storage resource.
*/
using AllocationHandle = std::shared_ptr<AllocationEntry>;
/**
* @brief Callback for metadata synchronization when a replica is added.
* Invoked after data copy is complete.
* Returns true if sync succeeds, false otherwise.
*/
using AddReplicaCallback = std::function<tl::expected<void, ErrorCode>(
const std::string& key, const UUID& tier_id, size_t size)>;
/**
* @brief Callback for metadata synchronization when a replica is removed.
* Returns true if sync succeeds, false otherwise.
*/
using RemoveReplicaCallback = std::function<tl::expected<void, ErrorCode>(
const std::string& key, const UUID& tier_id,
enum REMOVE_CALLBACK_TYPE type)>;
/**
* @brief Callback for segment lifecycle synchronization.
* Invoked when a tier is created (mount=true) or destroyed (mount=false).
* The callback should register/unregister the segment with Master.
*/
using SegmentSyncCallback = std::function<tl::expected<void, ErrorCode>(
const Segment& segment, bool mount)>;
/**
* @class TieredBackend
* @brief Data plane management class supporting tiered storage with RAII-based
* resource management.
*/
class TieredBackend {
public:
TieredBackend();
~TieredBackend();
/**
* @brief 1. stops any backend thread;
* 2. all public APIs will return SHUTTING_DOWN.
*/
void Stop();
/**
* @brief Unmounts segments from Master and cleans up resources.
*/
void Destroy();
tl::expected<void, ErrorCode> Init(
Json::Value root, TransferEngine* engine,
AddReplicaCallback add_replica_callback,
RemoveReplicaCallback remove_replica_callback,
SegmentSyncCallback segment_sync_callback);
// --- Client-Centric Operations ---
// All the following operations are designed for Client-Centric, Client
// should manage the resource by itself, and synchronize with Master when
// needed.
/**
* @brief Allocation
* reserves storage space. Returns a handle.
* If the handle goes out of scope without being committed, the space is
* auto-freed.
* @param size: Size in bytes to allocate
* @param preferred_tier: Preferred tier ID (optional)
* @param strict: If true, allocation MUST succeed on preferred_tier.
* Will trigger sync eviction if needed, no fallback.
* If false (default), will fallback to other tiers.
*/
tl::expected<AllocationHandle, ErrorCode> Allocate(
size_t size, std::optional<UUID> preferred_tier = std::nullopt,
bool strict = false);
/**
* @brief Execution (Write)
* Writes data to the location specified by the handle.
*/
tl::expected<void, ErrorCode> Write(const DataSource& source,
AllocationHandle handle);
/**
* @brief Commit (Register)
* Registers the handle in the local metadata index.
* @param expected_version: Optimistic Concurrency Control.
* If set, commit only if current version matches expected_version.
* Returns CAS_FAILED if mismatch.
*/
tl::expected<void, ErrorCode> Commit(
const std::string& key, AllocationHandle handle,
std::optional<uint64_t> expected_version = std::nullopt,
bool record_access = true);
/**
* @brief Checks if a key exists in the backend.
* @param key The key to check.
* @param tier_id Optional tier ID. If specified, checks only the given
* tier; if nullopt, checks any tier.
*/
bool Exist(const std::string& key,
std::optional<UUID> tier_id = std::nullopt) const;
/**
* @brief Get
* Returns a handle.
* @param out_version: If provided, returns the current version of the
* metadata entry.
*/
tl::expected<AllocationHandle, ErrorCode> Get(
const std::string& key, std::optional<UUID> tier_id = std::nullopt,
bool record_access = true, uint64_t* out_version = nullptr);
/**
* @brief Delete
* Removes the key from the metadata index.
* @param tier_id: If specified, removes only the replica on that tier.
* If nullopt, removes ALL replicas for this key (and the key entry itself).
*/
tl::expected<void, ErrorCode> Delete(
const std::string& key, std::optional<UUID> tier_id = std::nullopt);
// --- Composite Operations ---
tl::expected<void, ErrorCode> CopyData(
const std::string& key, const DataSource& source, UUID dest_tier_id,
std::optional<uint64_t> expected_version = std::nullopt,
bool record_access = true);
tl::expected<void, ErrorCode> Transfer(const std::string& key,
UUID source_tier_id,
UUID dest_tier_id,
bool record_access = true);
// --- Introspection & Internal ---
std::vector<TierView> GetTierViews() const;
std::vector<UUID> GetReplicaTierIds(const std::string& key) const;
const CacheTier* GetTier(UUID tier_id) const;
const DataCopier& GetDataCopier() const;
/**
* @brief Iterate all keys in batches.
* Iterates per-shard to minimize lock hold time.
* @param callback Receives each batch; return false to stop iteration.
*/
void ForEachKeyBatch(
const std::function<bool(std::vector<ReplicaLocation>&&)>& callback)
const;
/**
* @brief Get hot key statistics from the scheduler's StatsCollector.
*/
AccessStats GetHotKeyStats() const;
private:
tl::expected<void, ErrorCode> MountSegment(
UUID id, size_t capacity, int priority,
const std::vector<std::string>& tags, MemoryType memory_type);
struct TierInfo {
int priority;
std::vector<std::string> tags;
};
/**
* @struct MetadataEntry
* @brief Holds all replicas for a specific key.
* Uses a dedicated mutex to allow per-key concurrency.
*/
struct MetadataEntry {
mutable std::shared_mutex mutex; // Entry-level lock
std::vector<std::pair<UUID, AllocationHandle>>
replicas; // tier_id -> handle
uint64_t version = 0; // Monotonically increasing version
};
// Get list of Tier IDs sorted by priority (descending)
std::vector<UUID> GetSortedTiers() const;
// Low-level allocation logic
tl::expected<void, ErrorCode> AllocateInternalRaw(
size_t size, std::optional<UUID> preferred_tier,
TieredLocation* out_loc);
private:
// Map from tier ID to the actual CacheTier instance.
std::unordered_map<UUID, std::shared_ptr<CacheTier>> tiers_;
// Map from tier ID to static config info
std::unordered_map<UUID, TierInfo> tier_info_;
// Sharded Metadata Index: Key -> Entry
// Each shard has its own mutex for fine-grained locking.
struct MetadataShard {
mutable std::shared_mutex mutex;
std::unordered_map<std::string, std::shared_ptr<MetadataEntry>> index;
};
static constexpr size_t kMetadataShardCount = 64;
std::array<MetadataShard, kMetadataShardCount> metadata_shards_;
MetadataShard& GetMetadataShard(const std::string& key) {
return metadata_shards_[std::hash<std::string>{}(key) %
kMetadataShardCount];
}
const MetadataShard& GetMetadataShard(const std::string& key) const {
return metadata_shards_[std::hash<std::string>{}(key) %
kMetadataShardCount];
}
std::unique_ptr<DataCopier> data_copier_;
// Callbacks for metadata synchronization with Master
AddReplicaCallback add_replica_callback_;
RemoveReplicaCallback remove_replica_callback_;
// Callback for segment lifecycle synchronization with Master
SegmentSyncCallback segment_sync_callback_;
// Scheduler
std::unique_ptr<ClientScheduler> scheduler_;
// Shutdown flag — once set, all public APIs reject new requests.
std::atomic<bool> is_shutting_down_{false};
// Destroy flag
std::atomic<bool> is_destroyed_{false};
};
} // namespace mooncake

View File

@ -0,0 +1,161 @@
#pragma once
#include <atomic>
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "tiered_cache/tiers/cache_tier.h"
namespace mooncake {
/**
* @struct AscendUnifiedPointer
* @brief Encapsulates Ascend device pointer information.
*
* Contains the device pointer along with device ID and size,
* enabling proper device context management for multi-device scenarios.
*/
struct AscendUnifiedPointer {
void* device_ptr; // The actual device memory pointer
int device_id; // The device ID this pointer belongs to
size_t size; // Size of the allocated memory in bytes
};
/**
* @class AscendBuffer
* @brief Ascend NPU memory buffer wrapper inheriting from BufferBase.
*
* Implements RAII pattern for automatic device memory management.
* When the buffer goes out of scope, device memory is automatically freed.
*/
class AscendBuffer : public BufferBase {
public:
/**
* @brief Constructs an AscendBuffer taking ownership of device memory.
* @param unified_ptr unique_ptr to AscendUnifiedPointer containing device
* memory info.
*/
explicit AscendBuffer(std::unique_ptr<AscendUnifiedPointer> unified_ptr);
/**
* @brief Destructor that automatically releases device memory.
*/
~AscendBuffer() override;
// Disable copy operations
AscendBuffer(const AscendBuffer&) = delete;
AscendBuffer& operator=(const AscendBuffer&) = delete;
// Enable move operations
AscendBuffer(AscendBuffer&& other) noexcept;
AscendBuffer& operator=(AscendBuffer&& other) noexcept;
/**
* @brief Returns the device pointer address as uint64_t.
*
* Returns the raw device memory pointer. For accessing device context
* information (device_id), use GetUnifiedPointer() or GetDeviceId().
*/
uint64_t data() const override;
/**
* @brief Returns the size of the allocated memory.
*/
std::size_t size() const override;
/**
* @brief Gets the device ID for this buffer.
* @return Device ID, or -1 if buffer is invalid.
*/
int GetDeviceId() const;
/**
* @brief Gets the actual device pointer.
* @return Device pointer, or nullptr if buffer is invalid.
*/
void* GetDevicePtr() const;
/**
* @brief Gets the complete AscendUnifiedPointer structure.
* @return Pointer to AscendUnifiedPointer, or nullptr if invalid.
*/
const AscendUnifiedPointer* GetUnifiedPointer() const;
private:
std::unique_ptr<AscendUnifiedPointer> unified_ptr_;
// Internal function to release device memory
void ReleaseMemory();
};
/**
* @class AscendCacheTier
* @brief Ascend NPU cache tier implementation for the new CacheTier interface.
*
* Provides device memory allocation and management for Huawei Ascend NPU
* devices. Supports the Allocate/Free resource management pattern with RAII
* semantics.
*/
class AscendCacheTier : public CacheTier {
public:
/**
* @brief Constructs an AscendCacheTier.
* @param tier_id Unique identifier for this tier (UUID).
* @param capacity Total capacity in bytes.
* @param tags Optional tags for tier identification.
* @param device_id Ascend device ID (default: 0).
*/
AscendCacheTier(UUID tier_id, size_t capacity,
const std::vector<std::string>& tags, int device_id = 0);
~AscendCacheTier() override;
// Disable copy
AscendCacheTier(const AscendCacheTier&) = delete;
AscendCacheTier& operator=(const AscendCacheTier&) = delete;
// CacheTier interface implementation
tl::expected<void, ErrorCode> Init(TieredBackend* backend,
TransferEngine* engine) override;
tl::expected<void, ErrorCode> Allocate(size_t size,
DataSource& data) override;
tl::expected<void, ErrorCode> Free(DataSource data) override;
// Accessors
UUID GetTierId() const override { return tier_id_; }
size_t GetCapacity() const override { return capacity_; }
size_t GetUsage() const override;
MemoryType GetMemoryType() const override { return MemoryType::ASCEND_NPU; }
const std::vector<std::string>& GetTags() const override { return tags_; }
/**
* @brief Gets the device ID for this cache tier.
*/
int GetDeviceId() const { return device_id_; }
private:
UUID tier_id_;
size_t capacity_;
std::vector<std::string> tags_;
int device_id_;
// Memory usage tracking (atomic for thread safety)
std::atomic<size_t> current_usage_{0};
// Initialization state
bool is_initialized_{false};
mutable std::mutex init_mutex_;
// Internal device memory allocation
std::unique_ptr<AscendUnifiedPointer> AllocateDeviceMemory(size_t size);
// Check if sufficient space is available
bool HasSpace(size_t size) const;
};
} // namespace mooncake

View File

@ -0,0 +1,162 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <ylt/util/tl/expected.hpp>
#include "allocator.h"
#include "transfer_engine.h"
#include "types.h"
namespace mooncake {
class TieredBackend;
/**
* @class BufferBase
* @brief Base class for different types of memory buffers
*/
class BufferBase {
public:
virtual ~BufferBase() = default;
virtual uint64_t data() const = 0;
virtual std::size_t size() const = 0;
};
/**
* @class DRAMBuffer
* @brief Wrapper for DRAM AllocatedBuffer
*/
class DRAMBuffer : public BufferBase {
public:
explicit DRAMBuffer(std::unique_ptr<AllocatedBuffer> buffer)
: dram_buffer_(std::move(buffer)) {}
uint64_t data() const override {
return dram_buffer_ ? reinterpret_cast<uint64_t>(dram_buffer_->data())
: 0;
}
std::size_t size() const override {
return dram_buffer_ ? dram_buffer_->size() : 0;
}
private:
std::unique_ptr<AllocatedBuffer> dram_buffer_;
};
/**
* @class TempDRAMBuffer
* @brief Wrapper for temporary DRAM buffers with RAII memory management
*/
class TempDRAMBuffer : public BufferBase {
public:
// Constructor that takes ownership of the buffer
explicit TempDRAMBuffer(std::unique_ptr<char[]> buffer, size_t size)
: buffer_(std::move(buffer)), size_(size) {}
uint64_t data() const override {
return reinterpret_cast<uint64_t>(buffer_.get());
}
std::size_t size() const override { return size_; }
private:
std::unique_ptr<char[]>
buffer_; // Owns the memory, auto-releases on destruction
size_t size_;
};
/**
* @class RefBuffer
* @brief Helper class to wrap a raw pointer as BufferBase without taking
* ownership This is used for DataCopier operations where the source memory is
* owned elsewhere
*/
class RefBuffer : public BufferBase {
public:
explicit RefBuffer(void* ptr, size_t size) : ptr_(ptr), size_(size) {}
uint64_t data() const override { return reinterpret_cast<uint64_t>(ptr_); }
std::size_t size() const override { return size_; }
private:
void* ptr_;
size_t size_;
};
/**
* @struct DataSource
* @brief Describes a source of data for copy/write operations.
*/
struct DataSource {
std::unique_ptr<BufferBase> buffer;
MemoryType type; // Source memory type
};
/**
* @class CacheTier
* @brief Abstract base class for a single tier (e.g., DRAM, SSD).
* * Update: Supports decoupled Allocation/Write/Bind operations to allow
* flexible placement strategies (Client-centric vs Master-centric).
*/
class CacheTier {
public:
virtual ~CacheTier() = default;
/**
* @brief Initializes the cache tier.
* @return tl::expected<void, ErrorCode> indicating success or error code.
*/
virtual tl::expected<void, ErrorCode> Init(TieredBackend* backend,
TransferEngine* engine) = 0;
/**
* @brief Reserve Space (Allocation)
* Finds free space of `size` bytes. Does NOT copy data.
* @param size Bytes to allocate.
* @param data DataSource struct to fill with allocation info.
* @return tl::expected<void, ErrorCode> indicating success or error code.
*/
virtual tl::expected<void, ErrorCode> Allocate(size_t size,
DataSource& data) = 0;
/**
* @brief Free Space (Rollback/Cleanup)
* Releases space at offset. Used when writes fail or explicitly freeing
* anonymous blocks.
* @return tl::expected<void, ErrorCode> indicating success or error code.
*/
virtual tl::expected<void, ErrorCode> Free(DataSource data) = 0;
// --- Accessors & Metadata ---
virtual UUID GetTierId() const = 0;
virtual size_t GetCapacity() const = 0;
virtual size_t GetUsage() const = 0;
virtual MemoryType GetMemoryType() const = 0;
virtual const std::vector<std::string>& GetTags() const = 0;
/**
* @brief Commit (Register)
* Registers the key and finalizes the storage.
* For Storage Tiers, this triggers the actual persistence (or buffering).
*/
virtual tl::expected<void, ErrorCode> Commit(const std::string& key,
const DataSource& data) {
return {};
}
/**
* @brief Flush
* Forces any buffered data to be persisted to the underlying storage.
*/
virtual tl::expected<void, ErrorCode> Flush() { return {}; }
protected:
// A pointer to the parent backend, allowing tiers to access shared services
// like the DataCopier.
TieredBackend* backend_ = nullptr;
};
} // namespace mooncake

View File

@ -0,0 +1,47 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <unordered_map>
#include <optional>
#include "allocator.h"
#include "tiered_cache/tiers/cache_tier.h"
#include "transfer_engine.h"
namespace mooncake {
class DramCacheTier : public CacheTier {
public:
DramCacheTier(
UUID tier_id, size_t capacity, const std::vector<std::string>& tags,
std::optional<int> numa_node = std::nullopt,
BufferAllocatorType allocator_type = BufferAllocatorType::OFFSET);
~DramCacheTier() override;
tl::expected<void, ErrorCode> Init(TieredBackend* backend,
TransferEngine* engine) override;
tl::expected<void, ErrorCode> Allocate(size_t size,
DataSource& data) override;
tl::expected<void, ErrorCode> Free(DataSource data) override;
UUID GetTierId() const override { return tier_id_; }
size_t GetCapacity() const override { return capacity_; }
size_t GetUsage() const override;
const std::vector<std::string>& GetTags() const override { return tags_; }
MemoryType GetMemoryType() const override { return MemoryType::DRAM; }
private:
UUID tier_id_;
size_t capacity_;
std::vector<std::string> tags_;
std::optional<int> numa_node_;
BufferAllocatorType allocator_type_;
std::shared_ptr<BufferAllocatorBase> allocator_;
TransferEngine* engine_;
std::unique_ptr<char[], void (*)(char*)> memory_buffer_{
nullptr, [](char* p) { delete[] p; }};
};
} // namespace mooncake

View File

@ -0,0 +1,214 @@
#pragma once
#include <memory>
#include <map>
#include <string>
#include <vector>
#include <unordered_map>
#include <mutex>
#include <cstring>
#include <atomic>
#include <thread>
#include <condition_variable>
#include "tiered_cache/tiers/cache_tier.h"
#include "storage_backend.h"
namespace mooncake {
/**
* @class StorageBuffer
* @brief A unified buffer for storage tier that handles both staging (DRAM)
* and persisted (Disk) states.
*/
class StorageBuffer : public BufferBase {
public:
explicit StorageBuffer(std::unique_ptr<AllocatedBuffer> staging_buffer,
StorageBackendInterface* backend)
: staging_buffer_(std::move(staging_buffer)),
backend_(backend),
size_(staging_buffer_ ? staging_buffer_->size() : 0) {}
uint64_t data() const override {
// Return valid pointer only while data is still in the staging pool.
std::lock_guard<std::mutex> lock(data_mutex_);
if (!is_on_disk_.load(std::memory_order_acquire) && staging_buffer_) {
return reinterpret_cast<uint64_t>(staging_buffer_->data());
}
return 0;
}
std::size_t size() const override { return size_; }
// Helper to get raw pointer (only valid in staging mode).
char* data_ptr() {
std::lock_guard<std::mutex> lock(data_mutex_);
return is_on_disk_.load(std::memory_order_acquire) || !staging_buffer_
? nullptr
: static_cast<char*>(staging_buffer_->data());
}
const char* data_ptr() const {
std::lock_guard<std::mutex> lock(data_mutex_);
return is_on_disk_.load(std::memory_order_acquire) || !staging_buffer_
? nullptr
: static_cast<const char*>(staging_buffer_->data());
}
Slice ToSlice() const {
std::lock_guard<std::mutex> lock(data_mutex_);
if (is_on_disk_.load(std::memory_order_acquire) || !staging_buffer_) {
return Slice{nullptr, size_};
}
return Slice{static_cast<char*>(staging_buffer_->data()), size_};
}
void SetKey(const std::string& key) { key_ = key; }
const std::string& GetKey() const { return key_; }
// Transition from staging pool -> on disk.
void Persist() {
std::lock_guard<std::mutex> lock(data_mutex_);
if (is_on_disk_.load(std::memory_order_acquire)) return;
if (staging_buffer_) {
size_ = staging_buffer_->size();
staging_buffer_.reset();
}
is_on_disk_.store(true, std::memory_order_release);
}
bool IsPersisted() const {
return is_on_disk_.load(std::memory_order_acquire);
}
void SetFlushing(bool flushing) {
is_flushing_.store(flushing, std::memory_order_release);
}
bool IsFlushing() const {
return is_flushing_.load(std::memory_order_acquire);
}
// Read data to destination buffer (handles staging vs disk transparently).
tl::expected<void, ErrorCode> ReadTo(void* dst, size_t length) {
{
std::lock_guard<std::mutex> lock(data_mutex_);
if (!is_on_disk_.load(std::memory_order_acquire)) {
if (!staging_buffer_) {
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
}
if (length > size_) {
return tl::make_unexpected(ErrorCode::BUFFER_OVERFLOW);
}
std::memcpy(dst, staging_buffer_->data(), length);
return {};
}
}
// On disk: load from backend without holding the staging lock.
if (!backend_ || key_.empty()) {
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
std::unordered_map<std::string, Slice> batch;
batch[key_] = Slice{static_cast<char*>(dst), length};
return backend_->BatchLoad(batch);
}
private:
mutable std::mutex data_mutex_;
std::unique_ptr<AllocatedBuffer> staging_buffer_;
StorageBackendInterface* backend_ = nullptr;
std::string key_;
std::atomic<bool> is_on_disk_{false};
std::atomic<bool> is_flushing_{false};
size_t size_ = 0;
};
/**
* @class StorageTier
* @brief Storage tier implementation for SSD/NVMe storage.
*/
class StorageTier : public CacheTier {
public:
StorageTier(UUID tier_id, const std::vector<std::string>& tags,
size_t capacity = 0);
~StorageTier() override;
tl::expected<void, ErrorCode> Init(TieredBackend* backend,
TransferEngine* engine) override;
tl::expected<void, ErrorCode> Allocate(size_t size,
DataSource& data) override;
tl::expected<void, ErrorCode> Free(DataSource data) override;
tl::expected<void, ErrorCode> Commit(const std::string& key,
const DataSource& data) override;
tl::expected<void, ErrorCode> Flush() override;
// --- Accessors ---
UUID GetTierId() const override { return tier_id_; }
size_t GetCapacity() const override;
size_t GetUsage() const override;
MemoryType GetMemoryType() const override { return MemoryType::NVME; }
const std::vector<std::string>& GetTags() const override { return tags_; }
/**
* @brief Trigger bucket eviction to free up space.
* @param target_free_size Target amount of space to free (in bytes).
* If 0, evicts one bucket.
* @return tl::expected<size_t, ErrorCode>
* - On success: total amount of space freed (in bytes)
* - On failure: error code
*/
tl::expected<size_t, ErrorCode> TriggerBucketEviction(
size_t target_free_size = 0);
private:
static void FreeStagingMemory(char* ptr);
// Internal flush logic that triggers BatchOffload
tl::expected<void, ErrorCode> FlushInternal();
// Background flush thread worker
void FlushWorker();
UUID tier_id_;
std::vector<std::string> tags_;
std::shared_ptr<StorageBackendInterface> storage_backend_;
// Pending Write Buffer for aggregation
std::mutex batch_mutex_;
std::condition_variable flush_cv_; // Signaled when flush completes
std::unordered_map<std::string, StorageBuffer*> pending_batch_;
std::atomic<size_t> pending_batch_size_{0};
// Async flush thread
std::thread flush_thread_;
std::atomic<bool> stop_flush_thread_{false};
std::condition_variable flush_trigger_cv_;
std::mutex flush_trigger_mutex_;
std::atomic<bool> flush_requested_{false};
// Configurable thresholds
size_t batch_size_threshold_ = 64 * 1024 * 1024; // 64MB
size_t batch_count_threshold_ = 1000;
// Preallocated local staging pool for SSD writes.
std::shared_ptr<BufferAllocatorBase> staging_allocator_;
std::unique_ptr<char, void (*)(char*)> staging_memory_{
nullptr, &StorageTier::FreeStagingMemory};
size_t staging_buffer_capacity_ = 0;
// Live persisted value bytes. This excludes backend metadata and any
// bucket files already unlinked from the cache namespace but still pending
// async physical deletion.
size_t capacity_ = 0;
std::atomic<size_t> persisted_live_data_bytes_{0};
};
} // namespace mooncake

View File

@ -3,12 +3,11 @@
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <limits>
#include <unordered_map>
#include <vector>
#include <variant>
#include "Slab.h"
#include "ylt/struct_json/json_reader.h"
#include "ylt/struct_json/json_writer.h"
@ -30,8 +29,9 @@ static constexpr uint64_t DEFAULT_KV_SOFT_PIN_TTL_MS =
static constexpr bool DEFAULT_ALLOW_EVICT_SOFT_PINNED_OBJECTS = true;
static constexpr double DEFAULT_EVICTION_RATIO = 0.05;
static constexpr double DEFAULT_EVICTION_HIGH_WATERMARK_RATIO = 0.95;
static constexpr int64_t ETCD_MASTER_VIEW_LEASE_TTL = 5; // in seconds
static constexpr int64_t DEFAULT_CLIENT_LIVE_TTL_SEC = 10; // in seconds
static constexpr int64_t ETCD_MASTER_VIEW_LEASE_TTL = 5; // in seconds
static constexpr int64_t DEFAULT_CLIENT_LIVE_TTL_SEC = 10; // in seconds
static constexpr int64_t DEFAULT_CLIENT_CRASHED_TTL_SEC = 30; // in seconds
static const std::string DEFAULT_CLUSTER_ID = "mooncake_cluster";
static const std::string DEFAULT_ROOT_FS_DIR = "";
// default do not limit DFS usage, and use
@ -90,8 +90,9 @@ UUID generate_uuid();
* @brief Error codes for various operations in the system
*/
enum class ErrorCode : int32_t {
OK = 0, ///< Operation successful.
INTERNAL_ERROR = -1, ///< Internal error occurred.
OK = 0, ///< Operation successful.
INTERNAL_ERROR = -1, ///< Internal error occurred.
NOT_IMPLEMENTED = -2, ///< Not implemented.
// Buffer allocation errors (Range: -20 to -99)
BUFFER_OVERFLOW = -10, ///< Insufficient buffer space.
@ -101,6 +102,9 @@ enum class ErrorCode : int32_t {
SEGMENT_NOT_FOUND = -101, ///< No available segments found.
SEGMENT_ALREADY_EXISTS = -102, ///< Segment already exists.
CLIENT_NOT_FOUND = -103, ///< Client not found.
CLIENT_ALREADY_EXISTS = -104, ///< Client already exists.
CLIENT_UNHEALTHY =
-105, ///< Client is not in a healthy state for the operation.
// Handle selection errors (Range: -200 to -299)
NO_AVAILABLE_HANDLE =
@ -108,6 +112,7 @@ enum class ErrorCode : int32_t {
// Version errors (Range: -300 to -399)
INVALID_VERSION = -300, ///< Invalid version.
CAS_FAILED = -301, ///< Compare and Swap failed (Optimistic Locking).
// Key errors (Range: -400 to -499)
INVALID_KEY = -400, ///< Invalid key.
@ -119,17 +124,19 @@ enum class ErrorCode : int32_t {
INVALID_PARAMS = -600, ///< Invalid parameters.
ILLEGAL_CLIENT = -601, ///< Illegal client to do the operation.
// Engine operation errors (Range: -700 to -799)
// Engine operation errors (Range: -700 to -710)
INVALID_WRITE = -700, ///< Invalid write operation.
INVALID_READ = -701, ///< Invalid read operation.
INVALID_REPLICA = -702, ///< Invalid replica operation.
// Object errors (Range: -703 to -707)
REPLICA_IS_NOT_READY = -703, ///< Replica is not ready.
OBJECT_NOT_FOUND = -704, ///< Object not found.
OBJECT_ALREADY_EXISTS = -705, ///< Object already exists.
OBJECT_HAS_LEASE = -706, ///< Object has lease.
LEASE_EXPIRED = -707, ///< Lease expired before data transfer completed.
REPLICA_ALREADY_EXISTS = -708, ///< Replica already exists.
REPLICA_NOT_FOUND = -709, ///< Replica not found.
REPLICA_NUM_EXCEEDED = -710, ///< Replica number exceeded.
// Transfer errors (Range: -800 to -899)
TRANSFER_FAIL = -800, ///< Transfer operation failed.
@ -162,6 +169,17 @@ enum class ErrorCode : int32_t {
KEYS_ULTRA_LIMIT = -1203, ///< Keys ultra limit.
UNABLE_OFFLOAD = -1300, ///< The offload functionality is not enabled
UNABLE_OFFLOADING = -1301, ///< Unable offloading.
// Tiered backend errors (Range: -1400 to -1499)
EMPTY_REPLICAS = -1400,
TIER_NOT_FOUND = -1401,
DATA_COPY_FAILED = -1402,
// Store errors (Range: -1500 to -1599)
SHUTTING_DOWN = -1500, ///< Store is shutting down, rejecting new requests.
ASYNC_ENQUEUE_FAILED = -1501, ///< Async metadata notifier enqueue failed
///< (queue full/stopped).
INACCESSIBLE_MASTER = -1502
};
int32_t toInt(ErrorCode errorCode) noexcept;
@ -174,6 +192,28 @@ inline std::ostream& operator<<(std::ostream& os,
return os << toString(errorCode);
}
enum class DeploymentMode {
UNKNOWN = -1,
CENTRALIZATION = 0,
P2P,
};
inline std::ostream& operator<<(std::ostream& os,
const DeploymentMode& mode) noexcept {
switch (mode) {
case DeploymentMode::CENTRALIZATION:
os << "CENTRALIZATION";
break;
case DeploymentMode::P2P:
os << "P2P";
break;
default:
os << "UNKNOWN";
break;
}
return os;
}
/**
* @brief Represents a contiguous memory region
*/
@ -186,28 +226,105 @@ const static uint64_t kMinSliceSize = facebook::cachelib::Slab::kMinAllocSize;
const static uint64_t kMaxSliceSize =
facebook::cachelib::Slab::kSize - 16; // should be lower than limit
struct CentralizedSegmentExtraData {
uintptr_t base{0};
std::string te_endpoint;
YLT_REFL(CentralizedSegmentExtraData, base, te_endpoint);
};
/**
* @brief Represents a contiguous memory region
* @enum MemoryType
* @brief Defines the physical storage medium type for a cache tier.
*/
enum class MemoryType { DRAM, NVME, ASCEND_NPU, UNKNOWN };
static inline std::string MemoryTypeToString(MemoryType type) {
switch (type) {
case MemoryType::DRAM:
return "DRAM";
case MemoryType::NVME:
return "NVME";
case MemoryType::ASCEND_NPU:
return "ASCEND_NPU";
default:
return "UNKNOWN";
}
}
struct P2PSegmentExtraData {
int priority = 0;
std::vector<std::string> tags;
MemoryType memory_type = MemoryType::DRAM;
size_t usage = 0;
YLT_REFL(P2PSegmentExtraData, priority, tags, memory_type, usage);
};
/**
* @brief Represents a contiguous storage region
*/
struct Segment {
UUID id{0, 0};
std::string name{}; // Logical segment name used for preferred allocation
uintptr_t base{0};
size_t size{0};
// TE p2p endpoint (ip:port) for transport-only addressing
std::string te_endpoint{};
Segment() = default;
// Polymorphic extra data
std::variant<std::monostate, CentralizedSegmentExtraData,
P2PSegmentExtraData>
extra;
// Helper to check type
bool IsP2PSegment() const {
return std::holds_alternative<P2PSegmentExtraData>(extra);
}
bool IsCentralizedSegment() const {
return std::holds_alternative<CentralizedSegmentExtraData>(extra);
}
bool IsEmpty() const {
return std::holds_alternative<std::monostate>(extra);
}
CentralizedSegmentExtraData& GetCentralizedExtra() {
if (IsP2PSegment()) {
throw std::runtime_error(
"Segment already holds P2PSegmentExtraData; cannot assign "
"CentralizedSegmentExtraData");
}
if (IsEmpty()) extra = CentralizedSegmentExtraData{};
return std::get<CentralizedSegmentExtraData>(extra);
}
const CentralizedSegmentExtraData& GetCentralizedExtra() const {
return std::get<CentralizedSegmentExtraData>(extra);
}
P2PSegmentExtraData& GetP2PExtra() {
if (IsCentralizedSegment()) {
throw std::runtime_error(
"Segment already holds CentralizedSegmentExtraData; cannot "
"assign P2PSegmentExtraData");
}
if (IsEmpty()) extra = P2PSegmentExtraData{};
return std::get<P2PSegmentExtraData>(extra);
}
const P2PSegmentExtraData& GetP2PExtra() const {
return std::get<P2PSegmentExtraData>(extra);
}
};
YLT_REFL(Segment, id, name, base, size, te_endpoint);
YLT_REFL(Segment, id, name, size, extra);
/**
* @brief Client status from the master's perspective
* @brief Client status from the master's perspective.
*
* State machine: HEALTH -> DISCONNECTION (heartbeat timeout)
* DISCONNECTION -> HEALTH (heartbeat recovered)
* DISCONNECTION -> CRASHED (long-term timeout)
*/
enum class ClientStatus {
UNDEFINED = 0, // Uninitialized
OK, // Client is alive, no need to remount for now
NEED_REMOUNT, // Ping ttl expired, or the first time connect to master,
// so need to remount
UNDEFINED = 0, // Client does not exist
HEALTH, // Normal operation
DISCONNECTION, // Heartbeat lost, waiting for recovery
CRASHED, // Terminal state, all metadata will be cleaned up
};
/**
@ -217,14 +334,83 @@ inline std::ostream& operator<<(std::ostream& os,
const ClientStatus& status) noexcept {
static const std::unordered_map<ClientStatus, std::string_view>
status_strings{{ClientStatus::UNDEFINED, "UNDEFINED"},
{ClientStatus::OK, "OK"},
{ClientStatus::NEED_REMOUNT, "NEED_REMOUNT"}};
{ClientStatus::HEALTH, "HEALTH"},
{ClientStatus::DISCONNECTION, "DISCONNECTION"},
{ClientStatus::CRASHED, "CRASHED"}};
os << (status_strings.count(status) ? status_strings.at(status)
: "UNKNOWN");
return os;
}
/**
* @enum HAClientState
* @brief Client-side HA state for Master crash recovery.
* FULL: normal operation
* DEGRADED: Master unreachable, local-only mode
* SYNCING: re-syncing metadata to restarted Master
*/
enum class HAClientState : int32_t {
FULL = 0,
DEGRADED = 1,
SYNCING = 2,
};
inline std::ostream& operator<<(std::ostream& os,
const HAClientState& state) noexcept {
switch (state) {
case HAClientState::FULL:
os << "FULL";
break;
case HAClientState::DEGRADED:
os << "DEGRADED";
break;
case HAClientState::SYNCING:
os << "SYNCING";
break;
default:
os << "UNKNOWN";
break;
}
return os;
}
inline const char* toString(HAClientState state) noexcept {
switch (state) {
case HAClientState::FULL:
return "FULL";
case HAClientState::DEGRADED:
return "DEGRADED";
case HAClientState::SYNCING:
return "SYNCING";
default:
return "UNKNOWN";
}
}
/**
* @enum HAEvent
* @brief Events that drive client-side HA state transitions.
*/
enum class HAEvent {
MASTER_UNREACHABLE, // Consecutive heartbeat failures exceeded threshold
MASTER_RECONNECTED, // Master connection restored. Triggered on:
// 1. RegisterClient succeeded (Master restarted or
// client re-registered after reconnection)
// 2. Heartbeat recovered with HEALTH status after
// a prior MASTER_UNREACHABLE event
};
/**
* @struct ReplicaLocation
* @brief Describes a single replica's key, tier and size.
*/
struct ReplicaLocation {
std::string key;
UUID tier_id;
size_t size;
};
enum class BufferAllocatorType {
CACHELIB = 0, // CachelibBufferAllocator
OFFSET = 1, // OffsetBufferAllocator
@ -253,4 +439,43 @@ struct StorageObjectMetadata {
transport_endpoint);
};
/**
* @brief object iteration strategy in for-each interface
*/
enum class ObjectIterateStrategy {
// Iterate over objects in order
ORDERED = 0,
// Choose a random object
RANDOM = 1,
// Choose a object with the most available capacity
CAPACITY_PRIORITY = 2,
};
inline std::ostream& operator<<(
std::ostream& os, const ObjectIterateStrategy& strategy) noexcept {
static const std::unordered_map<ObjectIterateStrategy, std::string_view>
strategy_strings{
{ObjectIterateStrategy::ORDERED, "ORDERED"},
{ObjectIterateStrategy::RANDOM, "RANDOM"},
{ObjectIterateStrategy::CAPACITY_PRIORITY, "CAPACITY_PRIORITY"},
};
os << (strategy_strings.count(strategy) ? strategy_strings.at(strategy)
: "UNKNOWN");
return os;
}
} // namespace mooncake
namespace std {
template <>
struct hash<mooncake::UUID> {
std::size_t operator()(const mooncake::UUID& k) const {
std::size_t h1 = hash<uint64_t>{}(k.first);
std::size_t h2 = hash<uint64_t>{}(k.second);
return h1 ^ (h2 << 1);
}
};
} // namespace std

View File

@ -2,23 +2,41 @@
add_subdirectory(cachelib_memory_allocator)
set(MOONCAKE_STORE_SOURCES
async_memcpy_executor.cpp
replica.cpp
allocator.cpp
master_service.cpp
centralized_master_service.cpp
client_service.cpp
centralized_client_service.cpp
p2p_client_service.cpp
client_metric.cpp
types.cpp
master_client.cpp
centralized_master_client.cpp
p2p_master_client.cpp
utils.cpp
master_metric_manager.cpp
storage_backend.cpp
thread_pool.cpp
etcd_helper.cpp
ha_helper.cpp
segment.cpp
centralized_segment_manager.cpp
segment_manager.cpp
client_meta.cpp
client_manager.cpp
centralized_client_manager.cpp
centralized_client_meta.cpp
p2p_client_manager.cpp
p2p_master_service.cpp
p2p_client_meta.cpp
p2p_segment_manager.cpp
transfer_task.cpp
etcd_helper.cpp
ha_helper.cpp
rpc_service.cpp
centralized_rpc_service.cpp
p2p_rpc_service.cpp
offset_allocator.cpp
posix_file.cpp
client_buffer.cpp
@ -26,6 +44,20 @@ set(MOONCAKE_STORE_SOURCES
dummy_client.cpp
http_metadata_server.cpp
file_storage.cpp
tiered_cache/copier_registry.cpp
tiered_cache/data_copier.cpp
tiered_cache/tiered_backend.cpp
data_manager.cpp
route_cache.cpp
async_metadata_notifier.cpp
ha_recovery_manager.cpp
client_rpc_service.cpp
peer_client.cpp
tiered_cache/tiers/dram_tier.cpp
tiered_cache/tiers/storage_tier.cpp
tiered_cache/scheduler/simple_policy.cpp
tiered_cache/scheduler/lru_policy.cpp
tiered_cache/scheduler/client_scheduler.cpp
)
set(EXTRA_LIBS "")
@ -40,6 +72,12 @@ if(USE_3FS)
set(EXTRA_LIBS ${HF3FS_API_LIB})
endif()
# Ascend Cache Tier support
if(USE_ASCEND_CACHE_TIER)
list(APPEND MOONCAKE_STORE_SOURCES tiered_cache/tiers/ascend_tier.cpp)
message(STATUS "Ascend Cache Tier enabled")
endif()
# The cache_allocator library
include_directories(${Python3_INCLUDE_DIRS})
add_library(mooncake_store ${MOONCAKE_STORE_SOURCES})
@ -56,6 +94,35 @@ target_link_libraries(mooncake_store
PRIVATE
transfer_engine
)
# Link Ascend ACL library if USE_ASCEND_CACHE_TIER is enabled
if(USE_ASCEND_CACHE_TIER)
# Try to find Ascend ACL library
set(ASCEND_SEARCH_PATHS
$ENV{ASCEND_HOME}/lib64
/usr/local/Ascend/ascend-toolkit/latest/lib64
/usr/local/Ascend/nnrt/latest/lib64
)
find_library(ASCENDCL_LIB ascendcl PATHS ${ASCEND_SEARCH_PATHS} NO_DEFAULT_PATH)
if(NOT ASCENDCL_LIB)
message(WARNING "ascendcl library not found. Ascend cache tier will use fallback mode (no ACL).")
else()
message(STATUS "Found ascendcl library: ${ASCENDCL_LIB}")
target_link_libraries(mooncake_store PUBLIC ${ASCENDCL_LIB})
# Add include paths for Ascend headers
set(ASCEND_INCLUDE_PATHS
$ENV{ASCEND_HOME}/include
/usr/local/Ascend/ascend-toolkit/latest/include
/usr/local/Ascend/nnrt/latest/include
)
target_include_directories(mooncake_store PUBLIC ${ASCEND_INCLUDE_PATHS})
# Only define USE_ASCEND_CACHE_TIER when library is found
target_compile_definitions(mooncake_store PUBLIC USE_ASCEND_CACHE_TIER)
endif()
endif()
if (STORE_USE_ETCD)
add_dependencies(mooncake_store build_etcd_wrapper)
endif()

View File

@ -56,12 +56,14 @@ std::ostream& operator<<(std::ostream& os, const AllocatedBuffer& buffer) {
// Removed allocated_bytes parameter and member initialization
CachelibBufferAllocator::CachelibBufferAllocator(std::string segment_name,
size_t base, size_t size,
std::string transport_endpoint)
std::string transport_endpoint,
const UUID& segment_id)
: segment_name_(segment_name),
base_(base),
total_size_(size),
cur_size_(0),
transport_endpoint_(std::move(transport_endpoint)) {
transport_endpoint_(std::move(transport_endpoint)),
segment_id_(segment_id) {
VLOG(1) << "initializing_buffer_allocator segment_name=" << segment_name
<< " base_address=" << reinterpret_cast<void*>(base)
<< " size=" << size;
@ -121,7 +123,8 @@ std::unique_ptr<AllocatedBuffer> CachelibBufferAllocator::allocate(
<< " segment=" << segment_name_ << " address=" << buffer;
cur_size_.fetch_add(size);
MasterMetricManager::instance().inc_allocated_mem_size(segment_name_, size);
return std::make_unique<AllocatedBuffer>(shared_from_this(), buffer, size);
return std::make_unique<AllocatedBuffer>(shared_from_this(), buffer, size,
segment_id_);
}
void CachelibBufferAllocator::deallocate(AllocatedBuffer* handle) {
@ -145,12 +148,14 @@ void CachelibBufferAllocator::deallocate(AllocatedBuffer* handle) {
// OffsetBufferAllocator implementation
OffsetBufferAllocator::OffsetBufferAllocator(std::string segment_name,
size_t base, size_t size,
std::string transport_endpoint)
std::string transport_endpoint,
const UUID& segment_id)
: segment_name_(segment_name),
base_(base),
total_size_(size),
cur_size_(0),
transport_endpoint_(std::move(transport_endpoint)) {
transport_endpoint_(std::move(transport_endpoint)),
segment_id_(segment_id) {
VLOG(1) << "initializing_offset_buffer_allocator segment_name="
<< segment_name << " base_address=" << reinterpret_cast<void*>(base)
<< " size=" << size;
@ -212,7 +217,8 @@ std::unique_ptr<AllocatedBuffer> OffsetBufferAllocator::allocate(size_t size) {
// Create a custom AllocatedBuffer that manages the
// OffsetAllocationHandle
allocated_buffer = std::make_unique<AllocatedBuffer>(
shared_from_this(), buffer_ptr, size, std::move(allocation_handle));
shared_from_this(), buffer_ptr, size, segment_id_,
std::move(allocation_handle));
VLOG(1) << "allocation_succeeded size=" << size
<< " segment=" << segment_name_ << " address=" << buffer_ptr;
} catch (const std::exception& e) {

View File

@ -0,0 +1,130 @@
#include "async_memcpy_executor.h"
#include <algorithm>
#include <cstring>
namespace mooncake {
// ============================================================================
// ExecuteLocalCopyPlan
// ============================================================================
ErrorCode ExecuteLocalCopyPlan(const LocalCopyPlan& plan) {
if (plan.use_single_dest) {
if (!plan.single_dest_ptr) {
LOG(ERROR) << "Local copy destination buffer is null";
return ErrorCode::INVALID_PARAMS;
}
if (plan.single_dest_size < plan.source_size) {
LOG(ERROR) << "Local copy destination is too small, required="
<< plan.source_size
<< ", provided=" << plan.single_dest_size;
return ErrorCode::INVALID_PARAMS;
}
if (plan.source_size > 0) {
std::memcpy(plan.single_dest_ptr, plan.source_ptr,
plan.source_size);
}
return ErrorCode::OK;
}
size_t offset = 0;
for (const auto& slice : plan.dest_slices) {
if (offset >= plan.source_size) break;
const size_t copy_size =
std::min(slice.size, plan.source_size - offset);
if (copy_size == 0) continue;
if (!slice.ptr) {
LOG(ERROR) << "Local copy destination buffer is null";
return ErrorCode::INVALID_PARAMS;
}
std::memcpy(slice.ptr, plan.source_ptr + offset, copy_size);
offset += copy_size;
}
if (offset != plan.source_size) {
LOG(ERROR) << "Local copy did not complete, copied=" << offset
<< ", source_size=" << plan.source_size;
return ErrorCode::INTERNAL_ERROR;
}
return ErrorCode::OK;
}
// ============================================================================
// AsyncMemcpyExecutor
// ============================================================================
AsyncMemcpyExecutor::AsyncMemcpyExecutor(size_t worker_num) {
workers_.reserve(std::max<size_t>(1, worker_num));
for (size_t i = 0; i < std::max<size_t>(1, worker_num); ++i) {
workers_.emplace_back(&AsyncMemcpyExecutor::WorkerMain, this);
}
}
AsyncMemcpyExecutor::~AsyncMemcpyExecutor() { Shutdown(); }
void AsyncMemcpyExecutor::Shutdown() {
{
std::lock_guard<std::mutex> lock(mutex_);
if (shutting_down_) {
return;
}
shutting_down_ = true;
}
queue_not_empty_cv_.notify_all();
for (auto& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
workers_.clear();
std::queue<QueueTask> pending;
{
std::lock_guard<std::mutex> lock(mutex_);
pending.swap(tasks_);
}
while (!pending.empty()) {
auto task = std::move(pending.front());
if (task.cancel) {
task.cancel();
}
pending.pop();
}
}
void AsyncMemcpyExecutor::WorkerMain() {
while (true) {
QueueTask task;
{
std::unique_lock<std::mutex> lock(mutex_);
queue_not_empty_cv_.wait(
lock, [this] { return shutting_down_ || !tasks_.empty(); });
if (shutting_down_ && tasks_.empty()) {
return;
}
task = std::move(tasks_.front());
tasks_.pop();
}
try {
if (task.run) {
task.run();
}
} catch (const std::exception& e) {
LOG(ERROR) << "Async worker task threw exception: " << e.what();
if (task.cancel) {
task.cancel();
}
} catch (...) {
LOG(ERROR) << "Async worker task threw unknown exception";
if (task.cancel) {
task.cancel();
}
}
}
}
} // namespace mooncake

View File

@ -0,0 +1,479 @@
#include "async_metadata_notifier.h"
#include <glog/logging.h>
#include <algorithm>
namespace mooncake {
AsyncMetadataNotifier::AsyncMetadataNotifier(P2PMasterClient& master_client,
const UUID& client_id,
size_t sender_thread_count,
size_t max_batch_size,
size_t queue_capacity,
SyncFailureCallback failure_cb)
: master_client_(master_client),
client_id_(client_id),
sender_thread_count_(sender_thread_count),
max_batch_size_(max_batch_size),
batch_buffers_(sender_thread_count),
failure_cb_(std::move(failure_cb)) {
const size_t min_queue_capacity = max_batch_size_ * sender_thread_count_;
const size_t total_cap = std::max(queue_capacity, min_queue_capacity);
const size_t per_shard = total_cap / sender_thread_count_;
shards_.reserve(sender_thread_count_);
for (size_t i = 0; i < sender_thread_count_; ++i) {
auto shard = std::make_unique<SenderShard>();
shard->capacity = per_shard;
shard->normal_reserved = per_shard / 4;
shard->slots.resize(per_shard);
shard->free_stack.reserve(per_shard);
for (size_t j = 0; j < per_shard; ++j) {
shard->free_stack.push_back(j);
}
shards_.push_back(std::move(shard));
batch_buffers_[i].resize(max_batch_size_);
}
}
AsyncMetadataNotifier::~AsyncMetadataNotifier() { Stop(); }
void AsyncMetadataNotifier::Start() {
bool expected = false;
if (!running_.compare_exchange_strong(expected, true)) {
return; // already running
}
for (auto& shard : shards_) {
ResetShard(*shard);
}
LOG(INFO) << "AsyncMetadataNotifier starting with " << sender_thread_count_
<< " sender shards, max_batch_size=" << max_batch_size_;
for (size_t i = 0; i < sender_thread_count_; ++i) {
shards_[i]->sender_thread = std::thread([this, i]() { SenderLoop(i); });
}
}
void AsyncMetadataNotifier::Stop(bool drop_pending) {
bool expected = true;
if (!running_.compare_exchange_strong(expected, false)) {
return; // not running
}
if (drop_pending) {
drop_on_stop_.store(true, std::memory_order_release);
}
// Wake all sender threads (both shard CVs and stop CV for retry sleeps)
{
std::lock_guard<std::mutex> lk{stop_mutex_};
stop_cv_.notify_all();
}
for (auto& shard : shards_) {
shard->sender_cv.notify_all();
shard->producer_cv.notify_all();
}
for (auto& shard : shards_) {
if (shard->sender_thread.joinable()) {
shard->sender_thread.join();
}
}
// Clear all queues so next Start() begins clean.
for (auto& shard : shards_) {
ResetShard(*shard);
}
drop_on_stop_.store(false, std::memory_order_release);
consecutive_rpc_failures_.store(0, std::memory_order_release);
LOG(INFO) << "AsyncMetadataNotifier stopped";
}
void AsyncMetadataNotifier::ResetShard(SenderShard& shard) {
std::lock_guard<std::mutex> lock(shard.mutex);
if (!shard.IsEmpty()) {
LOG(WARNING) << "AsyncMetadataNotifier: discarding "
<< shard.normal_count << " normal + "
<< shard.recovery_count << " recovery pending ops";
}
shard.normal_head = shard.normal_tail = InvalidIdx;
shard.normal_count = 0;
shard.recovery_head = shard.recovery_tail = InvalidIdx;
shard.recovery_count = 0;
shard.recovery_in_flight = 0;
shard.free_stack.clear();
for (size_t i = 0; i < shard.capacity; ++i) {
shard.slots[i] = Slot{};
shard.free_stack.push_back(i);
}
shard.coalesce_index.clear();
}
// ============================================================================
// Enqueue
// ============================================================================
tl::expected<void, ErrorCode> AsyncMetadataNotifier::EnqueueAdd(
const std::string& key, const UUID& segment_id, size_t size) {
PendingOp op;
op.type = PendingOp::ADD;
op.key = key;
op.segment_id = segment_id;
op.size = size;
return DoEnqueue(std::move(op), /*is_recovery=*/false);
}
tl::expected<void, ErrorCode> AsyncMetadataNotifier::EnqueueRemove(
const std::string& key, const UUID& segment_id) {
PendingOp op;
op.type = PendingOp::REMOVE;
op.key = key;
op.segment_id = segment_id;
return DoEnqueue(std::move(op), /*is_recovery=*/false);
}
tl::expected<void, ErrorCode> AsyncMetadataNotifier::EnqueueRecoveryAdd(
const std::string& key, const UUID& segment_id, size_t size) {
PendingOp op;
op.type = PendingOp::ADD;
op.key = key;
op.segment_id = segment_id;
op.size = size;
return DoEnqueue(std::move(op), /*is_recovery=*/true);
}
tl::expected<void, ErrorCode> AsyncMetadataNotifier::DoEnqueue(
PendingOp&& op, bool is_recovery) {
if (!running_.load(std::memory_order_acquire)) {
LOG(WARNING)
<< "AsyncMetadataNotifier has stopped, fail to enqueue key="
<< op.key;
return tl::unexpected(ErrorCode::ASYNC_ENQUEUE_FAILED);
}
const size_t shard_idx =
std::hash<std::string>{}(op.key) % sender_thread_count_;
auto& shard = *shards_[shard_idx];
std::unique_lock<std::mutex> lock(shard.mutex);
// 1. Handle full pool
if (is_recovery ? shard.IsFullForRecovery() : shard.IsFull()) {
if (is_recovery) {
// Recovery: non-blocking, caller retries with sleep
return tl::unexpected(ErrorCode::ASYNC_ENQUEUE_FAILED);
}
// Normal: block until space available
bool ok = shard.producer_cv.wait_for(lock, EnqueueTimeout, [&] {
return !shard.IsFull() || !running_.load(std::memory_order_relaxed);
});
if (!ok || !running_.load(std::memory_order_relaxed)) {
LOG(WARNING) << "AsyncMetadataNotifier: enqueue timeout/shutdown"
<< ", key=" << op.key;
return tl::unexpected(ErrorCode::ASYNC_ENQUEUE_FAILED);
}
}
// 2. Coalescing: check for existing pending op
CoalesceKey ck{op.key, op.segment_id};
auto [ci, inserted] = shard.coalesce_index.emplace(ck, CoalesceEntry{});
auto& entry = ci->second;
// 2a. Duplicate same-type op already pending — treat as success, skip
if (op.type == PendingOp::ADD && entry.add_idx != InvalidIdx) {
return {};
}
if (op.type == PendingOp::REMOVE && entry.remove_idx != InvalidIdx) {
return {};
}
// 2b. Opposite-type op pending — cancel it
if (op.type == PendingOp::ADD && entry.remove_idx != InvalidIdx) {
// Cancel the pending REMOVE, don't insert this ADD
size_t cancel_idx = entry.remove_idx;
bool was_recovery = shard.slots[cancel_idx].is_recovery;
shard.Unlink(cancel_idx);
shard.FreeSlot(cancel_idx);
entry.remove_idx = InvalidIdx;
shard.coalesce_index.erase(ci);
lock.unlock();
shard.producer_cv.notify_one();
if (was_recovery) shard.recovery_drain_cv.notify_all();
return {};
}
if (op.type == PendingOp::REMOVE && entry.add_idx != InvalidIdx) {
// Cancel the pending ADD, don't insert this REMOVE
size_t cancel_idx = entry.add_idx;
bool was_recovery = shard.slots[cancel_idx].is_recovery;
shard.Unlink(cancel_idx);
shard.FreeSlot(cancel_idx);
entry.add_idx = InvalidIdx;
shard.coalesce_index.erase(ci);
lock.unlock();
shard.producer_cv.notify_one();
if (was_recovery) shard.recovery_drain_cv.notify_all();
return {};
}
// 3. No coalescing: alloc slot, write data, link to tail
size_t idx = shard.AllocSlot();
shard.slots[idx].op = std::move(op);
shard.LinkTail(idx, is_recovery);
if (shard.slots[idx].op.type == PendingOp::ADD) {
entry.add_idx = idx;
} else {
entry.remove_idx = idx;
}
lock.unlock();
shard.sender_cv.notify_one();
return {};
}
// ============================================================================
// Sender
// ============================================================================
void AsyncMetadataNotifier::SenderLoop(size_t shard_idx) {
auto& shard = *shards_[shard_idx];
auto& batch = batch_buffers_[shard_idx];
while (true) {
if (running_.load(std::memory_order_acquire) && IsPaused()) {
std::unique_lock<std::mutex> lock(shard.mutex);
shard.sender_cv.wait_for(lock, CircuitBreakerCooldown, [&] {
return !running_.load(std::memory_order_relaxed) || !IsPaused();
});
}
auto [n, recovery_n] = CollectBatch(shard, batch);
if (n == 0) {
if (!running_.load(std::memory_order_acquire)) break;
continue;
}
SendBatch(batch, n);
// Decrement recovery_in_flight after the batch is actually sent.
// Notify WaitForRecoveryDrain when all recovery ops are drained.
if (recovery_n > 0) {
std::lock_guard<std::mutex> lk(shard.mutex);
shard.recovery_in_flight -= recovery_n;
if (shard.recovery_in_flight == 0 && shard.recovery_count == 0) {
shard.recovery_drain_cv.notify_all();
}
}
}
}
std::pair<size_t, size_t> AsyncMetadataNotifier::CollectBatch(
SenderShard& shard, std::vector<PendingOp>& batch_out) {
std::unique_lock<std::mutex> lock(shard.mutex);
shard.sender_cv.wait_for(lock, BatchTimeout, [&] {
return !shard.IsEmpty() || !running_.load(std::memory_order_relaxed);
});
// drop mode: don't collect any more batches — let the sender exit cleanly
if (drop_on_stop_.load(std::memory_order_relaxed)) {
return {0, 0};
}
// Phase 1: collect from normal queue (high priority)
size_t collected =
CollectFromList(shard, batch_out, 0, max_batch_size_, false);
// Phase 2: fill remaining from recovery queue (low priority)
size_t remaining = max_batch_size_ - collected;
size_t recovery_collected = 0;
if (remaining > 0) {
recovery_collected =
CollectFromList(shard, batch_out, collected, remaining, true);
collected += recovery_collected;
}
if (collected > 0) {
if (recovery_collected > 0) {
shard.recovery_in_flight += recovery_collected;
}
lock.unlock();
shard.producer_cv.notify_all();
}
return {collected, recovery_collected};
}
size_t AsyncMetadataNotifier::CollectFromList(SenderShard& shard,
std::vector<PendingOp>& batch_out,
size_t offset, size_t max_count,
bool from_recovery) {
auto& head = from_recovery ? shard.recovery_head : shard.normal_head;
auto& count = from_recovery ? shard.recovery_count : shard.normal_count;
size_t collected = 0;
while (count > 0 && collected < max_count) {
size_t idx = head;
auto& slot = shard.slots[idx];
// Remove from coalesce index
CoalesceKey ck{slot.op.key, slot.op.segment_id};
auto ci = shard.coalesce_index.find(ck);
if (ci != shard.coalesce_index.end()) {
if (slot.op.type == PendingOp::ADD) {
ci->second.add_idx = InvalidIdx;
} else {
ci->second.remove_idx = InvalidIdx;
}
if (ci->second.add_idx == InvalidIdx &&
ci->second.remove_idx == InvalidIdx) {
shard.coalesce_index.erase(ci);
}
}
batch_out[offset + collected] = std::move(slot.op);
shard.Unlink(idx);
shard.FreeSlot(idx);
collected++;
}
return collected;
}
void AsyncMetadataNotifier::SendBatch(std::vector<PendingOp>& batch,
size_t count) {
BatchSyncReplicaRequest req;
req.client_id = client_id_;
req.add_keys.reserve(count);
req.add_sizes.reserve(count);
req.add_segment_ids.reserve(count);
req.remove_keys.reserve(count);
req.remove_segment_ids.reserve(count);
for (size_t i = 0; i < count; ++i) {
auto& op = batch[i];
if (op.type == PendingOp::ADD) {
req.add_keys.push_back(std::move(op.key));
req.add_sizes.push_back(op.size);
req.add_segment_ids.push_back(op.segment_id);
} else {
req.remove_keys.push_back(std::move(op.key));
req.remove_segment_ids.push_back(op.segment_id);
}
}
for (int attempt = 0; attempt < MaxRetryCount; ++attempt) {
auto result = master_client_.BatchSyncReplica(req);
if (result.has_value()) {
RecordSuccess();
auto& resp = result.value();
for (size_t i = 0; i < resp.add_results.size(); ++i) {
auto ec = resp.add_results[i];
if (ec != ErrorCode::OK &&
ec != ErrorCode::REPLICA_ALREADY_EXISTS) {
LOG(WARNING)
<< "BatchSyncReplica ADD key=" << req.add_keys[i]
<< " failed: " << toString(ec);
if (failure_cb_) {
failure_cb_(req.add_keys[i], req.add_segment_ids[i],
ec);
}
}
}
for (size_t i = 0; i < resp.remove_results.size(); ++i) {
if (resp.remove_results[i] != ErrorCode::OK) {
LOG(WARNING)
<< "BatchSyncReplica REMOVE key=" << req.remove_keys[i]
<< " failed: " << toString(resp.remove_results[i]);
}
}
return;
}
LOG(WARNING) << "BatchSyncReplica attempt " << attempt
<< " failed: " << toString(result.error());
if (attempt < MaxRetryCount - 1) {
// Use CV wait instead of sleep so Stop() can interrupt promptly
auto backoff = std::chrono::milliseconds(100 * (1 << attempt));
std::unique_lock<std::mutex> lk{stop_mutex_};
if (stop_cv_.wait_for(lk, backoff, [&] {
return !running_.load(std::memory_order_relaxed);
})) {
// Stop requested — abort retry, drop this batch
break;
}
}
}
RecordFailure();
LOG(ERROR) << "BatchSyncReplica failed after " << MaxRetryCount + 1
<< " attempts, dropping " << count << " ops";
// Rollback: notify failure for all ADD ops so local replicas get cleaned up
if (failure_cb_) {
for (size_t i = 0; i < req.add_keys.size(); ++i) {
failure_cb_(req.add_keys[i], req.add_segment_ids[i],
ErrorCode::INTERNAL_ERROR);
}
}
}
void AsyncMetadataNotifier::RecordSuccess() {
int32_t prev =
consecutive_rpc_failures_.exchange(0, std::memory_order_acq_rel);
if (prev < 0) {
LOG(INFO) << "AsyncMetadataNotifier: circuit breaker closed";
}
}
void AsyncMetadataNotifier::RecordFailure() {
int32_t old_val = consecutive_rpc_failures_.load(std::memory_order_acquire);
while (true) {
if (old_val < 0) return; // already paused, nothing to do
int32_t new_val = old_val + 1;
if (new_val >= CircuitBreakerThreshold) {
new_val = -new_val; // flip to paused
}
if (consecutive_rpc_failures_.compare_exchange_weak(
old_val, new_val, std::memory_order_acq_rel,
std::memory_order_acquire)) {
if (new_val < 0) {
LOG(ERROR)
<< "AsyncMetadataNotifier: circuit breaker OPEN after "
<< -new_val << " consecutive failures";
}
return;
}
// old_val updated by CAS, retry
}
}
bool AsyncMetadataNotifier::WaitForRecoveryDrain(
const std::function<bool()>& abort_fn, std::chrono::milliseconds timeout) {
auto deadline = std::chrono::steady_clock::now() + timeout;
// Wait for each shard's recovery queue to drain independently.
for (size_t i = 0; i < sender_thread_count_; ++i) {
auto& shard = *shards_[i];
std::unique_lock<std::mutex> lock(shard.mutex);
while (shard.recovery_count > 0 || shard.recovery_in_flight > 0) {
if (abort_fn && abort_fn()) return false;
auto remaining = deadline - std::chrono::steady_clock::now();
if (remaining <= std::chrono::milliseconds::zero()) {
LOG(WARNING) << "WaitForRecoveryDrain timed out, shard=" << i
<< ", remaining_count=" << shard.recovery_count;
return false;
}
auto wait_time = std::min(
remaining,
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::milliseconds(100)));
shard.recovery_drain_cv.wait_for(lock, wait_time);
}
}
return true;
}
} // namespace mooncake

View File

@ -0,0 +1,157 @@
#include "centralized_client_manager.h"
#include "centralized_client_meta.h"
#include <glog/logging.h>
namespace mooncake {
CentralizedClientManager::CentralizedClientManager(
const int64_t client_live_ttl_sec, const int64_t client_crashed_ttl_sec,
const BufferAllocatorType memory_allocator_type,
const ViewVersionId view_version)
: ClientManager(client_live_ttl_sec, client_crashed_ttl_sec, view_version),
memory_allocator_type_(memory_allocator_type),
allocation_strategy_(std::make_shared<RandomAllocationStrategy>()) {}
auto CentralizedClientManager::MountLocalDiskSegment(const UUID& client_id,
bool enable_offloading)
-> tl::expected<void, ErrorCode> {
auto client_meta = GetClient(client_id);
if (!client_meta) {
LOG(WARNING) << "MountLocalDiskSegment: client not found, client_id="
<< client_id;
return tl::make_unexpected(ErrorCode::CLIENT_NOT_FOUND);
}
auto centralized_meta =
std::static_pointer_cast<CentralizedClientMeta>(client_meta);
if (!centralized_meta) {
LOG(ERROR) << "MountLocalDiskSegment: client meta type mismatch";
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
}
auto ret = centralized_meta->MountLocalDiskSegment(enable_offloading);
if (!ret.has_value()) {
LOG(ERROR) << "fail to mount local disk segment"
<< ", client_id=" << client_id << ", ret=" << ret.error();
return ret;
}
return {};
}
auto CentralizedClientManager::OffloadObjectHeartbeat(const UUID& client_id,
bool enable_offloading)
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode> {
auto client_meta = GetClient(client_id);
if (!client_meta) {
LOG(WARNING) << "OffloadObjectHeartbeat: client not found, client_id="
<< client_id;
return tl::make_unexpected(ErrorCode::CLIENT_NOT_FOUND);
}
auto centralized_meta =
std::static_pointer_cast<CentralizedClientMeta>(client_meta);
if (!centralized_meta) {
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
}
auto ret = centralized_meta->OffloadObjectHeartbeat(enable_offloading);
if (!ret.has_value()) {
LOG(ERROR) << "fail to offload object heartbeat"
<< ", client_id=" << client_id << ", ret=" << ret.error();
return ret;
}
return ret;
}
auto CentralizedClientManager::PushOffloadingQueue(
const std::string& key, const int64_t size, const std::string& segment_name)
-> tl::expected<void, ErrorCode> {
SharedMutexLocker lock(&clients_mutex_, shared_lock);
// Find which client owns this segment
for (auto& [id, meta] : client_metas_) {
// QuerySegments in ClientMeta checks whether segment exists
auto query_res = meta->QuerySegments(segment_name);
if (query_res.has_value()) {
auto centralized_meta =
std::static_pointer_cast<CentralizedClientMeta>(meta);
if (centralized_meta) {
auto ret = centralized_meta->PushOffloadingQueue(key, size,
segment_name);
if (!ret.has_value()) {
LOG(ERROR) << "fail to push offloading queue"
<< ", key=" << key << ", size=" << size
<< ", segment_name=" << segment_name
<< ", ret=" << ret.error();
return ret;
}
return {};
}
}
}
LOG(ERROR) << "Segment not found for offloading: " << segment_name;
return tl::make_unexpected(ErrorCode::SEGMENT_NOT_FOUND);
}
std::shared_ptr<ClientMeta> CentralizedClientManager::CreateClientMeta(
const RegisterClientRequest& req) {
auto meta = std::make_shared<CentralizedClientMeta>(req.client_id,
memory_allocator_type_);
// Register allocator change callback to sync global_allocator_manager_
auto seg_mgr = std::static_pointer_cast<CentralizedSegmentManager>(
meta->GetSegmentManager());
if (seg_mgr) {
seg_mgr->SetAllocatorChangeCallback(
[this](const std::string& segment_name,
const std::shared_ptr<BufferAllocatorBase>& allocator,
bool is_add) -> tl::expected<void, ErrorCode> {
SharedMutexLocker lock(&global_allocator_mutex_);
if (is_add) {
global_allocator_manager_.addAllocator(segment_name,
allocator);
} else if (!global_allocator_manager_.removeAllocator(
segment_name, allocator)) {
LOG(WARNING)
<< "Failed to remove allocator: " << segment_name;
}
return {};
});
}
return meta;
}
auto CentralizedClientManager::Allocate(
const uint64_t slice_length, const size_t replica_num,
const std::vector<std::string>& preferred_segments)
-> tl::expected<std::vector<Replica>, ErrorCode> {
SharedMutexLocker lock(&global_allocator_mutex_, shared_lock);
// WARNING: This function must NOT acquire segment_mutex_ which is in the
// segment manager of client_meta.
// For example, in SegmentManager's MountSegment/UnmountSegment():
// 1. SegmentManager acquires SegmentManager lock (L1) at first
// 2. Then it calls the allocator change callback, which acquires the
// GlobalAllocatorManager lock (L2)
// 3. Then, in CentralizedClientManager::Allocate, it acquires L2 at first.
// Thus, if it attempts to acquire L1 here, it would cause a deadlock
// (L2 -> L1 vs L1 -> L2).
auto result =
allocation_strategy_->Allocate(global_allocator_manager_, slice_length,
replica_num, preferred_segments);
if (!result.has_value()) {
LOG(WARNING) << "No available replicas"
<< ", slice_length=" << slice_length
<< ", replica_num=" << replica_num;
}
return result;
}
HeartbeatTaskResult CentralizedClientManager::ProcessTask(
const UUID& client_id, const HeartbeatTask& task) {
HeartbeatTaskResult result;
result.type = task.type_;
result.error = ErrorCode::NOT_IMPLEMENTED;
return result;
}
} // namespace mooncake

View File

@ -0,0 +1,90 @@
#include "centralized_client_meta.h"
#include <glog/logging.h>
namespace mooncake {
CentralizedClientMeta::CentralizedClientMeta(const UUID& client_id,
BufferAllocatorType allocator_type)
: ClientMeta(client_id) {
segment_manager_ =
std::make_shared<CentralizedSegmentManager>(allocator_type);
}
std::shared_ptr<SegmentManager> CentralizedClientMeta::GetSegmentManager() {
return segment_manager_;
}
std::shared_ptr<CentralizedSegmentManager>
CentralizedClientMeta::GetCentralizedSegmentManager() {
return segment_manager_;
}
tl::expected<std::vector<std::string>, ErrorCode>
CentralizedClientMeta::QueryIp(const UUID& client_id) {
SharedMutexLocker lock(&client_mutex_, shared_lock);
auto check_ret = InnerStatusCheck();
if (!check_ret.has_value()) {
LOG(ERROR) << "fail to inner check client status"
<< ", client_id=" << client_id_
<< ", ret=" << check_ret.error();
return tl::make_unexpected(check_ret.error());
}
return segment_manager_->QueryIp();
}
tl::expected<void, ErrorCode> CentralizedClientMeta::MountLocalDiskSegment(
bool enable_offloading) {
SharedMutexLocker lock(&client_mutex_, shared_lock);
auto check_ret = InnerStatusCheck();
if (!check_ret.has_value()) {
LOG(ERROR) << "fail to inner check client status"
<< ", client_id=" << client_id_
<< ", ret=" << check_ret.error();
return check_ret;
}
return segment_manager_->MountLocalDiskSegment(enable_offloading);
}
auto CentralizedClientMeta::OffloadObjectHeartbeat(bool enable_offloading)
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode> {
SharedMutexLocker lock(&client_mutex_, shared_lock);
auto check_ret = InnerStatusCheck();
if (!check_ret.has_value()) {
LOG(ERROR) << "fail to inner check client status"
<< ", client_id=" << client_id_
<< ", ret=" << check_ret.error();
return tl::make_unexpected(check_ret.error());
}
return segment_manager_->OffloadObjectHeartbeat(enable_offloading);
}
tl::expected<void, ErrorCode> CentralizedClientMeta::PushOffloadingQueue(
const std::string& key, const int64_t size,
const std::string& segment_name) {
SharedMutexLocker lock(&client_mutex_, shared_lock);
auto check_ret = InnerStatusCheck();
if (!check_ret.has_value()) {
LOG(ERROR) << "fail to inner check client status"
<< ", client_id=" << client_id_
<< ", ret=" << check_ret.error();
return check_ret;
}
return segment_manager_->PushOffloadingQueue(key, size, segment_name);
}
void CentralizedClientMeta::DoOnDisconnected() {
auto ret = segment_manager_->SetGlobalVisibility(false);
if (!ret.has_value()) {
LOG(ERROR) << "Failed to hide allocators for client " << client_id_
<< " error=" << ret.error();
}
}
void CentralizedClientMeta::DoOnRecovered() {
auto ret = segment_manager_->SetGlobalVisibility(true);
if (!ret.has_value()) {
LOG(ERROR) << "Failed to show allocators for client " << client_id_
<< " error=" << ret.error();
}
}
} // namespace mooncake

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,233 @@
#include "centralized_master_client.h"
#include "centralized_rpc_service.h"
#include "utils/scoped_vlog_timer.h"
namespace mooncake {
template <>
struct RpcNameTraits<&WrappedCentralizedMasterService::PutStart> {
static constexpr const char* value = "PutStart";
};
template <>
struct RpcNameTraits<&WrappedCentralizedMasterService::BatchPutStart> {
static constexpr const char* value = "BatchPutStart";
};
template <>
struct RpcNameTraits<&WrappedCentralizedMasterService::PutEnd> {
static constexpr const char* value = "PutEnd";
};
template <>
struct RpcNameTraits<&WrappedCentralizedMasterService::BatchPutEnd> {
static constexpr const char* value = "BatchPutEnd";
};
template <>
struct RpcNameTraits<&WrappedCentralizedMasterService::PutRevoke> {
static constexpr const char* value = "PutRevoke";
};
template <>
struct RpcNameTraits<&WrappedCentralizedMasterService::BatchPutRevoke> {
static constexpr const char* value = "BatchPutRevoke";
};
template <>
struct RpcNameTraits<&WrappedCentralizedMasterService::BatchReplicaClear> {
static constexpr const char* value = "BatchReplicaClear";
};
template <>
struct RpcNameTraits<&WrappedCentralizedMasterService::GetFsdir> {
static constexpr const char* value = "GetFsdir";
};
template <>
struct RpcNameTraits<&WrappedCentralizedMasterService::GetStorageConfig> {
static constexpr const char* value = "GetStorageConfig";
};
template <>
struct RpcNameTraits<&WrappedCentralizedMasterService::MountLocalDiskSegment> {
static constexpr const char* value = "MountLocalDiskSegment";
};
template <>
struct RpcNameTraits<&WrappedCentralizedMasterService::OffloadObjectHeartbeat> {
static constexpr const char* value = "OffloadObjectHeartbeat";
};
template <>
struct RpcNameTraits<&WrappedCentralizedMasterService::NotifyOffloadSuccess> {
static constexpr const char* value = "NotifyOffloadSuccess";
};
tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
CentralizedMasterClient::PutStart(const std::string& key,
const std::vector<size_t>& slice_lengths,
const ReplicateConfig& config) {
ScopedVLogTimer timer(1, "CentralizedMasterClient::PutStart");
timer.LogRequest("key=", key, ", slice_count=", slice_lengths.size());
uint64_t total_slice_length = 0;
for (const auto& slice_length : slice_lengths) {
total_slice_length += slice_length;
}
auto result = invoke_rpc<&WrappedCentralizedMasterService::PutStart,
std::vector<Replica::Descriptor>>(
client_id_, key, total_slice_length, config);
timer.LogResponseExpected(result);
return result;
}
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
CentralizedMasterClient::BatchPutStart(
const std::vector<std::string>& keys,
const std::vector<std::vector<uint64_t>>& batch_slice_lengths,
const ReplicateConfig& config) {
ScopedVLogTimer timer(1, "CentralizedMasterClient::BatchPutStart");
timer.LogRequest("keys_count=", keys.size());
std::vector<uint64_t> total_slice_lengths;
total_slice_lengths.reserve(batch_slice_lengths.size());
for (const auto& slice_lengths : batch_slice_lengths) {
uint64_t total_slice_length = 0;
for (const auto& slice_length : slice_lengths) {
total_slice_length += slice_length;
}
total_slice_lengths.emplace_back(total_slice_length);
}
auto result =
invoke_batch_rpc<&WrappedCentralizedMasterService::BatchPutStart,
std::vector<Replica::Descriptor>>(
keys.size(), client_id_, keys, total_slice_lengths, config);
timer.LogResponse("result=", result.size(), " operations");
return result;
}
tl::expected<void, ErrorCode> CentralizedMasterClient::PutEnd(
const std::string& key, ReplicaType replica_type) {
ScopedVLogTimer timer(1, "CentralizedMasterClient::PutEnd");
timer.LogRequest("key=", key);
auto result = invoke_rpc<&WrappedCentralizedMasterService::PutEnd, void>(
client_id_, key, replica_type);
timer.LogResponseExpected(result);
return result;
}
std::vector<tl::expected<void, ErrorCode>> CentralizedMasterClient::BatchPutEnd(
const std::vector<std::string>& keys) {
ScopedVLogTimer timer(1, "CentralizedMasterClient::BatchPutEnd");
timer.LogRequest("keys_count=", keys.size());
auto result =
invoke_batch_rpc<&WrappedCentralizedMasterService::BatchPutEnd, void>(
keys.size(), client_id_, keys);
timer.LogResponse("result=", result.size(), " operations");
return result;
}
tl::expected<void, ErrorCode> CentralizedMasterClient::PutRevoke(
const std::string& key, ReplicaType replica_type) {
ScopedVLogTimer timer(1, "CentralizedMasterClient::PutRevoke");
timer.LogRequest("key=", key);
auto result = invoke_rpc<&WrappedCentralizedMasterService::PutRevoke, void>(
client_id_, key, replica_type);
timer.LogResponseExpected(result);
return result;
}
std::vector<tl::expected<void, ErrorCode>>
CentralizedMasterClient::BatchPutRevoke(const std::vector<std::string>& keys) {
ScopedVLogTimer timer(1, "CentralizedMasterClient::BatchPutRevoke");
timer.LogRequest("keys_count=", keys.size());
auto result =
invoke_batch_rpc<&WrappedCentralizedMasterService::BatchPutRevoke,
void>(keys.size(), client_id_, keys);
timer.LogResponse("result=", result.size(), " operations");
return result;
}
tl::expected<std::vector<std::string>, ErrorCode>
CentralizedMasterClient::BatchReplicaClear(
const std::vector<std::string>& object_keys, const UUID& client_id,
const std::string& segment_name) {
ScopedVLogTimer timer(1, "CentralizedMasterClient::BatchReplicaClear");
timer.LogRequest("object_keys_count=", object_keys.size(),
", client_id=", client_id,
", segment_name=", segment_name);
auto result =
invoke_rpc<&WrappedCentralizedMasterService::BatchReplicaClear,
std::vector<std::string>>(object_keys, client_id,
segment_name);
timer.LogResponseExpected(result);
return result;
}
tl::expected<std::string, ErrorCode> CentralizedMasterClient::GetFsdir() {
ScopedVLogTimer timer(1, "CentralizedMasterClient::GetFsdir");
timer.LogRequest("action=get_fsdir");
auto result =
invoke_rpc<&WrappedCentralizedMasterService::GetFsdir, std::string>();
timer.LogResponseExpected(result);
return result;
}
tl::expected<GetStorageConfigResponse, ErrorCode>
CentralizedMasterClient::GetStorageConfig() {
ScopedVLogTimer timer(1, "CentralizedMasterClient::GetStorageConfig");
timer.LogRequest("action=get_storage_config");
auto result = invoke_rpc<&WrappedCentralizedMasterService::GetStorageConfig,
GetStorageConfigResponse>();
timer.LogResponseExpected(result);
return result;
}
tl::expected<void, ErrorCode> CentralizedMasterClient::MountLocalDiskSegment(
const UUID& client_id, bool enable_offloading) {
ScopedVLogTimer timer(1, "CentralizedMasterClient::MountLocalDiskSegment");
timer.LogRequest("client_id=", client_id,
", enable_offloading=", enable_offloading);
auto result =
invoke_rpc<&WrappedCentralizedMasterService::MountLocalDiskSegment,
void>(client_id, enable_offloading);
timer.LogResponseExpected(result);
return result;
}
tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>
CentralizedMasterClient::OffloadObjectHeartbeat(const UUID& client_id,
bool enable_offloading) {
ScopedVLogTimer timer(1, "CentralizedMasterClient::OffloadObjectHeartbeat");
timer.LogRequest("client_id=", client_id,
", enable_offloading=", enable_offloading);
auto result =
invoke_rpc<&WrappedCentralizedMasterService::OffloadObjectHeartbeat,
std::unordered_map<std::string, int64_t>>(client_id,
enable_offloading);
return result;
}
tl::expected<void, ErrorCode> CentralizedMasterClient::NotifyOffloadSuccess(
const UUID& client_id, const std::vector<std::string>& keys,
const std::vector<StorageObjectMetadata>& metadatas) {
ScopedVLogTimer timer(1, "CentralizedMasterClient::NotifyOffloadSuccess");
timer.LogRequest("client_id=", client_id, ", keys_count=", keys.size(),
", metadatas_count=", metadatas.size());
auto result =
invoke_rpc<&WrappedCentralizedMasterService::NotifyOffloadSuccess,
void>(client_id, keys, metadatas);
timer.LogResponseExpected(result);
return result;
}
} // namespace mooncake

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,436 @@
#include "centralized_rpc_service.h"
#include "rpc_helper.h"
namespace mooncake {
WrappedCentralizedMasterService::WrappedCentralizedMasterService(
const WrappedMasterServiceConfig& config)
: WrappedMasterService(config),
master_service_(MasterServiceConfig(config)) {
init_centralized_http_server();
}
void WrappedCentralizedMasterService::init_centralized_http_server() {
using namespace coro_http;
http_server_.set_http_handler<GET>(
"/get_all_segments",
[&](coro_http_request& req, coro_http_response& resp) {
resp.add_header("Content-Type", "text/plain; version=0.0.4");
auto result = master_service_.GetAllSegments();
if (result) {
std::string ss = "";
auto segments = result.value();
for (const auto& segment_name : segments) {
ss += segment_name;
ss += "\n";
}
resp.set_status_and_content(status_type::ok, std::move(ss));
} else {
resp.set_status_and_content(status_type::internal_server_error,
"Failed to get all segments");
}
});
http_server_.set_http_handler<GET>(
"/query_segment",
[&](coro_http_request& req, coro_http_response& resp) {
auto segment = req.get_query_value("segment");
resp.add_header("Content-Type", "text/plain; version=0.0.4");
auto result = master_service_.QuerySegments(std::string(segment));
if (result) {
std::string ss = "";
auto [used, capacity] = result.value();
ss += segment;
ss += "\n";
ss += "Used(bytes): ";
ss += std::to_string(used);
ss += "\nCapacity(bytes) : ";
ss += std::to_string(capacity);
ss += "\n";
resp.set_status_and_content(status_type::ok, std::move(ss));
} else {
resp.set_status_and_content(status_type::internal_server_error,
"Failed to query segment");
}
});
http_server_.set_http_handler<GET>(
"/query_key", [&](coro_http_request& req, coro_http_response& resp) {
auto key = req.get_query_value("key");
auto get_result = master_service_.GetReplicaList(
std::string(key),
GetReplicaListRequestConfig(
GetReplicaListRequestConfig::RETURN_ALL_CANDIDATES));
resp.add_header("Content-Type", "text/plain; version=0.0.4");
if (get_result) {
std::string ss = "";
const std::vector<Replica::Descriptor>& replicas =
get_result.value().replicas;
for (size_t i = 0; i < replicas.size(); i++) {
if (replicas[i].is_memory_replica()) {
auto& memory_descriptors =
replicas[i].get_memory_descriptor();
std::string tmp = "";
struct_json::to_json(
memory_descriptors.buffer_descriptor, tmp);
ss += tmp;
ss += "\n";
}
}
resp.set_status_and_content(status_type::ok, std::move(ss));
} else {
resp.set_status_and_content(status_type::not_found,
toString(get_result.error()));
}
});
LOG(INFO) << "Centralized HTTP handlers initialized";
}
tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
WrappedCentralizedMasterService::PutStart(const UUID& client_id,
const std::string& key,
const uint64_t slice_length,
const ReplicateConfig& config) {
return execute_rpc(
"PutStart",
[&] {
return master_service_.PutStart(client_id, key, slice_length,
config);
},
[&](auto& timer) {
timer.LogRequest("client_id=", client_id, ", key=", key,
", slice_length=", slice_length);
},
[&] { MasterMetricManager::instance().inc_put_start_requests(); },
[] { MasterMetricManager::instance().inc_put_start_failures(); });
}
tl::expected<void, ErrorCode> WrappedCentralizedMasterService::PutEnd(
const UUID& client_id, const std::string& key, ReplicaType replica_type) {
return execute_rpc(
"PutEnd",
[&] { return master_service_.PutEnd(client_id, key, replica_type); },
[&](auto& timer) {
timer.LogRequest("client_id=", client_id, ", key=", key,
", replica_type=", replica_type);
},
[] { MasterMetricManager::instance().inc_put_end_requests(); },
[] { MasterMetricManager::instance().inc_put_end_failures(); });
}
tl::expected<void, ErrorCode> WrappedCentralizedMasterService::PutRevoke(
const UUID& client_id, const std::string& key, ReplicaType replica_type) {
return execute_rpc(
"PutRevoke",
[&] { return master_service_.PutRevoke(client_id, key, replica_type); },
[&](auto& timer) {
timer.LogRequest("client_id=", client_id, ", key=", key,
", replica_type=", replica_type);
},
[] { MasterMetricManager::instance().inc_put_revoke_requests(); },
[] { MasterMetricManager::instance().inc_put_revoke_failures(); });
}
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
WrappedCentralizedMasterService::BatchPutStart(
const UUID& client_id, const std::vector<std::string>& keys,
const std::vector<uint64_t>& slice_lengths, const ReplicateConfig& config) {
ScopedVLogTimer timer(1, "BatchPutStart");
const size_t total_keys = keys.size();
timer.LogRequest("client_id=", client_id, ", keys_count=", total_keys);
MasterMetricManager::instance().inc_batch_put_start_requests(total_keys);
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
results;
results.reserve(keys.size());
if (config.prefer_alloc_in_same_node) {
ReplicateConfig new_config = config;
for (size_t i = 0; i < keys.size(); ++i) {
auto result = master_service_.PutStart(
client_id, keys[i], slice_lengths[i], new_config);
results.emplace_back(result);
if ((i == 0) && result.has_value()) {
std::string preferred_segment;
for (const auto& replica : result.value()) {
if (replica.is_memory_replica()) {
auto handles =
replica.get_memory_descriptor().buffer_descriptor;
if (!handles.transport_endpoint_.empty()) {
preferred_segment = handles.transport_endpoint_;
}
}
}
if (!preferred_segment.empty()) {
new_config.preferred_segment = preferred_segment;
}
}
}
} else {
for (size_t i = 0; i < keys.size(); ++i) {
results.emplace_back(master_service_.PutStart(
client_id, keys[i], slice_lengths[i], config));
}
}
size_t failure_count = 0;
int no_available_handle_count = 0;
for (size_t i = 0; i < results.size(); ++i) {
if (!results[i].has_value()) {
failure_count++;
auto error = results[i].error();
if (error == ErrorCode::OBJECT_ALREADY_EXISTS) {
VLOG(1) << "BatchPutStart failed for key[" << i << "] '"
<< keys[i] << "': " << toString(error);
} else if (error == ErrorCode::NO_AVAILABLE_HANDLE) {
no_available_handle_count++;
} else {
LOG(ERROR) << "BatchPutStart failed for key[" << i << "] '"
<< keys[i] << "': " << toString(error);
}
}
}
if (no_available_handle_count > 0) {
LOG(WARNING) << "BatchPutStart failed for " << no_available_handle_count
<< " keys" << PUT_NO_SPACE_HELPER_STR;
}
if (failure_count == total_keys) {
MasterMetricManager::instance().inc_batch_put_start_failures(
failure_count);
} else if (failure_count != 0) {
MasterMetricManager::instance().inc_batch_put_start_partial_success(
failure_count);
}
timer.LogResponse("total=", results.size(),
", success=", results.size() - failure_count,
", failures=", failure_count);
return results;
}
std::vector<tl::expected<void, ErrorCode>>
WrappedCentralizedMasterService::BatchPutEnd(
const UUID& client_id, const std::vector<std::string>& keys) {
ScopedVLogTimer timer(1, "BatchPutEnd");
const size_t total_keys = keys.size();
timer.LogRequest("client_id=", client_id, ", keys_count=", total_keys);
MasterMetricManager::instance().inc_batch_put_end_requests(total_keys);
std::vector<tl::expected<void, ErrorCode>> results;
results.reserve(keys.size());
for (const auto& key : keys) {
results.emplace_back(
master_service_.PutEnd(client_id, key, ReplicaType::MEMORY));
}
size_t failure_count = 0;
for (size_t i = 0; i < results.size(); ++i) {
if (!results[i].has_value()) {
failure_count++;
auto error = results[i].error();
LOG(ERROR) << "BatchPutEnd failed for key[" << i << "] '" << keys[i]
<< "': " << toString(error);
}
}
if (failure_count == total_keys) {
MasterMetricManager::instance().inc_batch_put_end_failures(
failure_count);
} else if (failure_count != 0) {
MasterMetricManager::instance().inc_batch_put_end_partial_success(
failure_count);
}
timer.LogResponse("total=", results.size(),
", success=", results.size() - failure_count,
", failures=", failure_count);
return results;
}
std::vector<tl::expected<void, ErrorCode>>
WrappedCentralizedMasterService::BatchPutRevoke(
const UUID& client_id, const std::vector<std::string>& keys) {
ScopedVLogTimer timer(1, "BatchPutRevoke");
const size_t total_keys = keys.size();
timer.LogRequest("client_id=", client_id, ", keys_count=", total_keys);
MasterMetricManager::instance().inc_batch_put_revoke_requests(total_keys);
std::vector<tl::expected<void, ErrorCode>> results;
results.reserve(keys.size());
for (const auto& key : keys) {
results.emplace_back(
master_service_.PutRevoke(client_id, key, ReplicaType::MEMORY));
}
size_t failure_count = 0;
for (size_t i = 0; i < results.size(); ++i) {
if (!results[i].has_value()) {
failure_count++;
auto error = results[i].error();
LOG(ERROR) << "BatchPutRevoke failed for key[" << i << "] '"
<< keys[i] << "': " << toString(error);
}
}
if (failure_count == total_keys) {
MasterMetricManager::instance().inc_batch_put_revoke_failures(
failure_count);
} else if (failure_count != 0) {
MasterMetricManager::instance().inc_batch_put_revoke_partial_success(
failure_count);
}
timer.LogResponse("total=", results.size(),
", success=", results.size() - failure_count,
", failures=", failure_count);
return results;
}
tl::expected<std::vector<std::string>, ErrorCode>
WrappedCentralizedMasterService::BatchReplicaClear(
const std::vector<std::string>& object_keys, const UUID& client_id,
const std::string& segment_name) {
ScopedVLogTimer timer(1, "BatchReplicaClear");
const size_t total_keys = object_keys.size();
timer.LogRequest("object_keys_count=", total_keys,
", client_id=", client_id,
", segment_name=", segment_name);
MasterMetricManager::instance().inc_batch_replica_clear_requests(
total_keys);
auto result =
master_service_.BatchReplicaClear(object_keys, client_id, segment_name);
size_t failure_count = 0;
if (!result.has_value()) {
failure_count = total_keys;
LOG(WARNING) << "BatchReplicaClear failed: "
<< toString(result.error());
} else {
const size_t cleared_count = result.value().size();
failure_count = total_keys - cleared_count;
timer.LogResponse("total=", total_keys, ", cleared=", cleared_count,
", failed=", failure_count);
}
if (failure_count == total_keys) {
MasterMetricManager::instance().inc_batch_replica_clear_failures(
failure_count);
} else if (failure_count != 0) {
MasterMetricManager::instance().inc_batch_replica_clear_partial_success(
failure_count);
}
timer.LogResponseExpected(result);
return result;
}
tl::expected<std::string, ErrorCode>
WrappedCentralizedMasterService::GetFsdir() {
ScopedVLogTimer timer(1, "GetFsdir");
timer.LogRequest("action=get_fsdir");
auto result = master_service_.GetFsdir();
timer.LogResponseExpected(result);
return result;
}
tl::expected<GetStorageConfigResponse, ErrorCode>
WrappedCentralizedMasterService::GetStorageConfig() {
ScopedVLogTimer timer(1, "GetStorageConfig");
timer.LogRequest("action=get_storage_config");
auto result = master_service_.GetStorageConfig();
timer.LogResponseExpected(result);
return result;
}
tl::expected<void, ErrorCode>
WrappedCentralizedMasterService::MountLocalDiskSegment(const UUID& client_id,
bool enable_offloading) {
ScopedVLogTimer timer(1, "MountLocalDiskSegment");
timer.LogRequest("action=mount_local_disk_segment");
LOG(INFO) << "Mount local disk segment with client id is : " << client_id
<< ", enable offloading is: " << enable_offloading;
auto result =
master_service_.MountLocalDiskSegment(client_id, enable_offloading);
timer.LogResponseExpected(result);
return result;
}
tl::expected<std::unordered_map<std::string, int64_t, std::hash<std::string>>,
ErrorCode>
WrappedCentralizedMasterService::OffloadObjectHeartbeat(
const UUID& client_id, bool enable_offloading) {
ScopedVLogTimer timer(1, "OffloadObjectHeartbeat");
timer.LogRequest("action=offload_object_heartbeat");
auto result =
master_service_.OffloadObjectHeartbeat(client_id, enable_offloading);
return result;
}
tl::expected<void, ErrorCode>
WrappedCentralizedMasterService::NotifyOffloadSuccess(
const UUID& client_id, const std::vector<std::string>& keys,
const std::vector<StorageObjectMetadata>& metadatas) {
ScopedVLogTimer timer(1, "NotifyOffloadSuccess");
timer.LogRequest("action=notify_offload_success");
auto result =
master_service_.NotifyOffloadSuccess(client_id, keys, metadatas);
timer.LogResponseExpected(result);
return result;
}
void RegisterCentralizedRpcService(
coro_rpc::coro_rpc_server& server,
mooncake::WrappedCentralizedMasterService& wrapped_master_service) {
RegisterRpcService(server, wrapped_master_service);
server.register_handler<
&mooncake::WrappedCentralizedMasterService::BatchReplicaClear>(
&wrapped_master_service);
server
.register_handler<&mooncake::WrappedCentralizedMasterService::PutStart>(
&wrapped_master_service);
server.register_handler<&mooncake::WrappedCentralizedMasterService::PutEnd>(
&wrapped_master_service);
server.register_handler<
&mooncake::WrappedCentralizedMasterService::PutRevoke>(
&wrapped_master_service);
server.register_handler<
&mooncake::WrappedCentralizedMasterService::BatchPutStart>(
&wrapped_master_service);
server.register_handler<
&mooncake::WrappedCentralizedMasterService::BatchPutEnd>(
&wrapped_master_service);
server.register_handler<
&mooncake::WrappedCentralizedMasterService::BatchPutRevoke>(
&wrapped_master_service);
server
.register_handler<&mooncake::WrappedCentralizedMasterService::GetFsdir>(
&wrapped_master_service);
server.register_handler<
&mooncake::WrappedCentralizedMasterService::GetStorageConfig>(
&wrapped_master_service);
server.register_handler<
&mooncake::WrappedCentralizedMasterService::MountLocalDiskSegment>(
&wrapped_master_service);
server.register_handler<
&mooncake::WrappedCentralizedMasterService::OffloadObjectHeartbeat>(
&wrapped_master_service);
server.register_handler<
&mooncake::WrappedCentralizedMasterService::NotifyOffloadSuccess>(
&wrapped_master_service);
}
} // namespace mooncake

View File

@ -0,0 +1,279 @@
#include "centralized_segment_manager.h"
#include <unordered_set>
#include <glog/logging.h>
namespace mooncake {
void CentralizedSegmentManager::SetAllocatorChangeCallback(
AllocatorChangeCallback cb) {
allocator_change_cb_ = std::move(cb);
}
tl::expected<void, ErrorCode> CentralizedSegmentManager::SetGlobalVisibility(
bool visible) {
SharedMutexLocker lock_(&segment_mutex_, shared_lock);
if (!allocator_change_cb_) {
LOG(ERROR) << "allocator_change_cb_ is null";
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
ErrorCode first_error = ErrorCode::OK;
for (const auto& entry : mounted_segments_) {
auto mounted_seg =
std::static_pointer_cast<MountedCentralizedSegment>(entry.second);
if (mounted_seg->buf_allocator) {
auto ret = allocator_change_cb_(
mounted_seg->name, mounted_seg->buf_allocator, visible);
if (!ret.has_value()) {
LOG(ERROR) << "SetGlobalVisibility failed for segment="
<< mounted_seg->name << " visible=" << visible
<< " error=" << ret.error();
if (first_error == ErrorCode::OK) {
first_error = ret.error();
}
}
}
}
if (first_error != ErrorCode::OK) {
return tl::make_unexpected(first_error);
}
return {};
}
ErrorCode CentralizedSegmentManager::InnerCheckMountSegment(
const Segment& segment) {
if (!segment.IsCentralizedSegment()) {
LOG(ERROR) << "segment is not centralized";
return ErrorCode::INVALID_PARAMS;
}
const uintptr_t buffer = segment.GetCentralizedExtra().base;
const size_t size = segment.size;
// Check if parameters are valid before allocating memory.
if (buffer == 0 || size == 0) {
LOG(ERROR) << "buffer=" << buffer << " or size=" << size
<< " is invalid";
return ErrorCode::INVALID_PARAMS;
}
if (memory_allocator_ == BufferAllocatorType::CACHELIB &&
(buffer % facebook::cachelib::Slab::kSize ||
size % facebook::cachelib::Slab::kSize)) {
LOG(ERROR) << "buffer=" << buffer << " or size=" << size
<< " is not aligned to " << facebook::cachelib::Slab::kSize
<< " as required by Cachelib";
return ErrorCode::INVALID_PARAMS;
}
// Check if segment already exists
auto exist_segment_it = mounted_segments_.find(segment.id);
if (exist_segment_it != mounted_segments_.end()) {
LOG(WARNING) << "segment_name=" << segment.name
<< ", warn=segment_already_exists";
return ErrorCode::SEGMENT_ALREADY_EXISTS;
}
return ErrorCode::OK;
}
tl::expected<void, ErrorCode> CentralizedSegmentManager::InnerMountSegment(
const Segment& segment) {
ErrorCode ret = InnerCheckMountSegment(segment);
if (ret != ErrorCode::OK) {
LOG(ERROR) << "fail to inner check mount segment"
<< ", segment_name=" << segment.name << ", ret=" << ret;
return tl::make_unexpected(ret);
}
const uintptr_t buffer = segment.GetCentralizedExtra().base;
const size_t size = segment.size;
std::shared_ptr<BufferAllocatorBase> allocator;
// CachelibBufferAllocator may throw an exception if the size or base is
// invalid for the slab allocator.
try {
// Create allocator based on the configured type
switch (memory_allocator_) {
case BufferAllocatorType::CACHELIB:
allocator = std::make_shared<CachelibBufferAllocator>(
segment.name, buffer, size,
segment.GetCentralizedExtra().te_endpoint, segment.id);
break;
case BufferAllocatorType::OFFSET:
allocator = std::make_shared<OffsetBufferAllocator>(
segment.name, buffer, size,
segment.GetCentralizedExtra().te_endpoint, segment.id);
break;
default:
LOG(ERROR) << "segment_name=" << segment.name
<< ", error=unknown_memory_allocator="
<< static_cast<int>(memory_allocator_);
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
if (!allocator) {
LOG(ERROR) << "segment_name=" << segment.name
<< ", error=failed_to_create_allocator";
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
} catch (...) {
LOG(ERROR) << "segment_name=" << segment.name
<< ", error=exception_during_allocator_creation";
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
auto mounted_segment = std::make_shared<MountedCentralizedSegment>();
static_cast<Segment&>(*mounted_segment) = segment;
mounted_segment->buf_allocator = allocator;
if (allocator_change_cb_) {
// Callback to add the allocator to the global manager
auto ret = allocator_change_cb_(mounted_segment->name,
mounted_segment->buf_allocator, true);
if (!ret.has_value()) {
LOG(ERROR) << "Failed to add allocator to global manager. "
"Rolling back mount operation. "
<< "segment=" << mounted_segment->name
<< " error=" << ret.error();
return tl::make_unexpected(ret.error());
}
}
mounted_segments_[mounted_segment->id] = mounted_segment;
return {};
}
tl::expected<void, ErrorCode> CentralizedSegmentManager::MountLocalDiskSegment(
bool enable_offloading) {
SharedMutexLocker lock_(&segment_mutex_);
if (local_disk_segment_) {
LOG(WARNING) << "warn=local_disk_segment_already_exists";
return tl::make_unexpected(ErrorCode::SEGMENT_ALREADY_EXISTS);
}
local_disk_segment_ = std::make_shared<LocalDiskSegment>(enable_offloading);
return {};
}
auto CentralizedSegmentManager::OffloadObjectHeartbeat(bool enable_offloading)
-> tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode> {
SharedMutexLocker lock_(&segment_mutex_, shared_lock);
if (!local_disk_segment_) {
LOG(ERROR) << "Local disk segment not found";
return tl::make_unexpected(ErrorCode::SEGMENT_NOT_FOUND);
}
MutexLocker locker(&local_disk_segment_->offloading_mutex_);
local_disk_segment_->enable_offloading = enable_offloading;
if (enable_offloading) {
return std::move(local_disk_segment_->offloading_objects);
}
return {};
}
tl::expected<void, ErrorCode> CentralizedSegmentManager::PushOffloadingQueue(
const std::string& key, const int64_t size,
const std::string& segment_name) {
SharedMutexLocker lock_(&segment_mutex_, shared_lock);
if (!local_disk_segment_) {
LOG(ERROR) << "Local disk segment not found";
return tl::make_unexpected(ErrorCode::UNABLE_OFFLOADING);
}
// Check whether segment belongs to this client
bool found = false;
for (const auto& entry : mounted_segments_) {
if (entry.second->name == segment_name) {
found = true;
break;
}
}
if (!found) {
LOG(ERROR) << "Segment " << segment_name << " not found";
return tl::make_unexpected(ErrorCode::SEGMENT_NOT_FOUND);
}
MutexLocker locker(&local_disk_segment_->offloading_mutex_);
if (!local_disk_segment_->enable_offloading) {
LOG(ERROR) << "Offloading is not enabled";
return tl::make_unexpected(ErrorCode::UNABLE_OFFLOADING);
}
if (local_disk_segment_->offloading_objects.size() >=
OFFLOADING_QUEUE_LIMIT) {
LOG(ERROR) << "Offloading queue is full";
return tl::make_unexpected(ErrorCode::KEYS_ULTRA_LIMIT);
}
local_disk_segment_->offloading_objects.emplace(key, size);
return {};
}
tl::expected<std::vector<std::string>, ErrorCode>
CentralizedSegmentManager::QueryIp() {
SharedMutexLocker lock_(&segment_mutex_, shared_lock);
std::unordered_set<std::string> unique_ips;
// Iterate mounted segments for this client
for (const auto& entry : mounted_segments_) {
const auto& segment = entry.second;
if (!segment) {
LOG(ERROR) << "unexpected null segment";
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
} else if (!segment->IsCentralizedSegment()) {
LOG(ERROR) << "unexpected segment type"
<< ", segment_id=" << segment->id
<< ", segment_name=" << segment->name;
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
}
const auto& extra = segment->GetCentralizedExtra();
if (!extra.te_endpoint.empty()) {
size_t colon_pos = extra.te_endpoint.find(':');
if (colon_pos != std::string::npos) {
std::string ip = extra.te_endpoint.substr(0, colon_pos);
unique_ips.emplace(ip);
} else {
unique_ips.emplace(extra.te_endpoint);
}
}
}
if (unique_ips.empty()) {
LOG(WARNING) << "QueryIp: has no valid IP addresses";
return std::vector<std::string>{};
}
return std::vector<std::string>(unique_ips.begin(), unique_ips.end());
}
tl::expected<std::pair<size_t, size_t>, ErrorCode>
CentralizedSegmentManager::QuerySegments(const std::string& segment) {
SharedMutexLocker lock_(&segment_mutex_, shared_lock);
for (const auto& entry : mounted_segments_) {
if (entry.second->name == segment) {
auto mounted_seg =
std::static_pointer_cast<MountedCentralizedSegment>(
entry.second);
if (mounted_seg->buf_allocator) {
return std::make_pair(mounted_seg->buf_allocator->size(),
mounted_seg->buf_allocator->capacity());
} else {
LOG(ERROR) << "unexpected null allocator";
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
}
}
}
LOG(WARNING) << "the segment doesn't exist" << ", segment=" << segment;
return tl::make_unexpected(ErrorCode::SEGMENT_NOT_FOUND);
}
auto CentralizedSegmentManager::OnUnmountSegment(
const std::shared_ptr<Segment>& segment) -> tl::expected<void, ErrorCode> {
auto mounted_seg =
std::static_pointer_cast<MountedCentralizedSegment>(segment);
// Remove allocator from upper-level global_allocator_manager_ via callback
if (mounted_seg->buf_allocator) {
if (allocator_change_cb_) {
allocator_change_cb_(segment->name, mounted_seg->buf_allocator,
/*is_add=*/false);
}
}
return {};
}
} // namespace mooncake

View File

@ -94,7 +94,9 @@ std::vector<Slice> split_into_slices(BufferHandle& handle) {
uint64_t calculate_total_size(const Replica::Descriptor& replica) {
uint64_t total_length = 0;
if (replica.is_memory_replica() == false) {
if (replica.is_p2p_proxy_replica()) {
total_length = replica.get_p2p_proxy_descriptor().object_size;
} else if (replica.is_memory_replica() == false) {
auto& disk_descriptor = replica.get_disk_descriptor();
total_length = disk_descriptor.object_size;
} else {
@ -105,7 +107,7 @@ uint64_t calculate_total_size(const Replica::Descriptor& replica) {
int allocateSlices(std::vector<Slice>& slices,
const Replica::Descriptor& replica, void* buffer_ptr) {
if (replica.is_memory_replica() == false) {
if (replica.is_disk_replica()) {
// For disk-based replica, split into slices based on file size
uint64_t offset = 0;
uint64_t total_length = replica.get_disk_descriptor().object_size;
@ -115,14 +117,33 @@ int allocateSlices(std::vector<Slice>& slices,
slices.emplace_back(Slice{chunk_ptr, chunk_size});
offset += chunk_size;
}
} else {
} else if (replica.is_local_disk_replica()) {
slices.emplace_back(
Slice{buffer_ptr, replica.get_local_disk_descriptor().object_size});
} else if (replica.is_memory_replica()) {
// For memory-based replica, split into slices based on buffer
// descriptors
auto& handle = replica.get_memory_descriptor().buffer_descriptor;
void* chunk_ptr = buffer_ptr;
slices.emplace_back(Slice{chunk_ptr, handle.size_});
} else if (replica.is_p2p_proxy_replica()) {
slices.emplace_back(
Slice{buffer_ptr, replica.get_p2p_proxy_descriptor().object_size});
}
return 0;
}
std::vector<Slice> BuildSlicesFromBuffers(const std::vector<void*>& buffers,
const std::vector<size_t>& sizes,
uint64_t total_size) {
std::vector<Slice> slices;
uint64_t remaining = total_size;
for (size_t i = 0; i < buffers.size() && remaining > 0; ++i) {
size_t chunk = std::min(static_cast<uint64_t>(sizes[i]), remaining);
slices.emplace_back(Slice{buffers[i], chunk});
remaining -= chunk;
}
return slices;
}
} // namespace mooncake

View File

@ -0,0 +1,340 @@
#include "client_manager.h"
#include "master_metric_manager.h"
#include <glog/logging.h>
namespace mooncake {
ClientManager::ClientManager(const int64_t disconnect_timeout_sec,
const int64_t crash_timeout_sec,
const ViewVersionId view_version)
: view_version_(view_version) {
ClientMeta::SetTimeouts(disconnect_timeout_sec, crash_timeout_sec);
}
void ClientManager::Start() { StartClientMonitor(); }
void ClientManager::StartClientMonitor() {
if (client_monitor_running_) return;
client_monitor_running_ = true;
client_monitor_thread_ = std::thread([this]() {
while (client_monitor_running_) {
std::this_thread::sleep_for(
std::chrono::milliseconds(kClientMonitorSleepMs));
if (client_monitor_running_) ClientMonitorFunc();
}
});
VLOG(1) << "action=start_client_monitor_thread";
}
void ClientManager::Stop() { StopClientMonitor(); }
void ClientManager::StopClientMonitor() {
client_monitor_running_ = false;
if (client_monitor_thread_.joinable()) {
client_monitor_thread_.join();
}
}
ClientManager::~ClientManager() { Stop(); }
auto ClientManager::GetClient(const UUID& client_id)
-> std::shared_ptr<ClientMeta> {
SharedMutexLocker lock(&clients_mutex_, shared_lock);
auto it = client_metas_.find(client_id);
if (it == client_metas_.end()) {
return nullptr;
}
return it->second;
}
std::vector<std::shared_ptr<ClientMeta>> ClientManager::GetAllClients() {
SharedMutexLocker lock(&clients_mutex_, shared_lock);
std::vector<std::shared_ptr<ClientMeta>> clients;
clients.reserve(client_metas_.size());
for (const auto& [id, meta] : client_metas_) {
clients.push_back(meta);
}
return clients;
}
std::unique_ptr<ClientIterator> ClientManager::InnerBuildClientIterator(
ObjectIterateStrategy strategy) {
switch (strategy) {
case ObjectIterateStrategy::ORDERED:
return std::make_unique<OrderedClientIterator>(client_metas_);
case ObjectIterateStrategy::RANDOM:
return std::make_unique<RandomClientIterator>(client_metas_);
default:
return nullptr;
}
}
auto ClientManager::ForEachClient(ObjectIterateStrategy strategy,
const ClientVisitor& visitor)
-> tl::expected<void, ErrorCode> {
SharedMutexLocker lock(&clients_mutex_, shared_lock);
auto iterator = InnerBuildClientIterator(strategy);
if (!iterator) {
LOG(WARNING) << "fail to get client iterator"
<< ", strategy=" << strategy;
return tl::make_unexpected(ErrorCode::INTERNAL_ERROR);
}
while (auto client = iterator->Next()) {
auto ret = visitor(client);
if (!ret) {
LOG(WARNING) << "client visitor returned error"
<< ", strategy=" << strategy
<< ", client_id=" << client->get_client_id()
<< ", ret=" << ret.error();
return tl::make_unexpected(ret.error());
}
if (ret.value()) { // early stop
break;
}
}
return {};
}
tl::expected<std::vector<std::string>, ErrorCode>
ClientManager::GetAllSegments() {
SharedMutexLocker lock(&clients_mutex_, shared_lock);
std::vector<std::string> all_segments;
for (const auto& [id, meta] : client_metas_) {
auto segments_res = meta->GetSegments();
if (!segments_res) {
LOG(WARNING) << "GetAllSegments: failed to get segments"
<< ", client_id=" << id
<< ", error=" << segments_res.error();
continue;
}
for (const auto& seg : segments_res.value()) {
all_segments.emplace_back(std::move(seg.name));
}
}
return all_segments;
}
tl::expected<std::vector<std::string>, ErrorCode>
ClientManager::GetClientSegments(const UUID& client_id) {
SharedMutexLocker lock(&clients_mutex_, shared_lock);
auto it = client_metas_.find(client_id);
if (it == client_metas_.end()) {
LOG(WARNING) << "GetClientSegments: client not found"
<< ", client_id=" << client_id;
return tl::make_unexpected(ErrorCode::CLIENT_NOT_FOUND);
}
auto segments_res = it->second->GetSegments();
if (!segments_res) {
LOG(WARNING) << "GetClientSegments: failed to get segments"
<< ", client_id=" << client_id
<< ", error=" << segments_res.error();
return tl::make_unexpected(segments_res.error());
}
std::vector<std::string> segment_names;
segment_names.reserve(segments_res.value().size());
for (const auto& seg : segments_res.value()) {
segment_names.emplace_back(std::move(seg.name));
}
return segment_names;
}
tl::expected<std::pair<size_t, size_t>, ErrorCode> ClientManager::QuerySegments(
const std::string& segment) {
SharedMutexLocker lock(&clients_mutex_, shared_lock);
for (const auto& [id, meta] : client_metas_) {
auto ret = meta->QuerySegments(segment);
if (ret.has_value()) {
return ret;
} else if (ret.error() != ErrorCode::SEGMENT_NOT_FOUND) {
LOG(ERROR)
<< "QuerySegments: failed to query segments for client_id="
<< id << ", error=" << ret.error();
return ret;
}
}
LOG(WARNING) << "QuerySegments: segment not found"
<< ", segment=" << segment;
return tl::make_unexpected(ErrorCode::SEGMENT_NOT_FOUND);
}
tl::expected<std::vector<std::string>, ErrorCode> ClientManager::QueryIp(
const UUID& client_id) {
SharedMutexLocker lock(&clients_mutex_, shared_lock);
auto it = client_metas_.find(client_id);
if (it == client_metas_.end()) {
LOG(WARNING) << "QueryIp: client not found"
<< ", client_id=" << client_id;
return tl::make_unexpected(ErrorCode::CLIENT_NOT_FOUND);
}
return it->second->QueryIp(client_id);
}
tl::expected<std::shared_ptr<Segment>, ErrorCode> ClientManager::QuerySegment(
const UUID& client_id, const UUID& segment_id) {
SharedMutexLocker lock(&clients_mutex_, shared_lock);
auto it = client_metas_.find(client_id);
if (it == client_metas_.end()) {
LOG(WARNING) << "QuerySegment: client not found"
<< ", client_id=" << client_id;
return tl::make_unexpected(ErrorCode::CLIENT_NOT_FOUND);
}
return it->second->QuerySegment(segment_id);
}
void ClientManager::SetSegmentRemovalCallback(SegmentRemovalCallback cb) {
segment_removal_cb_ = std::move(cb);
}
auto ClientManager::RegisterClient(const RegisterClientRequest& req)
-> tl::expected<RegisterClientResponse, ErrorCode> {
// Architecture validation: client and master must use the same mode
if (req.deployment_mode != GetDeploymentMode()) {
LOG(ERROR) << "RegisterClient: architecture mismatch"
<< ", client_mode=" << static_cast<int>(req.deployment_mode)
<< ", master_mode=" << static_cast<int>(GetDeploymentMode())
<< ", client_id=" << req.client_id;
return tl::make_unexpected(ErrorCode::ILLEGAL_CLIENT);
}
const auto& client_id = req.client_id;
auto it = client_metas_.find(client_id);
if (it != client_metas_.end()) {
LOG(WARNING) << "RegisterClient: client already exists"
<< ", client_id=" << client_id;
return tl::make_unexpected(ErrorCode::CLIENT_ALREADY_EXISTS);
}
auto meta = CreateClientMeta(req);
if (segment_removal_cb_) {
meta->SetSegmentRemovalCallback(segment_removal_cb_);
}
for (const auto& segment : req.segments) {
auto result = meta->MountSegment(segment);
if (!result) {
LOG(ERROR) << "RegisterClient: failed to mount segment"
<< ", segment_name=" << segment.name
<< ", client_id=" << client_id
<< ", error=" << result.error();
return tl::make_unexpected(result.error());
}
}
OnClientRegistered(meta);
SharedMutexLocker lock(&clients_mutex_);
// Write to client_metas_ (overwrites if re-registering after crash)
client_metas_[client_id] = std::move(meta);
MasterMetricManager::instance().inc_active_clients();
RegisterClientResponse response;
response.view_version = view_version_;
LOG(INFO) << "RegisterClient: client_id=" << client_id
<< ", segments=" << req.segments.size()
<< ", view_version=" << response.view_version;
return response;
}
auto ClientManager::Heartbeat(const HeartbeatRequest& req)
-> tl::expected<HeartbeatResponse, ErrorCode> {
const auto& client_id = req.client_id;
HeartbeatResponse response;
response.view_version = view_version_;
SharedMutexLocker lock(&clients_mutex_, shared_lock);
auto it = client_metas_.find(client_id);
if (it == client_metas_.end()) {
// Client not in client_metas_: master restarted or client heartbeat
// timed out and the meta of client was cleaned up. Return UNDEFINED +
// view_version to inform client to re-register.
response.status = ClientStatus::UNDEFINED;
return response;
}
auto& meta = it->second;
// Update Heartbeat
auto [old_status, new_status] = meta->Heartbeat();
response.status = new_status;
if (new_status == ClientStatus::HEALTH) {
if (old_status != new_status) {
LOG(INFO) << "client recovered"
<< ", client_id=" << client_id;
meta->OnRecovered();
}
for (const auto& task : req.tasks) {
response.task_results.push_back(ProcessTask(client_id, task));
}
}
return response;
}
auto ClientManager::QueryClientStatus(const QueryClientStatusRequest& req)
-> tl::expected<QueryClientStatusResponse, ErrorCode> {
const auto& client_id = req.client_id;
QueryClientStatusResponse response;
SharedMutexLocker lock(&clients_mutex_, shared_lock);
auto it = client_metas_.find(client_id);
if (it == client_metas_.end()) {
response.status = ClientStatus::UNDEFINED;
} else {
response.status = it->second->get_health_state().status;
}
return response;
}
// 1. Phase 1 (Shared Lock): Check health status
// 2. Phase 2 (No Lock): Execute crashed client hooks
// 3. Phase 3 (Write Lock): Clean up crashed clients
void ClientManager::ClientMonitorFunc() {
// Attention:
// 1. DISCONNECTED is not finnal status. The clients in
// newly_disconnected might change its status.
// 2. CRASHED is finnal status. The clients in newly_crashed will always
// be crashed.
std::vector<std::shared_ptr<ClientMeta>> newly_disconnected;
std::vector<std::shared_ptr<ClientMeta>> newly_crashed;
// Phase 1: Check health status
{
SharedMutexLocker lock(&clients_mutex_, shared_lock);
for (auto& [client_id, meta] : client_metas_) {
auto [old_status, new_status] = meta->CheckHealth();
if (old_status != new_status) {
if (new_status == ClientStatus::DISCONNECTION) {
newly_disconnected.push_back(meta);
} else if (new_status == ClientStatus::CRASHED) {
newly_crashed.push_back(meta);
}
}
}
}
// Phase 2: Execute hooks (No client_mutex lock)
// We can safely execute hooks because we hold shared_ptrs to
// ClientMeta, so they won't be destroyed.
// And hooks don't need client_mutex because they are protected by
// client_meta itself
for (const auto& client : newly_disconnected) {
// The client might change to Healthy by concurrent heartbeat.
// So OnDisconnected() need to check the status again.
client->OnDisconnected();
}
for (const auto& client : newly_crashed) {
client->OnCrashed();
}
// Phase 3: Clean up crashed clients (Write Lock)
if (!newly_crashed.empty()) {
SharedMutexLocker lock(&clients_mutex_);
for (const auto& client : newly_crashed) {
client_metas_.erase(client->get_client_id());
}
}
}
} // namespace mooncake

View File

@ -0,0 +1,284 @@
#include "client_meta.h"
#include <glog/logging.h>
#include "master_metric_manager.h"
namespace mooncake {
// Define static timeout members
int64_t ClientMeta::disconnect_timeout_sec_ = 0;
int64_t ClientMeta::crash_timeout_sec_ = 0;
ClientMeta::ClientMeta(const UUID& client_id) : client_id_(client_id) {
health_state_.status = ClientStatus::HEALTH;
health_state_.last_heartbeat = std::chrono::steady_clock::now();
}
tl::expected<void, ErrorCode> ClientMeta::MountSegment(const Segment& segment) {
SharedMutexLocker lock(&client_mutex_, shared_lock);
auto check_ret = InnerStatusCheck();
if (!check_ret.has_value()) {
LOG(ERROR) << "fail to inner check client status"
<< ", client_id=" << client_id_
<< ", ret=" << check_ret.error();
return check_ret;
}
auto ret = GetSegmentManager()->MountSegment(segment);
if (!ret.has_value()) {
if (ret.error() == ErrorCode::SEGMENT_ALREADY_EXISTS) {
LOG(WARNING) << "attempt to mount segment but it already exists"
<< ", client_id=" << client_id_
<< ", segment_id =" << segment.id
<< ", segment_name=" << segment.name
<< ", ret=" << ret.error();
return {}; // ignore the errcode
} else {
LOG(ERROR) << "fail to mount segment"
<< ", client_id=" << client_id_
<< ", segment_id =" << segment.id
<< ", segment_name=" << segment.name
<< ", ret=" << ret.error();
return ret;
}
}
LOG(INFO) << "Mount segment success"
<< ", client_id=" << client_id_ << ", segment_id =" << segment.id
<< ", segment_name=" << segment.name;
return {};
}
tl::expected<void, ErrorCode> ClientMeta::UnmountSegment(
const UUID& segment_id) {
SharedMutexLocker lock(&client_mutex_, shared_lock);
auto check_ret = InnerStatusCheck();
if (!check_ret.has_value()) {
LOG(ERROR) << "fail to inner check client status"
<< ", client_id=" << client_id_
<< ", ret=" << check_ret.error();
return check_ret;
}
auto ret = GetSegmentManager()->UnmountSegment(segment_id);
if (!ret.has_value()) {
if (ret.error() == ErrorCode::SEGMENT_NOT_FOUND) {
LOG(WARNING) << "attempt to unmount segment but it does not exist"
<< ", client_id=" << client_id_
<< ", segment_id=" << segment_id
<< ", ret=" << ret.error();
return {}; // ignore the errcode
} else {
LOG(ERROR) << "fail to unmount segment"
<< ", client_id=" << client_id_
<< ", segment_id=" << segment_id
<< ", ret=" << ret.error();
return ret;
}
}
LOG(INFO) << "Unmount segment success"
<< ", client_id=" << client_id_ << ", segment_id =" << segment_id;
return {};
}
tl::expected<std::vector<Segment>, ErrorCode> ClientMeta::GetSegments() {
SharedMutexLocker lock(&client_mutex_, shared_lock);
auto check_ret = InnerStatusCheck();
if (!check_ret.has_value()) {
LOG(ERROR) << "fail to inner check client status"
<< ", client_id=" << client_id_
<< ", ret=" << check_ret.error();
return tl::make_unexpected(check_ret.error());
}
return GetSegmentManager()->GetSegments();
}
tl::expected<std::pair<size_t, size_t>, ErrorCode> ClientMeta::QuerySegments(
const std::string& segment_name) {
SharedMutexLocker lock(&client_mutex_, shared_lock);
auto check_ret = InnerStatusCheck();
if (!check_ret.has_value()) {
LOG(ERROR) << "fail to inner check client status"
<< ", client_id=" << client_id_
<< ", ret=" << check_ret.error();
return tl::make_unexpected(check_ret.error());
}
return GetSegmentManager()->QuerySegments(segment_name);
}
tl::expected<std::shared_ptr<Segment>, ErrorCode> ClientMeta::QuerySegment(
const UUID& segment_id) {
SharedMutexLocker lock(&client_mutex_, shared_lock);
auto check_ret = InnerStatusCheck();
if (!check_ret.has_value()) {
LOG(ERROR) << "fail to inner check client status"
<< ", client_id=" << client_id_
<< ", ret=" << check_ret.error();
return tl::make_unexpected(check_ret.error());
}
return GetSegmentManager()->QuerySegment(segment_id);
}
void ClientMeta::SetSegmentRemovalCallback(SegmentRemovalCallback cb) {
GetSegmentManager()->SetSegmentRemovalCallback(std::move(cb));
}
void ClientMeta::SetTimeouts(int64_t disconnect_sec, int64_t crash_sec) {
disconnect_timeout_sec_ = disconnect_sec;
crash_timeout_sec_ = crash_sec;
}
ClientHealthState ClientMeta::get_health_state() const {
SharedMutexLocker lock(&client_mutex_, shared_lock);
return health_state_;
}
bool ClientMeta::is_health() const {
SharedMutexLocker lock(&client_mutex_, shared_lock);
return health_state_.status == ClientStatus::HEALTH;
}
std::pair<ClientStatus, ClientStatus> ClientMeta::Heartbeat() {
SharedMutexLocker lock(&client_mutex_);
InnerUpdateHeartbeat();
return InnerUpdateHealthStatus();
}
std::pair<ClientStatus, ClientStatus> ClientMeta::CheckHealth() {
SharedMutexLocker lock(&client_mutex_);
return InnerUpdateHealthStatus();
}
void ClientMeta::InnerUpdateHeartbeat() {
if (health_state_.status == ClientStatus::CRASHED) {
LOG(WARNING) << "heartbeat received while in CRASHED state, "
"timestamp will not update"
<< ", client_id=" << client_id_;
return;
} else if (health_state_.status == ClientStatus::DISCONNECTION) {
LOG(WARNING) << "heartbeat received while in DISCONNECTION state, "
"the state might change to HEALTH as soon as possible"
<< ", client_id=" << client_id_;
}
health_state_.last_heartbeat = std::chrono::steady_clock::now();
}
std::pair<ClientStatus, ClientStatus> ClientMeta::InnerUpdateHealthStatus() {
auto now = std::chrono::steady_clock::now();
ClientStatus old_status = health_state_.status;
auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now - health_state_.last_heartbeat)
.count();
int64_t disconnect_timeout_ms = disconnect_timeout_sec_ * 1000;
int64_t crash_timeout_ms = crash_timeout_sec_ * 1000;
switch (health_state_.status) {
case ClientStatus::HEALTH: {
if (elapsed_ms >= disconnect_timeout_ms) {
if (elapsed_ms >= crash_timeout_ms) {
health_state_.status = ClientStatus::CRASHED;
} else {
health_state_.status = ClientStatus::DISCONNECTION;
}
}
break;
}
case ClientStatus::DISCONNECTION: {
if (elapsed_ms < disconnect_timeout_ms) {
health_state_.status = ClientStatus::HEALTH;
} else if (elapsed_ms >= crash_timeout_ms) {
health_state_.status = ClientStatus::CRASHED;
}
break;
}
case ClientStatus::CRASHED:
case ClientStatus::UNDEFINED:
// final states, do nothing
break;
}
if (health_state_.status != old_status) {
// client status changed
LOG(INFO) << "Client status changed"
<< ", client_id=" << client_id_
<< ", old_status=" << HealthToString(old_status)
<< ", new_status=" << HealthToString(health_state_.status);
}
return {old_status, health_state_.status};
}
tl::expected<void, ErrorCode> ClientMeta::InnerStatusCheck() const {
if (health_state_.status != ClientStatus::HEALTH) {
LOG(WARNING) << "Client is not HEALTH"
<< ", client_id=" << client_id_
<< ", status=" << HealthToString(health_state_.status);
return tl::make_unexpected(ErrorCode::CLIENT_UNHEALTHY);
}
return {};
}
void ClientMeta::OnDisconnected() {
SharedMutexLocker lock(&client_mutex_, shared_lock);
if (health_state_.status == ClientStatus::HEALTH) {
// concurrent heartbeat might have recovered the client, skip
return;
} else if (health_state_.status != ClientStatus::DISCONNECTION) {
LOG(ERROR) << "unexpected hook calling" << ", client_id=" << client_id_
<< ", current status="
<< HealthToString(health_state_.status)
<< ", expected status="
<< HealthToString(ClientStatus::DISCONNECTION);
return;
}
LOG(INFO) << "the client is disconnected" << ", client_id=" << client_id_;
DoOnDisconnected();
MasterMetricManager::instance().dec_active_clients();
}
void ClientMeta::OnRecovered() {
SharedMutexLocker lock(&client_mutex_, shared_lock);
if (health_state_.status != ClientStatus::HEALTH) {
LOG(ERROR) << "unexpected hook calling" << ", client_id=" << client_id_
<< ", current status="
<< HealthToString(health_state_.status)
<< ", expected status="
<< HealthToString(ClientStatus::HEALTH);
return;
}
LOG(INFO) << "the client is recovered" << ", client_id=" << client_id_;
DoOnRecovered();
MasterMetricManager::instance().inc_active_clients();
}
void ClientMeta::OnCrashed() {
LOG(INFO) << "the client is crashed, start to recycle meta"
<< ", client_id=" << client_id_;
SharedMutexLocker lock(&client_mutex_, shared_lock);
auto segments_res = GetSegmentManager()->GetSegments();
if (segments_res) {
for (const auto& seg : *segments_res) {
auto ret = GetSegmentManager()->UnmountSegment(seg.id);
if (!ret.has_value()) {
LOG(ERROR) << "Failed to unmount segment"
<< ", client_id=" << client_id_
<< ", segment_id=" << seg.id
<< " error=" << ret.error();
}
}
}
LOG(INFO) << "the client meta is recycled over"
<< ", client_id=" << client_id_;
}
std::string ClientMeta::HealthToString(ClientStatus status) const {
switch (status) {
case ClientStatus::HEALTH:
return "HEALTH";
case ClientStatus::DISCONNECTION:
return "DISCONNECTION";
case ClientStatus::CRASHED:
return "CRASHED";
case ClientStatus::UNDEFINED:
return "UNDEFINED";
}
return "UNKNOWN";
}
} // namespace mooncake

View File

@ -0,0 +1,108 @@
#include "client_rpc_service.h"
#include <glog/logging.h>
#include <ylt/coro_rpc/coro_rpc_server.hpp>
#include "utils/scoped_vlog_timer.h"
namespace mooncake {
ClientRpcService::ClientRpcService(DataManager& data_manager)
: data_manager_(data_manager) {}
tl::expected<void, ErrorCode> ClientRpcService::ReadRemoteData(
const RemoteReadRequest& request) {
ScopedVLogTimer timer(1, "ClientRpcService::ReadRemoteData");
timer.LogRequest("key=", request.key,
"buffer_count=", request.dest_buffers.size());
if (request.key.empty()) {
LOG(ERROR) << "ReadRemoteData: empty key";
timer.LogResponse("error_code=", ErrorCode::INVALID_PARAMS);
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
if (request.dest_buffers.empty()) {
LOG(ERROR) << "ReadRemoteData: empty destination buffers";
timer.LogResponse("error_code=", ErrorCode::INVALID_PARAMS);
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
// Validate buffers (segment name validation is done in DataManager)
for (const auto& buffer_desc : request.dest_buffers) {
if (buffer_desc.size == 0 || buffer_desc.addr == 0) {
LOG(ERROR)
<< "ReadRemoteData: invalid buffer (zero size or null address)";
timer.LogResponse("error_code=", ErrorCode::INVALID_PARAMS);
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
}
// Delegate to DataManager
auto result =
data_manager_.ReadRemoteData(request.key, request.dest_buffers);
if (!result.has_value()) {
LOG(ERROR) << "ReadRemoteData failed for key: " << request.key
<< ", error: " << toString(result.error());
timer.LogResponse("error_code=", result.error());
// Rectify stale route when key not found
if (result.error() == ErrorCode::OBJECT_NOT_FOUND) {
data_manager_.RectifyReadRoute(request.key);
}
return result;
}
timer.LogResponse("error_code=", ErrorCode::OK);
return {};
}
tl::expected<UUID, ErrorCode> ClientRpcService::WriteRemoteData(
const RemoteWriteRequest& request) {
ScopedVLogTimer timer(1, "ClientRpcService::WriteRemoteData");
timer.LogRequest("key=", request.key,
"buffer_count=", request.src_buffers.size());
if (request.key.empty()) {
LOG(ERROR) << "WriteRemoteData: empty key";
timer.LogResponse("error_code=", ErrorCode::INVALID_PARAMS);
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
if (request.src_buffers.empty()) {
LOG(ERROR) << "WriteRemoteData: empty source buffers";
timer.LogResponse("error_code=", ErrorCode::INVALID_PARAMS);
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
// Validate buffers (segment name validation is done in DataManager)
for (const auto& buffer_desc : request.src_buffers) {
if (buffer_desc.size == 0 || buffer_desc.addr == 0) {
LOG(ERROR) << "WriteRemoteData: invalid buffer (zero size or null "
"address)";
timer.LogResponse("error_code=", ErrorCode::INVALID_PARAMS);
return tl::make_unexpected(ErrorCode::INVALID_PARAMS);
}
}
// Delegate to DataManager
auto result = data_manager_.WriteRemoteData(
request.key, request.src_buffers, request.target_tier_id);
if (!result.has_value()) {
LOG(ERROR) << "WriteRemoteData failed for key: " << request.key
<< ", error: " << toString(result.error());
timer.LogResponse("error_code=", result.error());
return result;
}
timer.LogResponse("error_code=", ErrorCode::OK);
return result;
}
void RegisterClientRpcService(coro_rpc::coro_rpc_server& server,
ClientRpcService& service) {
server.register_handler<&ClientRpcService::ReadRemoteData>(&service);
server.register_handler<&ClientRpcService::WriteRemoteData>(&service);
}
} // namespace mooncake

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -262,11 +262,13 @@ ErrorCode DummyClient::connect(const std::string& server_address) {
auto pool = client_accessor_.GetClientPool();
// The client pool does not have native connection check method, so we need
// to use custom ServiceReady API.
auto result = invoke_rpc<&RealClient::service_ready_internal, void>();
auto result =
invoke_rpc<&RealClient::service_ready_internal, DeploymentMode>();
if (!result.has_value()) {
timer.LogResponse("error_code=", result.error());
return result.error();
}
deployment_mode_ = result.value();
timer.LogResponse("error_code=", ErrorCode::OK);
connected_ = true;
return ErrorCode::OK;
@ -341,11 +343,9 @@ int DummyClient::register_shm_via_ipc(const ShmHelper::ShmSegment* shm,
return 0;
}
int DummyClient::setup_dummy(size_t mem_pool_size, size_t local_buffer_size,
const std::string& server_address,
const std::string& ipc_socket_path) {
int DummyClient::setup(DummyClientConfig& config) {
void* base_addr = nullptr;
ErrorCode err = connect(server_address);
ErrorCode err = connect(config.real_client_addr);
if (err != ErrorCode::OK) {
LOG(ERROR) << "Failed to connect to real client";
return -1;
@ -353,13 +353,13 @@ int DummyClient::setup_dummy(size_t mem_pool_size, size_t local_buffer_size,
shm_helper_ = ShmHelper::getInstance();
try {
base_addr = shm_helper_->allocate(local_buffer_size);
base_addr = shm_helper_->allocate(config.local_buffer_size);
} catch (const std::exception& e) {
LOG(ERROR) << "Failed to allocate shared memory: " << e.what();
return -1;
}
ipc_socket_path_ = ipc_socket_path;
ipc_socket_path_ = config.ipc_socket_path;
// Attempt registration for the primary segment
auto local_buffer_shm = shm_helper_->get_shm(base_addr);
@ -475,21 +475,21 @@ uint64_t DummyClient::alloc_from_mem_pool(size_t size) {
}
int DummyClient::put(const std::string& key, std::span<const char> value,
const ReplicateConfig& config) {
const WriteConfig& config) {
return to_py_ret(invoke_rpc<&RealClient::put_dummy_helper, void>(
key, value, config, client_id_));
}
int DummyClient::put_batch(const std::vector<std::string>& keys,
const std::vector<std::span<const char>>& values,
const ReplicateConfig& config) {
const WriteConfig& config) {
return to_py_ret(invoke_rpc<&RealClient::put_batch_dummy_helper, void>(
keys, values, config, client_id_));
}
int DummyClient::put_parts(const std::string& key,
std::vector<std::span<const char>> values,
const ReplicateConfig& config) {
const WriteConfig& config) {
return to_py_ret(invoke_rpc<&RealClient::put_parts_dummy_helper, void>(
key, values, config, client_id_));
}
@ -541,15 +541,17 @@ int64_t DummyClient::getSize(const std::string& key) {
return to_py_ret(invoke_rpc<&RealClient::getSize_internal, int64_t>(key));
}
std::shared_ptr<BufferHandle> DummyClient::get_buffer(const std::string& key) {
std::shared_ptr<BufferHandle> DummyClient::get_buffer(
const std::string& key, const ReadRouteConfig& config) {
// Dummy client does not use BufferHandle, so we return nullptr
return nullptr;
}
std::tuple<uint64_t, size_t> DummyClient::get_buffer_info(
const std::string& key) {
auto result = invoke_rpc<&RealClient::get_buffer_info_dummy_helper,
std::tuple<uint64_t, size_t>>(key, client_id_);
const std::string& key, const ReadRouteConfig& config) {
auto result =
invoke_rpc<&RealClient::get_buffer_info_dummy_helper,
std::tuple<uint64_t, size_t>>(key, config, client_id_);
if (!result.has_value()) {
LOG(ERROR) << "Get buffer failed: " << toString(result.error());
return std::make_tuple(0, 0);
@ -558,13 +560,13 @@ std::tuple<uint64_t, size_t> DummyClient::get_buffer_info(
}
std::vector<std::shared_ptr<BufferHandle>> DummyClient::batch_get_buffer(
const std::vector<std::string>& keys) {
const std::vector<std::string>& keys, const ReadRouteConfig& config) {
// TODO: implement this function
return std::vector<std::shared_ptr<BufferHandle>>();
}
int64_t DummyClient::get_into(const std::string& key, void* buffer,
size_t size) {
int64_t DummyClient::get_into(const std::string& key, void* buffer, size_t size,
const ReadRouteConfig& config) {
// TODO: implement this function
return -1;
}
@ -576,7 +578,7 @@ std::string DummyClient::get_hostname() const {
std::vector<int> DummyClient::batch_put_from(
const std::vector<std::string>& keys, const std::vector<void*>& buffer_ptrs,
const std::vector<size_t>& sizes, const ReplicateConfig& config) {
const std::vector<size_t>& sizes, const WriteConfig& config) {
std::vector<uint64_t> buffers;
for (auto ptr : buffer_ptrs) {
buffers.push_back(reinterpret_cast<uint64_t>(ptr));
@ -595,21 +597,21 @@ std::vector<int> DummyClient::batch_put_from(
}
int DummyClient::put_from(const std::string& key, void* buffer, size_t size,
const ReplicateConfig& config) {
const WriteConfig& config) {
// TODO: implement this function
return -1;
}
std::vector<int64_t> DummyClient::batch_get_into(
const std::vector<std::string>& keys, const std::vector<void*>& buffer_ptrs,
const std::vector<size_t>& sizes) {
const std::vector<size_t>& sizes, const ReadRouteConfig& config) {
std::vector<uint64_t> buffers;
for (auto ptr : buffer_ptrs) {
buffers.push_back(reinterpret_cast<uint64_t>(ptr));
}
auto internal_results =
invoke_batch_rpc<&RealClient::batch_get_into_dummy_helper, int64_t>(
keys.size(), keys, buffers, sizes, client_id_);
keys.size(), keys, buffers, sizes, config, client_id_);
std::vector<int64_t> results;
results.reserve(internal_results.size());
@ -623,7 +625,7 @@ std::vector<int64_t> DummyClient::batch_get_into(
int DummyClient::put_from_with_metadata(const std::string& key, void* buffer,
void* metadata_buffer, size_t size,
size_t metadata_size,
const ReplicateConfig& config) {
const WriteConfig& config) {
// TODO: implement this function
return -1;
}
@ -632,7 +634,7 @@ std::vector<int> DummyClient::batch_put_from_multi_buffers(
const std::vector<std::string>& keys,
const std::vector<std::vector<void*>>& all_buffer_ptrs,
const std::vector<std::vector<size_t>>& all_sizes,
const ReplicateConfig& config) {
const WriteConfig& config) {
// TODO: implement this function
std::vector<int> vec(keys.size(), -1);
return vec;
@ -642,7 +644,7 @@ std::vector<int> DummyClient::batch_get_into_multi_buffers(
const std::vector<std::string>& keys,
const std::vector<std::vector<void*>>& all_buffer_ptrs,
const std::vector<std::vector<size_t>>& all_sizes,
bool prefer_alloc_in_same_node) {
bool aggregate_same_segment_task, const ReadRouteConfig& config) {
// TODO: implement this function
std::vector<int> vec(keys.size(), -1);
return vec;
@ -689,10 +691,10 @@ void DummyClient::ping_thread_main() {
while (ping_running_) {
auto ping_result =
invoke_rpc<&RealClient::ping, PingResponse>(client_id_);
invoke_rpc<&RealClient::ping, HeartbeatResponse>(client_id_);
if (ping_result.has_value() &&
ping_result.value().client_status == ClientStatus::OK) {
ping_result.value().status == ClientStatus::HEALTH) {
ping_fail_count = 0;
std::this_thread::sleep_for(
std::chrono::milliseconds(success_ping_interval_ms));
@ -734,7 +736,8 @@ void DummyClient::ping_thread_main() {
// Even if register_shm_via_ipc succeeded, we should check
// if RPC is responsive
auto check_rpc =
invoke_rpc<&RealClient::ping, PingResponse>(client_id_);
invoke_rpc<&RealClient::ping, HeartbeatResponse>(
client_id_);
if (check_rpc.has_value()) {
LOG(INFO) << "RPC connection restored";
ping_fail_count = 0;

View File

@ -126,7 +126,7 @@ bool FileStorageConfig::Validate() const {
return true;
}
FileStorage::FileStorage(std::shared_ptr<Client> client,
FileStorage::FileStorage(std::shared_ptr<CentralizedClientService> client,
const std::string& local_rpc_addr,
const FileStorageConfig& config)
: client_(client),
@ -390,7 +390,7 @@ tl::expected<void, ErrorCode> FileStorage::BatchQuerySegmentSlices(
for (size_t i = 0; i < batched_query_results.size(); ++i) {
if (batched_query_results[i]) {
for (const auto& descriptor :
batched_query_results[i].value().replicas) {
batched_query_results[i].value()->replicas) {
if (descriptor.is_memory_replica()) {
const auto& memory_descriptor =
descriptor.get_memory_descriptor();
@ -436,8 +436,10 @@ tl::expected<FileStorage::AllocatedBatch, ErrorCode> FileStorage::AllocateBatch(
const std::vector<std::string>& keys, const std::vector<int64_t>& sizes) {
AllocatedBatch result;
for (size_t i = 0; i < keys.size(); ++i) {
assert(sizes[i] <= kMaxSliceSize);
auto alloc_result = client_buffer_allocator_->allocate(sizes[i]);
assert(sizes[i] >= 0);
assert(static_cast<uint64_t>(sizes[i]) <= kMaxSliceSize);
auto alloc_result =
client_buffer_allocator_->allocate(static_cast<size_t>(sizes[i]));
if (!alloc_result) {
LOG(ERROR) << "Failed to allocate slice buffer, size = " << sizes[i]
<< ", key = " << keys[i];
@ -450,4 +452,4 @@ tl::expected<FileStorage::AllocatedBatch, ErrorCode> FileStorage::AllocateBatch(
return result;
}
} // namespace mooncake
} // namespace mooncake

View File

@ -1,6 +1,7 @@
#include "ha_helper.h"
#include "etcd_helper.h"
#include "rpc_service.h"
#include "centralized_rpc_service.h"
#include "p2p_rpc_service.h"
namespace mooncake {
@ -152,9 +153,16 @@ int MasterServiceSupervisor::Start() {
std::this_thread::sleep_for(std::chrono::seconds(waiting_time));
LOG(INFO) << "Starting master service...";
mooncake::WrappedMasterService wrapped_master_service(
mooncake::WrappedMasterServiceConfig(config_, view_version));
mooncake::RegisterRpcService(server, wrapped_master_service);
if (config_.deployment_mode == DeploymentMode::CENTRALIZATION) {
WrappedCentralizedMasterService wrapped_master_service(
WrappedMasterServiceConfig(config_, view_version));
RegisterCentralizedRpcService(server, wrapped_master_service);
} else {
WrappedP2PMasterService wrapped_master_service(
WrappedMasterServiceConfig(config_, view_version));
RegisterP2PRpcService(server, wrapped_master_service);
}
// Metric reporting is now handled by WrappedMasterService.
async_simple::Future<coro_rpc::err_code> ec =

View File

@ -0,0 +1,301 @@
#include "ha_recovery_manager.h"
#include <glog/logging.h>
#include <thread>
#include <unordered_set>
namespace mooncake {
HARecoveryManager::HARecoveryManager(
const UUID& client_id, P2PMasterClient& master_client,
std::optional<DataManager>& data_manager,
std::unique_ptr<AsyncMetadataNotifier>& notifier,
std::atomic<ViewVersionId>& view_version, HAClientState initial_state)
: client_id_(client_id),
master_client_(master_client),
data_manager_(data_manager),
notifier_(notifier),
view_version_(view_version),
state_(initial_state) {
LOG(INFO) << "HA recovery manager initialized with state: "
<< initial_state;
}
HARecoveryManager::~HARecoveryManager() { Stop(); }
void HARecoveryManager::Stop() {
std::lock_guard<std::mutex> lk(mutex_);
if (need_abort_) {
need_abort_->store(true, std::memory_order_release);
abort_cv_.notify_all();
}
if (recovery_thread_.joinable()) {
recovery_thread_.join();
}
if (state_.load(std::memory_order_relaxed) == HAClientState::SYNCING) {
TransitionState(HAClientState::DEGRADED, "shutdown");
}
}
tl::expected<void, ErrorCode> HARecoveryManager::SetSyncCompleted() {
auto result = master_client_.SetSyncCompleted(client_id_);
if (!result) {
LOG(ERROR) << "SetSyncCompleted RPC failed: " << result.error();
}
return result;
}
// ============================================================================
// State Machine
// ============================================================================
void HARecoveryManager::TransitionState(HAClientState to,
const std::string& reason) {
auto from = state_.load(std::memory_order_relaxed);
LOG(WARNING) << "HA state: " << from << " -> " << to
<< ", reason=" << reason
<< ", view_version=" << view_version_.load();
state_.store(to, std::memory_order_release);
}
void HARecoveryManager::HandleEvent(HAEvent event) {
std::lock_guard<std::mutex> lk(mutex_);
if (event == HAEvent::MASTER_UNREACHABLE &&
state_.load(std::memory_order_acquire) == HAClientState::DEGRADED) {
return;
}
// Abort + join under mutex_. Safe because recovery thread's exit path
// uses lock-free CAS (no mutex_ needed), so no deadlock.
if (need_abort_) {
need_abort_->store(true, std::memory_order_release);
abort_cv_.notify_all();
}
if (recovery_thread_.joinable()) {
recovery_thread_.join();
}
auto current = state_.load(std::memory_order_relaxed);
switch (event) {
case HAEvent::MASTER_UNREACHABLE:
if (current == HAClientState::FULL ||
current == HAClientState::SYNCING) {
TransitionState(HAClientState::DEGRADED,
"heartbeat failure threshold exceeded");
if (notifier_) notifier_->Stop(/*drop_pending=*/true);
}
break;
case HAEvent::MASTER_RECONNECTED:
if (current == HAClientState::DEGRADED) {
if (notifier_) notifier_->Start();
}
if (current != HAClientState::SYNCING) {
TransitionState(HAClientState::SYNCING,
"master connection ready, starting recovery");
}
StartRecoveryThread();
break;
}
}
// ============================================================================
// Recovery Thread Management
// ============================================================================
void HARecoveryManager::StartRecoveryThread() {
// Old thread already joined by HandleEvent before acquiring mutex_.
// Create fresh abort token and start new thread.
need_abort_ = std::make_shared<std::atomic<bool>>(false);
auto need_abort = need_abort_;
recovery_thread_ =
std::thread([this, need_abort]() { RecoveryPipelineMain(need_abort); });
}
bool HARecoveryManager::WaitForReady(const AbortToken& need_abort) {
// Wait for P2PClientService::Init to complete (InitStorage initializes
// data_manager_, and Init calls SetReadyForRecovery() at the end).
// This avoids data race on data_manager_ which is an std::optional.
while (!ready_for_recovery_.load(std::memory_order_acquire) &&
!need_abort->load(std::memory_order_acquire)) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
if (need_abort->load(std::memory_order_acquire)) {
LOG(INFO) << "Recovery pipeline aborted during wait for ready signal";
return false;
}
// After ready_for_recovery_ is set, data_manager_ should be initialized.
if (!data_manager_.has_value()) {
LOG(ERROR) << "DataManager not initialized, cannot run recovery";
TransitionState(HAClientState::DEGRADED,
"data_manager not initialized");
return false;
}
LOG(INFO) << "Recovery pipeline proceeding, service ready";
return true;
}
void HARecoveryManager::RecoveryPipelineMain(AbortToken need_abort) {
LOG(INFO) << "Recovery pipeline started";
if (!WaitForReady(need_abort)) {
return;
}
auto aborted = [&]() {
return need_abort->load(std::memory_order_acquire);
};
// Phase 1: Hot key sync — enqueue hot keys first for fastest recovery
auto hot_stats = data_manager_.value().GetHotKeyStats();
std::unordered_set<std::string> synced_keys;
size_t hot_count = 0;
for (const auto& entry : hot_stats.hot_keys) {
if (aborted()) return;
auto size_result = data_manager_.value().QueryObjectSize(entry.key);
if (!size_result || size_result.value() == 0) continue;
size_t size = size_result.value();
auto tier_ids = data_manager_.value().GetReplicaTierIds(entry.key);
for (const auto& tier_id : tier_ids) {
if (notifier_) {
// Hot keys go through normal (high-priority) queue
if (!EnqueueWithRetry(entry.key, tier_id, size,
/*is_hot=*/true, need_abort)) {
return; // aborted
}
hot_count++;
}
}
synced_keys.insert(entry.key);
}
LOG(INFO) << "Recovery Phase 1: enqueued " << hot_count
<< " hot key entries";
if (aborted()) return;
// Phase 2+3: Iterate all keys in batches.
// DRAM entries enqueued before storage entries within each batch.
auto tier_views = data_manager_.value().GetTierViews();
std::unordered_set<UUID, boost::hash<UUID>> dram_tiers;
for (const auto& tv : tier_views) {
if (tv.type == MemoryType::DRAM) {
dram_tiers.insert(tv.id);
}
}
size_t dram_count = 0, storage_count = 0;
bool was_aborted = false;
data_manager_.value().ForEachKeyBatch(
[&](std::vector<ReplicaLocation>&& batch) -> bool {
if (aborted()) {
was_aborted = true;
return false;
}
// Pass 1: DRAM entries first for fastest recovery
for (const auto& e : batch) {
if (synced_keys.count(e.key)) continue;
if (dram_tiers.count(e.tier_id)) {
if (e.size == 0) continue;
if (notifier_) {
if (!EnqueueWithRetry(e.key, e.tier_id, e.size,
/*is_hot=*/false, need_abort)) {
was_aborted = true;
LOG(WARNING)
<< "fail to enqueue route recovery list";
return false;
}
dram_count++;
}
}
}
// Pass 2: Storage entries
for (const auto& e : batch) {
if (synced_keys.count(e.key)) continue;
if (dram_tiers.count(e.tier_id)) continue;
if (e.size == 0) continue;
if (notifier_) {
if (!EnqueueWithRetry(e.key, e.tier_id, e.size,
/*is_hot=*/false, need_abort)) {
was_aborted = true;
LOG(WARNING) << "fail to enqueue route recovery list";
return false;
}
storage_count++;
}
}
return true;
});
if (was_aborted) {
LOG(INFO) << "Recovery aborted during key iteration";
return;
}
LOG(INFO) << "Recovery enqueue complete: hot=" << hot_count
<< ", dram=" << dram_count << ", storage=" << storage_count;
// Wait for recovery queue to drain (all ops sent to Master).
static constexpr auto kRecoveryDrainTimeout = std::chrono::minutes(10);
if (notifier_) {
bool drained = notifier_->WaitForRecoveryDrain(
[&]() { return aborted(); }, kRecoveryDrainTimeout);
if (!drained) {
if (aborted()) {
LOG(INFO) << "Recovery aborted during drain wait";
return;
}
LOG(WARNING) << "Recovery drain timed out, transitioning to FULL"
<< " with incomplete route sync";
}
}
// All recovery routes delivered. Notify Master.
// Retry indefinitely until success or abort — if Master restarts again,
// HandleEvent(MASTER_UNREACHABLE) will set need_abort and this thread
// exits.
while (true) {
if (aborted()) return;
auto sync_result = SetSyncCompleted();
if (sync_result) break;
LOG(WARNING) << "SetSyncCompleted failed: " << sync_result.error()
<< ", retrying in 500ms";
std::unique_lock<std::mutex> lk(abort_mutex_);
abort_cv_.wait_for(lk, std::chrono::milliseconds(500),
[&] { return aborted(); });
}
// Transition SYNCING→FULL if not aborted.
if (!need_abort->load(std::memory_order_acquire)) {
HAClientState expected = HAClientState::SYNCING;
if (state_.compare_exchange_strong(expected, HAClientState::FULL,
std::memory_order_acq_rel)) {
LOG(WARNING) << "HA state: SYNCING -> FULL"
<< ", reason=recovery complete";
}
}
LOG(INFO) << "Recovery pipeline completed";
}
// return false when abort
bool HARecoveryManager::EnqueueWithRetry(const std::string& key,
const UUID& tier_id, size_t size,
bool is_hot,
const AbortToken& need_abort) {
while (true) {
if (need_abort->load(std::memory_order_acquire)) return false;
auto r = is_hot ? notifier_->EnqueueAdd(key, tier_id, size)
: notifier_->EnqueueRecoveryAdd(key, tier_id, size);
if (r) return true;
// Queue full — yield to normal writes then retry
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
} // namespace mooncake

View File

@ -10,7 +10,8 @@
#include "default_config.h"
#include "ha_helper.h"
#include "http_metadata_server.h"
#include "rpc_service.h"
#include "centralized_rpc_service.h"
#include "p2p_rpc_service.h"
#include "types.h"
#include "master_config.h"
@ -67,8 +68,10 @@ DEFINE_string(
etcd_endpoints, "",
"Endpoints of ETCD server, separated by semicolon, required in HA mode");
DEFINE_int64(client_ttl, mooncake::DEFAULT_CLIENT_LIVE_TTL_SEC,
"How long a client is considered alive after the last ping, only "
"used in HA mode");
"How long a client is considered alive after the last heartbeat");
DEFINE_int64(
client_crashed_ttl, mooncake::DEFAULT_CLIENT_CRASHED_TTL_SEC,
"How long a client is considered crashed after the last heartbeat");
DEFINE_string(root_fs_dir, mooncake::DEFAULT_ROOT_FS_DIR,
"Root directory for storage backend, used in HA mode");
@ -100,6 +103,11 @@ DEFINE_bool(enable_disk_eviction, true,
DEFINE_uint64(
quota_bytes, 0,
"Quota for storage backend in bytes (0 = use default 90% of capacity)");
DEFINE_string(deployment_mode, "Centralization",
"the deployment mode of mooncake-store, master and client must "
"run in same mode. Options: Centralization, P2P");
DEFINE_uint64(max_replicas_per_key, 1,
"Maximum number of replicas per key in P2P mode (0 = no limit)");
void InitMasterConf(const mooncake::DefaultConfig& default_config,
mooncake::MasterConfig& master_config) {
@ -138,6 +146,8 @@ void InitMasterConf(const mooncake::DefaultConfig& default_config,
default_config.GetInt64("client_live_ttl_sec",
&master_config.client_live_ttl_sec,
FLAGS_client_ttl);
default_config.GetInt64("client_crashed_ttl_sec",
&master_config.client_crashed_ttl_sec, -1);
default_config.GetBool("enable_ha", &master_config.enable_ha,
FLAGS_enable_ha);
@ -175,6 +185,11 @@ void InitMasterConf(const mooncake::DefaultConfig& default_config,
FLAGS_enable_disk_eviction);
default_config.GetUInt64("quota_bytes", &master_config.quota_bytes,
FLAGS_quota_bytes);
default_config.GetUInt64("max_replicas_per_key",
&master_config.max_replicas_per_key,
FLAGS_max_replicas_per_key);
default_config.GetString("deployment_mode", &master_config.deployment_mode,
FLAGS_deployment_mode);
}
void LoadConfigFromCmdline(mooncake::MasterConfig& master_config,
@ -298,6 +313,10 @@ void LoadConfigFromCmdline(mooncake::MasterConfig& master_config,
!conf_set) {
master_config.client_live_ttl_sec = FLAGS_client_ttl;
}
if (google::GetCommandLineFlagInfo("client_crashed_ttl", &info) &&
!info.is_default) {
master_config.client_crashed_ttl_sec = FLAGS_client_crashed_ttl;
}
if ((google::GetCommandLineFlagInfo("cluster_id", &info) &&
!info.is_default) ||
!conf_set) {
@ -360,6 +379,16 @@ void LoadConfigFromCmdline(mooncake::MasterConfig& master_config,
!conf_set) {
master_config.quota_bytes = FLAGS_quota_bytes;
}
if ((google::GetCommandLineFlagInfo("deployment_mode", &info) &&
!info.is_default) ||
!conf_set) {
master_config.deployment_mode = FLAGS_deployment_mode;
}
if ((google::GetCommandLineFlagInfo("max_replicas_per_key", &info) &&
!info.is_default) ||
!conf_set) {
master_config.max_replicas_per_key = FLAGS_max_replicas_per_key;
}
}
// Function to start HTTP metadata server
@ -432,6 +461,24 @@ int main(int argc, char* argv[]) {
if (value && std::string_view(value) == "rdma") {
protocol = "rdma";
}
// Process client_crashed_ttl_sec logic
if (master_config.client_crashed_ttl_sec == -1) {
// Not set by config file and not set by CLI -> Default to 3x live ttl
master_config.client_crashed_ttl_sec =
master_config.client_live_ttl_sec * 3;
} else {
// Explicitly set (by file or CLI) -> Validate
if (master_config.client_crashed_ttl_sec <
master_config.client_live_ttl_sec) {
LOG(FATAL) << "client_crashed_ttl ("
<< master_config.client_crashed_ttl_sec
<< ") must be >= client_ttl ("
<< master_config.client_live_ttl_sec << ")";
return 1;
}
}
LOG(INFO) << "Master service started on port " << master_config.rpc_port
<< ", max_threads=" << master_config.rpc_thread_num
<< ", enable_metric_reporting="
@ -449,6 +496,7 @@ int main(int argc, char* argv[]) {
<< ", enable_offload=" << master_config.enable_offload
<< ", etcd_endpoints=" << master_config.etcd_endpoints
<< ", client_ttl=" << master_config.client_live_ttl_sec
<< ", client_crashed_ttl=" << master_config.client_crashed_ttl_sec
<< ", rpc_thread_num=" << master_config.rpc_thread_num
<< ", rpc_port=" << master_config.rpc_port
<< ", rpc_address=" << master_config.rpc_address
@ -471,8 +519,16 @@ int main(int argc, char* argv[]) {
<< ", put_start_discard_timeout_sec="
<< master_config.put_start_discard_timeout_sec
<< ", put_start_release_timeout_sec="
<< master_config.put_start_release_timeout_sec;
<< master_config.put_start_release_timeout_sec
<< ", max_replicas_per_key=" << master_config.max_replicas_per_key
<< ", deployment_mode=" << master_config.deployment_mode;
if (master_config.deployment_mode != "Centralization" &&
master_config.deployment_mode != "P2P") {
LOG(FATAL) << "Invalid deployment mode: "
<< master_config.deployment_mode;
return 1;
}
// Start HTTP metadata server if enabled
std::unique_ptr<mooncake::HttpMetadataServer> http_metadata_server;
if (master_config.enable_http_metadata_server) {
@ -505,10 +561,30 @@ int main(int argc, char* argv[]) {
if (value && std::string_view(value) == "rdma") {
server.init_ibv();
}
mooncake::WrappedMasterService wrapped_master_service(
mooncake::WrappedMasterServiceConfig(master_config, version));
mooncake::RegisterRpcService(server, wrapped_master_service);
// Declare service object outside if block to ensure it lives until
// server.start() completes
std::unique_ptr<mooncake::WrappedMasterService> master_service;
if (master_config.deployment_mode == "Centralization") {
master_service =
std::make_unique<mooncake::WrappedCentralizedMasterService>(
mooncake::WrappedMasterServiceConfig(master_config,
version));
mooncake::RegisterCentralizedRpcService(
server, static_cast<mooncake::WrappedCentralizedMasterService&>(
*master_service));
} else {
master_service =
std::make_unique<mooncake::WrappedP2PMasterService>(
mooncake::WrappedMasterServiceConfig(master_config,
version));
mooncake::RegisterP2PRpcService(
server, static_cast<mooncake::WrappedP2PMasterService&>(
*master_service));
}
return server.start();
}
}

View File

@ -1,9 +1,6 @@
#include "master_client.h"
#include <async_simple/coro/FutureAwaiter.h>
#include <async_simple/coro/Lazy.h>
#include <async_simple/coro/SyncAwait.h>
#include <coroutine>
#include <string>
#include <vector>
#include <ylt/coro_rpc/impl/coro_rpc_client.hpp>
@ -13,14 +10,10 @@
#include "rpc_service.h"
#include "types.h"
#include "utils/scoped_vlog_timer.h"
#include "master_metric_manager.h"
#include "version.h"
namespace mooncake {
template <auto Method>
struct RpcNameTraits;
template <>
struct RpcNameTraits<&WrappedMasterService::ExistKey> {
static constexpr const char* value = "ExistKey";
@ -31,11 +24,6 @@ struct RpcNameTraits<&WrappedMasterService::BatchExistKey> {
static constexpr const char* value = "BatchExistKey";
};
template <>
struct RpcNameTraits<&WrappedMasterService::GetReplicaList> {
static constexpr const char* value = "GetReplicaList";
};
template <>
struct RpcNameTraits<&WrappedMasterService::CalcCacheStats> {
static constexpr const char* value = "CalcCacheStats";
@ -46,51 +34,21 @@ struct RpcNameTraits<&WrappedMasterService::BatchQueryIp> {
static constexpr const char* value = "BatchQueryIp";
};
template <>
struct RpcNameTraits<&WrappedMasterService::BatchReplicaClear> {
static constexpr const char* value = "BatchReplicaClear";
};
template <>
struct RpcNameTraits<&WrappedMasterService::GetReplicaListByRegex> {
static constexpr const char* value = "GetReplicaListByRegex";
};
template <>
struct RpcNameTraits<&WrappedMasterService::GetReplicaList> {
static constexpr const char* value = "GetReplicaList";
};
template <>
struct RpcNameTraits<&WrappedMasterService::BatchGetReplicaList> {
static constexpr const char* value = "BatchGetReplicaList";
};
template <>
struct RpcNameTraits<&WrappedMasterService::PutStart> {
static constexpr const char* value = "PutStart";
};
template <>
struct RpcNameTraits<&WrappedMasterService::BatchPutStart> {
static constexpr const char* value = "BatchPutStart";
};
template <>
struct RpcNameTraits<&WrappedMasterService::PutEnd> {
static constexpr const char* value = "PutEnd";
};
template <>
struct RpcNameTraits<&WrappedMasterService::BatchPutEnd> {
static constexpr const char* value = "BatchPutEnd";
};
template <>
struct RpcNameTraits<&WrappedMasterService::PutRevoke> {
static constexpr const char* value = "PutRevoke";
};
template <>
struct RpcNameTraits<&WrappedMasterService::BatchPutRevoke> {
static constexpr const char* value = "BatchPutRevoke";
};
template <>
struct RpcNameTraits<&WrappedMasterService::Remove> {
static constexpr const char* value = "Remove";
@ -111,29 +69,24 @@ struct RpcNameTraits<&WrappedMasterService::MountSegment> {
static constexpr const char* value = "MountSegment";
};
template <>
struct RpcNameTraits<&WrappedMasterService::ReMountSegment> {
static constexpr const char* value = "ReMountSegment";
};
template <>
struct RpcNameTraits<&WrappedMasterService::UnmountSegment> {
static constexpr const char* value = "UnmountSegment";
};
template <>
struct RpcNameTraits<&WrappedMasterService::Ping> {
static constexpr const char* value = "Ping";
struct RpcNameTraits<&WrappedMasterService::Heartbeat> {
static constexpr const char* value = "Heartbeat";
};
template <>
struct RpcNameTraits<&WrappedMasterService::GetFsdir> {
static constexpr const char* value = "GetFsdir";
struct RpcNameTraits<&WrappedMasterService::RegisterClient> {
static constexpr const char* value = "RegisterClient";
};
template <>
struct RpcNameTraits<&WrappedMasterService::GetStorageConfig> {
static constexpr const char* value = "GetStorageConfig";
struct RpcNameTraits<&WrappedMasterService::QueryClientStatus> {
static constexpr const char* value = "QueryClientStatus";
};
template <>
@ -141,129 +94,33 @@ struct RpcNameTraits<&WrappedMasterService::ServiceReady> {
static constexpr const char* value = "ServiceReady";
};
template <>
struct RpcNameTraits<&WrappedMasterService::MountLocalDiskSegment> {
static constexpr const char* value = "MountLocalDiskSegment";
};
template <>
struct RpcNameTraits<&WrappedMasterService::OffloadObjectHeartbeat> {
static constexpr const char* value = "OffloadObjectHeartbeat";
};
template <>
struct RpcNameTraits<&WrappedMasterService::NotifyOffloadSuccess> {
static constexpr const char* value = "NotifyOffloadSuccess";
};
template <auto ServiceMethod, typename ReturnType, typename... Args>
tl::expected<ReturnType, ErrorCode> MasterClient::invoke_rpc(Args&&... args) {
auto pool = client_accessor_.GetClientPool();
// Increment RPC counter
if (metrics_) {
metrics_->rpc_count.inc({RpcNameTraits<ServiceMethod>::value});
}
auto start_time = std::chrono::steady_clock::now();
return async_simple::coro::syncAwait(
[&]() -> async_simple::coro::Lazy<tl::expected<ReturnType, ErrorCode>> {
auto ret = co_await pool->send_request(
[&](coro_io::client_reuse_hint,
coro_rpc::coro_rpc_client& client) {
return client.send_request<ServiceMethod>(
std::forward<Args>(args)...);
});
if (!ret.has_value()) {
LOG(ERROR) << "Client not available";
co_return tl::make_unexpected(ErrorCode::RPC_FAIL);
}
auto result = co_await std::move(ret.value());
if (!result) {
LOG(ERROR) << "RPC call failed: " << result.error().msg;
co_return tl::make_unexpected(ErrorCode::RPC_FAIL);
}
if (metrics_) {
auto end_time = std::chrono::steady_clock::now();
auto latency =
std::chrono::duration_cast<std::chrono::microseconds>(
end_time - start_time);
metrics_->rpc_latency.observe(
{RpcNameTraits<ServiceMethod>::value}, latency.count());
}
co_return result->result();
}());
}
template <auto ServiceMethod, typename ResultType, typename... Args>
std::vector<tl::expected<ResultType, ErrorCode>> MasterClient::invoke_batch_rpc(
size_t input_size, Args&&... args) {
auto pool = client_accessor_.GetClientPool();
// Increment RPC counter
if (metrics_) {
metrics_->rpc_count.inc({RpcNameTraits<ServiceMethod>::value});
}
auto start_time = std::chrono::steady_clock::now();
return async_simple::coro::syncAwait(
[&]() -> async_simple::coro::Lazy<
std::vector<tl::expected<ResultType, ErrorCode>>> {
auto ret = co_await pool->send_request(
[&](coro_io::client_reuse_hint,
coro_rpc::coro_rpc_client& client) {
return client.send_request<ServiceMethod>(
std::forward<Args>(args)...);
});
if (!ret.has_value()) {
LOG(ERROR) << "Client not available";
co_return std::vector<tl::expected<ResultType, ErrorCode>>(
input_size, tl::make_unexpected(ErrorCode::RPC_FAIL));
}
auto result = co_await std::move(ret.value());
if (!result) {
LOG(ERROR) << "Batch RPC call failed: " << result.error().msg;
std::vector<tl::expected<ResultType, ErrorCode>> error_results;
error_results.reserve(input_size);
for (size_t i = 0; i < input_size; ++i) {
error_results.emplace_back(
tl::make_unexpected(ErrorCode::RPC_FAIL));
}
co_return error_results;
}
if (metrics_) {
auto end_time = std::chrono::steady_clock::now();
auto latency =
std::chrono::duration_cast<std::chrono::microseconds>(
end_time - start_time);
metrics_->rpc_latency.observe(
{RpcNameTraits<ServiceMethod>::value}, latency.count());
}
co_return result->result();
}());
}
MasterClient::~MasterClient() = default;
ErrorCode MasterClient::Connect(const std::string& master_addr) {
ScopedVLogTimer timer(1, "MasterClient::Connect");
timer.LogRequest("master_addr=", master_addr);
MutexLocker lock(&connect_mutex_);
if (client_addr_param_ != master_addr) {
bool is_same_addr = (client_addr_param_ == master_addr);
if (!is_same_addr) {
// WARNING: The existing client pool cannot be erased. So if there are a
// lot of different addresses, there will be resource leak problems.
auto client_pool = client_pools_->at(master_addr);
client_accessor_.SetClientPool(client_pool);
client_addr_param_ = master_addr;
}
auto pool = client_accessor_.GetClientPool();
// The client pool does not have native connection check method, so we need
// to use custom ServiceReady API.
auto result =
invoke_rpc<&WrappedMasterService::ServiceReady, std::string>();
if (!result.has_value() && is_same_addr) {
timer.LogResponse("error_code=", result.error());
// Stale connection pool might still exist.
// Retrying once will force the pool to re-establish a new connection.
result = invoke_rpc<&WrappedMasterService::ServiceReady, std::string>();
}
if (!result.has_value()) {
timer.LogResponse("error_code=", result.error());
client_addr_param_.clear();
return result.error();
}
// Check if server version matches client version
@ -300,6 +157,46 @@ std::vector<tl::expected<bool, ErrorCode>> MasterClient::BatchExistKey(
return result;
}
tl::expected<GetReplicaListResponse, ErrorCode> MasterClient::GetReplicaList(
const std::string& key, const GetReplicaListRequestConfig& config) {
ScopedVLogTimer timer(1, "MasterClient::GetReplicaList");
timer.LogRequest("object_key=", key);
auto result = invoke_rpc<&WrappedMasterService::GetReplicaList,
GetReplicaListResponse>(key, config);
timer.LogResponseExpected(result);
return result;
}
async_simple::coro::Lazy<tl::expected<GetReplicaListResponse, ErrorCode>>
MasterClient::AsyncGetReplicaList(const std::string& key,
const GetReplicaListRequestConfig& config) {
auto result =
co_await invoke_rpc_async<&WrappedMasterService::GetReplicaList,
GetReplicaListResponse>(key, config);
co_return result;
}
std::vector<tl::expected<GetReplicaListResponse, ErrorCode>>
MasterClient::BatchGetReplicaList(const std::vector<std::string>& keys,
const GetReplicaListRequestConfig& config) {
ScopedVLogTimer timer(1, "MasterClient::BatchGetReplicaList");
timer.LogRequest("requests_count=", keys.size());
if (keys.empty()) {
return {};
}
auto result = invoke_rpc<
&WrappedMasterService::BatchGetReplicaList,
std::vector<tl::expected<GetReplicaListResponse, ErrorCode>>>(keys,
config);
if (result.has_value()) {
timer.LogResponse("result=", result.value().size(), " requests");
}
return result.value();
}
tl::expected<MasterMetricManager::CacheHitStatDict, ErrorCode>
MasterClient::CalcCacheStats() {
return invoke_rpc<&WrappedMasterService::CalcCacheStats,
@ -322,21 +219,6 @@ MasterClient::BatchQueryIp(const std::vector<UUID>& client_ids) {
return result;
}
tl::expected<std::vector<std::string>, ErrorCode>
MasterClient::BatchReplicaClear(const std::vector<std::string>& object_keys,
const UUID& client_id,
const std::string& segment_name) {
ScopedVLogTimer timer(1, "MasterClient::BatchReplicaClear");
timer.LogRequest("object_keys_count=", object_keys.size(),
", client_id=", client_id,
", segment_name=", segment_name);
auto result = invoke_rpc<&WrappedMasterService::BatchReplicaClear,
std::vector<std::string>>(object_keys, client_id,
segment_name);
timer.LogResponseExpected(result);
return result;
}
tl::expected<std::unordered_map<std::string, std::vector<Replica::Descriptor>>,
ErrorCode>
MasterClient::GetReplicaListByRegex(const std::string& str) {
@ -351,117 +233,6 @@ MasterClient::GetReplicaListByRegex(const std::string& str) {
return result;
}
tl::expected<GetReplicaListResponse, ErrorCode> MasterClient::GetReplicaList(
const std::string& object_key) {
ScopedVLogTimer timer(1, "MasterClient::GetReplicaList");
timer.LogRequest("object_key=", object_key);
auto result = invoke_rpc<&WrappedMasterService::GetReplicaList,
GetReplicaListResponse>(object_key);
timer.LogResponseExpected(result);
return result;
}
std::vector<tl::expected<GetReplicaListResponse, ErrorCode>>
MasterClient::BatchGetReplicaList(const std::vector<std::string>& object_keys) {
ScopedVLogTimer timer(1, "MasterClient::BatchGetReplicaList");
timer.LogRequest("keys_count=", object_keys.size());
auto result = invoke_batch_rpc<&WrappedMasterService::BatchGetReplicaList,
GetReplicaListResponse>(object_keys.size(),
object_keys);
timer.LogResponse("result=", result.size(), " operations");
return result;
}
tl::expected<std::vector<Replica::Descriptor>, ErrorCode>
MasterClient::PutStart(const std::string& key,
const std::vector<size_t>& slice_lengths,
const ReplicateConfig& config) {
ScopedVLogTimer timer(1, "MasterClient::PutStart");
timer.LogRequest("key=", key, ", slice_count=", slice_lengths.size());
uint64_t total_slice_length = 0;
for (const auto& slice_length : slice_lengths) {
total_slice_length += slice_length;
}
auto result = invoke_rpc<&WrappedMasterService::PutStart,
std::vector<Replica::Descriptor>>(
client_id_, key, total_slice_length, config);
timer.LogResponseExpected(result);
return result;
}
std::vector<tl::expected<std::vector<Replica::Descriptor>, ErrorCode>>
MasterClient::BatchPutStart(
const std::vector<std::string>& keys,
const std::vector<std::vector<uint64_t>>& slice_lengths,
const ReplicateConfig& config) {
ScopedVLogTimer timer(1, "MasterClient::BatchPutStart");
timer.LogRequest("keys_count=", keys.size());
std::vector<uint64_t> total_slice_lengths;
total_slice_lengths.reserve(slice_lengths.size());
for (const auto& slice_lengths : slice_lengths) {
uint64_t total_slice_length = 0;
for (const auto& slice_length : slice_lengths) {
total_slice_length += slice_length;
}
total_slice_lengths.emplace_back(total_slice_length);
}
auto result = invoke_batch_rpc<&WrappedMasterService::BatchPutStart,
std::vector<Replica::Descriptor>>(
keys.size(), client_id_, keys, total_slice_lengths, config);
timer.LogResponse("result=", result.size(), " operations");
return result;
}
tl::expected<void, ErrorCode> MasterClient::PutEnd(const std::string& key,
ReplicaType replica_type) {
ScopedVLogTimer timer(1, "MasterClient::PutEnd");
timer.LogRequest("key=", key);
auto result = invoke_rpc<&WrappedMasterService::PutEnd, void>(
client_id_, key, replica_type);
timer.LogResponseExpected(result);
return result;
}
std::vector<tl::expected<void, ErrorCode>> MasterClient::BatchPutEnd(
const std::vector<std::string>& keys) {
ScopedVLogTimer timer(1, "MasterClient::BatchPutEnd");
timer.LogRequest("keys_count=", keys.size());
auto result = invoke_batch_rpc<&WrappedMasterService::BatchPutEnd, void>(
keys.size(), client_id_, keys);
timer.LogResponse("result=", result.size(), " operations");
return result;
}
tl::expected<void, ErrorCode> MasterClient::PutRevoke(
const std::string& key, ReplicaType replica_type) {
ScopedVLogTimer timer(1, "MasterClient::PutRevoke");
timer.LogRequest("key=", key);
auto result = invoke_rpc<&WrappedMasterService::PutRevoke, void>(
client_id_, key, replica_type);
timer.LogResponseExpected(result);
return result;
}
std::vector<tl::expected<void, ErrorCode>> MasterClient::BatchPutRevoke(
const std::vector<std::string>& keys) {
ScopedVLogTimer timer(1, "MasterClient::BatchPutRevoke");
timer.LogRequest("keys_count=", keys.size());
auto result = invoke_batch_rpc<&WrappedMasterService::BatchPutRevoke, void>(
keys.size(), client_id_, keys);
timer.LogResponse("result=", result.size(), " operations");
return result;
}
tl::expected<void, ErrorCode> MasterClient::Remove(const std::string& key) {
ScopedVLogTimer timer(1, "MasterClient::Remove");
timer.LogRequest("key=", key);
@ -490,31 +261,6 @@ tl::expected<long, ErrorCode> MasterClient::RemoveAll() {
return result;
}
tl::expected<void, ErrorCode> MasterClient::MountSegment(
const Segment& segment) {
ScopedVLogTimer timer(1, "MasterClient::MountSegment");
timer.LogRequest("base=", segment.base, ", size=", segment.size,
", name=", segment.name, ", id=", segment.id,
", client_id=", client_id_);
auto result = invoke_rpc<&WrappedMasterService::MountSegment, void>(
segment, client_id_);
timer.LogResponseExpected(result);
return result;
}
tl::expected<void, ErrorCode> MasterClient::ReMountSegment(
const std::vector<Segment>& segments) {
ScopedVLogTimer timer(1, "MasterClient::ReMountSegment");
timer.LogRequest("segments_num=", segments.size(),
", client_id=", client_id_);
auto result = invoke_rpc<&WrappedMasterService::ReMountSegment, void>(
segments, client_id_);
timer.LogResponseExpected(result);
return result;
}
tl::expected<void, ErrorCode> MasterClient::UnmountSegment(
const UUID& segment_id) {
ScopedVLogTimer timer(1, "MasterClient::UnmountSegment");
@ -526,73 +272,53 @@ tl::expected<void, ErrorCode> MasterClient::UnmountSegment(
return result;
}
tl::expected<PingResponse, ErrorCode> MasterClient::Ping() {
ScopedVLogTimer timer(1, "MasterClient::Ping");
tl::expected<HeartbeatResponse, ErrorCode> MasterClient::Heartbeat(
const HeartbeatRequest& req) {
ScopedVLogTimer timer(1, "MasterClient::Heartbeat");
timer.LogRequest("client_id=", client_id_);
auto result =
invoke_rpc<&WrappedMasterService::Ping, PingResponse>(client_id_);
invoke_rpc<&WrappedMasterService::Heartbeat, HeartbeatResponse>(req);
timer.LogResponseExpected(result);
return result;
}
tl::expected<std::string, ErrorCode> MasterClient::GetFsdir() {
ScopedVLogTimer timer(1, "MasterClient::GetFsdir");
timer.LogRequest("action=get_fsdir");
tl::expected<QueryClientStatusResponse, ErrorCode>
MasterClient::QueryClientStatus(const UUID& client_id) {
ScopedVLogTimer timer(1, "MasterClient::QueryClientStatus");
timer.LogRequest("client_id=", client_id);
auto result = invoke_rpc<&WrappedMasterService::GetFsdir, std::string>();
QueryClientStatusRequest req;
req.client_id = client_id;
auto result = invoke_rpc<&WrappedMasterService::QueryClientStatus,
QueryClientStatusResponse>(req);
timer.LogResponseExpected(result);
return result;
}
tl::expected<GetStorageConfigResponse, ErrorCode>
MasterClient::GetStorageConfig() {
ScopedVLogTimer timer(1, "MasterClient::GetStorageConfig");
timer.LogRequest("action=get_storage_config");
tl::expected<void, ErrorCode> MasterClient::MountSegment(
const Segment& segment) {
ScopedVLogTimer timer(1, "MasterClient::MountSegment");
timer.LogRequest("segment_name=", segment.name, ", client_id=", client_id_);
auto result = invoke_rpc<&WrappedMasterService::GetStorageConfig,
GetStorageConfigResponse>();
auto result = invoke_rpc<&WrappedMasterService::MountSegment, void>(
segment, client_id_);
timer.LogResponseExpected(result);
return result;
}
tl::expected<void, ErrorCode> MasterClient::MountLocalDiskSegment(
const UUID& client_id, bool enable_offloading) {
ScopedVLogTimer timer(1, "MasterClient::MountLocalDiskSegment");
timer.LogRequest("client_id=", client_id,
", enable_offloading=", enable_offloading);
tl::expected<RegisterClientResponse, ErrorCode> MasterClient::RegisterClient(
const RegisterClientRequest& req) {
ScopedVLogTimer timer(1, "MasterClient::RegisterClient");
timer.LogRequest("client_id=", client_id_,
", segments_count=", req.segments.size(),
", deployment_mode=", req.deployment_mode);
auto result =
invoke_rpc<&WrappedMasterService::MountLocalDiskSegment, void>(
client_id, enable_offloading);
auto result = invoke_rpc<&WrappedMasterService::RegisterClient,
RegisterClientResponse>(req);
timer.LogResponseExpected(result);
return result;
}
tl::expected<std::unordered_map<std::string, int64_t>, ErrorCode>
MasterClient::OffloadObjectHeartbeat(const UUID& client_id,
bool enable_offloading) {
ScopedVLogTimer timer(1, "MasterClient::OffloadObjectHeartbeat");
timer.LogRequest("client_id=", client_id,
", enable_offloading=", enable_offloading);
auto result = invoke_rpc<&WrappedMasterService::OffloadObjectHeartbeat,
std::unordered_map<std::string, int64_t>>(
client_id, enable_offloading);
return result;
}
tl::expected<void, ErrorCode> MasterClient::NotifyOffloadSuccess(
const UUID& client_id, const std::vector<std::string>& keys,
const std::vector<StorageObjectMetadata>& metadatas) {
ScopedVLogTimer timer(1, "MasterClient::NotifyOffloadSuccess");
timer.LogRequest("client_id=", client_id, ", keys_count=", keys.size(),
", metadatas_count=", metadatas.size());
auto result = invoke_rpc<&WrappedMasterService::NotifyOffloadSuccess, void>(
client_id, keys, metadatas);
timer.LogResponseExpected(result);
return result;
}
} // namespace mooncake
} // namespace mooncake

View File

@ -5,6 +5,7 @@
#include <sstream> // For string building during serialization
#include <vector> // Required by histogram serialization
#include <cmath>
#include <new>
#include "utils.h"
@ -109,10 +110,24 @@ MasterMetricManager::MasterMetricManager()
remount_segment_failures_(
"master_remount_segment_failures_total",
"Total number of failed RemountSegment requests"),
ping_requests_("master_ping_requests_total",
"Total number of ping requests received"),
ping_failures_("master_ping_failures_total",
"Total number of failed ping requests"),
heartbeat_requests_("master_heartbeat_requests_total",
"Total number of heartbeat requests received"),
heartbeat_failures_("master_heartbeat_failures_total",
"Total number of failed heartbeat requests"),
get_write_route_requests_("master_get_write_route_requests_total",
"Total number of get write route requests"),
get_write_route_failures_(
"master_get_write_route_failures_total",
"Total number of failed get write route requests"),
add_replica_requests_("master_add_replica_requests_total",
"Total number of add replica requests"),
add_replica_failures_("master_add_replica_failures_total",
"Total number of failed add replica requests"),
remove_replica_requests_("master_remove_replica_requests_total",
"Total number of remove replica requests"),
remove_replica_failures_(
"master_remove_replica_failures_total",
"Total number of failed remove replica requests"),
// Initialize Batch Request Counters
batch_exist_key_requests_(
@ -217,6 +232,30 @@ MasterMetricManager::MasterMetricManager()
batch_put_revoke_failed_items_(
"master_batch_put_revoke_failed_items_total",
"Total number of failed items in BatchPutRevoke requests"),
batch_remove_replica_requests_(
"master_batch_remove_replica_requests_total",
"Total number of BatchRemoveReplica requests received"),
batch_remove_replica_failures_(
"master_batch_remove_replica_failures_total",
"Total number of failed BatchRemoveReplica requests"),
batch_remove_replica_partial_successes_(
"master_batch_remove_replica_partial_successes_total",
"Total number of partially successful BatchRemoveReplica requests"),
batch_remove_replica_items_(
"master_batch_remove_replica_items_total",
"Total number of items processed in BatchRemoveReplica requests"),
batch_remove_replica_failed_items_(
"master_batch_remove_replica_failed_items_total",
"Total number of failed items in BatchRemoveReplica requests"),
batch_get_write_route_requests_(
"master_batch_get_write_route_requests_total",
"Total number of BatchGetWriteRoute requests received"),
batch_get_write_route_failures_(
"master_batch_get_write_route_failures_total",
"Total number of failed BatchGetWriteRoute requests"),
batch_get_write_route_partial_successes_(
"master_batch_get_write_route_partial_successes_total",
"Total number of partially successful BatchGetWriteRoute requests"),
// Initialize cache hit rate metrics
mem_cache_hit_nums_("mem_cache_hit_nums_",
@ -250,10 +289,14 @@ MasterMetricManager::MasterMetricManager()
"master_put_start_discarded_staging_size",
"Total size of memory replicas in discarded but not yet released "
"PutStart operations") {
// Update all metrics once to ensure zero values are serialized
update_metrics_for_zero_output();
}
void MasterMetricManager::reset_all_metrics() {
this->~MasterMetricManager();
new (this) MasterMetricManager();
}
// --- Metric Interface Methods ---
void MasterMetricManager::update_metrics_for_zero_output() {
@ -294,8 +337,14 @@ void MasterMetricManager::update_metrics_for_zero_output() {
unmount_segment_failures_.inc(0);
remount_segment_requests_.inc(0);
remount_segment_failures_.inc(0);
ping_requests_.inc(0);
ping_failures_.inc(0);
heartbeat_requests_.inc(0);
heartbeat_failures_.inc(0);
get_write_route_requests_.inc(0);
get_write_route_failures_.inc(0);
add_replica_requests_.inc(0);
add_replica_failures_.inc(0);
remove_replica_requests_.inc(0);
remove_replica_failures_.inc(0);
// Update Batch Request Counters
batch_exist_key_requests_.inc(0);
@ -333,6 +382,14 @@ void MasterMetricManager::update_metrics_for_zero_output() {
batch_put_revoke_partial_successes_.inc(0);
batch_put_revoke_items_.inc(0);
batch_put_revoke_failed_items_.inc(0);
batch_remove_replica_requests_.inc(0);
batch_remove_replica_failures_.inc(0);
batch_remove_replica_partial_successes_.inc(0);
batch_remove_replica_items_.inc(0);
batch_remove_replica_failed_items_.inc(0);
batch_get_write_route_requests_.inc(0);
batch_get_write_route_failures_.inc(0);
batch_get_write_route_partial_successes_.inc(0);
// Update cache hit rate metrics
mem_cache_hit_nums_.inc(0);
@ -594,11 +651,29 @@ void MasterMetricManager::inc_remount_segment_requests(int64_t val) {
void MasterMetricManager::inc_remount_segment_failures(int64_t val) {
remount_segment_failures_.inc(val);
}
void MasterMetricManager::inc_ping_requests(int64_t val) {
ping_requests_.inc(val);
void MasterMetricManager::inc_heartbeat_requests(int64_t val) {
heartbeat_requests_.inc(val);
}
void MasterMetricManager::inc_ping_failures(int64_t val) {
ping_failures_.inc(val);
void MasterMetricManager::inc_heartbeat_failures(int64_t val) {
heartbeat_failures_.inc(val);
}
void MasterMetricManager::inc_get_write_route_requests(int64_t val) {
get_write_route_requests_.inc(val);
}
void MasterMetricManager::inc_get_write_route_failures(int64_t val) {
get_write_route_failures_.inc(val);
}
void MasterMetricManager::inc_add_replica_requests(int64_t val) {
add_replica_requests_.inc(val);
}
void MasterMetricManager::inc_add_replica_failures(int64_t val) {
add_replica_failures_.inc(val);
}
void MasterMetricManager::inc_remove_replica_requests(int64_t val) {
remove_replica_requests_.inc(val);
}
void MasterMetricManager::inc_remove_replica_failures(int64_t val) {
remove_replica_failures_.inc(val);
}
// Batch Operation Statistics (Counters)
@ -695,6 +770,34 @@ void MasterMetricManager::inc_batch_put_revoke_partial_success(
batch_put_revoke_partial_successes_.inc(1);
batch_put_revoke_failed_items_.inc(failed_items);
}
void MasterMetricManager::inc_batch_remove_replica_requests(int64_t items) {
batch_remove_replica_requests_.inc(1);
batch_remove_replica_items_.inc(items);
}
void MasterMetricManager::inc_batch_remove_replica_failures(
int64_t failed_items) {
batch_remove_replica_failures_.inc(1);
batch_remove_replica_failed_items_.inc(failed_items);
}
void MasterMetricManager::inc_batch_remove_replica_partial_success(
int64_t failed_items) {
batch_remove_replica_partial_successes_.inc(1);
batch_remove_replica_failed_items_.inc(failed_items);
}
void MasterMetricManager::inc_batch_get_write_route_requests(int64_t items) {
batch_get_write_route_requests_.inc(1);
(void)items;
}
void MasterMetricManager::inc_batch_get_write_route_failures(
int64_t failed_items) {
batch_get_write_route_failures_.inc(1);
(void)failed_items;
}
void MasterMetricManager::inc_batch_get_write_route_partial_success(
int64_t failed_items) {
batch_get_write_route_partial_successes_.inc(1);
(void)failed_items;
}
// PutStart Discard Metrics
void MasterMetricManager::inc_put_start_discard_cnt(int64_t count,
@ -797,6 +900,25 @@ int64_t MasterMetricManager::get_unmount_segment_failures() {
return unmount_segment_failures_.value();
}
int64_t MasterMetricManager::get_get_write_route_requests() {
return get_write_route_requests_.value();
}
int64_t MasterMetricManager::get_get_write_route_failures() {
return get_write_route_failures_.value();
}
int64_t MasterMetricManager::get_add_replica_requests() {
return add_replica_requests_.value();
}
int64_t MasterMetricManager::get_add_replica_failures() {
return add_replica_failures_.value();
}
int64_t MasterMetricManager::get_remove_replica_requests() {
return remove_replica_requests_.value();
}
int64_t MasterMetricManager::get_remove_replica_failures() {
return remove_replica_failures_.value();
}
int64_t MasterMetricManager::get_remount_segment_requests() {
return remount_segment_requests_.value();
}
@ -805,12 +927,12 @@ int64_t MasterMetricManager::get_remount_segment_failures() {
return remount_segment_failures_.value();
}
int64_t MasterMetricManager::get_ping_requests() {
return ping_requests_.value();
int64_t MasterMetricManager::get_heartbeat_requests() {
return heartbeat_requests_.value();
}
int64_t MasterMetricManager::get_ping_failures() {
return ping_failures_.value();
int64_t MasterMetricManager::get_heartbeat_failures() {
return heartbeat_failures_.value();
}
int64_t MasterMetricManager::get_batch_exist_key_requests() {
@ -953,6 +1075,38 @@ int64_t MasterMetricManager::get_batch_put_revoke_failed_items() {
return batch_put_revoke_failed_items_.value();
}
int64_t MasterMetricManager::get_batch_remove_replica_requests() {
return batch_remove_replica_requests_.value();
}
int64_t MasterMetricManager::get_batch_remove_replica_failures() {
return batch_remove_replica_failures_.value();
}
int64_t MasterMetricManager::get_batch_remove_replica_partial_successes() {
return batch_remove_replica_partial_successes_.value();
}
int64_t MasterMetricManager::get_batch_remove_replica_items() {
return batch_remove_replica_items_.value();
}
int64_t MasterMetricManager::get_batch_remove_replica_failed_items() {
return batch_remove_replica_failed_items_.value();
}
int64_t MasterMetricManager::get_batch_get_write_route_requests() {
return batch_get_write_route_requests_.value();
}
int64_t MasterMetricManager::get_batch_get_write_route_failures() {
return batch_get_write_route_failures_.value();
}
int64_t MasterMetricManager::get_batch_get_write_route_partial_successes() {
return batch_get_write_route_partial_successes_.value();
}
// Eviction Metrics
void MasterMetricManager::inc_eviction_success(int64_t key_count,
int64_t size) {
@ -1046,8 +1200,14 @@ std::string MasterMetricManager::serialize_metrics() {
serialize_metric(unmount_segment_failures_);
serialize_metric(remount_segment_requests_);
serialize_metric(remount_segment_failures_);
serialize_metric(ping_requests_);
serialize_metric(ping_failures_);
serialize_metric(heartbeat_requests_);
serialize_metric(heartbeat_failures_);
serialize_metric(get_write_route_requests_);
serialize_metric(get_write_route_failures_);
serialize_metric(add_replica_requests_);
serialize_metric(add_replica_failures_);
serialize_metric(remove_replica_requests_);
serialize_metric(remove_replica_failures_);
// Serialize Batch Request Counters
serialize_metric(batch_exist_key_requests_);
@ -1058,12 +1218,23 @@ std::string MasterMetricManager::serialize_metrics() {
serialize_metric(batch_replica_clear_failures_);
serialize_metric(batch_get_replica_list_requests_);
serialize_metric(batch_get_replica_list_failures_);
serialize_metric(batch_get_replica_list_partial_successes_);
serialize_metric(batch_get_replica_list_items_);
serialize_metric(batch_get_replica_list_failed_items_);
serialize_metric(batch_put_start_requests_);
serialize_metric(batch_put_start_failures_);
serialize_metric(batch_put_end_requests_);
serialize_metric(batch_put_end_failures_);
serialize_metric(batch_put_revoke_requests_);
serialize_metric(batch_put_revoke_failures_);
serialize_metric(batch_remove_replica_requests_);
serialize_metric(batch_remove_replica_failures_);
serialize_metric(batch_remove_replica_partial_successes_);
serialize_metric(batch_remove_replica_items_);
serialize_metric(batch_remove_replica_failed_items_);
serialize_metric(batch_get_write_route_requests_);
serialize_metric(batch_get_write_route_failures_);
serialize_metric(batch_get_write_route_partial_successes_);
// Serialize Eviction Counters
serialize_metric(eviction_success_);
@ -1223,6 +1394,14 @@ std::string MasterMetricManager::get_summary_string() {
int64_t batch_replica_clear_items = batch_replica_clear_items_.value();
int64_t batch_replica_clear_failed_items =
batch_replica_clear_failed_items_.value();
int64_t batch_remove_replica_requests =
batch_remove_replica_requests_.value();
int64_t batch_remove_replica_fails = batch_remove_replica_failures_.value();
int64_t batch_remove_replica_partial_successes =
batch_remove_replica_partial_successes_.value();
int64_t batch_remove_replica_items = batch_remove_replica_items_.value();
int64_t batch_remove_replica_failed_items =
batch_remove_replica_failed_items_.value();
// Eviction counters
int64_t eviction_success = eviction_success_.value();
@ -1230,9 +1409,9 @@ std::string MasterMetricManager::get_summary_string() {
int64_t evicted_key_count = evicted_key_count_.value();
int64_t evicted_size = evicted_size_.value();
// Ping counters
int64_t ping = ping_requests_.value();
int64_t ping_fails = ping_failures_.value();
// Heartbeat counters
int64_t heartbeat = heartbeat_requests_.value();
int64_t heartbeat_fails = heartbeat_failures_.value();
// Discard counters
int64_t put_start_discard_cnt = put_start_discard_cnt_.value();
@ -1265,7 +1444,8 @@ std::string MasterMetricManager::get_summary_string() {
ss << "Del=" << removes - remove_fails << "/" << removes << ", ";
ss << "DelAll=" << remove_all - remove_all_fails << "/" << remove_all
<< ", ";
ss << "Ping=" << ping - ping_fails << "/" << ping << ", ";
ss << "Heartbeat=" << heartbeat - heartbeat_fails << "/" << heartbeat
<< ", ";
// Batch request summary
ss << " | Batch Requests "
@ -1319,14 +1499,24 @@ std::string MasterMetricManager::get_summary_string() {
<< batch_replica_clear_requests << ", Item="
<< batch_replica_clear_items - batch_replica_clear_failed_items << "/"
<< batch_replica_clear_items << "), ";
ss << "RemoveRep:(Req="
<< batch_remove_replica_requests - batch_remove_replica_fails -
batch_remove_replica_partial_successes
<< "/" << batch_remove_replica_partial_successes << "/"
<< batch_remove_replica_requests << ", Item="
<< batch_remove_replica_items - batch_remove_replica_failed_items << "/"
<< batch_remove_replica_items << "), ";
// Eviction summary
ss << " | Eviction: " << "Success/Attempts=" << eviction_success << "/"
<< eviction_attempts << ", " << "keys=" << evicted_key_count << ", "
ss << " | Eviction: "
<< "Success/Attempts=" << eviction_success << "/" << eviction_attempts
<< ", "
<< "keys=" << evicted_key_count << ", "
<< "size=" << byte_size_to_string(evicted_size);
// Discard summary
ss << " | Discard: " << "Released/Total=" << put_start_release_cnt << "/"
ss << " | Discard: "
<< "Released/Total=" << put_start_release_cnt << "/"
<< put_start_discard_cnt << ", StagingSize="
<< byte_size_to_string(put_start_discarded_staging_size);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,95 @@
#include "p2p_client_manager.h"
#include "p2p_client_meta.h"
#include <glog/logging.h>
#include <algorithm>
namespace mooncake {
class CapacityPriorityIterator : public ClientIterator {
public:
CapacityPriorityIterator(
const std::unordered_map<UUID, std::shared_ptr<ClientMeta>,
boost::hash<UUID>>& client_metas) {
if (client_metas.empty()) return;
clients_.reserve(client_metas.size());
for (auto& client : client_metas) {
if (auto p2p_meta =
std::static_pointer_cast<P2PClientMeta>(client.second)) {
clients_.emplace_back(p2p_meta);
}
}
std::sort(clients_.begin(), clients_.end(),
[](const auto& a, const auto& b) {
return std::static_pointer_cast<P2PClientMeta>(a)
->GetAvailableCapacity() >
std::static_pointer_cast<P2PClientMeta>(b)
->GetAvailableCapacity();
});
}
};
P2PClientManager::P2PClientManager(const int64_t disconnect_timeout_sec,
const int64_t crash_timeout_sec,
const ViewVersionId view_version)
: ClientManager(disconnect_timeout_sec, crash_timeout_sec, view_version) {}
std::unique_ptr<ClientIterator> P2PClientManager::InnerBuildClientIterator(
ObjectIterateStrategy strategy) {
auto iterator = ClientManager::InnerBuildClientIterator(strategy);
if (iterator) {
return iterator;
}
switch (strategy) {
case ObjectIterateStrategy::CAPACITY_PRIORITY:
return std::make_unique<CapacityPriorityIterator>(client_metas_);
default:
return nullptr;
}
}
std::shared_ptr<ClientMeta> P2PClientManager::CreateClientMeta(
const RegisterClientRequest& req) {
auto meta = std::make_shared<P2PClientMeta>(
req.client_id, req.ip_address.value_or(""), req.rpc_port.value_or(0));
return meta;
}
HeartbeatTaskResult P2PClientManager::ProcessTask(const UUID& client_id,
const HeartbeatTask& task) {
HeartbeatTaskResult result;
result.type = task.type_;
switch (task.type_) {
case HeartbeatTaskType::SYNC_SEGMENT_META: {
auto client_meta =
std::static_pointer_cast<P2PClientMeta>(GetClient(client_id));
const auto* param = std::get_if<SyncSegmentMetaParam>(&task.param_);
if (client_meta && param) {
auto sync_res =
client_meta->UpdateSegmentUsages(param->tier_usages);
result.detail = sync_res;
for (const auto& sub : sync_res.sub_results) {
if (sub.error != ErrorCode::OK) {
// result.error means the task is failed.
// here just sub task error, don't affect task result.
LOG(ERROR) << "fail to update segment usages"
<< ", client_id=" << client_id
<< ", segment_id=" << sub.segment_id
<< ", error=" << sub.error;
}
}
} else {
result.error = ErrorCode::INVALID_PARAMS;
}
break;
}
default:
result.error = ErrorCode::NOT_IMPLEMENTED;
break;
}
return result;
}
} // namespace mooncake

View File

@ -0,0 +1,152 @@
#include "p2p_client_meta.h"
#include <algorithm>
#include <glog/logging.h>
namespace mooncake {
P2PClientMeta::P2PClientMeta(const UUID& client_id,
const std::string& ip_address, uint16_t rpc_port)
: ClientMeta(client_id), ip_address_(ip_address), rpc_port_(rpc_port) {
segment_manager_ = std::make_shared<P2PSegmentManager>();
segment_manager_->SetSegmentChangeCallbacks(
[this](const Segment& segment) {
// OnSegmentAddedCallback
SpinRWLockLocker lock(&capacity_mutex_);
client_capacity_ += segment.size;
client_usage_ += segment.GetP2PExtra().usage;
},
[this](const Segment& segment) {
// OnSegmentRemovedCallback
SpinRWLockLocker lock(&capacity_mutex_);
client_capacity_ -= segment.size;
client_usage_ -= segment.GetP2PExtra().usage;
});
}
std::shared_ptr<SegmentManager> P2PClientMeta::GetSegmentManager() {
return segment_manager_;
}
tl::expected<std::vector<std::string>, ErrorCode> P2PClientMeta::QueryIp(
const UUID& client_id) {
SharedMutexLocker lock(&client_mutex_, shared_lock);
auto check_ret = InnerStatusCheck();
if (!check_ret.has_value()) {
LOG(ERROR) << "fail to inner check client status"
<< ", client_id=" << client_id_
<< ", ret=" << check_ret.error();
return tl::make_unexpected(check_ret.error());
}
return std::vector<std::string>{ip_address_};
}
SyncSegmentMetaResult P2PClientMeta::UpdateSegmentUsages(
const std::vector<TierUsageInfo>& usages) {
SyncSegmentMetaResult result;
SpinRWLockLocker lock(&capacity_mutex_);
for (const auto& usage : usages) {
SyncSegmentMetaResult::SubResult sub_res;
sub_res.segment_id = usage.segment_id;
auto old_usage =
segment_manager_->UpdateSegmentUsage(usage.segment_id, usage.usage);
if (!old_usage.has_value()) {
LOG(WARNING) << "fail to update segment usage"
<< ", client_id: " << client_id_
<< ", segment_id: " << usage.segment_id
<< ", usage: " << usage.usage
<< ", error: " << old_usage.error();
sub_res.error = old_usage.error();
result.sub_results.push_back(sub_res);
continue;
}
client_usage_ = client_usage_ - old_usage.value() + usage.usage;
sub_res.error = ErrorCode::OK;
result.sub_results.push_back(sub_res);
}
return result;
}
size_t P2PClientMeta::GetAvailableCapacity() const {
SpinRWLockLocker lock(&capacity_mutex_, shared_lock);
if (client_capacity_ <= client_usage_) return 0;
return client_capacity_ - client_usage_;
}
auto P2PClientMeta::CollectWriteRouteCandidates(
const WriteRouteRequest& req, std::vector<WriteCandidate>& candidates)
-> tl::expected<bool, ErrorCode> {
SharedMutexLocker lock(&client_mutex_, shared_lock);
// Check health status under lock protection
auto check_ret = InnerStatusCheck();
if (!check_ret.has_value()) {
LOG(WARNING) << "client could not route"
<< ", client_id: " << client_id_;
return false; // skip unhealthy client, candidaes are not enough
}
// localhost is not allowed, skip current client
if (!req.config.allow_local && client_id_ == req.client_id)
return false; // candidaes are not enough
// iterate segments to find candidates
bool trigger_stop_early = false;
segment_manager_->ForEachSegment([&](const Segment& seg) -> bool {
// In ForEachSegment callback:
// 1. return false means continue to process next segment
// (skip current segment or finish processing)
// 2. return true means early stop
size_t usage = seg.GetP2PExtra().usage;
if (seg.size - usage < req.size)
return false; // usage does not enough, candidaes are not enough
const auto& p2p_extra = seg.GetP2PExtra();
// exclude segments that contain any tags in tag_filters
bool hit_filter_tag = false;
for (const auto& tag : req.config.tag_filters) {
if (std::find(p2p_extra.tags.begin(), p2p_extra.tags.end(), tag) !=
p2p_extra.tags.end()) {
hit_filter_tag = true;
break;
}
}
if (hit_filter_tag) return false; // hit excluding tag, skip segment
if (p2p_extra.priority < req.config.priority_limit)
return false; // priority does not enough, candidaes are not enough
int priority = p2p_extra.priority;
if (req.config.prefer_local && client_id_ == req.client_id) {
// hit localhost and prefer local, add infinite priority
priority += INF_PRIORITY;
}
WriteCandidate candidate;
candidate.available_capacity = seg.size - usage;
candidate.priority = priority;
candidate.replica.client_id = client_id_;
candidate.replica.segment_id = seg.id;
candidate.replica.ip_address = ip_address_;
candidate.replica.rpc_port = rpc_port_;
candidate.replica.object_size = req.size;
candidates.push_back(std::move(candidate));
if (req.config.early_return &&
candidates.size() >= req.config.max_candidates &&
req.config.max_candidates !=
WriteRouteRequestConfig::RETURN_ALL_CANDIDATES) {
// current candidates are enough, early stop
trigger_stop_early = true;
return true; // stop ForEachSegment
}
return false; // process next segment
});
// early stop, if trigger_stop_early == true, stop ForEachClient
return trigger_stop_early;
}
} // namespace mooncake

Some files were not shown because too many files have changed in this diff Show More