Files
irrlichtandClaude Opus 5 62e1923a72 Quadlet statt eigener Service-Unit, zwei Umgebungsdateien
Prod fuehrt Phase 1 als Quadlet unter
~/.config/containers/systemd/wurzelwerk-ingest.container; Phase 2 legt
sich daneben. Damit faellt die eigene .service-Unit weg - zwei Wege,
dasselbe zu tun, sind einer zuviel.

Drei Dinge, die dabei nicht offensichtlich sind:

  Type=oneshot          Quadlet setzt sonst Type=notify, und die Unit
                        gaelte als gestartet, sobald der Container
                        laeuft, nicht wenn er durch ist.
  kein RemainAfterExit  Das Handbuch empfiehlt es fuer oneshot, aber bei
                        Timer-Aktivierung bliebe der Auftrag im Zustand
                        "started" und der Timer loeste ihn nie wieder
                        aus.
  --env-file            EnvironmentFile= im Abschnitt [Container] ist
                        nicht systemds EnvironmentFile=. Quadlet reicht
                        die Datei an podman weiter, und podman entfernt
                        keine Anfuehrungszeichen um Werte.

Network=bebop-net steht jetzt direkt in der Unit; P2_NETZ entfaellt.
Type=oneshot schaltet ausserdem die Startzeitgrenze ab, womit der Grund
fuer TimeoutStartSec=1800 wegfaellt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NupWEBPzsyohVPG47HsTz8
2026-09-08 16:37:43 +02:00

4.7 KiB

Phase 2 auf Prod in Betrieb nehmen

Reihenfolge ist verbindlich. Jeder Schritt hat eine Prüfung; schlägt sie fehl, hilft der nächste Schritt nicht.

Alles läuft als Benutzer podman, dessen Home /home/podman ist — %h in den Units löst dorthin auf. Die Datenbankverbindung steht bereits in ~/.config/wurzelwerk/database.env.

0. Migrationen — erledigt

psql "$DATABASE_URL" -c 'select name, angewandt_am from schema_migrationen order by name'

Sechs Zeilen, p2-000 bis p2-005. Fehlt eine, siehe README, Abschnitt „Migrationen" — p2-002 und p2-003 brauchen einen Superuser.

1. Abbild bauen

cd /pfad/zum/repo && git pull
podman build -t wurzelwerk-p2 .

2,12 GB, davon 568 MB spaCy-Modell und 196 MB torch. Prüfung:

podman run --rm --entrypoint ls localhost/wurzelwerk-p2 /app/lexikon/

Zwölf Dateien. Fehlen landtage.json, regierung.json oder sondergesandte.json, ist das Abbild älter als der Stand vom 08.09.2026.

2. Lexikon in die Tabellen laden

akteure.py liest aus akteure und amtsinhaber, nicht aus den JSON-Dateien. Ohne diesen Schritt läuft Modul 4 durch und löst nichts auf, ohne zu scheitern — der teuerste stille Fehler der ganzen Einrichtung.

podman run --rm --network=bebop-net \
    --env-file ~/.config/wurzelwerk/database.env \
    localhost/wurzelwerk-p2 lexikon.py --laden

Erwartet: person 5631, organisation 2319, partei 682, staat 195, amt 29, amtsinhaber 418. Die Ueberschneidung:-Zeilen sind die Betriebssicht auf Doppelspitzen und Bremer Bürgermeister, kein Fehler.

Das Netz ist nicht schmückend: DATABASE_URL zeigt auf den Rechnernamen postgres, und den löst nur Podmans DNS innerhalb von bebop-net auf. Ohne --network=bebop-net scheitert die Namensauflösung, nicht die Anmeldung — die Fehlermeldung nennt dann den Namen, nicht das Passwort.

3. Modell-Volume anlegen und einmal füllen

podman volume create wurzelwerk-modelle

Der erste Lauf braucht Netz und lädt 2,2 GB. Er muss von Hand laufen, nicht aus dem Timer: der Erstbestand über den gesamten Korpus dauert etwa 17 Minuten je 5000 Items, und TimeoutStartSec=1800 ist dafür knapp.

podman run --rm --network=bebop-net --memory=8g --shm-size=1g \
    --volume wurzelwerk-modelle:/cache:U \
    --env-file ~/.config/wurzelwerk/database.env \
    --env OMP_NUM_THREADS=4 --env HF_HUB_OFFLINE=0 \
    localhost/wurzelwerk-p2 phase2.py --backfill

Prüfung — vier Ebenen, je eine coder_version, alle gleich hoch:

psql "$DATABASE_URL" -c \
  'select ebene, coder_version, count(*) from codings group by 1,2 order by 1'
psql "$DATABASE_URL" -c 'select count(*) from item_embeddings'

Stehen für eine Ebene zwei Versionen nebeneinander, ist der Lauf unterbrochen worden und wurde mit geändertem Code fortgesetzt. Dann die ältere löschen und den Backfill wiederholen.

4. Umgebung für Phase 2

cp deploy/p2.env.beispiel ~/.config/wurzelwerk/p2.env
chmod 600 ~/.config/wurzelwerk/p2.env
$EDITOR ~/.config/wurzelwerk/p2.env      # HF_HUB_OFFLINE=1 setzen

HF_HUB_OFFLINE=1 erst jetzt, nach dem erfolgreichen Erstlauf. Danach geht das Modell nie wieder ins Netz und lädt in zwei Sekunden aus dem Volume.

5. Units einhängen

cp deploy/wurzelwerk-p2.container ~/.config/containers/systemd/
cp deploy/wurzelwerk-p2.timer     ~/.config/systemd/user/
systemctl --user daemon-reload

# Was Quadlet daraus erzeugt hat, vor dem ersten Start ansehen:
/usr/lib/systemd/user-generators/podman-user-generator --dryrun
systemctl --user start wurzelwerk-p2.service    # ein Lauf von Hand
journalctl --user -u wurzelwerk-p2 -n 40

Erst wenn dieser eine Lauf sauber durchgeht — er sollte kein offener Slot melden, weil der Backfill alles kodiert hat —, den Timer scharfstellen:

systemctl --user enable --now wurzelwerk-p2.timer
systemctl --user list-timers wurzelwerk-p2.timer

Feuert *:03,18,33,48, versetzt zum 15-Minuten-Raster von Phase 1.

Damit die Units auch ohne angemeldete Sitzung laufen:

loginctl enable-linger podman

6. Nach der ersten Stunde

psql "$DATABASE_URL" -c \
  "select modul, coder_version, items_gesehen, items_kodiert, items_fehler,
          begonnen_am
   from p2_laeufe order by begonnen_am desc limit 12"

Vier Zeilen je Slot, items_fehler = 0. Ein Modul, das dauerhaft fehlt, ist abgestürzt, ohne die anderen mitzureissen — genau dafür ist die Schichtung da, aber es fällt dann eben auch nur hier auf.

Was diese Phase nicht tut

raw_items wird nie geschrieben. Der gesamte Phase-2-Bestand — codings, item_embeddings, p2_laeufe — ist verwerfbar und aus raw_items neu herstellbar. Ein Rückbau ist deshalb truncate, kein Wiederherstellen aus einem Backup.