Stopping the stack kills an in-flight podcast job, but its status stays "running" in the database forever: it never finishes, it blocks prune_podcast_data.py (which deliberately refuses to run while a job is active), and it cannot even be retried — /retry only accepts episodes in state "failed". I walked into this myself: I ran `docker compose down` for the non-root switch without checking for running jobs, and killed a podcast that had already produced 37 clips. update_stack.sh now refuses to start when a podcast job is running, and says why. Verified against a genuinely running job: it aborts before touching the stack. BEDIENUNGSANLEITUNG documents how to recover an existing zombie. The status does not live on the episode but on the linked `command` record (the episode's own job_status is null), so the fix is to set that record to 'failed' via SurrealDB, after which the normal retry works. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
120 lines
4.6 KiB
Bash
Executable file
120 lines
4.6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Aktualisiert Open Notebook auf das neueste veroeffentlichte Image — mit Rueckfallpunkt.
|
|
#
|
|
# Ablauf:
|
|
# 1. Neuen Digest aus der Registry holen (nichts zu tun, wenn schon aktuell).
|
|
# 2. Stack stoppen und Daten sichern (surreal_data + notebook_data + docker-compose.yml).
|
|
# 3. Digest in docker-compose.yml eintragen, Stack starten.
|
|
# 4. Rauchtest (scripts/smoke_test.sh).
|
|
# 5. Rot -> alles zurueckrollen (Daten UND Compose-Datei). Gruen -> fertig.
|
|
#
|
|
# Der Rollback der Daten ist der Grund fuer das Backup: Open Notebook migriert die
|
|
# SurrealDB beim Start, und ein Downgrade der Anwendung kann eine migrierte Datenbank
|
|
# nicht wieder lesen. Ohne Backup waere ein missgluecktes Update eine Einbahnstrasse.
|
|
#
|
|
# ./scripts/update_stack.sh # auf neuestes Image
|
|
# ./scripts/update_stack.sh --dry-run # nur zeigen, was passieren wuerde
|
|
set -euo pipefail
|
|
|
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$REPO"
|
|
|
|
TAG="lfnovo/open_notebook:v1-latest"
|
|
BACKUP_DIR="$REPO/backups"
|
|
KEEP_BACKUPS=3
|
|
DRY_RUN=0
|
|
[ "${1:-}" = "--dry-run" ] && DRY_RUN=1
|
|
|
|
say() { printf '\n\033[1m%s\033[0m\n' "$1"; }
|
|
|
|
# Gezielt aus der open_notebook-Zeile. Ein blindes grep auf den ersten Digest der Datei
|
|
# erwischt surrealdb (steht weiter oben) — das sed weiter unten wuerde dann den falschen
|
|
# Digest ersetzen und die Datenbank austauschen statt der Anwendung.
|
|
pinned="$(grep -oP 'lfnovo/open_notebook\S*@\Ksha256:[0-9a-f]+' docker-compose.yml | head -1)"
|
|
[ -n "$pinned" ] || { echo "FEHLER: kein open_notebook-Digest in docker-compose.yml gepinnt." >&2; exit 1; }
|
|
|
|
say "1/5 Registry abfragen"
|
|
remote="$(docker buildx imagetools inspect "$TAG" --format '{{.Manifest.Digest}}')"
|
|
echo " gepinnt: $pinned"
|
|
echo " neu : $remote"
|
|
if [ "$remote" = "$pinned" ]; then
|
|
echo " Bereits aktuell — nichts zu tun."
|
|
exit 0
|
|
fi
|
|
if [ "$DRY_RUN" = 1 ]; then
|
|
echo " (--dry-run: hier wuerde das Update starten)"
|
|
exit 0
|
|
fi
|
|
|
|
# Ein laufender Podcast ueberlebt das Stoppen nicht: der Job ist weg, sein Status bleibt
|
|
# aber auf "running" stehen. Er wird nie fertig, blockiert prune_podcast_data.py und laesst
|
|
# sich nicht per /retry wiederholen (das will "failed"). Also vorher fragen, nicht nachher
|
|
# aufraeumen.
|
|
running_jobs="$(curl -sf -m 10 http://127.0.0.1:5055/api/podcasts/episodes 2>/dev/null |
|
|
python3 -c "
|
|
import json,sys
|
|
try: eps = json.load(sys.stdin)
|
|
except Exception: sys.exit(0)
|
|
print('; '.join(e['name'] for e in eps if e.get('job_status') in ('running','pending','submitted')))" 2>/dev/null)"
|
|
if [ -n "${running_jobs// /}" ]; then
|
|
echo
|
|
echo "ABBRUCH: Es laeuft gerade ein Podcast-Job: $running_jobs"
|
|
echo " Das Stoppen wuerde ihn abschiessen und als Karteileiche zuruecklassen."
|
|
echo " Bitte warten, bis er fertig ist, und danach erneut starten."
|
|
exit 1
|
|
fi
|
|
|
|
ts="$(date +%Y%m%d-%H%M%S)"
|
|
backup="$BACKUP_DIR/$ts"
|
|
mkdir -p "$backup"
|
|
|
|
say "2/5 Stack stoppen und sichern -> backups/$ts"
|
|
docker compose down
|
|
cp docker-compose.yml "$backup/docker-compose.yml"
|
|
# Direkt auf dem Host, weil die Container als Host-User laufen (user: "1000:1000")
|
|
# und die Daten uns gehoeren. Liefen sie als root, scheiterte das an den Rechten.
|
|
tar -czf "$backup/data.tgz" surreal_data notebook_data
|
|
echo " $(du -sh "$backup/data.tgz" | cut -f1) gesichert"
|
|
|
|
restore() {
|
|
say "ROLLBACK"
|
|
docker compose down || true
|
|
cp "$backup/docker-compose.yml" docker-compose.yml
|
|
rm -rf surreal_data notebook_data
|
|
tar -xzf "$backup/data.tgz"
|
|
docker compose up -d
|
|
echo " Alter Stand ($pinned) ist wiederhergestellt, Daten aus backups/$ts."
|
|
echo " Das neue Image bleibt lokal liegen — Ursache pruefen, dann erneut versuchen."
|
|
}
|
|
|
|
say "3/5 Auf neues Image umstellen"
|
|
sed -i "s|@$pinned|@$remote|" docker-compose.yml
|
|
docker compose up -d
|
|
echo " gestartet, warte auf die API..."
|
|
for _ in $(seq 1 30); do
|
|
curl -sf -m 3 http://127.0.0.1:5055/api/models/providers >/dev/null 2>&1 && break
|
|
sleep 5
|
|
done
|
|
|
|
say "4/5 Rauchtest"
|
|
if ./scripts/smoke_test.sh; then
|
|
say "5/5 Fertig"
|
|
newver="$(docker compose exec -T open_notebook grep -m1 '^version' /app/pyproject.toml |
|
|
grep -oP '(?<=")[^"]+' || echo '?')"
|
|
echo " Update auf $newver erfolgreich, Rauchtest gruen."
|
|
echo " Backup: backups/$ts (bleibt liegen)"
|
|
echo
|
|
echo " Bitte den neuen Digest committen:"
|
|
echo " git add docker-compose.yml && git commit -m 'Update Open Notebook auf $newver'"
|
|
else
|
|
echo
|
|
echo " Rauchtest ROT — das Update wird zurueckgenommen."
|
|
restore
|
|
exit 1
|
|
fi
|
|
|
|
# Alte Backups aufraeumen (Platte ist knapp).
|
|
ls -1dt "$BACKUP_DIR"/*/ 2>/dev/null | tail -n +$((KEEP_BACKUPS + 1)) | while read -r old; do
|
|
echo " raeume altes Backup weg: $(basename "$old")"
|
|
rm -rf "$old"
|
|
done
|