Repository navigation
[Bugfix] clean up partially initialized TENT RDMA contexts - #3325
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes RDMA resource leaks when RdmaContext::enable() fails part-way through initialization by introducing an explicit DEVICE_INITIALIZING state and making cleanup independent of the prior logical “disabled” state.
Changes:
- Add
DEVICE_INITIALIZINGand switchenable()to transitionDISABLED -> INITIALIZING -> (ENABLED|PAUSED)with explicit rollback on failure. - Refactor teardown into
cleanupResources()/rollbackEnable()and make destruction clean up any residual partially-created RDMA handles. - Block inbound RDMA connection setup and MR operations while a context is initializing.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| mooncake-transfer-engine/tent/src/transport/rdma/rdma_transport.cpp | Tightens inbound connection gating to only allow ENABLED/PAUSED contexts. |
| mooncake-transfer-engine/tent/src/transport/rdma/context.cpp | Adds DEVICE_INITIALIZING, introduces rollback/cleanup helpers, and updates enable/disable + MR guards. |
| mooncake-transfer-engine/tent/include/tent/transport/rdma/context.h | Exposes the new status and declares cleanup/rollback helpers. |
Suppressed comments (2)
mooncake-transfer-engine/tent/src/transport/rdma/context.cpp:620
- The added
DEVICE_INITIALIZINGguard now trips thisLOG(FATAL)path too, but the message says "not constructed". During initialization the context is constructed, it’s just not ready/enabled yet, which makes debugging harder.
if (status_ == DEVICE_DISABLED || status_ == DEVICE_INITIALIZING ||
status_ == DEVICE_UNINIT) {
LOG(FATAL) << "RDMA context " << name() << " not constructed";
return -1;
}
mooncake-transfer-engine/tent/src/transport/rdma/context.cpp:645
- The added
DEVICE_INITIALIZINGguard now trips thisLOG(FATAL)path too, but the message says "not constructed". During initialization the context is constructed, it’s just not ready/enabled yet, which makes debugging harder.
if (status_ == DEVICE_DISABLED || status_ == DEVICE_INITIALIZING ||
status_ == DEVICE_UNINIT) {
LOG(FATAL) << "RDMA context " << name() << " not constructed";
return -1;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (2)
mooncake-transfer-engine/tent/src/transport/rdma/context.cpp:488
cleanupResources()iterates and clearsmr_set_without holdingmr_set_mutex_, whileregisterMemReg()/unregisterMemReg()mutate the same set under that mutex. This breaks the locking discipline and can lead to a data race/UB if cleanup runs concurrently with MR registration/unregistration.
void RdmaContext::cleanupResources() {
for (auto& entry : mr_set_) {
int ret = verbs_.ibv_dereg_mr(entry);
if (ret) PLOG(ERROR) << "ibv_dereg_mr";
}
mr_set_.clear();
mooncake-transfer-engine/tent/include/tent/transport/rdma/context.h:130
- The comment above
cleanupResources()says resources are released in “reverse construction order”, but the implementation does not strictly follow that order (e.g.,event_fd_is closed before completion channels are destroyed). Either update the comment or reorder cleanup to match, to avoid misleading future maintainers.
// Release every resource currently owned by this context in reverse
// construction order. This is intentionally state-independent so it can
// roll back a partially completed enable() and can safely be retried.
void cleanupResources();
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
mooncake-transfer-engine/tent/src/transport/rdma/context.cpp:540
- The error message says "not constructed" even when the context is constructed but merely DISABLED/INITIALIZING. This makes failures harder to diagnose (especially now that INITIALIZING is a distinct state). Consider updating the message to reflect the actual invalid state (e.g., "not enabled" / "not ready" and ideally include the current status) and apply the same fix to the similar LOG(FATAL) messages in warmup/unregister paths.
if (status_ == DEVICE_DISABLED || status_ == DEVICE_INITIALIZING ||
status_ == DEVICE_UNINIT) {
LOG(FATAL) << "RDMA context " << name() << " not constructed";
return nullptr;
mooncake-transfer-engine/tent/src/transport/rdma/context.cpp:315
- The logic for "do we currently own any resources that need cleanup" is embedded as a long predicate here (and a similar but not identical predicate exists in
disable()). Consider extracting a small helper (e.g.,hasOwnedResources()) and reusing it in both places to reduce the chance of future resource fields being missed or the predicates drifting apart.
if (status_ != DEVICE_UNINIT || native_context_ || native_pd_ ||
event_fd_ >= 0 || !comp_channel_.empty() || !cq_list_.empty() ||
notify_cq_) {
disable();
}
mooncake-transfer-engine/tent/src/transport/rdma/rdma_transport.cpp:662
- This error is now emitted for states beyond "down" (e.g.,
DEVICE_INITIALIZINGandDEVICE_UNINIT). To improve debuggability, consider including the actual context status in the log message (or using a status-specific message) so callers can distinguish "initializing" vs "disabled" vs "uninitialized".
auto ctx_status = context->status();
if (ctx_status != RdmaContext::DEVICE_ENABLED &&
ctx_status != RdmaContext::DEVICE_PAUSED) {
std::stringstream ss;
ss << "Device is down: " << peer_desc.local_nic_path;
LOG(ERROR) << ss.str();
staryxchen
left a comment
There was a problem hiding this comment.
Thanks for the contribution. The core leak fix is correct, but I think the state-machine extension is more than the current call graph requires.
cleanupResources() (nullity-based, state-independent cleanup) is the real fix for the PD / epoll / comp_channel / CQ / device_context leaks — that part belongs in disable() and a simpler disable() early-return on resource-nullity would suffice. The std::make_unique() change is also correct and necessary: a CQ
that fails construct() never enters cq_list_, so disable() can't reclaim it — this must be fixed at the allocation site.
What I'd question is DEVICE_INITIALIZING + rollbackEnable(). In the TENT call graph, enable() is only called from construct() (single-threaded install()), and disable() only from ~RdmaContext() and enable()'s own failure path — both same-thread, externally serialized by the transport lifecycle. There's no concurrent
caller the new state coordinates against today. Once disable()'s early-return is resource-based, enable() could keep calling disable() directly; rollbackEnable() is needed only because disable() now rejects DEVICE_INITIALIZING.
The new state + CAS are defensible as future-proofing, but for the present code they add machinery without addressing a current race. I'd lean toward trimming them unless there's a concurrent caller I'm missing.
|
@staryxchen As you noted, there's no concurrent caller today that would need the CAS/state machinery. I've made the following changes:
Please take another look. |
| } | ||
|
|
||
| void RdmaContext::cleanupResources() { | ||
| for (auto& entry : mr_set_) { |
There was a problem hiding this comment.
mr_set_ is iterated and cleared here without holding mr_set_mutex_, while every other access site takes the lock. In the current call graph disable() runs during teardown (same thread, after workers are stopped), so this isn't a live race today — but the broken locking discipline could bite if teardown ordering ever changes. Consider holding mr_set_mutex_ around the iterate-and-clear to keep the invariant, or add a comment documenting the relies-on-teardown-ordering assumption
| private: | ||
| int openDevice(const std::string &device_name, uint8_t port); | ||
|
|
||
| // Release every resource currently owned by this context in reverse |
There was a problem hiding this comment.
the comment says "reverse construction order", but the actual cleanup isn't strictly reverse — cq_list_ is cleaned before notify_cq_ (constructed after it), and event_fd_ before comp_channel_ (also constructed after). Functionally the order is fine (no interdependency requiring strict reverse), but the comment misleads future readers. It's better to drop "reverse construction order" from the comment.
|
@staryxchen |
staryxchen
left a comment
There was a problem hiding this comment.
Thanks for the contribution! LGTM
Description
During
RdmaContextinitialization, the context remained in theDEVICE_DISABLEDstate while RDMA resources were being allocated. If initialization failed after partially creating resources, the error path calleddisable()for rollback. However,disable()returned immediately for aDEVICE_DISABLEDcontext, leaving resources such as the device context, protection domain, epoll file descriptor, completion channels, and completion queues unreleased.Because the state machine had no explicit initialization state, it could not distinguish a fully disabled context from a partially initialized one. Cleanup therefore relied on an inaccurate logical state instead of the resources actually owned by the context, potentially causing resource leaks and inconsistent lifecycle behavior after initialization failures.
To fix it, I've made the following changes:
DEVICE_INITIALIZINGstateModule
mooncake-transfer-engine)mooncake-store)mooncake-ep)mooncake-pg)mooncake-integration)mooncake-p2p-store)mooncake-wheel)mooncake-common)mooncake-rl)Type of Change
How Has This Been Tested?
Test results:
The test I've performed manually is described below:
Mock or wrap the
ibv_*calls used byRdmaContext::enable()and inject failures at each initialization stage, including PD, epoll, completion channel, CQ, notification CQ, and port query creation.For each failure, verify that:
initialization returns an error;
context status returns to
DEVICE_DISABLED;all previously created resources are released exactly once;
all handles, pointers, and containers are reset.
Call
disable()again to verify idempotent cleanup, then remove the failure and callenable()to verify successful retry.Checklist
./scripts/code_format.shpre-commit run --all-filesand all hooks passAI Assistance Disclosure
VSCode Copilot assisted with reading and editing the source files. I have reviewed every changed line and can defend the change end-to-end.