Skip to content

fix(compass-app): stop runStackUp/Down hanging on a fire-and-return stack (SEA-1685) - #227

Open
seal-agent wants to merge 2 commits into
mainfrom
compass-native-1685-stack-stderr-hang
Open

fix(compass-app): stop runStackUp/Down hanging on a fire-and-return stack (SEA-1685)#227
seal-agent wants to merge 2 commits into
mainfrom
compass-native-1685-stack-stderr-hang

Conversation

@seal-agent

Copy link
Copy Markdown
Contributor

runStackUp/runStackDown captured the compass-stack child's stderr into a
bytes.Buffer. os/exec backs a non-*os.File stderr writer with an OS pipe whose
copy goroutine Cmd.Wait blocks on until EOF. compass-stack up is
fire-and-return: it exits 0 once the stack is Ready while its
postgres/server/runner children keep running, and those children inherit the
pipe's write-end — so EOF never arrives and Wait hangs for the children's whole
lifetime. Embedded launch therefore never completed against a real stack (the
T4.1/T4.2 unit tests passed only because their /bin/sh children leave no
survivors).

Capture stderr to a temp *os.File instead (extracted into captureStderr, shared
by both seams): an *os.File is dup'd straight into the child (no pipe, no
goroutine), so Wait returns the instant compass-stack itself exits regardless of
lingering children, and the children write to a plain file that never EPIPEs, so
the capture never signals the stack the app must keep alive. The failure-copy
contract (surface the child's stderr on a non-zero exit) is preserved.

Regression: TestRunStackUpReturnsWhileChildrenLinger drives /bin/sh that
backgrounds a long sleep holding stderr then exits 0 — the fire-and-return
shape. Pre-fix it hangs for the sleep's lifetime; the fix returns in
milliseconds.

Surfaced by the SEA-1685 T4.3 e2e integration test driving the real embedded
composition root against a real compass-stack (up -> real WhoAmI resolved a live
account id, DL-111). The full T4.3 e2e + its teardown assertions are gated on
SEA-1880 (compass-stack down is a no-op across a process boundary) and land once
Matt rules the teardown mechanism.

Co-authored-by: Matt Wilkinson matt@sealedsecurity.com

…tack (SEA-1685)

runStackUp/runStackDown captured the compass-stack child's stderr into a
bytes.Buffer. os/exec backs a non-*os.File stderr writer with an OS pipe whose
copy goroutine Cmd.Wait blocks on until EOF. `compass-stack up` is
fire-and-return: it exits 0 once the stack is Ready while its
postgres/server/runner children keep running, and those children inherit the
pipe's write-end — so EOF never arrives and Wait hangs for the children's whole
lifetime. Embedded launch therefore never completed against a real stack (the
T4.1/T4.2 unit tests passed only because their /bin/sh children leave no
survivors).

Capture stderr to a temp *os.File instead (extracted into captureStderr, shared
by both seams): an *os.File is dup'd straight into the child (no pipe, no
goroutine), so Wait returns the instant compass-stack itself exits regardless of
lingering children, and the children write to a plain file that never EPIPEs, so
the capture never signals the stack the app must keep alive. The failure-copy
contract (surface the child's stderr on a non-zero exit) is preserved.

Regression: TestRunStackUpReturnsWhileChildrenLinger drives /bin/sh that
backgrounds a long sleep holding stderr then exits 0 — the fire-and-return
shape. Pre-fix it hangs for the sleep's lifetime; the fix returns in
milliseconds.

Surfaced by the SEA-1685 T4.3 e2e integration test driving the real embedded
composition root against a real compass-stack (up -> real WhoAmI resolved a live
account id, DL-111). The full T4.3 e2e + its teardown assertions are gated on
SEA-1880 (compass-stack down is a no-op across a process boundary) and land once
Matt rules the teardown mechanism.

Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown

SEA-1685

… read note, shorter test sleep (SEA-1685)

Three low-severity review polish items (no high/medium findings; reviewer
APPROVE):

- captureStderr: name the returns (read/cleanup/err) so the signature
  self-documents; note that read's dropped ReadFile error is intentional
  best-effort degradation to the generic failure message.
- TestRunStackUpReturnsWhileChildrenLinger: shrink the lingering grandchild
  from `sleep 60` to `sleep 5` — still far past the 1s regression bound, but a
  regressed run's leaked child self-reaps in seconds not a minute (shared-box
  hygiene under repeat/-count loops).

No behavior change; union gate green (gofmt/vet/golangci 0-issues/-race) both
tag sets.

Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant