[Store] Free NoF heartbeat probe buffers on segment unmount - #4059
[Store] Free NoF heartbeat probe buffers on segment unmount#4059pjdurden wants to merge 1 commit into
Conversation
Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
There was a problem hiding this comment.
🟡 Changes recommended
Outstanding critical and moderate probe-lifecycle race issues must be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR releases NoF heartbeat probe DMA buffers when segments are unmounted, with lifecycle tracking and regression tests.
Changes:
- Adds probe-resource release and in-flight tracking.
- Centralizes heartbeat cleanup with shared-endpoint awareness.
- Adds explicit and heartbeat-triggered unmount tests.
File summaries
| File | Summary | Findings |
|---|---|---|
mooncake-store/tests/nof_heartbeat_test.cpp |
Adds resource-release regression tests. | Nit (1 vote): Missing shared-endpoint coverage. |
mooncake-store/src/spdk/spdk_wrapper.cpp |
Implements buffer release and request tracking. | Critical (3 votes): Timed-out buffers may be reused while DMA remains outstanding. Moderate (1 vote each): Release lacks deferred cleanup after in-flight completion; probe/unmount handoff can recreate an unreleased buffer. |
mooncake-store/src/master_service.cpp |
Integrates cleanup into heartbeat-state removal. | Moderate (1 vote): Unmount can race with an already-selected heartbeat probe, leaving a buffer unreleased. |
mooncake-store/include/spdk/spdk_wrapper.h |
Declares probe lifecycle APIs and state. | None. |
mooncake-store/include/master_service.h |
Declares release callbacks and cleanup helpers. | None. |
Review details
Suppressed comments (5)
mooncake-store/src/master_service.cpp:12343
- This release can race with the heartbeat thread, which selects
probe_targetand then probes outsidenof_heartbeat_mutex_. If the thread has selected a target but has not yet calledGetOrCreateProbeBuffer, an unmount can erase the old buffer here; the stale probe then creates a new buffer after its heartbeat state is gone, leaving no later state erase to release it. Coordinate endpoint release with in-progress/stale probes or perform a post-probe release.
std::string te_endpoint = std::move(it->second.te_endpoint);
nof_heartbeat_states_.erase(it);
ReleaseNoFProbeResourcesLocked({te_endpoint});
mooncake-store/src/spdk/spdk_wrapper.cpp:444
- This early return races with the heartbeat thread's probe handoff:
NofHeartbeatThreadFuncreleasesnof_heartbeat_mutex_beforeProbeNoFSegmentreachesGetOrCreateProbeBuffer. If an unmount erases the state in that window, this sees no entry, but the already-selected probe then allocates and submits a new buffer; with no heartbeat state left, no later path releases it. Coordinate state removal with probe start or record a deferred release for probes already selected.
if (it == probe_buffers_.end()) {
return;
mooncake-store/src/spdk/spdk_wrapper.cpp:452
- When this branch skips an in-flight buffer, no follow-up release is recorded. The probe may complete afterward and
MarkProbeBufferIdleonly flips the flag; because the heartbeat state has already been erased, no later call reachesReleaseProbeResources, so even a successfully completed request retains its DMA buffer until process shutdown. Add deferred-release state and perform the release from the completion/termination path once the endpoint is no longer tracked.
if (it->second && it->second->in_flight) {
// The last probe never completed, so the controller may still DMA
// into this buffer. Keep it until Cleanup() tears the device down.
LOG(WARNING) << "endpoint=" << tr_str
<< ", action=skip_release_nof_probe_buffer"
<< ", reason=probe_in_flight";
return;
mooncake-store/src/spdk/spdk_wrapper.cpp:515
- After the loop exits with
completion_timeout, this is the last place that can callMarkProbeBufferIdlefor that probe. If the request completes later,ProbeReadCompleteonly setsdoneand recycles the context, so the buffer can remain marked in flight; an unmount that follows that late completion (before another probe resets the flag) will still skip it even though DMA has finished. Associate the endpoint or buffer with the request context and clear it from the completion callback while preserving the timeout case.
if (probe_ctx->done.load(std::memory_order_acquire)) {
// The request completed, so nothing references the buffer any more.
MarkProbeBufferIdle(tr_str);
}
mooncake-store/tests/nof_heartbeat_test.cpp:223
- The added tests cover only a single segment per endpoint. They do not exercise the
still_trackedbranch inReleaseNoFProbeResourcesLocked, so a regression that releases shared endpoint resources on the first unmount would still pass. Add a test with two segments sharing one endpoint and assert release happens only after the second segment is removed.
TEST_F(NoFHeartbeatTest, UnmountReleasesNoFProbeResources) {
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| probe_buffer->in_flight = true; | ||
| return probe_buffer.get(); |
he-yufeng
left a comment
There was a problem hiding this comment.
Read the whole diff carefully; this is the right shape. A few things I specifically checked:
Lock ordering holds: every caller of EraseNoFHeartbeatStateLocked / ReleaseNoFProbeResourcesLocked takes nof_heartbeat_mutex_ first and nof_probe_fn_mutex_ second, and I couldn't find a reverse path anywhere (the probe fns only ever take nof_probe_fn_mutex_ alone), so there's no deadlock cycle.
The in-flight discipline is the load-bearing part and it's right: a buffer whose probe timed out stays marked in_flight forever, so a late DMA landing after timeout can never write into freed memory. The cost is bounded (one buffer per endpoint, only when a probe never completed) and the log line makes it observable. Reuse through GetOrCreateProbeBuffer still works in that state, which matches the pre-existing memoization behavior.
The shared-endpoint refcounting via the any_of scan over remaining heartbeat states is correct for the "several segments, one transport endpoint" case, and the stale-entry sweep in NofHeartbeatThreadFunc (client offboarded without Unmount) was the gap I would have asked about; it's covered.
The two new tests exercise both release paths (explicit unmount and heartbeat-failure unmount) with mocked probes, so they run without NoF hardware.
One question, not blocking: on the timeout path the buffer stays in_flight, so ReleaseProbeResources will skip it at unmount and it lives until Cleanup(). That's the safe choice, just confirming it's intended to be permanent for the life of the device rather than retried on a later release.
COMMENT only since I can't exercise the real SPDK path locally; from the code and the mock-based tests this looks ready.
Issue #4038 — Master retains NoF heartbeat probe resources after segment unmount
1. Root cause
The NoF heartbeat probe allocates a per-endpoint SPDK DMA buffer and caches it
forever.
SpdkWrapper::ProbeNofSegment()reads one block from the remote namespace todecide whether a NoF segment is alive. The bounce buffer for that read comes
from
SpdkWrapper::GetOrCreateProbeBuffer(), which memoizes it instd::map<std::string, std::unique_ptr<ProbeBuffer>> probe_buffers_; // keyed by te_endpointThe only code that ever frees those buffers is
SpdkWrapper::Cleanup(), i.e.process shutdown.
On the master side, unmounting a NoF segment does tear down the heartbeat
bookkeeping —
MasterService::UnmountNoFSegment(),TryUnmountNoFSegmentByHeartbeat()and the stale-entry sweep inNofHeartbeatThreadFunc()allnof_heartbeat_states_.erase(segment_id)— butnothing told
SpdkWrapperthat the endpoint was gone. So every transportendpoint the master ever probed kept a hugepage-backed DMA buffer for the
lifetime of the process, even after its segment was unmounted and could never
be probed again.
With mount/unmount churn (clients restarting, NoF nodes rotating, segments
auto-unmounted by the heartbeat itself after a device dies)
probe_buffers_grows monotonically, one entry per distinct endpoint, and the DMA pool is
never returned.
2. The fix and why
Release the probe resources for a transport endpoint at the moment the master
stops tracking the segment behind it.
SpdkWrappergainsReleaseProbeResources(tr_str), which frees the cachedDMA buffer and drops the map entry. It is idempotent, so it is safe on the
repeated/racing unmount paths.
Freeing a DMA buffer is only safe if no NVMe request still targets it.
ProbeNofSegment()gives up afternof_heartbeat_probe_timeout_msand returnscompletion_timeoutwhile the read is still outstanding, so the buffer canstill be written by the controller at that point. A
ProbeBuffer::in_flightflag (set when the buffer is handed to a probe, cleared on submit failure and
on completion) makes
ReleaseProbeResources()skip — and warn about — exactlythose buffers instead of freeing memory a pending DMA targets. They are
reclaimed by
Cleanup()as before, and reused/cleared if the endpoint isremounted and probes succeed again.
MasterServicegains a single choke point,EraseNoFHeartbeatStateLocked(segment_id), which replaces the three rawnof_heartbeat_states_.erase(...)calls, plusReleaseNoFProbeResourcesLocked(endpoints)used by the heartbeat thread'sstale-entry sweep (that sweep is what catches segments removed out-of-band,
e.g. by client offboarding). Releasing is skipped while any other tracked
segment still names the same endpoint, so a shared endpoint keeps its buffer
until its last segment is gone.
The actual release goes through an injectable
NoFProbeReleaseFn, mirroringthe existing
NoFProbeFnhook, so the behaviour is unit-testable without anSPDK device (
SetNoFProbeReleaseFnForTesting). Likenof_probe_fn_, it isonly wired to
SpdkWrapperunderUSE_NOF.3. Files changed
mooncake-store/include/spdk/spdk_wrapper.hReleaseProbeResources/MarkProbeBufferIdle; addProbeBuffer::in_flightmooncake-store/src/spdk/spdk_wrapper.cppin_flightaround the probe readmooncake-store/include/master_service.hNoFProbeReleaseFn+nof_probe_release_fn_,SetNoFProbeReleaseFnForTesting, the two new private helpersmooncake-store/src/master_service.cppmooncake-store/tests/nof_heartbeat_test.cpp4. Risk / uncertainty
out (
completion_timeout), its buffer is kept rather than freed, because thecontroller may still DMA into it and SPDK keeps hugepages mapped, so a late
write would silently corrupt whatever is allocated there next. Retention is
then bounded by the number of distinct endpoints whose final probe hung,
instead of every endpoint ever probed. Closing that last gap properly needs
the probe's qpair to be reset/drained, which is a larger change to the
OpenNofSegmenthandle cache and is out of scope here.EraseNoFHeartbeatStateLockedruns undernof_heartbeat_mutex_and reachesprobe_buffers_mutex_; the releasecallback is copied out from under
nof_probe_fn_mutex_and invoked outsideit. No path takes
nof_heartbeat_mutex_while holding either of the othertwo, so there is no inversion.
ReleaseProbeResourcesonly holdsprobe_buffers_mutex_briefly and never during the probe's polling loop, soan RPC-thread unmount cannot stall behind a slow probe.
USE_NOF=ONandan NVMe-oF target; neither SPDK nor the project's other build dependencies
are available in this environment, so the SPDK-side change is reviewed and
reasoned about, not executed. See below.
in_flightflag is plainboolguarded byprobe_buffers_mutex_, notatomic; every read and write of it is inside that lock.
5. How I verified it
MasterServicehelpers were extracted intoa standalone C++20 translation unit with stub types and compiled with
g++ -std=c++20 -Wall -Wextra, then exercised with assertions covering:sole-user endpoint is released; endpoint still used by another tracked
segment is not; it is released once its last user goes away; erasing an
unknown segment id is a no-op; and the batch path dedups, skips empty
endpoints and skips still-tracked ones. All assertions pass.
dependencies (glog, ylt, boost, SPDK) are not installed here, and
nof_heartbeat_testis only built whenUSE_NOF=ON(
mooncake-store/tests/CMakeLists.txt:136).scripts/code_format.shrefusesto run because it requires clang-format 20 and only 16 is present, so
formatting was matched to the surrounding code by hand and checked against
the 80-column limit (no new line exceeds it).
mooncake-store/tests/nof_heartbeat_test.cpp,both using the injected release hook:
UnmountReleasesNoFProbeResources— mount, wait until the heartbeat threadtracks the segment, call
UnmountNoFSegment, assert the endpoint's proberesources were released and the heartbeat state is gone. Fails before the
fix (nothing is ever released).
HeartbeatUnmountReleasesNoFProbeResources— a permanently failing probedrives the auto-unmount path; assert the release also happens there.
Both snapshot the recorded endpoints under their own mutex before asserting,
so an assertion never holds that mutex while calling back into the service.
nof_heartbeat_states_mutation toconfirm all four now route through the new helpers, and of the
in_flightstate machine for the submit-failure, completion and timeout paths.