Conversation
'TTL expiration during forkless bgsave' issued 1200 synchronous EXPIRE round trips but computed every expiry deadline from a timestamp taken before that loop began, so its model of when key i dies lagged reality by however long the loop took. For keys where i % 10 == 0 the built-in slack was exactly one second, so any environment slower than about 0.9 ms per round trip failed the 'must be gone' assertion. Record each key's deadline as its own EXPIREs are issued, and re-sample the clock per key in the verification loop instead of once per outer pass over up to 1200 EXISTS calls. Test-only. Upstream: valkey-io/valkey#4611 Signed-off-by: Madelyn Olson <matolson@amazon.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TTL expiration during forkless bgsavesets TTLs with 1200 synchronous EXPIRE round trips, then computes every expiry deadline from a timestamp captured before that loop starts, so its model of when keyidies lags reality by however long the loop took. For keys wherei % 10 == 0the built-in slack is exactly one second, so any environment slower than about 0.9 ms per round trip fails the "must be gone" assertion on a key that is still legitimately alive. This records each key's deadline as its own EXPIREs are issued, and re-samples the clock per key in the verification loop instead of once per outer pass over up to 1200 EXISTS calls. Test-only; the server was expiring correctly the whole time.Details
Upstream: valkey-io/valkey#4611. Introduced by
acba5ddd0"Forkless Save (#4460)", which added the test.Problem
tests/integration/rdb.tcl:1014tookset start_time [clock milliseconds]before the loop at1015-1021that issues 100 keys x 12 prefixes = 1200 synchronousEXPIREcalls withttl = $i/10 + 1(integer division,1016). The gone-threshold at1062waselapsed_time > $i/10.0 + 2, measured fromstart_time.Assumed deadline:
start_time + i/10 + 1. Real deadline:t_expire(i) + floor(i/10) + 1, wheret_expire(i)is the wall time fromstart_timeuntil keyi's EXPIREs execute,12 * iround trips in. SoFor
i % 10 == 0the fractional term is 0 and the whole margin is1 s - t_expire(i).Second, independent flaw:
elapsed_timewas sampled once per outer pass at1053and then reused across up to 1200 EXISTS commands at1055-1077. That one biases the opposite branch ("not yet expired, must exist"), so it is not what CI hit, but it fails the same way.Reproduction
The knob is per-command latency.
appendfsync alwaysmakes everyEXPIREwait on an fsync, which pushes the round trip from ~34 us to ~2.8 ms and blows the 1 s budget deterministically. Both configs are set globally by--config, sostart_server's{forkless-infrastructure-enabled yes save ""}overrides still apply.Instrumented, the EXPIRE loop takes 3.36 s instead of 0.041 s:
And at the failure the key is not stuck, it simply has TTL left, because its EXPIRE ran 1.3 s after
start_time:That is what rules out a forkless-save defect:
PTTLreturns a live, positive TTL. Lost expire metadata would make keys immortal and fail ati = 0on every platform every run, not 1 in 7 daily runs.Runs
Before, 5/5 fail, same assertion and same relative line 56 as the CI job:
After, 5/5 pass under the same knob, and 5/5 without it:
Full file, unmodified invocation:
Slack preserved
The rewrite keeps the original tolerances, just measured against a correct per-key deadline.
deadline($i)is sampled before the 12 EXPIREs, so it sits at or below every real deadline, which is the safe direction for both branches. The exist branch keeps the original 1 s margin as$now + 1000 < $deadline($i); the gone branch keeps 2 s as$now > $deadline($i) + 2000.Alternative rejected
Switching the loop to
PEXPIREAT start_time + ttl*1000also makes the arithmetic exact, but on a slow machine the 1 s TTLs fori = 0..9land in the past and those keys are deleted beforebgsaveruns, so the test would stop covering keys-with-TTL during a forkless snapshot in exactly the environments that flake.Base branch
madolson/valkey-agentsunstableis 66 commits behindvalkey-io/valkeyunstableand has no forkless save at all (grep -c forkless src/rdb.creturns 0), so this targetsupstream-unstable-d6415e766, an unmodified copy ofvalkey-io/valkey@d6415e766, following the pattern of #15.This was generated by AI but verified, with love, by a human.