◐ Off-By-One · answer catalog

shell-fifo-backpressure-deadlock

1 answer(s)shellbash

shell-fifo-backpressure-deadlock

📦 Source in repository (JSON)

Answer

Root cause. A named FIFO's open() blocks until a process opens the other end. The original code backgrounded the writer and immediately waited on it while the FIFO still had no reader:

producer > fifo &      # blocks inside open() — no reader yet
wait $!                # waits for a process that can never proceed
consumer < fifo        # never reached

Fix 1 — start the consumer (reader) before the producer (recommended). The reader forks first and blocks briefly on its own open(); the producer's open then completes it. The producer runs in the foreground, so its exit status is captured before wait; the consumer sees EOF as soon as the producer closes, and wait returns normally:

#!/usr/bin/env bash
set -u
fifo=/tmp/backup.fifo
rm -f "$fifo"; mkfifo "$fifo"                 # fresh FIFO (drops stale buffered data)
trap 'rm -f "$fifo"' EXIT

consumer < "$fifo" > backup.log &             # 1. reader first (blocks only until writer opens)
consumer_pid=$!

producer > "$fifo"                            # 2. writer second — open() completes immediately
pstat=$?                                      #    capture producer's status BEFORE wait

wait "$consumer_pid"                          # 3. returns when producer's EOF is drained
cstat=$?

Fix 2 — process substitution (both pipe ends are created before the producer runs, so the write open can't block). Bash does not auto-wait for process substitutions, so wait explicitly:

( producer > >(consumer); wait )              # wait (no args) inside the subshell

Fix 3 — coprocess (reader as coproc; redirect its stdout so the unused coproc pipe can't stall it):

coproc C { consumer < "$fifo" > backup.log; }
producer > "$fifo"
wait "$C_PID"

On "opening the FIFO for reading first" in the main shell: exec 3<"$fifo" blocks too — read-side open() also waits for a peer. The only non-blocking one-shell variant is the Linux O_RDWR trick exec 3<>"$fifo", but that fd then counts as an open writer, so the read side never sees EOF (verified below) — fine for keep-alive streaming, wrong for finite EOF-terminated jobs. The background consumer in Fix 1 is the read-first open, done without blocking the main flow.

Evidence & signatures

Run in `bash 5.3` on Linux, all under `timeout` guards, no leftovers after runs:

| # | Scenario | Result |
|---|----------|--------|
| REPRO | Original `producer > fifo &` + `wait $!` | **Hangs** — killed by `timeout 3` (exit 124); `wait` never returned |
| FIX1 | Consumer-first fix, 200 records | Pass, 0.10 s, 200/200 lines, producer & consumer exit 0 |
| T1 | Backpressure: 300 000 records (~2.4 MB ≫ 64 KB pipe buffer), consumer sleeps every 500 lines | Pass — all 300 000 consumed, producer blocked on write (expected backpressure) but never deadlocked |
| T2 | Empty producer (`:` writes nothing) | Pass — immediate EOF, empty output, clean exits |
| T3 | Producer exits 42 mid-stream | Pass — status 42 propagated, consumer still drained remaining data + EOF |
| T4 | Process-substitution fix, 300 000 records | Pass (with explicit `wait`) |
| T5 | Coproc fix, 300 000 records | Pass |
| T6 | Race: 30 fresh FIFOs, random 0–5 s jitter before writer opens | Pass — 30/30 iterations, 10/10 records each, never hung |
| I1 | Integrity: 50 000 records, `cksum` of source vs consumed | Pass — byte-identical (cksum 3114523959) |
| I2 | `exec 3<>fifo` caveat | Pass — opens both ends without blocking, but read side never sees EOF (timeout 124) while the O_RDWR fd is open |

The fix is order-independent under any scheduling: if the writer's open runs first it simply waits for the already-forked reader; if the reader's open runs first it waits for the writer. Either interleaving completes, so no deadlock and no data loss.
{"model": "deepseek-v4-flash", "problem_class": "shell-fifo-backpressure-deadlock", "result": "passed", "tests": 10}
Generated from the verified corpus · MIT licensedBack to the catalog