Archiv: je Version quellen.tar und arbeit.tar statt einzelner Dateien – SMB schafft 25 Dateien/s, am Stück 90 MB/s

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
team3andClaude Fable 5.1 committed 2026-10-14 16:14:49 +02:00
1 parent 7ba08b68e0
commit 54d1d6a8da
3 files changed
+26 -26

No files matched your search

+2 -2
View File
@@ -108,5 +108,5 @@ storage/<Thema>/
``` ```
Alte Ablagen (alles direkt unter `storage/<Thema>/`) werden beim Start nach `v1/` verschoben, fehlende Slugs nachgetragen. Alte Ablagen (alles direkt unter `storage/<Thema>/`) werden beim Start nach `v1/` verschoben, fehlende Slugs nachgetragen.
Bei fertigen Versionen liegen `quellen/` (außer QUELLEN.md) und `arbeit/` im Archiv (`BUCHWERK_ARCHIV/<Thema>/vN/`), wenn eines Bei fertigen Versionen liegen `quellen/` (außer QUELLEN.md) und `arbeit/` als `quellen.tar` und `arbeit.tar` im Archiv
konfiguriert ist; Ereignisse dazu tragen den Agenten `archiv`. (`BUCHWERK_ARCHIV/<Thema>/vN/`), wenn eines konfiguriert ist; Ereignisse dazu tragen den Agenten `archiv`.
+4 -3
View File
@@ -24,9 +24,10 @@ make logs / make stop
`storage/` und `.claude-data/` (Sitzungen der CLI) werden als Volumes eingebunden; die Claude-Code-CLI `storage/` und `.claude-data/` (Sitzungen der CLI) werden als Volumes eingebunden; die Claude-Code-CLI
liegt als Binary im SDK, Node wird im Container nicht gebraucht. liegt als Binary im SDK, Node wird im Container nicht gebraucht.
Archiv: ist `ARCHIV_DIR` in `.env` gesetzt (auf dem Server die Storage Box, Mount mit `uid=1000`, da der Container als Archiv: ist `ARCHIV_DIR` in `.env` gesetzt (auf dem Server ein Ordner der Storage Box, deren Mount `gid=1000` hat, da der
`app` schreibt), verschiebt `backend/archiv.py` nach jedem fertigen Lauf `quellen/` (außer QUELLEN.md) und `arbeit/` Container als `app` schreibt), packt `backend/archiv.py` nach jedem fertigen Lauf `quellen/` (außer QUELLEN.md) und `arbeit/`
der Version dorthin – das sind die Gigabytes an Klonen. Beim Start holt er nach, was fehlt. Ohne Variable bleibt alles in `storage/`. der Version als `quellen.tar` und `arbeit.tar` dorthin und löscht sie – das sind die Gigabytes an Klonen. Beim Start holt er
nach, was fehlt. Ohne Variable bleibt alles in `storage/`.
Ablauf (backend/pipeline.py): Stufen seriell, innerhalb einer Stufe bis zu `PARALLEL` Agenten gleichzeitig. Ablauf (backend/pipeline.py): Stufen seriell, innerhalb einer Stufe bis zu `PARALLEL` Agenten gleichzeitig.
Jeder Agent arbeitet im Themenverzeichnis `storage/<Thema>/`; sein Abschlussbericht landet unter `berichte/` Jeder Agent arbeitet im Themenverzeichnis `storage/<Thema>/`; sein Abschlussbericht landet unter `berichte/`
+20 -21
View File
@@ -1,9 +1,12 @@
"""Quellen und Arbeitsdateien fertiger Versionen ins Archiv verschieben (config.ARCHIV, auf dem Server eine Storage Box). """Quellen und Arbeitsdateien fertiger Versionen als tar ins Archiv (config.ARCHIV, auf dem Server die Storage Box).
Nichts liest danach noch daraus: neue Versionen recherchieren neu, fertige Läufe setzen nie fort. QUELLEN.md bleibt im Speicher, Je Version quellen.tar (ohne QUELLEN.md, die bleibt im Speicher und steht in der Buchansicht) und arbeit.tar. Ein tar statt
die Buchansicht zeigt sie. Erst kopieren, dann löschen – ein Abbruch lässt den Speicher vollständig, der nächste Start holt nach.""" einzelner Dateien, weil SMB je Datei mehrere Round-Trips braucht (25 Dateien/s), am Stück aber 90 MB/s schafft.
Nichts liest danach noch daraus: neue Versionen recherchieren neu, fertige Läufe setzen nie fort.
Erst packen, dann löschen – ein Abbruch lässt den Speicher vollständig, der nächste Start holt nach."""
import asyncio import asyncio
import logging import logging
import shutil import shutil
import subprocess
from pathlib import Path from pathlib import Path
from . import config, state from . import config, state
@@ -25,32 +28,28 @@ def _eintraege(d: Path) -> list[Path]:
def auslagern(key: str) -> None: def auslagern(key: str) -> None:
"""Verschiebt quellen/ (außer QUELLEN.md) und arbeit/ nach ARCHIV/<key>/. Blockiert – im Thread aufrufen.""" """Packt quellen/ (außer QUELLEN.md) und arbeit/ nach ARCHIV/<key>/<ordner>.tar und löscht sie. Blockiert – im Thread aufrufen."""
v = state.verzeichnis(key) v = state.verzeichnis(key)
ziel = config.ARCHIV / key
ziel.mkdir(parents=True, exist_ok=True)
for u in ORDNER: for u in ORDNER:
for p in _eintraege(v / u): eintraege = _eintraege(v / u)
_kopieren(p, config.ARCHIV / key / u / p.name) if not eintraege:
continue
tmp = ziel / f"{u}.tar.tmp"
r = subprocess.run(["tar", "-cf", str(tmp), "-C", str(v / u), *[p.name for p in eintraege]], capture_output=True, text=True)
if r.returncode:
raise OSError(f"tar {u}/ von {key}: {r.stderr.strip()[-300:]}")
tmp.replace(ziel / f"{u}.tar")
for p in eintraege:
if p.is_dir() and not p.is_symlink(): if p.is_dir() and not p.is_symlink():
shutil.rmtree(p) shutil.rmtree(p)
else: else:
p.unlink() p.unlink()
def _kopieren(quelle: Path, ziel: Path) -> None:
"""Ohne Rechte, Zeiten und Symlinks: CIFS kennt beides nicht. Links in Klonen entfallen."""
if quelle.is_symlink():
return
if quelle.is_dir():
ziel.mkdir(parents=True, exist_ok=True)
for k in quelle.iterdir():
_kopieren(k, ziel / k.name)
elif quelle.is_file():
ziel.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(quelle, ziel)
async def _auslagern(key: str) -> None: async def _auslagern(key: str) -> None:
state.ereignis(key, "archiv", "start", f"Quellen und Arbeitsdateien nach {config.ARCHIV / key}") state.ereignis(key, "archiv", "start", f"Quellen und Arbeitsdateien als tar nach {config.ARCHIV / key}")
try: try:
await asyncio.to_thread(auslagern, key) await asyncio.to_thread(auslagern, key)
state.ereignis(key, "archiv", "ende", "ausgelagert") state.ereignis(key, "archiv", "ende", "ausgelagert")
@@ -69,7 +68,7 @@ def anstossen(key: str) -> None:
async def nachholen() -> None: async def nachholen() -> None:
"""Beim Start: alle fertigen Versionen mit Restbestand, nacheinander (Bestand und abgebrochene Kopien).""" """Beim Start: alle fertigen Versionen mit Restbestand, nacheinander (Bestand und abgebrochene Läufe)."""
if not config.ARCHIV: if not config.ARCHIV:
return return
for t in state.themen(): for t in state.themen():