fix(hetzner/server-1): make the crow agent's podman API socket survive node loss #6

Open
Alpers-Ai-Agent wants to merge 1 commit from Alpers-Ai-Agent/MyServers:agent/crow-agent-socket-fix into main
Contributor

What

Restructures the Crow local-agent podman API socket wiring with systemd-native mechanisms, after the Oct 31 outage: the agent unit wedge-looped (start-limit-hit, podman exit 125 statfs /run/podman-crow-ci.sock: no such file or directory) because the socket's filesystem node was unlinked while the socket unit stayed active — an orphaned listener that every probe saw as healthy and nothing could connect to.

Root cause in one line

A bind-mounted socket file pins the inode: the node vanished (unlinked by an unidentified process during the Oct 31 deploy window — unit files, tmpfiles, and the socket/service timestamps rule out config and systemd itself), the listener survived invisibly, and the agent's next podman run was the only thing on the box that ever re-validates the path.

The fix — three native mechanisms, no script logic

  1. Socket in its own directory, container mounts the directory. ListenStream=/run/podman-ci/podman.sock (dir created via a declarative systemd.tmpfiles.settings rule, 0750 root:crow-podman); the agent mounts /run/podman-ci:/run/podman instead of pinning the socket file. Node unlink + recreate becomes transparent to the container; the in-container path stays /run/podman/podman.sock, so the agent's socket auto-detection is untouched.
  2. Lifecycle chain via PartOf/Requires/After. podman-ci.service is PartOf=podman-ci.socket (a socket restart mints a fresh listener, so the service must restart to serve it), and the agent unit is Requires+After=podman-ci.socket, PartOf=podman-ci.service — restarts propagate socket → service → agent instead of leaving a container holding a stale mount. This wiring was entirely missing before (the socket floated on sockets.target with no consumer relation).
  3. A .path unit + conditioned recovery oneshot — the systemd-native detector for the failure class systemd otherwise cannot see (active unit, missing node). PathChanged=/run/podman-ci triggers podman-ci-socket-recover.service, a oneshot whose ConditionPathIsSocket=!</run/podman-ci/podman.sock> makes it a silent no-op when the node exists — which also breaks the restart-fires-event-fires-restart loop, since the node its own recovery creates satisfies the condition and skips the next trigger. Recovery = reset-failed on the agent (clears start-limit) + try-restart socket and service.

Deploy behavior

No manual root commands needed: the deploy restarts the changed socket/service/agent units (after reset-failed), which creates the new dir + node and heals the currently-failed agent; the old /run/podman-crow-ci.sock orphan listener is closed/unlinked by the socket unit restart.

Validation

  • nix-instantiate --parse + nixfmt --check clean
  • All five rendered units inspected at the level systemd acts on: socket (ListenStream=/run/podman-ci/podman.sock), service (PartOf=podman-ci.socket), agent (Requires/After podman-ci.socket, PartOf=podman-ci.service), path unit (PathChanged=/run/podman-ci), recovery (ConditionPathIsSocket=!…, two ordered ExecStarts)
  • Container volumes evals to ["/run/podman-ci:/run/podman"]
  • systemd-analyze verify clean on socket/service/path/recovery units
## What Restructures the Crow local-agent podman API socket wiring with systemd-native mechanisms, after the Oct 31 outage: the agent unit wedge-looped (`start-limit-hit`, podman exit 125 `statfs /run/podman-crow-ci.sock: no such file or directory`) because the socket's filesystem node was unlinked while the socket unit stayed active — an orphaned listener that every probe saw as healthy and nothing could connect to. ## Root cause in one line A bind-mounted **socket file** pins the inode: the node vanished (unlinked by an unidentified process during the Oct 31 deploy window — unit files, tmpfiles, and the socket/service timestamps rule out config and systemd itself), the listener survived invisibly, and the agent's next `podman run` was the only thing on the box that ever re-validates the path. ## The fix — three native mechanisms, no script logic 1. **Socket in its own directory, container mounts the directory.** `ListenStream=/run/podman-ci/podman.sock` (dir created via a declarative `systemd.tmpfiles.settings` rule, 0750 root:crow-podman); the agent mounts `/run/podman-ci:/run/podman` instead of pinning the socket file. Node unlink + recreate becomes transparent to the container; the in-container path stays `/run/podman/podman.sock`, so the agent's socket auto-detection is untouched. 2. **Lifecycle chain via `PartOf`/`Requires`/`After`.** `podman-ci.service` is `PartOf=podman-ci.socket` (a socket restart mints a fresh listener, so the service must restart to serve it), and the agent unit is `Requires`+`After=podman-ci.socket`, `PartOf=podman-ci.service` — restarts propagate socket → service → agent instead of leaving a container holding a stale mount. This wiring was entirely missing before (the socket floated on `sockets.target` with no consumer relation). 3. **A `.path` unit + conditioned recovery oneshot** — the systemd-native detector for the failure class systemd otherwise cannot see (active unit, missing node). `PathChanged=/run/podman-ci` triggers `podman-ci-socket-recover.service`, a oneshot whose `ConditionPathIsSocket=!</run/podman-ci/podman.sock>` makes it a silent no-op when the node exists — which also breaks the restart-fires-event-fires-restart loop, since the node its own recovery creates satisfies the condition and skips the next trigger. Recovery = `reset-failed` on the agent (clears start-limit) + `try-restart` socket and service. ## Deploy behavior No manual root commands needed: the deploy restarts the changed socket/service/agent units (after `reset-failed`), which creates the new dir + node and heals the currently-failed agent; the old `/run/podman-crow-ci.sock` orphan listener is closed/unlinked by the socket unit restart. ## Validation - `nix-instantiate --parse` + `nixfmt --check` clean - All five rendered units inspected at the level systemd acts on: socket (`ListenStream=/run/podman-ci/podman.sock`), service (`PartOf=podman-ci.socket`), agent (`Requires/After podman-ci.socket`, `PartOf=podman-ci.service`), path unit (`PathChanged=/run/podman-ci`), recovery (`ConditionPathIsSocket=!…`, two ordered `ExecStart`s) - Container `volumes` evals to `["/run/podman-ci:/run/podman"]` - `systemd-analyze verify` clean on socket/service/path/recovery units
The Oct 31 outage: the socket node was unlinked behind an active socket unit, wedging the agent in a start-limit loop while every probe stayed green. Now: socket lives in /run/podman-ci and the container mounts the directory (node recreation is transparent); Requires/After/PartOf wire socket->service->agent restarts; a path unit plus a ConditionPathIsSocket-guarded recovery oneshot heals an out-of-band node loss without a restart loop.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u agent/crow-agent-socket-fix:Alpers-Ai-Agent-agent/crow-agent-socket-fix
git switch Alpers-Ai-Agent-agent/crow-agent-socket-fix

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff Alpers-Ai-Agent-agent/crow-agent-socket-fix
git switch Alpers-Ai-Agent-agent/crow-agent-socket-fix
git rebase main
git switch main
git merge --ff-only Alpers-Ai-Agent-agent/crow-agent-socket-fix
git switch Alpers-Ai-Agent-agent/crow-agent-socket-fix
git rebase main
git switch main
git merge --no-ff Alpers-Ai-Agent-agent/crow-agent-socket-fix
git switch main
git merge --squash Alpers-Ai-Agent-agent/crow-agent-socket-fix
git switch main
git merge --ff-only Alpers-Ai-Agent-agent/crow-agent-socket-fix
git switch main
git merge Alpers-Ai-Agent-agent/crow-agent-socket-fix
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Alper/MyServers!6
No description provided.