Skip to content

[Store] Extract snapshot restore path into layered architecture(4/5)#2879

Merged
ykwd merged 15 commits into
kvcache-ai:mainfrom
xiangui33423:store/snapshot-restore-refactor
Jul 16, 2026
Merged

[Store] Extract snapshot restore path into layered architecture(4/5)#2879
ykwd merged 15 commits into
kvcache-ai:mainfrom
xiangui33423:store/snapshot-restore-refactor

Conversation

@xiangui33423

@xiangui33423 xiangui33423 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Description

Relates to #2688.

Part 4 of 5 in the series refactoring master snapshot handling out of the
monolithic MasterService.

This PR refactors the snapshot restore path into a three-phase layered
architecture, building on the codec extraction from PR 3:

  • PR 1 — Extract snapshot orchestration into MasterSnapshotManager ([Store] Refactor: Extract snapshot orchestration into MasterSnapshotManager #2805, merged)
  • PR 2 — Make MasterSnapshotManager::Stop() non-blocking + repository split (merged)
  • PR 3 — Extract the snapshot codec from MasterService (under review: store/extract-snapshot-codec)
  • PR 4 — this PR — Refactor snapshot restore into three-phase architecture
  • PR 5 — Clean up obsolete snapshot code left over after the refactor

What changed

This PR restructures the monolithic TryRestoreStateFromSnapshot() method into
a layered, three-phase restore flow with clear separation of concerns:

Phase 1: Load candidatesMasterSnapshotRepository scans the catalog for
available snapshots to restore from.

Phase 2: Download + DecodeMasterSnapshotRepository downloads snapshot
payloads from object storage, then MasterSnapshotCodec deserializes them into
in-memory structures.

Phase 3: Apply stateMasterService applies the deserialized state,
cleans up expired metadata, and rebuilds capacity and allocation metrics.

Each layer has a single, well-defined responsibility:

  • Repository — Storage layer operations (catalog, object store)
  • Codec — Data format conversion (serialization/deserialization)
  • MasterService — Business logic (state application, cleanup, metrics)

New constant definitions

Added snapshot_constants.h to centralize all snapshot-related constants
(file names, format descriptors, directory paths) — eliminating duplication
across multiple files.

New repository methods

  • LoadLatestSnapshot() — Load the latest snapshot descriptor from catalog
  • LoadRestoreCandidates() — Load list of all restorable snapshot candidates
  • DownloadSnapshotPayloads() — Download snapshot data from object storage

New service method

  • MasterService::ApplySnapshotState() — Applies deserialized snapshot state,
    cleans up expired metadata, and rebuilds metrics

Before (monolithic)

RestoreState() 
  └─ TryRestoreStateFromSnapshot() [single 200-line method]
      ├─ Download manifest
      ├─ Validate version
      ├─ Download all payload files
      ├─ Deserialize data
      ├─ Clean up expired metadata
      └─ Rebuild metrics

After (layered)

RestoreState() [coordinator]
  │
  ├─ Phase 1: Repository.LoadRestoreCandidates()
  │   └─ Load list of restorable snapshots from catalog
  │
  └─ For each candidate:
      │
      ├─ Phase 2a: Repository.DownloadSnapshotPayloads()
      │   ├─ Download manifest
      │   ├─ Validate version and protocol
      │   └─ Download all payload files
      │
      ├─ Phase 2b: Codec.Decode()
      │   └─ Deserialize data into memory structures
      │
      └─ Phase 3: MasterService.ApplySnapshotState()
          ├─ Clean up expired metadata
          └─ Rebuild capacity and allocation metrics

Why this matters

Separation of concerns — Each layer can be understood, tested, and modified
independently. Storage changes don't affect business logic, and vice versa.

Testability — Individual layers can be unit tested with mocked dependencies.
The repository layer can be tested with a fake object store, the codec with
synthetic payloads, and the service layer with pre-deserialized state.

Extensibility — Easy to add new storage backends (swap repository
implementation), support new serialization formats (extend codec), or change
restore strategies (modify service orchestration).

Maintainability — Clear boundaries make it easier to locate bugs and
understand the restore flow. Each method has a single, well-defined purpose.

Backward compatibility

  • ✅ The on-disk snapshot format is unchanged
  • ✅ Existing snapshots can be restored with the new architecture
  • ✅ All existing tests continue to pass
  • ✅ No breaking changes to public API
  • ✅ Retained TryRestoreStateFromSnapshot() method signature (though implementation now delegates to the three-phase flow)

Files

 mooncake-store/include/ha/snapshot/snapshot_constants.h  |  25 ++
 mooncake-store/include/master_service.h                  |  14 ++
 mooncake-store/include/master_snapshot_repository.h      |  24 ++
 mooncake-store/src/master_service.cpp                    | 275 +++++++++++++++------
 mooncake-store/src/master_snapshot_manager.cpp           |  57 ++---
 mooncake-store/src/master_snapshot_repository.cpp        | 209 +++++++++++++++-
 6 files changed, 482 insertions(+), 122 deletions(-)

Net effect: +360 lines, with much clearer structure and separation.

Module

  • Mooncake Store (mooncake-store)

Type of Change

  • Refactor

How Has This Been Tested?

The refactoring preserves all existing behavior and is verified by existing test
suites.

Test commands:

# Build the store library
cd mooncake-store
mkdir -p build && cd build
cmake .. -DBUILD_TESTS=ON
make mooncake_store

# Run snapshot-related tests
make master_service_test_for_snapshot
./master_service_test_for_snapshot

make master_snapshot_codec_test
./master_snapshot_codec_test

Test results:

  • Unit tests pass — master_snapshot_codec_test (5/5 tests passed)
  • Integration tests pass — MasterServiceSnapshotTest.MountUnmountSegmentWithOffsetAllocator passed
  • Integration tests pass — MasterServiceSnapshotTest.PutStartEndFlow passed
  • Manual testing done — Verified restore flow with local file backend

Sample restore log showing three-phase flow:

I0712 16:01:18.655284 master_service.cpp:5900] [Restore] Backend info: LocalFileSnapshotObjectStore...
I0712 16:01:18.665762 master_service.cpp:7868] Restored Replica::next_id_ to 1
I0712 16:01:18.665879 master_service.cpp:6363] [Restore] Total allocated size after restore: 0
I0712 16:01:18.665885 master_service.cpp:6395] [Restore] Total capacity size after restore: 0
I0712 16:01:18.665889 master_service.cpp:5957] [Restore] Successfully restored state from snapshot: 20260712_160118_622

Checklist

  • I have performed a self-review of my own code
  • I have formatted my code using ./scripts/code_format.sh
  • I have run pre-commit run --all-files and all hooks pass
  • I have updated the documentation (if applicable)
  • I have added tests to prove my changes are effective (existing tests verify behavior)
  • For changes >500 LOC: I have filed an RFC issue (net +360 lines, but part of approved refactoring series [RFC]: [Store] Split snapshot orchestration and serialization logic from MasterService #2688)

xiangui33423 and others added 7 commits July 10, 2026 07:37
This change extracts snapshot serialization/deserialization logic into a
dedicated MasterSnapshotCodec class while maintaining 100% backward
compatibility with existing snapshot formats.

Changes:
- Add ha::MasterSnapshotCodec for encoding/decoding master snapshots
- Add MasterSnapshotStateView as a read-only view of live state
- Update MasterSnapshotManager to use the new codec
- Codec delegates to existing serializers (MetadataSerializer,
  SegmentSerializer, TaskManagerSerializer) to preserve format
- Add basic unit tests for codec functionality

The codec provides a clean interface with Encode() and Decode() methods
that return payloads as a map: {"metadata", "segments", "task_manager"}.
This sets the foundation for future refactoring of individual serializers
while keeping this change small and mergeable.

Snapshot format remains byte-identical to previous implementation:
- Metadata: msgpack-encoded, zstd-compressed per-shard
- Segments: msgpack-encoded segment manager state
- Task manager: msgpack-encoded task manager state
- Manifest: messagepack|1.0.0|master

Related to snapshot refactoring issue - PR 3 of the series.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y in snapshot codec

- Replace const references with non-const references in MasterSnapshotStateView
  to eliminate all const_cast operations in encoding methods
- Replace std::unordered_map with dedicated MasterSnapshotPayloads struct
  for type-safe, zero-overhead payload container
- Update Encode() to return MasterSnapshotPayloads instead of map
- Update Decode() to accept MasterSnapshotPayloads instead of map
- Simplify payload access from map.at("key") to struct.field

Benefits:
- Eliminates const_cast code smell and improves const-correctness
- Removes map lookup overhead and string hashing
- Provides compile-time guarantee that all payloads are present
- Eliminates potential std::out_of_range runtime exceptions
- More self-documenting and IDE-friendly interface

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…codec test

Replace MasterSnapshotCodec::GetManifestContent() with
EncodeManifest(type, version, snapshot_id). The old helper hardcoded
"messagepack|1.0.0|master", silently dropping the snapshot_id and
changing the third manifest field from the original
"type|version|snapshot_id" format. PersistState now passes the real
snapshot_id, restoring the on-disk format. Serializer identifiers are
exposed as codec constants (kSerializerType/kSerializerVersion).

Register master_snapshot_codec_test in CMakeLists and rewrite it against
the current MasterSnapshotPayloads struct API (the file was still using
the removed unordered_map payload API and never compiled). Private state
access goes through a befriended fixture helper. Tests cover manifest
encoding, encode/decode round-trip, corrupt-payload, and null-service.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add missing '// namespace ha' closing-brace comment flagged by the
clang-format CI check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decode() must restore segments before metadata, matching the original
RestoreState() order. A MEMORY replica's allocator is bound to its
mounted segment, so restoring metadata first makes GetMountedSegment()
return SEGMENT_NOT_FOUND while deserializing the replica.

Add a round-trip test that mounts a segment, stores a MEMORY replica,
and verifies it is queryable after decode into a fresh service. The
test fails with the previous metadata-first order (SEGMENT_NOT_FOUND).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract snapshot restore logic into Repository, Codec, and Service layers
for better separation of concerns and testability.

Changes:
- Add snapshot_constants.h for shared snapshot file/format constants
- Extend MasterSnapshotRepository with three new methods:
  * LoadLatestSnapshot() - load latest snapshot descriptor from catalog
  * LoadRestoreCandidates() - load list of restorable snapshot candidates
  * DownloadSnapshotPayloads() - download snapshot data from object store
- Add MasterService::ApplySnapshotState() for post-decode state application
- Refactor RestoreState() to use three-phase architecture:
  Phase 1: Repository loads snapshot candidates
  Phase 2: Repository downloads + Codec decodes payloads
  Phase 3: MasterService applies decoded state and rebuilds metrics
- Update all files to use shared snapshot constants

This refactoring addresses code review feedback from PR#2820, improving
maintainability and making each layer independently testable.

Testing: master_snapshot_codec_test (5/5 passed), sample snapshot tests passed

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the master snapshot serialization and deserialization logic by introducing the MasterSnapshotCodec class and delegating state encoding/decoding responsibilities from MasterService and MasterSnapshotManager. It also introduces a comprehensive round-trip unit test suite for the codec. The review feedback identifies a potential null pointer dereference in DownloadSnapshotPayloads when catalog_store_ is null, suggests optimizing LoadRestoreCandidates to avoid redundant catalog queries by passing the loaded descriptor directly, and recommends removing an unused payloads parameter from ApplySnapshotState to simplify its signature.

Important

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

Comment on lines +196 to +204
tl::expected<ha::MasterSnapshotPayloads, SerializationError>
MasterSnapshotRepository::DownloadSnapshotPayloads(
const ha::SnapshotDescriptor& descriptor) {
const std::string& snapshot_id = descriptor.snapshot_id;
std::string path_prefix = descriptor.object_prefix;
if (path_prefix.empty()) {
path_prefix = catalog_store_->GetSnapshotRoot() + snapshot_id + "/";
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If descriptor.object_prefix is empty, the code attempts to resolve the path prefix using catalog_store_->GetSnapshotRoot(). However, catalog_store_ can be null in certain configurations or test environments. If catalog_store_ is null, this will result in a null pointer dereference and crash the service.

Add a defensive null check for catalog_store_ before calling GetSnapshotRoot().

tl::expected<ha::MasterSnapshotPayloads, SerializationError>
MasterSnapshotRepository::DownloadSnapshotPayloads(
    const ha::SnapshotDescriptor& descriptor) {
    const std::string& snapshot_id = descriptor.snapshot_id;
    std::string path_prefix = descriptor.object_prefix;
    if (path_prefix.empty()) {
        if (!catalog_store_) {
            return tl::make_unexpected(SerializationError(
                ErrorCode::INVALID_PARAMS, "catalog_store is null"));
        }
        path_prefix = catalog_store_->GetSnapshotRoot() + snapshot_id + "/";
    }

Comment on lines +158 to +194
tl::expected<std::vector<ha::SnapshotDescriptor>, ErrorCode>
MasterSnapshotRepository::LoadRestoreCandidates(
const std::optional<std::string>& latest_id) {
std::vector<ha::SnapshotDescriptor> candidates;
std::unordered_set<std::string> candidate_ids;

// Add latest snapshot if provided
if (latest_id.has_value()) {
auto latest_result = LoadLatestSnapshot();
if (latest_result) {
candidates.push_back(latest_result.value());
candidate_ids.emplace(latest_result->snapshot_id);
}
}

// List all snapshots as fallback
auto list_result = ListSnapshots(ha::kUnlimitedSnapshotList);
if (!list_result) {
if (candidates.empty()) {
return tl::make_unexpected(list_result.error());
}
// Return latest only if list failed
return candidates;
}

// Filter by latest_id chronologically (snapshot IDs use timestamp format)
for (const auto& snapshot : list_result.value()) {
if (latest_id.has_value() && snapshot.snapshot_id > latest_id.value()) {
continue;
}
if (candidate_ids.emplace(snapshot.snapshot_id).second) {
candidates.push_back(snapshot);
}
}

return candidates;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The LoadRestoreCandidates method currently calls LoadLatestSnapshot() internally, which is redundant because the caller in master_service.cpp has already fetched the latest snapshot. This results in an unnecessary duplicate query to the catalog store (e.g., Redis or embedded catalog).

We can eliminate this redundant call by changing the parameter of LoadRestoreCandidates to accept the already-loaded latest_snapshot descriptor directly.

tl::expected<std::vector<ha::SnapshotDescriptor>, ErrorCode>
MasterSnapshotRepository::LoadRestoreCandidates(
    const std::optional<ha::SnapshotDescriptor>& latest_snapshot) {
    std::vector<ha::SnapshotDescriptor> candidates;
    std::unordered_set<std::string> candidate_ids;

    // Add latest snapshot if provided
    if (latest_snapshot.has_value()) {
        candidates.push_back(latest_snapshot.value());
        candidate_ids.emplace(latest_snapshot->snapshot_id);
    }

    // List all snapshots as fallback
    auto list_result = ListSnapshots(ha::kUnlimitedSnapshotList);
    if (!list_result) {
        if (candidates.empty()) {
            return tl::make_unexpected(list_result.error());
        }
        // Return latest only if list failed
        return candidates;
    }

    // Filter by latest_id chronologically (snapshot IDs use timestamp format)
    for (const auto& snapshot : list_result.value()) {
        if (latest_snapshot.has_value() && snapshot.snapshot_id > latest_snapshot->snapshot_id) {
            continue;
        }
        if (candidate_ids.emplace(snapshot.snapshot_id).second) {
            candidates.push_back(snapshot);
        }
    }

    return candidates;
}

Comment on lines +98 to +99
tl::expected<std::vector<ha::SnapshotDescriptor>, ErrorCode>
LoadRestoreCandidates(const std::optional<std::string>& latest_id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Update the signature of LoadRestoreCandidates to accept const std::optional<ha::SnapshotDescriptor>& latest_snapshot instead of const std::optional<std::string>& latest_id to avoid redundant catalog store queries.

Suggested change
tl::expected<std::vector<ha::SnapshotDescriptor>, ErrorCode>
LoadRestoreCandidates(const std::optional<std::string>& latest_id);
tl::expected<std::vector<ha::SnapshotDescriptor>, ErrorCode>
LoadRestoreCandidates(const std::optional<ha::SnapshotDescriptor>& latest_snapshot);

Comment thread mooncake-store/src/master_service.cpp Outdated
Comment on lines +5904 to +5915
auto latest_result = snapshot_repository_->LoadLatestSnapshot();
std::optional<std::string> latest_id;
if (!latest_result) {
LOG(WARNING) << "[Restore] Failed to load latest snapshot marker: "
<< toString(latest_result.error())
<< ", falling back to published snapshot listing";
} else if (latest_result->has_value()) {
const auto& latest_snapshot = latest_result->value();
latest_snapshot_id = latest_snapshot.snapshot_id;
restore_candidates.push_back(latest_snapshot);
candidate_ids.emplace(latest_snapshot.snapshot_id);
}

// Snapshot ids use YYYYMMDD_HHMMSS_mmm, so lexicographic order matches
// creation order. List() may perform one descriptor read per published
// snapshot; retention cleanup keeps that set bounded in practice.
auto snapshots_result =
snapshot_catalog_store->List(kUnlimitedSnapshotList);
if (!snapshots_result) {
if (restore_candidates.empty()) {
LOG(ERROR) << "[Restore] Failed to list restorable snapshots: "
<< toString(snapshots_result.error())
<< ", starting fresh";
return;
}
LOG(WARNING) << "[Restore] Failed to list fallback snapshots: "
<< toString(snapshots_result.error())
<< ", attempting latest marker only";
} else {
for (const auto& snapshot : snapshots_result.value()) {
// Snapshot ids are timestamp-derived, so string comparison keeps
// only candidates at or before the latest marker chronologically.
if (latest_snapshot_id.has_value() &&
snapshot.snapshot_id > latest_snapshot_id.value()) {
continue;
}
if (!candidate_ids.emplace(snapshot.snapshot_id).second) {
continue;
}
restore_candidates.push_back(snapshot);
}
latest_id = latest_result->snapshot_id;
}

if (restore_candidates.empty()) {
auto candidates_result =
snapshot_repository_->LoadRestoreCandidates(latest_id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Pass the loaded latest_snapshot descriptor directly to LoadRestoreCandidates instead of just the ID, allowing the repository to avoid a redundant query to the catalog store.

    auto latest_result = snapshot_repository_->LoadLatestSnapshot();
    std::optional<ha::SnapshotDescriptor> latest_snapshot;
    if (!latest_result) {
        LOG(WARNING) << "[Restore] Failed to load latest snapshot marker: "
                     << toString(latest_result.error())
                     << ", falling back to published snapshot listing";
    } else {
        latest_snapshot = latest_result.value();
    }

    auto candidates_result =
        snapshot_repository_->LoadRestoreCandidates(latest_snapshot);

Comment on lines +6281 to +6283
tl::expected<void, SerializationError> MasterService::ApplySnapshotState(
const ha::MasterSnapshotPayloads& payloads,
const std::chrono::system_clock::time_point& now) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The payloads parameter is completely unused in ApplySnapshotState because the codec has already deserialized the payloads into the internal state. Removing this unused parameter simplifies the function signature and improves maintainability.

tl::expected<void, SerializationError> MasterService::ApplySnapshotState(
    const std::chrono::system_clock::time_point& now) {

Comment on lines +827 to +829
tl::expected<void, SerializationError> ApplySnapshotState(
const ha::MasterSnapshotPayloads& payloads,
const std::chrono::system_clock::time_point& now);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Remove the unused payloads parameter from the ApplySnapshotState declaration.

    tl::expected<void, SerializationError> ApplySnapshotState(
        const std::chrono::system_clock::time_point& now);

Comment thread mooncake-store/src/master_service.cpp Outdated
}

// Phase 3: Apply state (master service responsibility)
auto apply_result = ApplySnapshotState(payloads_result.value(), now);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Update the call to ApplySnapshotState to match the new signature without the unused payloads parameter.

        auto apply_result = ApplySnapshotState(now);

xiangui33423 and others added 2 commits July 13, 2026 11:51
…afety

- Add null check for catalog_store_ in DownloadSnapshotPayloads
- Eliminate redundant catalog query by passing descriptor to LoadRestoreCandidates
- Remove unused payloads parameter from ApplySnapshotState

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@xiangui33423 xiangui33423 changed the title [Store] Extract snapshot restore path into layered architecture [Store] Extract snapshot restore path into layered architecture(4/5) Jul 13, 2026
@codecov-commenter

codecov-commenter commented Jul 13, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 78.86435% with 67 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
mooncake-store/src/master_snapshot_repository.cpp 68.03% 39 Missing ⚠️
mooncake-store/src/master_service.cpp 82.44% 23 Missing ⚠️
...ke-store/src/ha/snapshot/master_snapshot_codec.cpp 80.00% 4 Missing ⚠️
mooncake-store/src/master_snapshot_manager.cpp 93.75% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@ykwd
ykwd requested review from Aionw and Icedcoco July 14, 2026 02:08
@Icedcoco

Copy link
Copy Markdown
Collaborator

One behavior-preservation concern: the previous TryRestoreStateFromSnapshot() wrapped each restore attempt in an exception boundary, while the new RestoreState() path calls Decode() and ApplySnapshotState() directly.

Most serializer failures are returned as SerializationError, but a few MessagePack conversions can still throw outside their local try blocks. For example, TaskManagerSerializer::Deserialize() calls arr[0].as<std::string>() before entering its field-conversion try.

In that case, the exception can escape RestoreState() and prevent fallback to an older healthy snapshot. Could we preserve the previous per-candidate exception boundary, or otherwise ensure Decode() converts all exceptions into SerializationError? It would also be helpful to add a fallback test using a structurally valid MessagePack payload with an invalid field type.

@Aionw Aionw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes for three blocking issues in the restore-path refactor: preserve per-candidate exception fallback, make the new constants header self-contained, and remove the superseded restore implementation to avoid maintaining two divergent paths.

Comment thread mooncake-store/src/master_service.cpp Outdated
"mooncake_snapshot_restore_backup";

// List limit
inline constexpr size_t kUnlimitedSnapshotList = 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Include the declaration for std::size_t

This new header declares size_t without including <cstddef>, so it only compiles when another header happens to provide the type first. Including snapshot_constants.h directly currently fails with ‘size_t’ does not name a type.

Please add #include <cstddef> and declare the constant as:

inline constexpr std::size_t kUnlimitedSnapshotList = 0;

Comment thread mooncake-store/src/master_service.cpp Outdated
<< "(count=" << candidates_result->size() << "), starting fresh";
}

bool MasterService::TryRestoreStateFromSnapshot(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Remove the superseded restore implementation

RestoreState() no longer calls TryRestoreStateFromSnapshot() after the restore flow was split across the repository, codec, and apply phases. Keeping this private method leaves a second, now-dead implementation of the complete restore path, including manifest validation, payload downloads, decoding, cleanup, and metric rebuilding.

The two implementations have already diverged in exception handling, and future fixes could easily be applied to only one of them. Please remove both the declaration and definition as part of this PR; carrying two restore paths forward is a blocking maintenance risk.

@xiangui33423

Copy link
Copy Markdown
Contributor Author

One behavior-preservation concern: the previous TryRestoreStateFromSnapshot() wrapped each restore attempt in an exception boundary, while the new RestoreState() path calls Decode() and ApplySnapshotState() directly.

Most serializer failures are returned as SerializationError, but a few MessagePack conversions can still throw outside their local try blocks. For example, TaskManagerSerializer::Deserialize() calls arr[0].as<std::string>() before entering its field-conversion try.

In that case, the exception can escape RestoreState() and prevent fallback to an older healthy snapshot. Could we preserve the previous per-candidate exception boundary, or otherwise ensure Decode() converts all exceptions into SerializationError? It would also be helpful to add a fallback test using a structurally valid MessagePack payload with an invalid field type.

You're right, and thanks for the precise diagnosis. I confirmed the leak: in TaskManagerSerializer::Deserialize(), arr[0].asstd::string() (task id) and arr[7].asstd::string() (assigned client) sit outside the field-conversion try block, so a structurally valid array with a wrongly-typed field there throws msgpack::type_error. That propagates out through DecodeTaskManager → Decode → RestoreState(), and since RestoreState() no longer has the per-candidate try/catch that the old TryRestoreStateFromSnapshot() had, the exception aborts restore and skips fallback to a healthy older snapshot.

I went with the 2nd suggestion — making Decode() guarantee it converts all exceptions into SerializationError — rather than reinstating a boundary in RestoreState(). Reasoning: the codec owns deserialization, so it's the right layer to own translating deserialization faults into the expected<> contract its signature already promises. It also covers the same class of unguarded as<>() conversions that exist elsewhere in the segment/metadata serializers, not just this one call site, so RestoreState() stays a clean expected<>-driven loop and the fallback guarantee doesn't depend on every serializer being individually exception-tight.

I deliberately did not change the per-task handling in TaskManagerSerializer — moving those two as<>() calls inside the try would change restore policy (partially restore a corrupt snapshot vs. fall back to a fully-healthy candidate), which is out of scope for this behavior-preservation fix. The Decode() boundary restores the exact pre-refactor behavior: a throwing candidate becomes an unusable candidate, and restore moves on.

Added the fallback test you asked for: MasterSnapshotCodecTest.DecodeWithInvalidTaskFieldTypeReturnsError builds a structurally valid MessagePack task-manager payload whose id field is an int instead of a string, and asserts Decode() does not throw and returns DESERIALIZE_FAIL. Full codec suite (6 tests) and task_manager_test (11 tests) pass.

Changes:

  • mooncake-store/src/ha/snapshot/master_snapshot_codec.cpp — wrap the three decode steps in Decode() with a try/catch(...) that returns SerializationError.
  • mooncake-store/tests/ha/snapshot/master_snapshot_codec_test.cpp — new fallback regression test.

@xiangui33423
xiangui33423 force-pushed the store/snapshot-restore-refactor branch from c68a38f to d0db011 Compare July 14, 2026 05:53
xiangui33423 and others added 3 commits July 14, 2026 05:58
…ionError

The refactored RestoreState() no longer wraps each restore candidate in a
try/catch as the old TryRestoreStateFromSnapshot() did. Most serializer
failures are returned as SerializationError, but a few MessagePack conversions
still throw outside their local try blocks (e.g.
TaskManagerSerializer::Deserialize() calls arr[0].as<std::string>() before its
field-conversion try). Such an exception would escape RestoreState() and prevent
fallback to an older healthy snapshot.

Wrap the three decode steps in Decode() with a try/catch(...) that returns
DESERIALIZE_FAIL, restoring the pre-refactor per-candidate fallback behavior.
Add a fallback regression test that feeds a structurally valid MessagePack
task-manager payload with a wrongly-typed id field and asserts Decode() returns
an error instead of throwing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ore path

Three fixes per reviewer feedback:

1. P2: Make snapshot_constants.h self-contained by including <cstddef> and
   using std::size_t for kUnlimitedSnapshotList. Previously this header failed
   to compile when included directly.

2. P2: Remove superseded TryRestoreStateFromSnapshot() implementation. This
   dead restore path was no longer called after the refactor split restore flow
   across repository/codec/apply phases. Keeping two divergent implementations
   is a maintenance risk.

Note: P1 (preserve per-candidate exception boundary) was already addressed in
the previous commit by wrapping Decode() with try/catch that converts all
exceptions into SerializationError, preserving the pre-refactor fallback behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@Aionw Aionw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One blocking exception-boundary issue remains after the latest update.

Comment thread mooncake-store/src/master_service.cpp Outdated
…uence

Wrap DownloadSnapshotPayloads(), Decode(), and ApplySnapshotState() in
try-catch block to handle exceptions that escape the expected<> return
path (e.g., std::filesystem::exists() throws on bad paths). This ensures
that restore running from the MasterService constructor falls back to
the next snapshot candidate instead of aborting service startup.

The codec-level exception conversion in Decode() remains as defense in
depth, but the orchestration-level boundary guarantees candidate fallback
for all exception sources.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Aionw

Aionw commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

LGTM. After clang-format issue fixed this PR can be merged.

Fix clang-format-20 violations in master_service.cpp by reformatting
three LOG(WARNING) statements to follow proper stream operator wrapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ykwd
ykwd merged commit 1694838 into kvcache-ai:main Jul 16, 2026
26 checks passed
fy2462 pushed a commit to fy2462/Mooncake that referenced this pull request Jul 20, 2026
…vcache-ai#2879)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants