Repository navigation
[Store] HA: release the leader lease after the SIGTERM drain (part 2 of #4710) - #4814
Conversation
FinishShutdown now stops the term's renewal polling guard and calls ReleaseLeadership once, after the admin drain and before the exit, so a planned leader restart does not wait for the lease TTL. A release that an ordinary failure path already made is not repeated; its recorded result decides the exit status. A release that is not confirmed ends with status 2 and a WARNING. Standby, candidate and starting phases hold no lease and release nothing. Tests cover ordering, a failed release through a TCP relay, and failover latency after SIGTERM and SIGKILL.
Icedcoco
left a comment
There was a problem hiding this comment.
Thanks for putting this together. The motivation is clear, and the distinction between a confirmed release and an unconfirmed one is helpful. I also appreciate that the documentation calls out the Kubernetes limitation. I had a few questions while reviewing the shutdown path, in case I’m missing an existing safeguard:
-
Background work around lease release.
stop_term_resourcesstops the RPC workers, leadership monitor, and OpLog writer, while the remote HTTP metadata cleanup worker appears to be stopped and joined only inMasterService’s destructor. Since the shutdown path ends with_exit, that destructor does not run. When remote metadata cleanup is enabled, could a queued or in-flight DELETE therefore complete after the lease is released? If the successor has already started serving and the same segment metadata has been recreated, I wonder whether the old cleanup could affect the new entry. This seems like a conditional timing risk rather than an inevitable outcome, but would it be worth stopping or fencing that worker before releasing the lease? -
In-flight admin writes.
SigtermReleasesAfterAdminDrainverifies that the admin server has stopped before release, but it does not hold an already admitted quota write across shutdown. The RFC called out that specific case. The server’s existing stop behavior may already provide the required guarantee; would a test that observes the write completing before release help make that ordering explicit? -
Kubernetes release semantics. The deployment guide already notes that the Kubernetes wrapper reports cancellation being accepted, rather than confirming that the lease was actually released. Could we clarify whether that limitation is acceptable for this PR’s intended scope, or whether the “confirmed release” behavior should be described as applying specifically to etcd?
-
Etcd endpoint parsing in the tests.
FirstEtcdHostPort()splits the endpoint list on commas, whereas the production parser also accepts semicolon-separated endpoints. Could a valid semicolon-separated configuration cause the new relay or TTL probe to parse the endpoint incorrectly? Reusing the production parsing behavior, or skipping configurations the helper cannot handle, might make these tests more portable.
I’m raising these as points to clarify the intended guarantees, not to suggest that each scenario necessarily causes a failure. The etcd release flow itself—stopping renewal polling before releasing the lease—looks reasonable to me.
The remote HTTP metadata cleanup worker is stopped and joined only in ~MasterService, which the terminal _exit skips. A queued or in-flight DELETE can therefore outlive the lease release and land after a successor has re-created the segment metadata. Stop the worker before the release: drop the queued segments, let at most the in-flight request finish or time out, and skip the rest of the batch. The stop is wired through WrappedMasterService into stop_term_resources and the serving exit. Add a handshake test that holds a DELETE, starts the stop on another thread, and proves that the stop joins the in-flight request and skips the rest of the batch. Mutation controls cover the join, the second-key guard, the batch break and the late-enqueue guard. Normalize ';' to ',' in the test endpoint helper, as the Go wrapper does, and cover the semicolon form. Document that statuses 0 and 2 describe the etcd backend, and name the cleanup stop in the shutdown step list.
|
Thanks for the careful read and for the four questions. Answers below, with the changes now in the branch.
The changes are in |
The spell check reads the plural form as a misspelling. Reword the comment so the run passes.
Also trim all ASCII whitespace in the test endpoint helper, matching the Go wrapper's TrimSpace.
[Store] HA: release the leader lease after the SIGTERM drain (part 2 of #4710)
Description
This is the second half of #4710, on top of #4745. #4745 made an HA master end SIGTERM/SIGINT with a bounded, ordered drain and a terminal
_exit, and left the lease to TTL expiry. This PR adds the one release it reserved:FinishShutdownnow stops the term's renewal polling guard and callsReleaseLeadershiponce, after the admin drain and immediately before the same exit. Reverting this PR restores #4745's behavior.What changes:
FinishShutdown(master_service_supervisor.cpp:194): when the supervisor holds the lease, it stops theLeadershipRenewalGuard, releases once, logs the result and returns the status.ShutdownLeasecarries the coordinator and the guard; they are set where the lease is acquired (:597) and where the guard is created (:623), so noshutdown_exitcall site changes.~MasterServicestops the worker outside HA, so a non-HA shutdown can now leave the second key of one segment uncleaned; before, the final batch ran to completion, while queued segments were already dropped. This is a deliberate trade-off.Exit statuses after this PR: 0 = the selected drain completed and the release succeeded or was not needed (released, already released, or none held); 2 = the drain completed but the release was not confirmed; 1 = watchdog expiry; 130/143 = second SIGINT/SIGTERM; 255 = request seen only after a fatal backend error made
Start()return -1.Status 2 means the release was not confirmed, not that the lease is certainly still held. If the lease remains, TTL expiry removes it. A timeout can also mean that the lease had already expired, or that the revoke reached etcd and its reply was lost. On the Kubernetes backend, 0 means the cancel request was accepted; it does not confirm the release.
Failover, on etcd. Before this PR a planned leader restart cost the old lease's remaining TTL plus the successor's warmup. Now, when the release is confirmed, the old key is removed during the shutdown, after the drain and before the process exits, so the successor does not wait for the TTL; warmup is unchanged. Measured in the new tests, from the signal, on etcd v3.6.1 with a v3.5.34 client and the default 5 s TTL (20 runs each; min / median / max):
Both modes run with a remaining-TTL margin of at least 2.5 s read from the server just before the signal (2998 to 2999 ms in every run). The serving time includes the successor's 5 s warmup in both rows. The same loops on etcd v3.5.21 gave the same shape: SIGTERM key removal 18 to 29 ms and serving 5027 to 5037 ms; SIGKILL key removal 4132 to 4277 ms and serving 9142 to 9287 ms. The SIGTERM threshold in the test (the successor acquires within 2 s of the signal) is a performance target validated on 40 runs across the two servers, not an etcd guarantee. The SIGKILL lower bound comes from the lease's remaining TTL as read from the etcd server just before the kill. In a healthy local setup the saving is roughly the remaining TTL, typically 3-5 s per planned restart (4.2 s median here), minus the drain and release time; with a slow drain or release it can be smaller or negative. With the OpLog enabled the successor still runs its producer claim, catch-up and promotion before warmup.
Test notes. The admin-ordering test proves that the admin server has stopped before leadership release. It does not hold an admitted quota write across shutdown; that completion guarantee relies on the admin server's existing stop semantics. The RFC's admitted-quota-write test is not included here and can be added in a follow-up. The failed-release test cuts the child's etcd connection through a local TCP relay and, while the child's release fails, keeps the real lease alive from the test process with a keepalive only (never a revoke), so that the key can be checked at exit; it then stops that keepalive and waits for the key to expire by TTL.
What this PR does not change: independent lease loss, external revocation and keepalive failure keep today's limits; a client can still lose the response to a request that completed during the drain; the successor's warmup is not shortened; the k8s backend's release (
CancelElectionwithReleaseOnCancel) now runs on SIGTERM but is not covered by these tests, which use etcd. Its wrapper returns only the cancel result, and when no election monitor thread is running it returns without waiting for the election goroutine, so status 0 does not confirm a Kubernetes release. Planned restarts of a standalone (non-HA) master, such as the index-preserving handoff in #4666 and #4804, are a separate path and are unchanged here.Size. About 1,400 lines changed (1,356 added, 42 deleted), mostly tests and docs; production is 110 changed lines (95 added, 15 deleted), the docs section is 49 lines, and the test file is 972 changed lines. The RFC estimated ~440 lines for part 2 when it still held the watchdog; the watchdog and its tests moved into #4745. The actual size is more than twice that estimate and above 500 lines. The excess is in the test file: the shared helpers (local TCP relay, test-side keepalive, key watch with event capture, failover test helpers) are about 500 lines, the five HA cases about 360, and the cleanup-stop tests about 250. The RFC (#4710) covers the series.
RFC: #4710. Builds on #4745.
Suggested Merge Priority
Module
mooncake-transfer-engine)mooncake-store)mooncake-conductor)mooncake-reshard)mooncake-ep)mooncake-pg)mooncake-integration)mooncake-p2p-store)mooncake-wheel)mooncake-common)mooncake-rl)Type of Change
How Has This Been Tested?
high_availability_test, default build, against a real etcd: a serving leader, a leader in warmup and a predicate-driven shutdown exit 0 withmaster_viewalready gone at exit; SIGTERM racing a lease revoke exits 0 with one release result; a standby exits 0 and releases nothing; a leader-plus-standby pair measures failover from the signal with a watch started before it: after SIGTERM the successor acquires within 2 s (performance target), after SIGKILL the old key outlives the kill by at least the remaining TTL read from the server just before it (at least 2.5 s). Result: 29 passed, 9 skipped (the nine need fail points). The suite also includes a parser test for semicolon-separated endpoints, which needs no etcd.-DMOONCAKE_ENABLE_TEST_FAILPOINTS=ON): at a new fail point just before the release, the admin server is already stopped and the key is still present; with the child's etcd connection cut at that point, the release fails, the process exits 2, the key is present at exit and expires by TTL after the test's keepalive stops; a request after an ordinary release keeps that result and makes no second release; an ordinary release that fails and is followed by a request exits 2 with the single recorded result and no second release; a second SIGTERM during the drain still exits 143 and leaves the key to TTL. Result: 38 passed.git diff --statonmooncake-store/srcandmooncake-store/includeis empty; the only edit to the test file iskReleaseFailedStatus = 2in place of the new header constant), fails the release assertions. The filter selects the serving-leader, warmup, predicate-driven and SIGTERM-failover cases: "Running 4 tests", "4 FAILED TESTS", none passed. The serving-leader case reports "the release precedes the exit: the key is gone at once" (Value of: ReadEtcdKey(key).has_value(), actual true, expected false). The failover case reports(m.acquired_ms) < (2000), actual: 4058 vs 2000, "after SIGTERM the standby must acquire without waiting for the TTL". The same filter on this PR's build: 4 passed. This control ran on the test file before the last two helper fixes; those fixes change neither production code nor the four assertions.make html SPHINXOPTS=-Wfromdocs/succeeds; the new section renders with its two tables and appears in the page contents between "Promotion and Failover" and "Example: HA Deployment with etcd".Test commands (reproduction commands against an already-running local etcd):
Test results:
shutdown_request_test(15),master_service_config_test(19) andstandby_controller_test(4 passed, 1 skipped) are unchanged and pass.master_service_testpasses 73 in the fail-point build, including the two new cleanup-stop tests; the first default-build run had one unrelated timing failure inMasterServiceTest.CancelDrainJobRestoresSegmentStatus, which passed 10 of 10 single-test reruns and a full-suite rerun (73 passed).high_availability_testagainst etcd v3.6.1 (client v3.5.34), default build 29 passed and 9 skipped, fail-point build 38 passed.Who ran each step and where: the implementation and every test execution above (unit, integration, the 20-run loops and the negative control) were performed by an AI coding assistant (see the AI Assistance Disclosure) in a local development environment. All runs were on one Linux x86_64 host (kernel 5.10, GCC 13) against single-member local etcd servers, v3.6.1 and v3.5.21, started with default flags on non-default ports. No result in this description comes from a test run by a human. All
pre-commithooks pass on the changed files, andcode_format.sh --check --changed-linespasses.Checklist
git clang-format(clang-format 20.1.8);./scripts/code_format.sh --check --changed-lines --base origin/mainpassed after committing. Transcript:./scripts/code_format.sh --check --changed-lines --base origin/main(exit 0)AI Assistance Disclosure
AI tools assisted the design/review and implementation; the human submitter is responsible for reviewing every changed line and validating the reported results. Tools used: the implementation and all test executions were done by an AI coding assistant (Claude Sonnet 5.5); the design was written by an AI coding assistant (Claude Opus 5.5); the design and implementation reviews were done by AI assistants (Claude Opus 5.5 and OpenAI GPT-6 Astra).