Skip to content

fix(port-check): cgroup-based same-env ownership + Bug B loopback ZFS boot docs (#176) - #176

Merged
samo-agent merged 4 commits into
mainfrom
fix/port-check-failed-same-env-unit
Jul 16, 2026
Merged

fix(port-check): cgroup-based same-env ownership + Bug B loopback ZFS boot docs (#176)#176
samo-agent merged 4 commits into
mainfrom
fix/port-check-failed-same-env-unit

Conversation

@samo-agent

Copy link
Copy Markdown
Collaborator

Summary

Fixes two bugs surfaced during samograph preview recovery (2026-07-16).

Bug A — port-check deletes vhost for failed/sibling same-env unit (HIGH)

Root cause: samohost_port_check_ok(port, unit) used systemctl is-active as the sole ownership signal. For multi-service apps with a single-port allocation (where all listeners share one port value, e.g. PORT=WS_HUB_PORT=INGEST_PORT=3111), the web unit holds the port while the live unit is FAILED. The check sees: port IS held + target unit (live@env) is NOT active → misclassifies as foreign squatter → runs rm -f SAMOHOST_CADDY_SNIPPET + caddy reload → CF 525 on every heal cycle.

Fix:

  1. Add samohost_read_cgroup_for_pid() helper (reads /proc/<pid>/cgroup, stubbed in tests).
  2. Extend samohost_port_check_ok with a third env_name argument.
  3. After the systemctl is-active fast path fails, iterate PIDs holding the port via ss -tlnHp and check /proc/<pid>/cgroup for @<envName>.service. Any match = our env = not foreign.
  4. Update all call sites to pass sq(t.name) as the third arg.

Tests (TDD, RED → GREEN):

  • f1: sibling same-env unit in cgroup → must return 0 (own, not foreign)
  • f2: same unit in FAILED state → cgroup match → must return 0
  • f3: foreign process cgroup → must reject (non-zero)
  • f4–f7: content + call-site + backward-compat assertions

2056/2056 tests pass (0 regressions).

Bug B — dblab-loopback.service doesn't start on reboot (per-VM artifact)

Root cause: dblab-loopback.service on the samograph cx23 VM declared Before=zfs-import.target but zfs-import-cache.service started independently (racing the loopback setup) and aborted (signal=ABRT). Every reboot left the ZFS pool unmounted and all DBLab previews broken until zpool import dblab was run manually.

Fix: This is a per-VM artifact not in samohost code. Added runbook steps to:

  • Rewrite dblab-loopback.service with Before=zfs-import-cache.service (not just zfs-import.target)
  • Add drop-in zfs-import-cache.service.d/10-dblab-loopback.conf with Requires=dblab-loopback.service

Applied live on samograph VM. Documented in docs/dblab-install-runbook.md + docs/stack/dblab.md.

Platform gap: DBLab is not auto-provisioned by samohost (known gap, docs/stack/dblab.md). This fix must be baked into the future cloud-init module (PR #128 follow-up).

Preview re-provisioning (done live)

After Bug B workaround + destroy-then-recreate of PR #111/112:

Preview URL Status
PR #111 (claude/dynamic-presence-lip-sync-ntddw7) samograph-claude-dynamic-presence-lip-sync-ntddw7.samo.cat 200
PR #112 (claude/avatar-call-wiring-a1b2c3) samograph-claude-avatar-call-wiring-a1b2c3.samo.cat 200 (web only)
PR #174 (docs/polish-user-facing-text) samograph-docs-polish-user-facing-text.samo.cat 200 (unchanged)
prod samograph.samo.team 200 (unchanged)

Note on PR #111/112 app-api + ws-hub: Both branches are missing apps/app-api/server.ts and apps/ws-hub/dev-live-server.ts — these modules were added after the branches were cut. The web (Next.js) builds and serves 200. Alex must rebase these branches on main to get the full multi-service stack working.

Root-cause analysis (Bug A)

The port-check was designed to prevent a foreign process from squatting on a port before the preview's systemd unit could bind it. The ownership signal (systemctl is-active) is correct for the single-service case but insufficient for multi-service apps: when all listeners share one port value (legacy single-port allocation), ANY unit in the same env (web, live, etc.) is the legitimate owner — yet the check only tests the specific listener's unit.

The cgroup-based fallback is the correct fix: /proc/<pid>/cgroup is written by systemd for every spawned process and contains the unit name (@<envName>.service), readable without sudo. A truly foreign process has no samohost unit name in its cgroup.

Commits

  • d13df04 RED test: same-env sibling/failed unit must not be foreign squatter
  • 8435628 GREEN fix: cgroup-based same-env ownership (Bug A)
  • d8cb842 docs: loopback ZFS boot ordering fix (Bug B)

Test plan

  • All 2056 tests pass on branch (bun test)
  • f1–f7 (new) confirm the cgroup ownership path
  • Existing port-check tests (a, a', b, c, d, e) still pass (backward compat)
  • Multi-service test suite passes
  • samograph prod 200 after merge + deploy
  • Operator applies Bug B loopback fix on any new cx23 loopback-ZFS VM per runbook

🤖 Generated with Claude Code

samo-agent and others added 4 commits July 16, 2026 22:31
…ign squatter

Reproduces Bug A: for multi-service apps the port-check calls
`samohost_port_check_ok <port> <listener-unit>@<env>.service` for each
listener. When the listener's unit is FAILED but the port is held by a
sibling unit of the same env, `systemctl is-active` returns false and
the check misclassifies the port as a foreign squatter, triggering
`rm -f SAMOHOST_CADDY_SNIPPET` + Caddy reload — killing the preview vhost.

Observed on samograph VM (2026-07-16): samograph-docs-add-robot-to-readme
and other envs with all ports = 3111 (single-port allocation); the web@env
unit holds 3111 while live@env unit fails with EADDRINUSE, and heal cycles
delete the Caddy snippet on every pass (CF 525).

New tests (f1–f7):
- f1: sibling same-env unit in cgroup → must return 0 (own, not foreign)
- f2: same unit in FAILED state → cgroup match → must return 0
- f3: foreign process cgroup → must reject (non-zero)
- f4: generated function must declare env_name param
- f5: generated function must call samohost_read_cgroup_for_pid
- f6: call site must pass env name as 3rd arg
- f7: is-active fast path must be preserved (backward compat)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t no longer classified as foreign squatter (Bug A)

Root cause: `samohost_port_check_ok(port, unit)` used `systemctl is-active`
as the sole ownership signal. For multi-service apps (samograph: web + live
services), when `samograph-live@env.service` is FAILED but a sibling unit
`samograph-web@env.service` holds the same port (both got port=3111 in a
single-port allocation), the check returns false-positive "foreign squatter"
→ `rm -f SAMOHOST_CADDY_SNIPPET` + `caddy reload` → CF 525 on every heal
cycle.

Fix:
1. Add `samohost_read_cgroup_for_pid()` helper (reads /proc/<pid>/cgroup,
   stubbed in tests without real /proc access).
2. Extend `samohost_port_check_ok` with a third `env_name` argument.
   After the `is-active` fast path fails, iterate PIDs holding the port
   via `ss -tlnHp` and check `/proc/<pid>/cgroup` for `@<envName>.service`.
   Any process whose cgroup matches our env's suffix is classified as OURS
   (not foreign) — covers both: a failed same-env unit being restarted, and
   a sibling service that shares the same port value.
3. Update all call sites to pass `sq(t.name)` as the third argument.

Backward-compat: `systemctl is-active` fast path preserved; `env_name`
defaults to "" so callers that omit it get the old behaviour.

Verified: 2056/2056 tests pass (0 regressions). Real-world: samograph VM
had samograph-docs-add-robot-to-readme and samograph-docs-polish-user-facing-text
vhosts deleted on every heal cycle; fix prevents the deletion path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… VMs

Root cause: `dblab-loopback.service` (a per-VM artifact, not in samohost
provisioning) declared `Before=zfs-import.target` but `zfs-import-cache.service`
started independently before the loopback was set up, causing it to abort
(signal=ABRT) — leaving the ZFS pool `dblab` unmounted on every reboot.

Manual workaround (operator steps):
1. Rewrite `dblab-loopback.service` to explicitly declare
   `Before=zfs-import-cache.service` (not just `zfs-import.target`).
2. Add a drop-in `/etc/systemd/system/zfs-import-cache.service.d/10-dblab-loopback.conf`
   with `Requires=dblab-loopback.service` + `After=dblab-loopback.service`.
   This makes the ZFS import wait for the loop device to exist.

Applied live on samograph VM 2026-07-16.

Bug B is NOT a samohost-code bug — the unit is a per-VM artifact installed
manually per the runbook. The platform gap (DBLab not auto-provisioned by
cloud-init) is tracked in docs/stack/dblab.md. The fix must be baked into
the future cloud-init module (PR #128 follow-up).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n; tighten f6 env-name assertion

samorev REQUEST_CHANGES — two blocking fixes:

1. awk portability (finding 1): the 3-arg match(\$0, /pid=([0-9]+)/, a) form
   is a gawk extension. On mawk (Ubuntu's default awk alternative when gawk is
   not installed, which it is not in the bootstrap list), it errors silently:
   zero PIDs are extracted, the cgroup ownership check is skipped entirely, and
   the function falls through to return 1 (foreign squatter) — reproducing the
   exact bug this PR fixes. Replaced with:
     ss -tlnHp | grep -F ":${port} " | grep -oP 'pid=\K[0-9]+'
   GNU grep is always present on Ubuntu and the -oP / \K forms work correctly.
   Added integration test f8 that runs the grep fragment against the host's
   actual grep with a synthetic ss line and asserts both PIDs are extracted.
   f8 also asserts the gawk 3-arg pattern is NOT present in the generated fn.

2. f6 too weak (finding 2): the old regex matched any 3-token call. New test
   extracts every samohost_port_check_ok call line from the script, strips
   quoting from the third positional token, and compares it to target().name
   ("field-record-1-feat-x") exactly. A blank, wrong, or missing env name
   would now fail the test.

Also added inline comment at the ss -tlnHp block noting the grep -oP
portability rationale (non-blocking suggestion from samorev).

2057/2057 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@samo-agent
samo-agent merged commit 4b2b959 into main Jul 16, 2026
1 check passed
@samo-agent
samo-agent deleted the fix/port-check-failed-same-env-unit branch July 16, 2026 23:06
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