Skip to content

[Store] Decouple HTTP metadata cleanup from MasterService#2957

Open
Aionw wants to merge 3 commits into
kvcache-ai:mainfrom
Aionw:codex/http-metadata-cleanup-worker
Open

[Store] Decouple HTTP metadata cleanup from MasterService#2957
Aionw wants to merge 3 commits into
kvcache-ai:mainfrom
Aionw:codex/http-metadata-cleanup-worker

Conversation

@Aionw

@Aionw Aionw commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Description

Decouple stale HTTP metadata cleanup from MasterService.

MasterService now publishes a typed ClientLeaseExpiredEvent after the expired client's resources have been reclaimed and all internal locks have been released. A standalone HttpMetadataCleanupWorker, owned by WrappedMasterService, subscribes to that event and asynchronously removes the client's ram/ and rpc_meta/ entries for both co-located and remote HTTP metadata servers.

This keeps HTTP backend configuration, key construction, queueing, and worker lifecycle outside the core master service while preserving the existing best-effort behavior, including no retry or shutdown-drain guarantee.

Overlap notes:

Module

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

Type of Change

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

How Has This Been Tested?

Test commands:

cmake -S . -B /tmp/mooncake-79c4-build   -DBUILD_UNIT_TESTS=ON -DWITH_STORE_RUST=OFF -DUSE_HTTP=ON   -DCMAKE_BUILD_TYPE=Debug
cmake --build /tmp/mooncake-79c4-build   --target master_service_test http_metadata_cleanup_worker_test -j8
ctest --test-dir /tmp/mooncake-79c4-build   -R '^master_service_test$' --output-on-failure
/tmp/mooncake-79c4-build/mooncake-store/tests/http_metadata_cleanup_worker_test

cmake -S . -B /tmp/mooncake-79c4-nohttp-build   -DBUILD_UNIT_TESTS=ON -DWITH_STORE_RUST=OFF -DUSE_HTTP=OFF   -DCMAKE_BUILD_TYPE=Debug
cmake --build /tmp/mooncake-79c4-nohttp-build   --target http_metadata_cleanup_worker_test -j8
/tmp/mooncake-79c4-nohttp-build/mooncake-store/tests/http_metadata_cleanup_worker_test

Test results:

  • Unit tests pass
  • Integration tests pass (not applicable)
  • Manual testing done
  • Full master_service_test: passed in 327.68 seconds.
  • Worker tests pass with both USE_HTTP=ON and USE_HTTP=OFF.
  • Relevant non-CMake pre-commit hooks and git diff --check pass.
  • Documentation build was attempted but sphinx-build is unavailable in the environment.
  • The repository-wide format hooks rewrite unrelated baseline files; those unrelated edits were reverted, and touched C++ files pass the clang-format hook.

Checklist

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

AI Assistance Disclosure

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

OpenAI Codex helped design and implement the event boundary and cleanup worker extraction, add tests and documentation, run validation, and draft this PR description. The human submitter must review every changed line and be able to defend the change end-to-end before marking this PR ready for review.

@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 HTTP metadata cleanup logic out of MasterService into a dedicated HttpMetadataCleanupWorker class, introducing a callback-based event system (ClientLeaseExpiredEvent) to decouple resource reclamation from metadata cleanup. The reviewer feedback focuses on improving thread safety and lifetime management: specifically, suggesting the use of std::shared_ptr and std::weak_ptr for the cleanup worker to prevent potential use-after-free issues in callbacks, and introducing a separate lifecycle_mutex_ in HttpMetadataCleanupWorker to avoid potential deadlocks during Start() and Stop() operations.

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 +309 to +312
// Declared before master_service_ so it is destroyed afterwards. This
// keeps the callback target alive until MasterService has stopped and
// joined its client monitor thread.
std::unique_ptr<HttpMetadataCleanupWorker> http_metadata_cleanup_worker_;

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

Relying on member declaration order for thread safety and lifetime management is fragile and prone to future regressions if the members are reordered. Using std::shared_ptr for the worker and capturing a std::weak_ptr in the callback lambda is a much more robust and idiomatic C++ pattern.

Suggested change
// Declared before master_service_ so it is destroyed afterwards. This
// keeps the callback target alive until MasterService has stopped and
// joined its client monitor thread.
std::unique_ptr<HttpMetadataCleanupWorker> http_metadata_cleanup_worker_;
std::shared_ptr<HttpMetadataCleanupWorker> http_metadata_cleanup_worker_;

Comment on lines +21 to 34
if (http_metadata_server || !http_metadata_remote_url.empty()) {
http_metadata_cleanup_worker_ =
std::make_unique<HttpMetadataCleanupWorker>(
http_metadata_server, http_metadata_remote_url);
if (http_metadata_cleanup_worker_->Start()) {
master_service_.RegisterClientLeaseExpiredCallback(
[worker = http_metadata_cleanup_worker_.get()](
const ClientLeaseExpiredEvent& event) {
worker->Enqueue(event);
});
} else {
http_metadata_cleanup_worker_.reset();
}
}

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

Instead of capturing a raw pointer (worker = http_metadata_cleanup_worker_.get()) in the callback lambda, capture a std::weak_ptr to prevent potential use-after-free issues if the worker is destroyed before the callback is executed.

    if (http_metadata_server || !http_metadata_remote_url.empty()) {
        http_metadata_cleanup_worker_ =
            std::make_shared<HttpMetadataCleanupWorker>(
                http_metadata_server, http_metadata_remote_url);
        if (http_metadata_cleanup_worker_->Start()) {
            master_service_.RegisterClientLeaseExpiredCallback(
                [worker_weak = std::weak_ptr<HttpMetadataCleanupWorker>(http_metadata_cleanup_worker_)](
                    const ClientLeaseExpiredEvent& event) {
                    if (auto worker = worker_weak.lock()) {
                        worker->Enqueue(event);
                    }
                });
        } else {
            http_metadata_cleanup_worker_.reset();
        }
    }

Comment on lines +56 to +58
std::mutex mutex_;
std::condition_variable cv_;
std::vector<ClientLeaseExpiredEvent> queue_;

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

Add a separate lifecycle_mutex_ to protect lifecycle operations (Start() and Stop()) from concurrent races. Using mutex_ for this is unsafe because holding it during thread_.join() can cause a deadlock if the worker thread is waiting on the condition variable.

    std::mutex mutex_;
    std::mutex lifecycle_mutex_;
    std::condition_variable cv_;
    std::vector<ClientLeaseExpiredEvent> queue_;

Comment on lines +82 to +112
bool HttpMetadataCleanupWorker::Start() {
if (!remove_key_) {
return false;
}

bool expected = false;
if (!running_.compare_exchange_strong(expected, true)) {
return true;
}
try {
thread_ = std::thread(&HttpMetadataCleanupWorker::ThreadFunc, this);
} catch (...) {
running_ = false;
throw;
}
return true;
}

void HttpMetadataCleanupWorker::Stop() {
{
std::lock_guard<std::mutex> lock(mutex_);
if (!running_.exchange(false)) {
return;
}
queue_.clear();
}
cv_.notify_all();
if (thread_.joinable()) {
thread_.join();
}
}

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

Implement thread-safe Start() and Stop() using the newly introduced lifecycle_mutex_ to prevent concurrency races on thread_ and avoid deadlocks.

bool HttpMetadataCleanupWorker::Start() {
    if (!remove_key_) {
        return false;
    }

    std::lock_guard<std::mutex> lock(lifecycle_mutex_);
    if (running_) {
        return true;
    }
    running_ = true;
    try {
        thread_ = std::thread(&HttpMetadataCleanupWorker::ThreadFunc, this);
    } catch (...) {
        running_ = false;
        throw;
    }
    return true;
}

void HttpMetadataCleanupWorker::Stop() {
    std::lock_guard<std::mutex> lock(lifecycle_mutex_);
    if (!running_) {
        return;
    }
    {
        std::lock_guard<std::mutex> queue_lock(mutex_);
        running_ = false;
        queue_.clear();
    }
    cv_.notify_all();
    if (thread_.joinable()) {
        thread_.join();
    }
}

