Skip to content

engine plugins: backend detection is heuristic, not tracked — risks datadir deletion + collateral kill of sibling engines #78

Description

@ikeikeikeike

Surfaced by the wave-2 retrospective /code-review sweep of PR #2/#3/#4/#6 (postgres/redis/elasticsearch providers + the shared Docker backend). Two related gaps in how a running engine's plugin process is identified at teardown time; this sweep applied a narrow mitigation for the first but the underlying design gap remains for both.

1. usingDockerBackend() guesses the backend from container existence, not from what Up() actually used

DownReq and ReadyCheckRequest carry no backend field (see plugins/engine/api/proto/*.proto — only UpRequest has extras/backend). So Down()/ReadyCheck() in all four plugins (mysql/postgres/redis/elasticsearch docker.go) reconstruct "was this docker or nix" by calling usingDockerBackend(), which just checks "does a container with this name exist and is it running".

This sweep tightened the check to also require ContainerInspect(...).State.Running (previously any container, including a long-stopped one, counted), which closes the most concrete false-positive. But the deeper issue remains: a transient Docker daemon error during Down() (dockerutil.NewClient() or LookupByName() failing) makes usingDockerBackend() return false even when the engine really is docker-backed, so Down() falls through to the nix-style lsof+SIGTERM path instead of dockerDown() — the container is never stopped/removed, and a later Cleanup() (rm -rf the bind-mounted datadir) can then race a still-running container process.

Proper fix: extend UpRequest/DownRequest/ReadyCheckRequest (proto regen required) with an explicit backend field the host persists (e.g. in the registry, alongside the allocated ports) and threads through every subsequent RPC for that engine instance, instead of re-derived heuristically per call.

2. killStrayProcessCompose can't tell engines within the same worktree apart

All four plugins' nix-backend Down() ends with killStrayProcessCompose(req.WorktreeRoot): pgrep -f process-compose + a cwd-prefix match, SIGTERM every match. But internal/cli/create.go's startEngines computes one engineProviderWorktree value per bough create call and passes it as WorktreeRoot to every declared engine — so a worktree running mysql+redis+es together (this project's own documented topology, see CLAUDE.md) has all three engines' nix run ... up processes sharing the identical cwd.

This sweep fixed the separate cross-worktree false-positive (.../auba-api-139 matching as a prefix of .../auba-api-1394) by anchoring the prefix match to a path-separator boundary. That fix does not help here — the cwd is byte-for-byte identical across engines in the same worktree, so tearing down any one engine's Down() call SIGTERMs the still-running process-compose supervisors of every other engine in that same worktree too, bypassing their own graceful shutdown.

Proper fix: give each engine's nix run invocation a way to be identified individually — e.g. a pidfile written at Up() time to a per-engine-kind path (<engineProviderWorktree>/.local/<kind>-process-compose.pid), with killStrayProcessCompose reading and validating that specific PID instead of a heuristic cwd sweep. This needs design sign-off since it changes the stray-cleanup contract all four plugins share.

Files

  • plugins/engine/{mysql,postgres,redis,elasticsearch}/docker.go (usingDockerBackend)
  • plugins/engine/{mysql,postgres,redis,elasticsearch}/*.go (killStrayProcessCompose)
  • plugins/engine/api/proto/*.proto, internal/cli/create.go (startEngines)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions