Conversation
The helper deleted nodes.conf and then created a directory in its place using two separate exec calls. The server owns that path and can recreate it via rename() at any moment, so a save landing between the two calls left a regular file behind and made 'mkdir -p' fail EEXIST, killing the whole test client with an [exception]. Replace the pair with a wait_for_condition that retries delete-then-mkdir until the directory sticks. 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.
create_nodes_conf_folderdeletesnodes.confand then creates a directory in its place using two separateexeccalls. The server owns that path and legitimately recreates it viarename()whenever gossip changes the topology, so a save landing between the two calls leaves a regular file behind and the followingmkdir -pfails EEXIST. That is an[exception], so it kills the test client and fails the whole job rather than one test. This replaces the pair with await_for_conditionthat retries delete-then-mkdir until the directory sticks.Details
Problem
tests/unit/cluster/misc.tcl:108(upstreamd6415e766has the same helper attests/unit/cluster/misc.tcl:192):Two forks against a path the server writes.
mkdir -ptolerates an existing directory but not an existing regular file, so ifrename()fills the hole in between, it fails:The save side is
clusterSaveConfigImplrenaming a temp file into place atsrc/cluster_legacy.c:1163. In best-effort mode it runs on a BIO thread (src/cluster_legacy.c:7013->:1243->:1207->clusterSaveConfigFromBioat:1230), so the main thread returns fromCLUSTER FAILOVER TAKEOVERwhile the write is still pending.CLUSTER_TODO_SAVE_CONFIGis set from ~50 sites insrc/cluster_legacy.c, many of them gossip paths (:4276,:4407,:4436,:4459), so a save can fire at any moment. Arename()onto a path the test just deleted succeeds and logs nothing, which is why the recreation is invisible in the server log.Reproduction
The window is a couple of milliseconds wide, so CI hits it rarely. It goes to 100% by forcing a save to be in flight across it: issue
CLUSTER BUMPEPOCHon a deferring client and do not read the reply, then run the helper.clusterBumpConfigEpochWithoutConsensussetsCLUSTER_TODO_SAVE_CONFIG | CLUSTER_TODO_FSYNC_CONFIGatsrc/cluster_legacy.c:2605, which queues the BIO save while Tcl is between its two forks.Repro suite, built from the two helper procs copied verbatim out of upstream
misc.tcl(sed -n '183,203p' tests/unit/cluster/misc.tcl) plus this loop:start_cluster 1 1 {tags {external:skip cluster} overrides {cluster-config-save-behavior best-effort}} { test {REPRO-36 create_nodes_conf_folder races the BIO cluster-config save} { set d0 [valkey_deferring_client 0] set d1 [valkey_deferring_client -1] remove_nodes_conf_folder 0 remove_nodes_conf_folder 1 for {set i 0} {$i < 300} {incr i} { # Queue a config save on both nodes without waiting for the reply, so the # BIO save is in flight while the helper does its rm/mkdir pair. $d0 cluster bumpepoch $d1 cluster bumpepoch create_nodes_conf_folder 1 remove_nodes_conf_folder 1 $d0 read $d1 read } $d0 close $d1 close } }Run at
d6415e766with the unfixed helper,./runtest --single unit/cluster/repro36, five invocations (--loopscannot be used because an[exception]aborts the run):5/5. Same stack as CI:
Appending the same loop to the real upstream
misc.tclreproduces it too, on the first run, through the real helper.With the fix applied to the helper:
5/5.
Fix
file delete -forceplusfile mkdirare Tcl builtins rather than forks, which shrinks the window on its own, andfile mkdiron an existing directory is a no-op so the retry is safe to repeat. Thewait_for_conditioncloses the window: 50 attempts at 100ms is generous against any save rate the server can produce.Alternative considered
Draining the BIO queue first (
DEBUG BIO-DRAIN BIO_CLUSTER_SAVE) before the delete. That loses, because gossip can setCLUSTER_TODO_SAVE_CONFIGagain immediately after the drain returns; there is no point at which the test can know the server will not write. Retrying is the only correct shape.Note on this branch
agents/unstablepredatesf1addd8f3(#2555), the commit that put the best-effort save on a BIO thread and added the remove-save-recreate cycle to the test. The helper on this branch is the older variant (rm -f, inline path lookup) but has the identical delete-then-create shape, so it gets the identical fix. The reproduction above was run against upstreamd6415e766where the BIO save exists.Testing
./runtest --single unit/cluster/misc --loops 5on this branch:All tests passed without errors!, 75[ok].Fixes #36
This was generated by AI but verified, with love, by a human.