@github-actions github-actions Bot added documentation Improvements or additions to documentation run-ci Store labels Jul 16, 2026
@Aionw
Aionw marked this pull request as ready for review July 16, 2026 12:13
@codecov-commenter

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 73.78641% with 81 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ooncake-store/src/http_metadata_cleanup_worker.cpp 57.98% 50 Missing ⚠️
mooncake-store/src/rpc_service.cpp 5.88% 16 Missing ⚠️
mooncake-store/src/master_service.cpp 82.66% 13 Missing ⚠️
...-store/tests/http_metadata_cleanup_worker_test.cpp 97.72% 1 Missing ⚠️
mooncake-store/tests/master_service_test.cpp 98.14% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@he-yufeng he-yufeng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the decoupling end to end. The direction is good and the tricky parts are handled well.

The HttpMetadataCleanupWorker concurrency is solid: Stop() flips running_ and clears the queue under mutex_, notifies, and joins, while ThreadFunc swaps the queue into a local batch and does the (slow) HTTP removals outside the lock; an in-flight batch finishes and unstarted work is dropped, matching the documented contract. Enqueue no-ops once running_ is false, so a late lease-expiry callback after Stop() is safe, and the destructor's Stop() is idempotent. Publishing is done the right way too: expirations are buffered into expiration_events under the master locks and the callbacks fire afterward, and Enqueue only takes the worker's own mutex_, so there's no lock-ordering hazard even if that ever changed.

I also chased the teardown lifetime, since the callback captures a raw worker pointer: it's correct today because http_metadata_cleanup_worker_ is declared before master_service_, so ~MasterService runs first and joins client_monitor_thread_ (the thread that fires NotifyClientLeaseExpired) before the worker is destroyed. Good that this is called out in a comment.

One thing worth hardening (agreeing with the bot, but concretely): that safety rests entirely on the declaration order plus the raw-pointer capture, and a future member reorder or a second callback-firing path would reintroduce a use-after-free silently. Two robust options: (a) give WrappedMasterService a destructor body that explicitly stops master_service_'s client monitor (or clears the lease-expired callbacks) before the members tear down, so ordering no longer matters; or (b) hold the worker as a shared_ptr and capture a weak_ptr in the callback, so lock() keeps it alive for the duration of the call or bails if it's gone. Non-blocking for this PR given the current order is correct and documented, but I'd take one of these as a fast follow-up.

Separately, note the async removal of rpc_meta/<segment> was already async before this PR (this just moves it out of MasterService), so no regression here; if a restarted client can re-register the same segment name inside the cleanup window, that reuse race predates this change and is a separate item.

LGTM.

@Icedcoco

Copy link
Copy Markdown
Collaborator

Thanks for the refactor. Moving HTTP metadata configuration, key construction, queueing, and worker lifecycle out of MasterService makes the boundary much clearer. I have two questions about failure handling and asynchronous cleanup ordering before merging:

  1. Could we handle std::thread construction failures as a best-effort cleanup failure? HttpMetadataCleanupWorker::Start() currently restores its internal state and then rethrows, while WrappedMasterService does not catch the exception. If thread creation fails because of temporary resource exhaustion, would that cause this optional cleanup feature to prevent the entire Master service from starting? Since Start() already returns bool, perhaps returning false after resetting the state would preserve the previous best-effort behavior.

  2. Could we clarify how a stale cleanup event is prevented from deleting metadata published by a newer client incarnation? The event is queued after the segment and snapshot locks are released, and the worker later deletes ram/<segment_name> and rpc_meta/<segment_name> using only the segment name. If a restarted client republishes the same keys and completes MountSegment before the queued cleanup runs, it looks like the old event could delete the new metadata. This window previously existed for the remote asynchronous path, but this PR also makes the co-located path asynchronous. Would it make sense to keep the co-located cleanup ordered with reclamation for now, or make deletion conditional on an incarnation/version before moving it fully asynchronous?

Could you please confirm whether I am missing an ordering or ownership guarantee that rules out either case?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation run-ci Store

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants