mvcc: avoid a non-positive interval when resetting the watch resync ticker - #22297
mvcc: avoid a non-positive interval when resetting the watch resync ticker#22297martin-k-m wants to merge 1 commit into
Conversation
syncWatchersLoop resets its delay ticker to the time the last syncWatchers pass took, so that catching up lagging watchers does not starve other store operations. time.Ticker.Reset panics on a non-positive interval, and time.Since can return exactly zero on platforms whose monotonic clock is coarser than a sync pass takes. On windows/amd64 that resolution is about 500us, so any pass finishing faster than that panics the goroutine and takes the process down. Extract the delay computation into resyncDelay and fall back to watchResyncPeriod when the measured duration is not positive. Signed-off-by: Martin Muskov <martinkmuskov@gmail.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: martin-k-m The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
Hi @martin-k-m. Thanks for your PR. I'm waiting for a etcd-io member to verify that this patch is reasonable to test. If it is, they should reply with Regular contributors should join the org to skip this step. Once the patch is verified, the new status will be reflected by the I understand the commands that are listed here. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
|
Verified locally on darwin/arm64, go1.26.6. The mvcc suite is green on this branch with Two things that strengthen the case for fixing it. etcd ships a windows/amd64 artifact, On the open question about clamping, I would prefer the clamp, for three reasons. Precedent for exactly this shape exists twice in the repo. The fallback changes the loop's cadence on the platform being fixed, by a large factor. When a pass measures zero, the loop now waits 100ms rather than roughly the time the pass took. On this machine a full 512-watcher batch runs 0.5ms to 1.1ms and a partial pass runs in tens of microseconds, so on a platform whose tick is coarser than the pass, a yield that was microseconds becomes 100ms. That is a three-orders-of-magnitude change in resync cadence, on exactly the platform that motivated the patch. The cost you weighed against a clamp does not quite apply. That said, the fallback is the more conservative of the two and it does fix the crash, so treat this as a preference rather than a blocker. Three smaller points.
|
|
Never seen the issue in production, doesn't seem like a real issue. |
|
Correcting my own comment above. I offered the windows/amd64 release artifact as evidence that this is more than a developer-machine annoyance, and that argument is weaker than I made it sound. The supported-platforms policy puts AMD64 Windows in Tier 3, where the guarantee is that etcd builds, testing may be light or absent, and the platform is to be considered unstable; Tier 3 CI requires a successful build and nothing more. A shipped binary is not the same as a supported runtime, and I should have checked the tier before leaning on the artifact. What holds is narrower. The mechanism is real, and it is not reachable on Tier 1: the branch runs only after a pass that made measurable progress, and at nanosecond granularity such a pass does not measure zero. That is consistent with @serathius not having seen it in production. Whether a Tier 3 runtime bug earns a patch is a maintainer call and I am not pressing one. Putting the tier on the record here mainly for whoever decides, and for #22296. |
|
What? So what's the impact of this change? Respond in 1 sentence or using prose. |
|
Nothing on Linux, where the duration is never zero. On Windows a panic that kills the process becomes a 100ms wait |
Fixes #22296
What
Extract the delay computation from
syncWatchersLoopinto a small helper and make it fall backto
watchResyncPeriodwhen the measured duration is not positive.The loop body becomes a single
delayTicker.Reset(resyncDelay(...)), replacing the previousreset-then-maybe-reset-again pair.
Behaviour change
Only for the case that currently panics. Whenever
syncDuration > 0, the returned delay isidentical to today. When it measures as zero, the loop waits
watchResyncPeriodinstead ofcrashing.
I considered clamping to a small non-zero minimum instead, so that a very fast pass still loops
promptly rather than waiting the full 100ms. I did not do that because it introduces a new tuning
constant, and a pass that completes in under the clock resolution had almost nothing to do. Happy
to switch if reviewers prefer it.
Test
TestResyncDelayis a table test over the four combinations that matter, including thezero-duration case. Each case asserts the expected delay, asserts it is positive, and calls
time.Ticker.Resetwith the value so that the actual panic condition is exercised rather thanonly approximated.
Test evidence
Run on Linux,
golang:1.26container. The test is platform independent, so it reproduces thebug on Linux too even though the panic itself only fires on Windows.
Without the fix (guard removed, helper and test present):
With the fix:
Original panic, on Windows 11 native, before and after the fix, 5 runs each of
go test -count=1 -run "TestWatch" ./storage/mvcc/:Full
servermodule unit suite on Linux: no failures.Lint:
Not fixed here
While reproducing this I hit two other Windows-only failures in the same package. Both look
unrelated and are left alone:
TestStoreRevfails in cleanup withTempDir RemoveAll cleanup: unlinkat ...: The process cannot access the file because it is being used by another process, which is bbolt's mmapholding a handle open.
TestWatchVictimstimes out. There is already an open PR, mvcc: DeflakeTestWatchVictimswithsynctime#22269, "mvcc: deflakeTestWatchVictims with synctime".
Checklist
make verify-lintcleanservermodule unit tests pass on LinuxmainA note on the test, so a reviewer is not misled
TestResyncDelaycoversresyncDelaydirectly. It cannot be run againstunpatched
mainto show it failing, because the function it tests is the onethis change introduces: reverting
watchable_store.goalone leaves the testunable to compile. What is demonstrable without the patch is the mechanism, and
both halves of it were measured on this machine (Windows 11, go1.26.6):
So
syncDurationis zero on this platform essentially whenever a sync pass isshorter than the clock's resolution, and a zero interval is exactly what
Ticker.Resetrefuses. The guard issyncDuration > 0.This PR was written in part with the assistance of generative AI. Every change was reviewed, built and tested before submitting, and no AI co-author or
assisted-bytrailers are used, per https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#ai-guidance