Skip to content

[Store] Drain Client TE paths before tearDown (#3909) - #3969

Open
quantz8a wants to merge 7 commits into
kvcache-ai:mainfrom
quantz8a:fix-3909-client-api-drain
Open

[Store] Drain Client TE paths before tearDown (#3909)#3969
quantz8a wants to merge 7 commits into
kvcache-ai:mainfrom
quantz8a:fix-3909-client-api-drain

Conversation

@quantz8a

@quantz8a quantz8a commented Sep 9, 2026

Copy link
Copy Markdown

Summary

Follow-up to #3943 for Mooncake #3909 (short MC_RPC_TIMEOUT_MS + concurrent get × close → asio SIGSEGV / exit 139).

#3943 alone This PR tip
Drains MasterClient RPC (rpc_drain_) + process-wide pool registry Also drains Client/TE API paths still in TransferRead after Master returns

Tip commit (review focus)

  • Client::api_drain_ + DrainInflightOperations(); ScopedCall on Get/Put paths
  • RealClient::client_op_drain_ around full get_buffer (incl. observe)
  • tearDownAll_internal: drain client ops → DrainInflightOperations → then unregister/reset TE

Branch = rebased #3943 + TE-drain + clang-format fix. Prefer review with/after #3943; happy to squash/retarget once it lands.

Test plan

Refs: #3909 · #3943

he-yufeng and others added 2 commits September 9, 2026 14:16
Teardown of a read-side client after consecutive RPC timeouts released
the client pool while request coroutines were still suspended in it,
and ylt only documents send_request as thread-safe, so the resumed
coroutine touched freed state and segfaulted in asio's epoll_reactor
(kvcache-ai#3909). Two layers, both verified against current main:

- A shared RpcDrainGuard (mooncake-common) stops admitting new calls
  once draining and makes the destructor wait for in-flight ones, with
  a 30s bound that logs loudly rather than silently proceeding. Wired
  into MasterClient, DummyClient and ClientRequester entry points.
- RpcClientPool now hands out pools from a process-wide registry keyed
  by address instead of freeing them on teardown or address flaps. ylt
  pools own background reconnect coroutines that reference pool
  storage, so no amount of request draining makes pool destruction
  safe mid-retry; pools are one-per-master-address and deliberately
  process-lifetime.

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
MasterClient rpc_drain alone still UAF when Get is in TransferRead
during close. Add Client::api_drain_ and RealClient::client_op_drain_
so tearDown waits for in-flight API/TE work before freeing TE.
@quantz8a
quantz8a force-pushed the fix-3909-client-api-drain branch from 8e75a63 to 033524c Compare September 9, 2026 06:17
@quantz8a

quantz8a commented Sep 9, 2026

Copy link
Copy Markdown
Author

Review accelerator

CI: format failure was a single extra blank line after get_buffer — fixed in 8854b573. Please re-check Check code format on the latest push.

Suggested review order (keeps wall-clock down):

  1. Skim tip commit only (Drain Client TE paths…) if you already trust / are reviewing [Store] Drain in-flight RPCs and keep pools alive across client teardown #3943.
  2. Key hunks: client_service.h/cpp (api_drain_, DrainInflightOperations) + real_client.h/cpp (client_op_drain_, tearDown ordering).
  3. Repro: short timeout × concurrent get/close — unpatched/3943-only exit 139, with this tip exit 0.

cc @he-yufeng (stacked on your #3943) · reviewers already requested: @ykwd @stmatengss @XucSh @YiXR — happy to address comments quickly.

Co-authored-by: Cursor <cursoragent@cursor.com>
@quantz8a

Copy link
Copy Markdown
Author

CTest red on prior tip — not a unit-test regression

Job annotation on run 34319106134:

The hosted runner lost communication with the server…

Failed step was Build project and unit tests (~7.5m into -j128 build); Run all CTest was skipped (no ctest-diagnostics-pr artifact). So this was infra/OOM-style runner death, not a failing assert from the TE-drain commits.

Pushed empty ff5c670 to retrigger. Note: sibling #3943 / recent main have separately failed real CTest cases (dmabuf_export_test / master_service_ssd_test_for_snapshot) unrelated to this tip.

Co-authored-by: Cursor <cursoragent@cursor.com>
@quantz8a

Copy link
Copy Markdown
Author

Gentle review nudge

Tip 3f39e8aCI Gate / CTest / wheels all green (run 34433061377). Earlier reds were infra (runner lost communication; NVIDIA apt mirror size mismatch — tracked in #3989), not the TE-drain change.

Still OPEN · MERGEABLE · no review yet (~1.5d). Would appreciate a pass when you have a slot:

cc @ykwd @stmatengss @XucSh @YiXR @he-yufeng — happy to address comments same day.

@Icedcoco

Copy link
Copy Markdown
Collaborator

Thanks for putting this follow-up together. I understand that #3969 is intended to be stacked on top of #3943, and that its first commit currently contains the #3943 changes because the official stacked-PR workflow was not used.

After looking through the incremental changes in #3969, I noticed two points that seem specific to this PR and may be worth addressing:

  1. When client_op_drain_.drain_for(30s) or Client::DrainInflightOperations() times out, teardown still continues and releases the Client, Transfer Engine, allocator, and related state. Could we avoid releasing resources while operations may still be in flight, or otherwise explicitly cancel and join those operations before proceeding? The current log mentions the remaining UAF risk, so I am concerned that the timeout path may still preserve the original lifetime issue in exceptional cases.

  2. The new api_drain_ currently covers the single-object Get and Put paths, but the batch and offload paths such as Client::BatchGet, Client::BatchPut, and Client::BatchGetOffloadObject do not appear to enter the same guard. Could we either extend the guard to those shared entry points or clarify why they are safe during teardown? A small regression test covering a batch operation concurrent with tearDownAll() would also help confirm the intended coverage.

The pool-registry lifetime and configuration questions seem more naturally tied to the base change in #3943. I plan to wait for #3943 to be reviewed and merged first, then review #3969 again against that final base with a more detailed pass.

Thanks again for working on this; the additional TE-side drain appears useful, and I would be happy to continue the review once these points are clarified.

zhangyupeng added 2 commits September 11, 2026 18:20
Resolve MasterClient conflicts: keep RpcDrainGuard alongside HA
control/probe pools; drain at invoke_rpc_with_client_pool entry;
keep teardown drain test and main HA timeout tests.
…uard batch APIs

On RealClient tearDown, fail with RPC_TIMEOUT and keep TE/Client if
client_op or API drain times out instead of logging UAF risk and
continuing. Client dtor waits until idle after a timed-out drain.

Wrap BatchGet / BatchPut / BatchGetOffloadObject with api_drain_
ScopedCall. Add RpcDrainGuard regression that a timed-out drain keeps
blocking new calls.
@quantz8a

Copy link
Copy Markdown
Author

@Icedcoco Follow-up on your two points is in tip 22feb12e (on top of the main merge):

  1. Drain timeout: RealClient::tearDownAll_internal now drains before claiming closed_ / freeing TE. If client_op_drain_ or Client::DrainInflightOperations times out, teardown aborts with RPC_TIMEOUT and keeps Client/TE alive (no more "continuing teardown (UAF risk)"). Client::~Client keeps waiting until idle if a timed drain fails.

  2. Batch coverage: api_drain_ ScopedCall now wraps BatchGet (both overloads), BatchPut (stager path), and BatchGetOffloadObject. New RpcDrainGuardTest.TimedOutDrainKeepsBlockingNewCalls covers the timeout-must-keep-blocking contract.

Happy to take a detailed pass when you have a slot — #3943 can still land independently; this tip is already mergeable vs current main.

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.

3 participants