Skip to content

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"; fi

prints 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"; then

That 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

  1. added
    kind/bugCategorizes issue or PR as related to a bug
    area/ciIssues or PRs related to CI workflows, GitHub Actions, automation
    on Sep 7, 2026
  2. lexfrei commented on Sep 7, 2026

    @lexfrei
    ContributorAuthor

    Captured in production rather than reproduced synthetically, and the two lines land together in the log:

    hack/check-rd-presets.sh: line 39: printf: write error: Broken pipe
    FAIL: packages/system/postgres-rd/cozyrds/postgres.yaml resourcesPreset enum missing: s1.nano
    make: *** [Makefile:120: rd-presets-check] Error 1
    

    That is run 34163203912, on a pull request that touches packages/apps/vm-instance and nothing under postgres-rd. s1.nano is present in packages/system/postgres-rd/cozyrds/postgres.yaml on main and 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: printf was still writing when grep -q exited on the match, and pipefail turned that into the failure the if ! reads as absence.

    Worth noting what this costs beyond the red check. The job fails at rd-presets-check before 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.

  3. lexfrei commented on Sep 8, 2026

    @lexfrei
    ContributorAuthor

    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 $enums is 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, so printf completes its single write before grep can leave, and there is no SIGPIPE for it to take.

    Measured rather than argued, on bash 5.3:

    • The real 47-preset list, searching for the first element (the worst case, since grep leaves immediately): 3000 runs, zero false "missing". Another 3000 with the CPU saturated the way a parallel make saturates a runner: still zero.
    • The reproducer in this issue, searching the last element: 3000 runs, zero. Same list searching the first element: 3000 runs, zero.
    • The construct does fail as described once the input outgrows the buffer. seq 1 200000 piped into grep -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 tests job 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: gh was returning Gateway Timeout on actions/runs, actions/workflows and GraphQL while these failures were being looked at, and helm plugin install was failing at Validating Checksum in 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.

  4. lexfrei commented on Sep 11, 2026

    @lexfrei
    ContributorAuthor

    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.medium at 11:40:31, and packages/system/rabbitmq-rd/cozyrds/rabbitmq.yaml resourcesPreset enum missing: s1.xlarge at 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 named redis-rd and t1.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 the 2>/dev/null on 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.

  5. lexfrei commented on Sep 18, 2026

    @lexfrei
    ContributorAuthor

    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.

  6. added
    triage/acceptedIndicates an issue is ready to be actively worked on
    and removed
    triage/needs-triageIndicates an issue needs triage by a maintainer
    on Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/ciIssues or PRs related to CI workflows, GitHub Actions, automationkind/bugCategorizes issue or PR as related to a bugtriage/acceptedIndicates an issue is ready to be actively worked on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions