Skip to content

Improve P2P primary oplog persistence handling#2977

Open
skygragon wants to merge 2 commits into
kvcache-ai:P2P-Mooncake-Storefrom
skygragon:feat/oplog-ha-p2p-record-oplog-v7
Open

Improve P2P primary oplog persistence handling#2977
skygragon wants to merge 2 commits into
kvcache-ai:P2P-Mooncake-Storefrom
skygragon:feat/oplog-ha-p2p-record-oplog-v7

Conversation

@skygragon

@skygragon skygragon commented Jul 17, 2026

Copy link
Copy Markdown

Description

Improve P2P primary metadata OpLog persistence handling.

  • Add explicit synchronous/asynchronous persistence control to OpLogManager.
  • Propagate Redis OpLog write failures back to P2P metadata operations.
  • Keep replica-removal OpLogs synchronous to avoid silently losing required metadata updates.
  • Add an optional asynchronous path for add-replica OpLogs.
  • Refactor Redis OpLog persistence and batching behavior.
  • Add coverage for persistence failures, sync/async writes, and standby application.

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?

Built and tested in the local development container with Redis support enabled.

Test commands:

clang-format --dry-run --Werror <changed-files>

cmake --build build-redis-oplog \
  --target mooncake_master p2p_oplog_test redis_oplog_store_test -j2

ctest --test-dir build-redis-oplog --output-on-failure \
  -R 'p2p_oplog_test|redis_oplog_store_test'

Test results:

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

Checklist

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

AI Assistance Disclosure

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

Codex was used to help rebase the branch, review the changes, run local formatting/build/tests, and prepare the PR description. The submitter reviewed the resulting changes.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces asynchronous oplog writing and a committed watermark model in RedisOpLogStore to improve HA throughput, replacing the synchronous Lua-script-based writes with a multi-worker async queue. It also updates P2PMasterService to support optional async oplog recording for replica additions and refactors oplog cleanup. The review feedback identifies three critical issues in the new Redis store: a retry loop bug in CompleteWrite that can permanently mark successful writes as failed, a silent data loss risk in ReadOpLogSince when reading trimmed sequences, and a thread-blocking issue in UpdateLatestSequenceId where in-flight writes are not properly marked as completed.

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 +414 to +418
ErrorCode latest_err = AdvanceCommittedLatestUnlocked(ctx);
if (latest_err != ErrorCode::OK) {
pending_writes_.push_front(pending);
async_cv_.notify_one();
}

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 AdvanceCommittedLatestUnlocked fails (returning a non-OK error code), the pending write is pushed back to the front of pending_writes_ to be retried. However, because the entry was already successfully written to Redis during the first attempt, the retried PersistEntryNoLatest call will execute SET NX and fail with REDIS_REPLY_NIL (key already exists). This returns ErrorCode::INVALID_PARAMS, which permanently marks the write as failed, even though the entry was successfully persisted.

To fix this, we should either implement idempotency checks in PersistEntryNoLatest (e.g., treating identical existing content as success) or avoid retrying the write itself when only the watermark advancement failed.

Comment on lines +503 to +507
const uint64_t effective_start =
std::max(start_sequence_id, trimmed_sequence_id);
if (effective_start >= latest_sequence_id) {
return ErrorCode::OK;
}

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 start_sequence_id < trimmed_sequence_id, it means the requested sequence range has already been deleted (trimmed) from Redis. Silently taking std::max(start_sequence_id, trimmed_sequence_id) and returning the remaining entries starting from trimmed_sequence_id + 1 will cause the standby to silently miss mutations. This leads to metadata inconsistency and silent data corruption.

Instead, we should return ErrorCode::OPLOG_ENTRY_NOT_FOUND when start_sequence_id < trimmed_sequence_id so the standby knows it has fallen behind the trim horizon and must trigger a full bootstrap/snapshot sync.

Suggested change
const uint64_t effective_start =
std::max(start_sequence_id, trimmed_sequence_id);
if (effective_start >= latest_sequence_id) {
return ErrorCode::OK;
}
if (start_sequence_id < trimmed_sequence_id) {
LOG(ERROR) << "RedisOpLogStore::ReadOpLogSince: requested sequence has been trimmed"
<< ", start_sequence_id=" << start_sequence_id
<< ", trimmed_sequence_id=" << trimmed_sequence_id;
return ErrorCode::OPLOG_ENTRY_NOT_FOUND;
}
const uint64_t effective_start = start_sequence_id;
if (effective_start >= latest_sequence_id) {
return ErrorCode::OK;
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

We’ll address trim-horizon detection together with standby snapshot/bootstrap recovery in a follow-up change. Added a TODO for now.

Comment on lines +641 to +647
{
std::lock_guard<std::mutex> async_lock(async_mutex_);
committed_sequence_id_ = sequence_id;
persisted_sequences_.erase(
persisted_sequences_.begin(),
persisted_sequences_.upper_bound(sequence_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

When committed_sequence_id_ is advanced administratively via UpdateLatestSequenceId, any matching PendingWrites in inflight_writes_ that have sequence_id <= sequence_id are not marked as done = true or erased. This causes the callers of EnqueueWrite to remain blocked waiting on sync_cv_ until they timeout (30 seconds), even though their writes are now considered committed. Furthermore, if the worker thread later fails to write them, they will be marked as failed, which is incorrect.

We should iterate through inflight_writes_ and mark any writes with sequence_id <= sequence_id as successful and erase them.

    {
        std::lock_guard<std::mutex> async_lock(async_mutex_);
        committed_sequence_id_ = sequence_id;
        persisted_sequences_.erase(
            persisted_sequences_.begin(),
            persisted_sequences_.upper_bound(sequence_id));
        for (auto it = inflight_writes_.begin(); it != inflight_writes_.end(); ) {
            if (it->first <= sequence_id) {
                it->second->done = true;
                it->second->result = ErrorCode::OK;
                it = inflight_writes_.erase(it);
            } else {
                ++it;
            }
        }
    }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is a recovery/admin API inherited from OpLogStore and is not used in the normal Redis write path. Added a comment clarifying that it must not run concurrently with writes.

@codecov-commenter

codecov-commenter commented Jul 17, 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 72.95082% with 132 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
mooncake-store/src/ha/oplog/redis_oplog_store.cpp 65.10% 104 Missing ⚠️
...ake-store/tests/ha/oplog/p2p_record_oplog_test.cpp 73.80% 22 Missing ⚠️
mooncake-store/src/p2p_master_service.cpp 90.90% 4 Missing ⚠️
...re/tests/ha/oplog/p2p_hot_standby_service_test.cpp 83.33% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

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.

2 participants