Repository navigation
check-rd-presets reports a present preset as missing when printf loses a SIGPIPE race under pipefail #4153
Description
hack/check-rd-presets.sh reports a preset that is present as missing, at random, on loaded runners. It is a SIGPIPE race, not a content problem, and it fails the Unit & controller tests job of whatever pull request happens to be running when it fires.
Line 39 is if ! printf '%s\n' "$enums" | grep -Fqx -- "$want"; then, under set -euo pipefail from line 7. grep -q exits the instant it matches. If printf is still writing at that moment it takes SIGPIPE and exits 141, pipefail hands 141 up as the pipeline's status, and if ! reads the failure as "not found". So the branch that runs is the one that reports a preset the file actually contains as absent.
Reproduced with the same construct rather than argued:
set -euo pipefail
small=$(printf "a\nb\ns1.nano\n")
if ! printf "%s\n" "$small" | grep -Fqx -- "s1.nano"; then echo "SMALL: reported MISSING"; else echo "SMALL: found"; fi
large=$(seq 1 200000)
if ! printf "%s\n" "$large" | grep -Fqx -- "1"; then echo "LARGE: reported MISSING"; else echo "LARGE: found"; fiprints SMALL: found and LARGE: reported MISSING. The value is present in both cases; only the size of the write differs.
That is why it is intermittent and why it passes locally. Forty-odd short lines usually fit the pipe buffer, so printf finishes before grep exits and nothing goes wrong. It needs a runner loaded enough for the writer to lose that race. It also explains the shape of the failures: two different pull requests running in the same minute failed the same line naming different presets, postgres-rd missing s1.nano in one and nats-rd missing s1.4xlarge in the other. A stale-content failure names the same thing every time; a race names whatever it happened to be checking.
The fix is to remove the pipe rather than to work around the exit code, since the pipe is what creates the writer that can be killed:
if ! grep -Fqx -- "$want" <<<"$enums"; thenThat change already exists unmerged: commit 99d16a21b on feat/externalip-api makes it, with a comment naming the same mechanism. Until that branch lands the race stays live on main and any pull request can still fail this line.
Two things have to hold together for this to bite: pipefail, so the writer's SIGPIPE becomes the pipeline's status, and a writer with enough left to write that it is still going when grep -q exits. hack/ has many | grep -q sites and only a few scripts that set pipefail, and line 39 here is the only place the two meet. The other sites run without pipefail, so grep's status is the pipeline's status and the writer's death never surfaces. seaweedfs-naming-audit.sh is the clearest of those: it is POSIX sh on purpose, with a comment saying dash rejects pipefail and that cozytest.sh sources the script into dash.
Activity
lexfrei commentedon Sep 7, 2026
Captured in production rather than reproduced synthetically, and the two lines land together in the log:
That is run 34163203912, on a pull request that touches
packages/apps/vm-instanceand nothing underpostgres-rd.s1.nanois present inpackages/system/postgres-rd/cozyrds/postgres.yamlonmainand on the branch alike, so the preset the check calls missing is there in both trees the job could have been looking at.The broken-pipe warning immediately preceding the FAIL is the mechanism naming itself:
printfwas still writing whengrep -qexited on the match, andpipefailturned that into the failure theif !reads as absence.Worth noting what this costs beyond the red check. The job fails at
rd-presets-checkbefore the rest of the target completes, so a pull request that trips this loses the coverage of whatever ran after it, not just the preset check. And because the victim is whichever file the loop happened to be on, the failure message points at a package the author never touched, which sends whoever reads it to look in the wrong tree first.lexfrei commentedon Sep 8, 2026
I filed #4158 on the same line before finding this one, so that one is a duplicate and I have closed it. Bringing over the measurement from it, because it does not agree with the reasoning here.
The claim that
$enumsis long enough to fill the pipe buffer does not hold. The largest enum any RD file yields is 3009 bytes (seaweedfs); every other one is smaller. A pipe buffer is 64 KiB, soprintfcompletes its single write beforegrepcan leave, and there is no SIGPIPE for it to take.Measured rather than argued, on bash 5.3:
makesaturates a runner: still zero.seq 1 200000piped intogrep -Fqx -- "1"reports the element as missing, and the pipeline returns 141.So the mechanism is real and the fix is right, but it is not what made these jobs red, and the issue currently states a cause it has not established.
One more data point against the SIGPIPE reading: the failing
Unit & controller testsjob on #3594 went green on a rerun with no change to the tree. Whatever differs between the two runs, it is not the script and not the input.Worth noting alongside:
ghwas returningGateway Timeoutonactions/runs,actions/workflowsand GraphQL while these failures were being looked at, andhelm plugin installwas failing atValidating Checksumin the same window. Three failed network calls in one window is not a diagnosis either, but it is a better lead than a race that does not reproduce at this input size.The sweep of the other call sites stands on its own and is worth doing regardless of which cause is found, for the reason the issue gives: the construct turns any write error into a wrong answer about the data instead of surfacing a broken read.
lexfrei commentedon Sep 11, 2026
I filed #4178 as a separate report before finding this one. Closing it as a duplicate; two things from it are worth carrying over here.
The victim is not stable across attempts of the same run. Run 34516348714 reported
packages/system/opensearch-rd/cozyrds/opensearch.yaml resourcesPreset enum missing: u1.mediumat 11:40:31, andpackages/system/rabbitmq-rd/cozyrds/rabbitmq.yaml resourcesPreset enum missing: s1.xlargeat 13:10:11. Same run id, different file, different preset, both present in the file named. So matching the failure against what a PR changed will never clear it, and a re-run does not fix it — it reshuffles which name appears. A third sighting today on an unrelated PR namedredis-rdandt1.xlarge.The line-39 attribution is right, and the other pipe in that loop is ruled out. The same function has a second writer into a pipe at lines 27-32,
enums=$(printf '%s' "$schema" | jq -r '...' 2>/dev/null || true), which writes the whole schema blob and is therefore a far more plausible EPIPE site by size. It is not the one: bash attributes an error inside a multi-line command to that command's last line, so an EPIPE there would be reported as line 32. Note also that the2>/dev/nullon that line binds to jq, not to printf, so a printf warning from that site would still have reached the log under its own line number.One expectation worth setting for anyone who tries to reproduce it by hand. At the real input size it does not reproduce on a developer machine: the enum list is a few kilobytes, one
write()well inside the 64K pipe buffer, so printf finishes before grep can close the read end and there is no window. Several hundred iterations locally gave zero false failures. Forcing the window open needs an oversized writer, which demonstrates the mechanism and says nothing about the real size. Expect green when you try it; that is not evidence against the bug.lexfrei commentedon Sep 18, 2026
Fixed on main by #4204. The failures in #4178 from 17 and 18 September were re-runs of runs created on 9 September, and a re-run keeps the merge commit it was created with, so they ran the old script.
One correction to my comment above. bash sets stdout to line buffering at startup (
sh_setlinebuf (stdout)in shell.c), so the builtin printf flushes at newline boundaries and the 47-line list goes out in a dozen or so writes rather than one (strace on bash 5.2 with glibc: the first lines one per write, the rest in chunks of several lines). grep -q leaves after the first match and every write after that gets EPIPE, at any list size. The here-string has no such window: bash writes the whole document into a pipe or a temp file before the command starts.Main still has no test for this script. #4191 and #4194 both carry one. Whichever gets rebased should keep it. Closing.