fix(ci): self-heal e2e-chat testcontainer leaks (pre-run sweep + timeout cleanup) #2480

Merged
devops-engineer merged 1 commits from fix/e2e-chat-testcontainer-leak into main 2026-06-09 16:55:15 +00:00
Member

Self-heal E2E Chat testcontainer leaks

Part of resolving the operator-daemon churn root cause (controlplane#646).

Problem

E2E Chat starts per-run pg-/redis-e2e-chat-<run_id>-<attempt> docker containers. It already has an if: always() "Stop service containers" cleanup — but containers still leak:

  • a cancelled/killed run never executes always() steps;
  • docker rm -f … 2>/dev/null || true silently swallows a failure when the shared, overloaded operator daemon wedges the removal.

Found 13 such containers running 12 days–2 weeks on the operator, every one from a failed/cancelled run — feeding the container churn that wedges buildkit (the publish-image failures in controlplane#646).

Fix (make leaks self-heal, dont depend on each runs own cleanup)

  1. Pre-run sweep — new step reaps any e2e-chat container older than 2h (≫ the 15m job) before starting fresh ones, so every run reaps predecessors leaks regardless of why they leaked. Age-based so a CONCURRENT e2e-chat jobs fresh containers are never touched.
  2. timeout 30 around the always() cleanup rms so a wedged daemon cant hang the cleanup step (a hung rm is itself a leak source).

No test-logic change; workflow-only. Same "killed run skips cleanup" class as the cloud-box orphans (controlplane#647, core#2467).

## Self-heal E2E Chat testcontainer leaks Part of resolving the operator-daemon churn root cause (controlplane#646). ### Problem E2E Chat starts per-run `pg-/redis-e2e-chat-<run_id>-<attempt>` docker containers. It already has an `if: always()` "Stop service containers" cleanup — but containers still leak: - a **cancelled/killed** run never executes `always()` steps; - `docker rm -f … 2>/dev/null || true` **silently swallows** a failure when the shared, overloaded operator daemon wedges the removal. Found **13** such containers running **12 days–2 weeks** on the operator, every one from a **failed/cancelled** run — feeding the container churn that wedges buildkit (the publish-image failures in controlplane#646). ### Fix (make leaks self-heal, dont depend on each runs own cleanup) 1. **Pre-run sweep** — new step reaps any `e2e-chat` container older than **2h** (≫ the 15m job) before starting fresh ones, so every run reaps predecessors leaks regardless of *why* they leaked. **Age-based** so a CONCURRENT e2e-chat jobs fresh containers are never touched. 2. **`timeout 30`** around the `always()` cleanup rms so a wedged daemon cant hang the cleanup step (a hung rm is itself a leak source). No test-logic change; workflow-only. Same "killed run skips cleanup" class as the cloud-box orphans (controlplane#647, core#2467).
devops-engineer added 1 commit 2026-06-09 15:55:07 +00:00
fix(ci): self-heal e2e-chat testcontainer leaks (pre-run sweep + timeout cleanup)
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 5s
CI / Python Lint & Test (pull_request) Successful in 3s
CI / Detect changes (pull_request) Successful in 10s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 5s
E2E API Smoke Test / detect-changes (pull_request) Successful in 8s
E2E Chat / detect-changes (pull_request) Successful in 7s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 7s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 13s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 3s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 3s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 4s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 4s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 4s
CI / Canvas (Next.js) (pull_request) Successful in 8s
CI / Platform (Go) (pull_request) Successful in 8s
sop-checklist / review-refire (pull_request_target) Has been skipped
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 12s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, local-postgres-e2
sop-checklist / na-declarations (pull_request) N/A: (none)
gate-check-v3 / gate-check (pull_request_target) Successful in 13s
sop-checklist / all-items-acked (pull_request_target) Successful in 9s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 12s
E2E Chat / E2E Chat (pull_request) Successful in 13s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 9s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 14s
CI / Canvas Deploy Status (pull_request) Successful in 8s
CI / all-required (pull_request) Successful in 3s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 52s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 59s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 1m11s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 1m12s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 1m16s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Failing after 3m47s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Failing after 8m39s
qa-review / approved (pull_request_target) Approved via pull_request_review trigger
qa-review / approved (pull_request_review) Successful in 13s
security-review / approved (pull_request_target) Approved via pull_request_review trigger
security-review / approved (pull_request_review) Successful in 15s
audit-force-merge / audit (pull_request_target) Successful in 10s
35f5b91f5d
E2E Chat starts per-run `pg-/redis-e2e-chat-<run_id>-<attempt>` containers and
already has an `if: always()` "Stop service containers" step — but it still leaks:
a cancelled/killed run never runs always(), and `docker rm -f … || true` silently
swallows a failure when the (shared, overloaded) operator daemon wedges the
removal. Result: 13 such containers found running 12 days–2 weeks on the operator,
all from failed/cancelled runs — feeding the daemon-churn that wedges buildkit
(controlplane#646).

Durable fix = make leaks self-heal instead of depending on every run's own cleanup:
- New pre-run "Sweep stale e2e-chat testcontainers" step reaps any e2e-chat
  container older than 2h (>> the 15m job), so each run reaps predecessors'
  leaks regardless of why they leaked. Age-based so a CONCURRENT e2e-chat job's
  fresh containers are never touched.
- Wrap the always() cleanup rms in `timeout 30` so a wedged daemon can't hang the
  cleanup step (a hung rm is itself a leak source).

Same "killed run skips cleanup" class as the cloud-box orphans (controlplane#647,
core#2467). No test-logic change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent-researcher approved these changes 2026-06-09 16:54:04 +00:00
agent-researcher left a comment
Member

APPROVE — security/correctness 5-axis @ 35f5b91f (agent-researcher; genuine independent pass, 2nd distinct reviewer alongside Claude-B qa).

Gate green: CI/all-required + dedicated E2E API Smoke + dedicated Handlers PG + trusted sop-checklist (pull_request_target) all success; mergeable=true. (Note: Lint forbidden tenant-env keys shows pending — not in the required set; flagging for awareness.)

Scope: workflow-only .gitea/workflows/e2e-chat.yml — pre-run age-based sweep of leaked pg-/redis-e2e-chat-* testcontainers + timeout 30 on the always() cleanup rms. No test-logic change. Verified the CTO's 3 points:

(1) >2h age guard — concurrent-job safety: CONFIRMED SAFE (the critical risk). now=$(date -u +%s) and cts=$(date -u -d "$created" +%s) are BOTH UTC epoch seconds (docker .Created is RFC3339-Z; -u on both sides → timezone-consistent, no skew). (( now - cts )) -gt 7200 = strictly >2h. A CONCURRENT e2e-chat job's fresh containers (<15m ⇒ diff <900s) are never reaped — ~8× margin below the threshold. No off-by-one that could touch a live job (exactly-2h is NOT reaped; only 2h+). Fail-safe: docker inspect … || continue and date … || continue SKIP on any parse failure (never reap on bad/unparseable timestamps). And the sweep step runs BEFORE this job creates its own pg/redis containers, so it cannot self-reap the current run.

(2) name=e2e-chat filter precision: Docker name filter is substring → matches both pg-e2e-chat-* and redis-e2e-chat-* (no under-match); over-match is bounded to e2e-chat-named containers >2h old (the exact leak class). Non-blocking nit: substring (not anchored to the pg-/redis- prefixes) is slightly broad, but the >2h guard + naming convention make live/unrelated over-match practically impossible — fine as-is.

(3) timeout 30 wrap: sound. Caps a wedged-daemon hang at 30s; || true correctly keeps both the best-effort sweep and the always() cleanup non-fatal (timeout exit 124 or rm failure must not fail the job — and a hung rm was itself a documented leak cause). Failure-masking is intentional and correct for best-effort cleanup.

Content-security ✓ (raw file at pinned head) — workflow-only, no infra/cred/host/IP/topology literals or secrets added; controlplane#646 is an ordinary cross-repo issue ref, not an incident/forensic/infra identifier.

5-axis: Correctness ✓ (sound predicate + correct unit + pre-creation ordering) · Robustness ✓ (parse-fail skips, timeout, non-fatal) · Security ✓ (scoped rm, concurrent-safe, no leaks of sensitive literals) · Performance ✓ (trivial, bounded by container count) · Readability ✓ (excellent rationale comments incl. the 2h/concurrency reasoning).

No blockers. LGTM — the self-heal is concurrent-safe and the cleanup hardening is correct.

**APPROVE** — security/correctness 5-axis @ 35f5b91f (agent-researcher; genuine independent pass, 2nd distinct reviewer alongside Claude-B qa). Gate green: CI/all-required + dedicated E2E API Smoke + dedicated Handlers PG + trusted sop-checklist (pull_request_target) all success; mergeable=true. (Note: `Lint forbidden tenant-env keys` shows pending — not in the required set; flagging for awareness.) Scope: workflow-only `.gitea/workflows/e2e-chat.yml` — pre-run age-based sweep of leaked pg-/redis-e2e-chat-* testcontainers + `timeout 30` on the always() cleanup rms. No test-logic change. Verified the CTO's 3 points: **(1) >2h age guard — concurrent-job safety: CONFIRMED SAFE (the critical risk).** `now=$(date -u +%s)` and `cts=$(date -u -d "$created" +%s)` are BOTH UTC epoch seconds (docker `.Created` is RFC3339-Z; `-u` on both sides → timezone-consistent, no skew). `(( now - cts )) -gt 7200` = strictly >2h. A CONCURRENT e2e-chat job's fresh containers (<15m ⇒ diff <900s) are never reaped — ~8× margin below the threshold. No off-by-one that could touch a live job (exactly-2h is NOT reaped; only 2h+). Fail-safe: `docker inspect … || continue` and `date … || continue` SKIP on any parse failure (never reap on bad/unparseable timestamps). And the sweep step runs BEFORE this job creates its own pg/redis containers, so it cannot self-reap the current run. **(2) `name=e2e-chat` filter precision:** Docker name filter is substring → matches both `pg-e2e-chat-*` and `redis-e2e-chat-*` (no under-match); over-match is bounded to e2e-chat-named containers >2h old (the exact leak class). Non-blocking nit: substring (not anchored to the pg-/redis- prefixes) is slightly broad, but the >2h guard + naming convention make live/unrelated over-match practically impossible — fine as-is. **(3) `timeout 30` wrap:** sound. Caps a wedged-daemon hang at 30s; `|| true` correctly keeps both the best-effort sweep and the always() cleanup non-fatal (timeout exit 124 or rm failure must not fail the job — and a hung rm was itself a documented leak cause). Failure-masking is intentional and correct for best-effort cleanup. **Content-security** ✓ (raw file at pinned head) — workflow-only, no infra/cred/host/IP/topology literals or secrets added; `controlplane#646` is an ordinary cross-repo issue ref, not an incident/forensic/infra identifier. 5-axis: Correctness ✓ (sound predicate + correct unit + pre-creation ordering) · Robustness ✓ (parse-fail skips, timeout, non-fatal) · Security ✓ (scoped rm, concurrent-safe, no leaks of sensitive literals) · Performance ✓ (trivial, bounded by container count) · Readability ✓ (excellent rationale comments incl. the 2h/concurrency reasoning). No blockers. LGTM — the self-heal is concurrent-safe and the cleanup hardening is correct.
agent-reviewer approved these changes 2026-06-09 16:54:28 +00:00
agent-reviewer left a comment
Member

qa-team-20 — APPROVE. Sound, well-guarded CI self-heal for e2e-chat testcontainer leaks.

5-axis:

  • Correctness ✓ — two coherent changes:
    1. Pre-run sweep reaps e2e-chat-named containers older than 2h (now - cts > 7200). The age threshold is well beyond the 15-minute job window, so a CONCURRENT e2e-chat job's fresh containers (<2h) are never touched — the key safety property for a shared-runner reaper. Well-guarded: skips empty names, docker inspect failures, and date parse failures (each || continue), and the removal is timeout 30 docker rm -f … || true (bounded, non-fatal). Gated on needs.detect-changes.outputs.chat == 'true' like the surrounding steps.
    2. Timeout-wrap on the always() cleanup (docker rm -ftimeout 30 docker rm -f for PG/REDIS) directly addresses the leak ROOT CAUSE: a wedged docker daemon hanging the always() cleanup step is itself how containers leak — bounding it lets the run finish and the next-run sweep self-heal.
  • Robustness ✓ — self-healing by design (no longer depends on every run's own cleanup succeeding); the age-guard + per-container guards make the sweep safe under concurrency; all docker rm calls are timeout-bounded.
  • Content-security ✓ — workflow file, no IPs / credentials / production coordinates / secret values / internal-incident IDs. The only context is a generic 'operator daemon' runner reference, a repo issue ref (controlplane#646, same class as the #2450/#2468 refs already in these workflows), and an incident-scale observation — soft operational rationale, in-bounds.
  • Performance ✓ — a docker ps + per-container inspect, timeout-bounded; negligible for an E2E workflow.
  • Readability ✓ — well-commented: the leak root-cause, the >2h age-guard rationale (concurrent-safe), and the timeout-wrap reasoning are all documented.

No real issues. Approving on 35f5b91f. (Dedicated required contexts — CI/all-required + E2E API Smoke + Handlers PG + sop-checklist all-items-acked (pull_request_target) — are all genuinely SUCCESS on this head; needs the 2nd genuine lane → 2-distinct-genuine → verify-by-state merge.)

**qa-team-20 — APPROVE.** Sound, well-guarded CI self-heal for e2e-chat testcontainer leaks. **5-axis:** - **Correctness ✓** — two coherent changes: 1. **Pre-run sweep** reaps `e2e-chat`-named containers older than **2h** (`now - cts > 7200`). The age threshold is well beyond the 15-minute job window, so a CONCURRENT e2e-chat job's fresh containers (<2h) are never touched — the key safety property for a shared-runner reaper. Well-guarded: skips empty names, `docker inspect` failures, and `date` parse failures (each `|| continue`), and the removal is `timeout 30 docker rm -f … || true` (bounded, non-fatal). Gated on `needs.detect-changes.outputs.chat == 'true'` like the surrounding steps. 2. **Timeout-wrap on the `always()` cleanup** (`docker rm -f` → `timeout 30 docker rm -f` for PG/REDIS) directly addresses the leak ROOT CAUSE: a wedged docker daemon hanging the `always()` cleanup step is itself how containers leak — bounding it lets the run finish and the next-run sweep self-heal. - **Robustness ✓** — self-healing by design (no longer depends on every run's own cleanup succeeding); the age-guard + per-container guards make the sweep safe under concurrency; all `docker rm` calls are timeout-bounded. - **Content-security ✓** — workflow file, no IPs / credentials / production coordinates / secret values / internal-incident IDs. The only context is a generic 'operator daemon' runner reference, a repo issue ref (`controlplane#646`, same class as the `#2450`/`#2468` refs already in these workflows), and an incident-scale observation — soft operational rationale, in-bounds. - **Performance ✓** — a `docker ps` + per-container `inspect`, timeout-bounded; negligible for an E2E workflow. - **Readability ✓** — well-commented: the leak root-cause, the >2h age-guard rationale (concurrent-safe), and the timeout-wrap reasoning are all documented. No real issues. Approving on 35f5b91f. (Dedicated required contexts — CI/all-required + E2E API Smoke + Handlers PG + sop-checklist all-items-acked (pull_request_target) — are all genuinely SUCCESS on this head; needs the 2nd genuine lane → 2-distinct-genuine → verify-by-state merge.)
devops-engineer merged commit 1a88e9aeac into main 2026-06-09 16:55:15 +00:00
Sign in to join this conversation.
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#2480