fix(openclaw): harden probes, runtime self-healing preflight and session recovery

This commit is contained in:
Moritz Graf
2026-10-07 08:43:28 +02:00
parent a0e461a0b7
commit 53f132d446
4 changed files with 59 additions and 1 deletions
+40
View File
@@ -537,3 +537,43 @@ This document captures fixed issues, operational quirks, and incident root-cause
- Never configure diffusion or raster generation models (`*-image` without chat/text output) as `imageModel` for visual input understanding. - Never configure diffusion or raster generation models (`*-image` without chat/text output) as `imageModel` for visual input understanding.
- Employ dedicated multimodal frontier models (Gemini Flash) with proven OCR and label comprehension for vision tasks. - Employ dedicated multimodal frontier models (Gemini Flash) with proven OCR and label comprehension for vision tasks.
---
### Issue 18: Spinning Disk I/O Bottlenecks, In-Pod Restart Bypass, and Gateway Preflight Self-Healing
- **Date**: 2026-10-07
- **Severity**: Critical (Koch agent unresponsive after interrupted turn; container crash loop / freeze on in-pod restart)
- **Symptom**:
Moritz sent an audio message in Telegram Topic 21 (`#Kochen`):
*„Ich frühstücke wieder meine Oats, bestehend aus 50 Gramm meiner eigenen Mischung, 160 Milliliter Oatley Hafermilch und 50 Gramm Früchte gefroren.“*
The bot never responded. Subsequent messages in Topic 21 were completely ignored.
- **Root Cause**:
1. **Spinning Disk I/O Latency**: Host `haumdaucher` uses mechanical HDD (`/dev/sda2`, `rotational: 1`). Under heavy background disk operations (nightly backups, index generation), random 4KB read operations are throttled to ~70–100 IOPS, blocking Node.js synchronous operations.
2. **Aggressive Liveness Probe Failure**: OpenClaw's liveness probe had a 120s tolerance window (`failureThreshold: 12, periodSeconds: 10`). An event-loop pause during disk I/O exceeded the deadline 12 consecutive times, causing Kubelet to kill the `openclaw` container mid-turn.
3. **The InitContainer Bypass Trap**: When Kubelet restarts an application container inside an existing Pod, **`initContainers` do NOT re-run**. The session repair and quarantine clearing scripts situated exclusively in `initContainers` were completely bypassed!
4. **Session Deadlock & Stale Lock**: Because container 1 was killed mid-turn:
- The session node `agent:koch:telegram:group:-1004326921510:topic:21` was left with `status: "running"` and `activeWriterRunId: "..."`. OpenClaw's `reply-admission-ticket.js` rejected all subsequent incoming Telegram messages on that topic as locked.
- Stale `/tmp/openclaw-1000/gateway.state.lock` caused container 2 to crash with `gateway.state.lock.reclaim: ENOENT`.
5. **Cold Database Opening Delays**: Container 3 started cold and was forced to open all 12 agent SQLite databases (~1.9 GB total) directly from HDD seeks, blocking the event loop for up to 50 minutes.
- **Resolution**:
1. **Immediate State Recovery**:
- Pre-warmed all SQLite database files into the Linux OS page cache via sequential streaming (`cat ... > /dev/null`), dropping read latency from 50 minutes to 312 milliseconds.
- Unlocked the Koch session node and session window in SQLite (`status: "done"`, `activeWriterRunId: null`, `entry_valid = 1`).
2. **Intake Logged & Views Rendered**:
- Recorded the Oats breakfast into DuckDB via `datastore.py add --kind nutrition.intake` (260 g, ~311 kcal, 13.9 g Eiweiß, 37.9 g KH, 9.2 g Fett, 7.9 g Ballaststoffe, 0.24 g Salz) with `food_id = null` (composite dish) with timestamp `2026-10-07T07:55:00+02:00`.
- Regenerated Markdown views (`entities/nutrition-log/moritz/2026-10.md`) and HTML dashboards with `render_views.py`, committed and pushed to `knowledge-wiki`.
- Sent confirmation with complete macro totals directly to Telegram Topic 21 via `openclaw message send`.
3. **Permanent Gateway Preflight Self-Healing (`NODE_OPTIONS`)**:
- Configured `NODE_OPTIONS="--max-old-space-size=3072 --import=/home/node/.openclaw/scripts/gateway-preflight.mjs"`.
- Because Node.js evaluates `NODE_OPTIONS` on **every single process launch**, `gateway-preflight.mjs` executes before OpenClaw gateway starts—even during in-pod container restarts where `initContainers` are bypassed.
- Performs automatic lock cleanup, quarantine clearing, SQLite page cache pre-warming, and un-wedges any orphaned `running` sessions in <600ms.
4. **Probe Hardening**:
- Increased liveness tolerance window in `k8s/openclaw/openclaw-instance.secret.yaml` to 450s (`failureThreshold: 30, periodSeconds: 15, timeoutSeconds: 15`).
- Increased startup probe timeout to 15s to tolerate transient event-loop spikes without false kills.
5. **Nightly Compaction Cron Enhancement**:
- Updated `setup-compaction-cron.sh` and `compact_sessions.py` to run session recovery, `PRAGMA optimize;`, and `VACUUM;` nightly to prevent database bloat and optimize query plans.
- **How to Avoid Recurrence**:
- Never place critical recovery logic solely in `initContainers`; use application runtime hooks or `NODE_OPTIONS` for in-pod restart resilience.
- Set probe failure thresholds and timeouts with high tolerance on rotational HDD nodes.
- Maintain page cache pre-warming for SQLite databases to convert slow random seek I/O into fast sequential streaming.
Binary file not shown.
+18
View File
@@ -192,11 +192,29 @@ def checkpoint_wal_journals():
print(f" ✗ Error checkpointing {db}: {e}") print(f" ✗ Error checkpointing {db}: {e}")
print(f"WAL Checkpoint complete: {wal_freed / (1024*1024):.2f} MB merged to disk.") print(f"WAL Checkpoint complete: {wal_freed / (1024*1024):.2f} MB merged to disk.")
def vacuum_and_optimize():
print("=== Running SQLite VACUUM and OPTIMIZE across all databases ===")
dbs = ["/home/node/.openclaw/state/openclaw.sqlite"] + glob.glob("/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite")
for db in dbs:
try:
con = sqlite3.connect(db, timeout=60.0)
con.execute("PRAGMA optimize;")
con.execute("VACUUM;")
con.close()
except Exception as e:
print(f" ✗ Error vacuuming {db}: {e}")
print("SQLite VACUUM and optimization complete.")
def main(): def main():
prewarm_page_cache() prewarm_page_cache()
try:
subprocess.run(["python3", "/home/node/.openclaw/scripts/recover_sessions.py"], check=False)
except Exception:
pass
prune_stale_cron_sessions() prune_stale_cron_sessions()
compact_active_sessions() compact_active_sessions()
checkpoint_wal_journals() checkpoint_wal_journals()
vacuum_and_optimize()
if __name__ == "__main__": if __name__ == "__main__":
main() main()