Skip to content

refactor+fix: cleanup backlog — pkg/procutil, internal/fsutil, circuit breaker (#82/#86/#87)#93

Merged
ikeikeikeike merged 7 commits into
mainfrom
refactor/cleanup-82-86-87
Jul 5, 2026
Merged

refactor+fix: cleanup backlog — pkg/procutil, internal/fsutil, circuit breaker (#82/#86/#87)#93
ikeikeikeike merged 7 commits into
mainfrom
refactor/cleanup-82-86-87

Conversation

@ikeikeikeike

Copy link
Copy Markdown
Member

Closes #86, #87. Addresses #82 (items 1, 3, 4, 5, 6, 7).

Cleanup backlog from the retrospective review sweep:

Not in scope: #82 item 2 (fixed in wave3), item 8 (fixed in #74), item 9 (SELinux :Z — deferred: PLAUSIBLE and untestable on macOS / dev-linux; see the issue comment).

Verified: go test ./... (incl. new procutil / fsutil + circuit-breaker + heap tests) green; golangci-lint 0 issues; mysql + elasticsearch docker conformance pass on the refactored binaries.

…rocutil

deployFlake, lsofListener and killStrayProcessCompose were duplicated
byte-for-byte across all four engine plugins' <engine>.go files, and
lsofListener was duplicated a fifth time in internal/cli/status.go (as
lsofListen). Hoist them into pkg/procutil — the non-docker sibling of
pkg/dockerutil — so there is one copy to fix.

DeployFlake takes the plugin's embedded assets as an fs.FS parameter
(embed.FS satisfies it) instead of hardcoding a package-level nixAssets,
so the one implementation serves every plugin. The package is split so
the portable helpers (DeployFlake, LsofListener) carry no build tag and
can be imported by the untagged internal/cli/status.go, while
KillStrayProcessCompose (which needs syscall.Kill) stays in a
darwin||linux file.

This commit migrates mysql / postgres / redis and status.go; the
elasticsearch migration rides with its heap/timeout hardening in a
later commit (same file). Behaviour is unchanged — the extracted bodies
are identical to the originals, now covered by pkg/procutil unit tests.

Refs #82 (items 4, 5).
expandHome existed three times (internal/cli/ecc_import.go,
internal/registry/registry.go, internal/gitwt/gitwt.go) with three
different behaviours, and copyFile existed twice (an atomic tmp+rename
in ecc_import, a non-atomic ReadFile+WriteFile in registry).
Consolidate into internal/fsutil with one correct implementation of
each:

  - ExpandHome adopts gitwt's behaviour (the most complete): a bare "~"
    and "~/x" expand, "~user" is left untouched. This fixes registry
    (which mishandled a bare "~") and ecc_import (which mangled
    "~user").
  - CopyFile standardises on the atomic tmp+rename and now also
    preserves the source's mode bits (neither original did), upgrading
    registry's previously non-atomic, mode-dropping backup copy.

ecc_import keeps its copyFile as a thin dispatcher — the .md
instinct-healing branch is domain logic — and delegates the generic
branch to fsutil.CopyFile.

Refs #87.
The daemon-lifetime limiter's circuit breaker never opened: tickOnce
called limiter.Acquire() but runObserverOnceQuiet swallowed the pass
exit status (`_ = c.Run()`), so RecordFailure / RecordSuccess were never
called and the failure tally stayed at zero forever. A wedged or
crashing extraction pass would keep firing indefinitely.

runObserverOnceQuiet now returns the subprocess exit status, and
tickOnce records it on the limiter so the breaker trips after
CircuitBreakerN consecutive failures. A pass killed because the daemon
is shutting down (ctx cancelled -> our Cancel SIGKILLs it) is NOT
counted — a clean stop must not leave the breaker open.

Refs #86.
…dening

elasticsearch:
  - Reject a malformed engines[].extras.heap (heapSizePattern
    ^\d+[kmgKMG]?$) before it reaches the ES_JAVA_OPTS="-Xms${heap}..."
    shell interpolation in the nix flake, closing a shell-injection seam
    (#82 item 6).
  - Bump the nix-backend graceful shutdown budget from 15s to 60s to
    match the docker backend's dockerStopTimeoutSec — ES translog flush
    is slow on both paths, so the 4x gap was inconsistent (#82 item 7).
  - Extract the nix/process helpers to pkg/procutil (the elasticsearch
    half of the item 4/5 de-dup, kept in this commit because it shares
    the file with the heap/timeout changes).

CLI hardening:
  - remove.go: log the pre_remove hook's error instead of swallowing it,
    matching every other best-effort teardown step (#82 item 1).
  - plugins_list.go: pathEnv reads os.Getenv("PATH") directly instead of
    forking `sh -c` for a "fakeExec" test seam that never existed
    (#82 item 3).

Refs #82 (items 1, 3, 5, 6, 7).
Up() validated the heap format after opening the startup log file, so
an early return on an invalid heap leaked that file handle (the
sibling cmd.Start() failure path a few lines later does close it).
Validating before flakeDir/logPath setup fixes the leak at its root
instead of adding another Close() call to remember.

The docker backend never validated heap at all -- pickHeap's result
went straight into the container env, so a malformed value only
surfaced later as an opaque JVM/container startup failure. Both
backends now share one validateHeap check.
Every earlier failure branch in CopyFile removes the temp file before
returning; the final os.Rename did not, orphaning a fully-written temp
file on disk whenever the rename itself failed (e.g. onto an existing
non-empty directory).
Unifying the three expandHome implementations onto ExpandHome (which
never errors) silently dropped ecc_import's prior behavior of
propagating a UserHomeDir failure as an explicit error. With $HOME
unset, `bough ecc import` continued with a literal, un-expanded
"~/..." path and failed later with a generic "ECC root not found"
message instead of the real cause.

ExpandHomeStrict returns the same result as ExpandHome plus the
UserHomeDir error when the input can't be resolved; ExpandHome is now
a thin wrapper around it. ecc_import is the only caller that needs the
strict variant -- registry and gitwt keep the silent-fallback behavior
they already relied on.
@ikeikeikeike
ikeikeikeike merged commit 8a8da4e into main Jul 5, 2026
9 checks passed
ikeikeikeike added a commit that referenced this pull request Jul 6, 2026
The local main ref this branch forked from was stale — PR #91/#92/#93
were merged via `gh pr merge` (GitHub API) directly against the
remote, per this session's standing rule to never touch the shared
checkout's local main HEAD while a parallel session might be using
it, but the local main ref was never fetched afterward. This branch
was created from that stale local ref, so it never had PR #91's newly
added "UpReuse" conformance phase (Up called a second time while the
service is already running, must be a no-op) locally to test against.
ikeikeikeike added a commit that referenced this pull request Jul 6, 2026
Merging origin/main pulled in PR #91's new conformance UpReuse phase
(Up called a second time while the service is already running, must
be a no-op) -- this branch predates that merge locally, since #91/#92/
#93 were merged via `gh pr merge` directly against the remote without
ever fetching the result back into this session's shared checkout, so
this branch forked from a stale local main ref.

That phase caught a real bug: Up() checked dockerutil.IsPortFree
unconditionally, so a second Up() against an already-running
container -- which legitimately holds the port -- was rejected as a
"foreign" conflict instead of being reused. dockerutil.UpOrReuse now
runs first (same order the mysql/postgres/redis/elasticsearch
docker.go plugins already use): if a container named
bough-compose-<port> is already running, Up reuses it and skips the
port-conflict check entirely; IsPortFree is only consulted on a
genuine fresh start. Sidecar state is now written unconditionally
(reuse or fresh) since Down/EnvVars/ReadyCheck must work correctly
even from a freshly-spawned plugin process that did not itself create
the container.

Not reproducible on this macOS dev machine: Docker Desktop's proxy
layer does not visibly bind host ports the way native Linux does, so
IsPortFree(port) returned true even with the container's own port
already published, masking the bug locally. Confirmed fixed via the
local conformance suite (UpReuse phase passing) after merging in the
newer conformance package; the original failure was only caught by
the ubuntu-24.04 CI runner.
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.

observer daemon: circuit breaker never trips (fresh Limiter per tick)

1 participant