Repository navigation
[Bugfix] Fail fast when the Python HTTP metadata server cannot bind its port - #2924
toffee-desuwa wants to merge 2 commits into
Conversation
…ent, import cleanup - Test: replace run_forever call-count guard (CPython white-box, hang risk on future versions) with a readiness-driven interrupt keyed off the startup future's done callback. - Server: comment why the handshake path catches BaseException (preserve KeyboardInterrupt/SystemExit type during startup). - Remove pre-existing unused 'import os' (F401) and regroup the concurrent.futures from-import below the plain-import block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request improves the startup synchronization of the HTTP metadata server by utilizing a concurrent.futures.Future to block the calling thread until the background server thread has fully initialized or failed. It also introduces comprehensive unit tests to validate server startup, bind failures, and exception propagation. The feedback suggests adding a timeout to startup.result() in the test suite to prevent potential hangs during CI runs.
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.
| finally: | ||
| asyncio.set_event_loop(None) | ||
|
|
||
| self.assertIsNone(startup.result()) |
There was a problem hiding this comment.
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
The Python-based HTTP metadata server is rarely used. Does the built-in C++ metadata server have the same issue? |
|
Yeah, the C++ one has the same issue. That said, #2345 already seems to be fixing that exact method, so I'd rather not duplicate it — happy to keep this PR to the Python server and leave the C++ side to #2345, unless you'd prefer I fold it in here. |
he-yufeng
left a comment
There was a problem hiding this comment.
Approving the Python side. The Future-based handshake is the right fix, and I walked the paths to convince myself it is tight: "started" is logged strictly after site.start() returns, a failed bind is re-raised to the caller instead of dying silently on the daemon thread, the except BaseException re-raises non-Exception types so KeyboardInterrupt/SystemExit keep their identity, and the finally + if not startup.done() guard both prevents a run() hang and avoids a double set_exception. The three tests cover the meaningful cases (bind-failure propagation with no false "started" log, ready server + clean shutdown, and a BaseException after readiness not being swallowed).
One non-blocking note: startup.result() has no timeout, so the only startup path this does not fail-fast on is a site.start() that hangs rather than raises. That is rare and not a regression, but a bounded timeout would close the last gap if you think it is worth it.
On @stmatengss's question about the C++ server and @toffee-desuwa's answer, I confirmed it. HttpMetadataServer::start() (mooncake-store/src/http_metadata_server.cpp:110-118) calls server_->async_start(), drops the returned Future<error_code>, then unconditionally sets running_ = true, logs "started", and returns true. poll() then reports KVPoll::Success off running_, so a failed bind is invisible on the C++ path exactly like it was on the Python one. Worth tracking so the two servers behave the same. Happy to send a follow-up that awaits the async_start() future and surfaces the error, or leave it to you if you would rather keep it in this line of work.
|
Opened #2942 for the C++ side we discussed — it mirrors |
|
Thanks for the careful review, and for taking the C++ side in #2942 — that fully covers @stmatengss's question: this PR stays Python-only, and the C++ path gets the same treatment over there. |
HttpMetadataServer::start() called server_->async_start() and dropped its result, then unconditionally set running_ and returned true. coro_http_server's async_start() binds synchronously and hands back a future that is already resolved (hasResult()) when the bind failed, so a failed bind (e.g. the port is already in use) was reported as a healthy server: start() returned true and poll() reported KVPoll::Success off running_. Check the future the same way MasterAdminServer::Start() does for the same server type: return false and log on a failed bind, leaving running_ unset. This mirrors the Python fix in #2924 (the C++ gap was raised by @stmatengss and confirmed by @toffee-desuwa in that PR's discussion). Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
…e-ai#2942) HttpMetadataServer::start() called server_->async_start() and dropped its result, then unconditionally set running_ and returned true. coro_http_server's async_start() binds synchronously and hands back a future that is already resolved (hasResult()) when the bind failed, so a failed bind (e.g. the port is already in use) was reported as a healthy server: start() returned true and poll() reported KVPoll::Success off running_. Check the future the same way MasterAdminServer::Start() does for the same server type: return false and log on a failed bind, leaving running_ unset. This mirrors the Python fix in kvcache-ai#2924 (the C++ gap was raised by @stmatengss and confirmed by @toffee-desuwa in that PR's discussion). Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
|
@stmatengss the C++ side is merged now (#2942) — anything else needed on this one? |
Description
The Python HTTP metadata server (
mooncake-wheel/mooncake/http_metadata_server.py) started its aiohttp server on a background daemon thread, then unconditionally logged "started" and slept forever without waiting for the bind to succeed. If the port was already in use, the background thread failed the bind (logging the error and setting internal state toFailed), but the main thread had already reported success and kept the process alive. A supervisor then sees a live process plus a success log while nothing is listening — a silent failure that misleads health checks and deployment.This adds a startup handshake:
run()waits on aconcurrent.futures.Futurethat the server thread completes only afterTCPSite.start()succeeds (ready) or fails (exception). "started" is logged only on success; on bind failure the exception is propagated torun()and re-raised, so the CLI exits non-zero instead of masking the failure.This is a source-inspection find with no existing issue. Note: it touches the same file as #2527 but a different region — this changes the startup path around
run()/_run_server, while #2527 changes_handle_putidempotency. The two do not overlap.The added lines pass
ruffandcodespell. The file predates the repo'sruff-formathook, so the diff keeps the surrounding style unchanged rather than adding an unrelated whole-file reformat.Module
mooncake-wheel)Type of Change
How Has This Been Tested?
Test commands:
Test results:
New regression tests: occupying a loopback port asserts
run()raises and logs no success message; binding to port0asserts normal startup and clean shutdown. A separate test confirms a runtime exception raised during startup reachesrun()without being swallowed. Verified on Windows 11 (WinError 10048); the logic is platform-independent (LinuxErrno 98).Checklist
./scripts/code_format.shpre-commit run --all-filesand all hooks passAI Assistance Disclosure
The implementation was AI-assisted and independently reviewed across multiple models before submission. The diagnosis and fix were verified against the source and the regression tests were run locally (results above).