mirror of
https://github.com/kvcache-ai/Mooncake.git
synced 2026-11-05 01:17:55 +00:00
[GH-ISSUE #3349] [Bug]: graceful_shutdown_test can hang after fork in USE_ETCD CI builds #851
Labels
No labels
ascend
auto-closed
bug
bug
CI/Build
Common
documentation
enhancement
feature request
good first issue
help wanted
high priority
MetaX
nightly-failure
pull-request
PyTorch Backend
question
RFC
Roadmap
stale
Store
Tests
Transfer Engine
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/Mooncake#851
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @Icedcoco on GitHub (Aug 10, 2026).
Original GitHub issue: https://github.com/kvcache-ai/Mooncake/issues/3349
Bug Report
Summary
GracefulShutdownTest.SigtermTriggersCleanExitintermittently hangs in the Linux CI build and is killed by the test after a five-second timeout.This failure is not specific to the PR on which it appears. The same signature has occurred on
mainand on multiple unrelated PRs.The current evidence points to a bug in the test process model:
USE_ETCD=ON,graceful_shutdown_testloads the Go c-shared etcd wrapper, whose runtime starts background threads before the tests run.fork()from this already-multithreaded process.TransferEngine, takes C++ locks, allocates memory, initializes logging/runtime state, and creates astd::thread.fork()andexec()in the child of a multithreaded process.The fixed 100 ms sleep used by the parent is a separate synchronization bug that makes the failure more likely under CI load.
Observed failure
Example job:
https://github.com/kvcache-ai/Mooncake/actions/runs/31331251297/job/93289807744?pr=3344
The
WIFEXITED(status)failure is secondary. After waiting for five seconds,waitChildWithTimeout()sendsSIGKILLto the child, so the resulting status is necessarily signaled rather than normally exited.Expected behavior
The test child should:
TransferEnginestate.128 + signo.The test must also terminate within a bounded time when child setup fails.
Affected CI runs
I found the same failure signature in at least six independent workflow runs:
These runs include both
mainand unrelated PRs. One occurrence is in a Python 3.10 job, so this does not appear to be specific to Python 3.12.The example PR, #3344, only removes unused declarations from:
It does not modify Transfer Engine, graceful shutdown, signal handling, or this test.
Relevant CI configuration
The main Linux CI config enables the Go etcd wrapper and ASan:
The test is then run as part of:
Local reproduction
Using a build matching the important CI options, especially
USE_ETCD=ONandENABLE_ASAN=ON, I stress-ran the failing test concurrently:In one captured run, 8 out of 500 invocations reproduced the same approximately 5.1-second timeout.
With a minimal build using
USE_ETCD=OFF, 1,000 concurrent invocations completed without this timeout.The exact rate is scheduling-dependent, but enabling the Go c-shared etcd wrapper makes the original CI failure reproducible.
Root-cause analysis
1. The test forks from a multithreaded process
The test currently does the following:
With
USE_ETCD=ON,transfer_enginelinks:This library is built with:
Tracing the CI-equivalent test binary shows that the Go runtime has created roughly 60 threads before GTest reaches
fork().After
fork(), only the calling thread exists in the child. The child nevertheless performs substantial non-async-signal-safe work, including:TransferEngineImpl;shared_ptrand shutdown token state;g_registry_mutexandg_install_mutex;std::thread.POSIX requires the child of a multithreaded process to execute only async-signal-safe operations until a successful
exec():https://pubs.opengroup.org/onlinepubs/9799919799/functions/fork.html
The test therefore relies on undefined/unsupported post-fork behavior.
The Go wrapper is the trigger that makes this easy to reproduce, but this should not be treated as an etcd or Go runtime bug. Any background thread holding runtime, allocator, logging, or library state at the time of
fork()can expose the same test defect.The exact runtime-internal lock or loop where every failed child stalls has not been reduced to one symbol. However, tracing confirms that failed children remain running or stuck before completing their ready state, and the test already violates the required process contract independently of that internal detail.
2. A fixed sleep is used as readiness synchronization
The parent currently does:
The 100 ms delay does not prove that the child has completed either:
or:
Under ASan, Go runtime initialization, and concurrent CI load, the parent can send SIGTERM while the child is still initializing.
In a simpler single-threaded build, this race can appear as an immediate signal termination before the Mooncake handler is installed. In the
USE_ETCD=ONbuild, it can interact with the invalid post-fork runtime state and produce the observed hang.Relationship to existing reports and fixes
#3271 and #3278
Issue #3271 and PR #3278 addressed a different production bug:
PR #3278 fixed that by blocking SIGTERM/SIGINT while creating the watcher so the watcher inherits the blocked mask:
https://github.com/kvcache-ai/Mooncake/pull/3278
The failing CI commits already contain that fix. The current failure therefore does not indicate that #3278 is missing or ineffective for its intended case.
#3274
PR #3274 replaces the fixed 100 ms sleep with a readiness pipe:
https://github.com/kvcache-ai/Mooncake/pull/3274
That addresses the readiness race, but it does not address the multithreaded
fork()problem. The child still constructsTransferEngineand starts the watcher beforeexec().I applied the #3274 patch to the CI-equivalent build and stress-ran it. In one 500-run batch, six children hung before sending the readiness byte.
The current #3274 parent also uses an unbounded blocking
read(). If the child never reaches the readiness notification, the parent can wait indefinitely instead of failing after five seconds.For these reasons, #3274 is not sufficient by itself.
Proposed fix
Run the signal target in a fresh process image instead of constructing
TransferEnginedirectly in the post-fork child.A minimal implementation can keep everything in the existing test source:
Give
graceful_shutdown_testa custommain()instead of linkinggtest_main.Add an internal helper mode, for example:
From the GTest parent, create a readiness pipe and use
posix_spawn()to execute/proc/self/exein helper mode.In helper mode, after the new process image has initialized normally:
enableGracefulShutdown();In the parent, use
poll()with a finite timeout to wait for readiness.After readiness, send the signal and wait for the expected exit status.
On every error or timeout path, close the pipe, kill the child if needed, and reap it with
waitpid().The process flow becomes:
The following existing cases can share this helper:
SigtermTriggersCleanExitSigintTriggersCleanExitEngineDestroyedBeforeSignalMultipleEnginesIdempotentEnablecan remain an in-process test.ForkAfterInstallDoesNotHangChildSignalshould remain a directfork()test because it specifically verifies inherited handler behavior. In that test, the engine and handler are installed beforefork(), and the child only needs to call async-signal-safe operations such aspause()and_exit(). It does not need a fixed sleep because the handler state already exists whenfork()returns.The expected implementation scope is limited to:
No production graceful shutdown change should be required.
Approaches that would only hide the issue
The following changes may reduce the failure rate but do not fix the invalid process model:
RUN_SERIAL;USE_ETCDfor this test;pthread_atfork().In particular,
pthread_atfork()cannot generally repair inherited Go, sanitizer, allocator, logging, and arbitrary C++ library state.Acceptance criteria
USE_ETCD=ONandENABLE_ASAN=ON.USE_ETCD=OFF.Impact
The confirmed impact is flaky CI and unrelated PRs being blocked by a Transfer Engine test failure.
This investigation does not show a failure in the normal graceful shutdown path of an application that installs the handlers without first entering an unsafe post-fork child state.
Applications that fork from a multithreaded process and then construct or reinitialize Mooncake components in the child without
exec()would have the same general process-safety problem, but that is separate from the normal supported shutdown flow.Before submitting...
@github-actions[bot] commented on GitHub (Aug 10, 2026):
Thanks for opening this issue, @Icedcoco!
102317026A maintainer will triage this when possible. To help us respond faster, please include:
Useful links: Documentation · Contributing guide