#!/bin/bash # t-260929210755 / закрытие эскалации e-260929234911: рестарт демона лестницы # БЕЗ убийства живых LLM-воркеров (рестарт при живом воркере = застрявшая # running-задача, тот самый инцидент). Ждём «тихого окна» — нет процессов # harness/_llm_worker.py, с подтверждением 3с, потом systemctl restart. # # Гарантия на гонку «проверил → стартовал новый воркер → рестарт»: orphan-requeue # (commit 42743f9) возвращает running-задачи с мёртвым воркером в очередь — # даже худший случай не теряет работу, просто повторит её. # # Использование: scripts/safe-restart.sh [max_wait_sec] (по умолчанию 3600) set -u MAX_WAIT="${1:-3600}" LOG="${SIMBIOT_RESTART_LOG:-/tmp/simbiot-safe-restart.log}" SERVICE="${SIMBIOT_SERVICE:-simbiot-harness}" # якорь в конец cmdline: не матчит sh -c команды, которые лишь УПОМИНАют воркера PAT='/harness/_llm_worker\.py$' deadline=$(( $(date +%s) + MAX_WAIT )) echo "[$(date -u '+%F %T')] safe-restart: ждём тихое окно (макс ${MAX_WAIT}s), сервис ${SERVICE}" >> "$LOG" while :; do if ! pgrep -f "$PAT" > /dev/null; then sleep 3 # окно должно быть устойчивым if ! pgrep -f "$PAT" > /dev/null; then echo "[$(date -u '+%F %T')] safe-restart: тихое окно поймано" >> "$LOG" break fi fi if [ "$(date +%s)" -ge "$deadline" ]; then echo "[$(date -u '+%F %T')] safe-restart: тихого окна нет ${MAX_WAIT}s — рестарт всё равно (orphan-requeue подхватит)" >> "$LOG" break fi sleep 5 done if systemctl restart "$SERVICE"; then echo "[$(date -u '+%F %T')] safe-restart: демон ${SERVICE} перезапущен" >> "$LOG" else echo "[$(date -u '+%F %T')] safe-restart: FAIL — systemctl restart ${SERVICE}" >> "$LOG" exit 1 fi # ── пост-рестарт верификация: сек-тик НОВОГО демона отвечает на команду без LLM ── # (инъекция синтетического сообщения в стор; через N секунд в сторе должен быть # ReplySent от сек-тика — это доказывает, что новый код жив в продакшене) sleep 6 # демон поднимается, первые сек-тики проходят VERIFY_MSG_ID="${SIMBIOT_VERIFY_MSG_ID:-990102}" cd /root/simbiot-harness || exit 1 ./venv/bin/python - "$VERIFY_MSG_ID" <<'EOF' >> "$LOG" 2>&1 import sys, time, sqlite3 mid_num = sys.argv[1] from harness import es, tg_inbox mid = tg_inbox.receive_message({'update_id': int(mid_num), 'message': { 'message_id': int(mid_num), 'chat': {'id': 7569223131}, 'from': {'id': 7569223131, 'username': 'alex_test'}, 'text': '/лестница', 'date': int(time.time())}}) print(f"verify: инъекция {mid} (/лестница), ждём ответ сек-тика демона...") time.sleep(8) db = sqlite3.connect(es.DB_PATH); db.row_factory = sqlite3.Row rows = db.execute("SELECT event_type, data FROM events WHERE aggregate_id=? ORDER BY id", (mid,)).fetchall() types = [r["event_type"] for r in rows] ok = "ReplySent" in types and "CommandHandled" in types for r in rows: print(f"verify: {r['event_type']} → {r['data'][:120]}") print(f"verify: {'OK — сек-тик нового демона ответил на команду без LLM' if ok else 'FAIL — ответа нет, смотреть журнал демона'}") sys.exit(0 if ok else 2) EOF rc=$? if [ "$rc" -eq 0 ]; then echo "[$(date -u '+%F %T')] safe-restart: верификация ПРОЙДЕНА (сек-тик отвечает на команды)" >> "$LOG" elif [ "$rc" -eq 2 ]; then echo "[$(date -u '+%F %T')] safe-restart: верификация ПРОВАЛЕНА — смотри журнал демона!" >> "$LOG" journalctl -u "$SERVICE" --since "2 minutes ago" --no-pager | tail -20 >> "$LOG" fi exit "$rc"