Backend: atomare Datei-Writes (fsutil), Prozess-Tracking-Hygiene

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
team3
2026-06-12 07:53:46 +02:00
parent 63280d88d6
commit 38db80296c
2 changed files with 28 additions and 14 deletions

22
backend/fsutil.py Normal file
View File

@@ -0,0 +1,22 @@
"""Atomare Datei-Writes: erst .tmp im selben Verzeichnis, dann os.replace.
Ein Crash hinterlässt höchstens eine .tmp-Datei — nie eine halb geschriebene
Zieldatei. Die .tmp wird beim nächsten erfolgreichen Write überschrieben.
"""
import json
import os
from pathlib import Path
def atomic_write_text(path: Path, text: str) -> None:
tmp = path.with_name(path.name + ".tmp")
with open(tmp, "w", encoding="utf-8") as f:
f.write(text)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, path)
def atomic_write_json(path: Path, obj, **dumps_kwargs) -> None:
atomic_write_text(path, json.dumps(obj, ensure_ascii=False, **dumps_kwargs))

View File

@@ -18,6 +18,7 @@ from config import (
MAX_CONCURRENT_GENERATIONS, MAX_CONCURRENT_GENERATIONS,
) )
from database import update_guide from database import update_guide
from fsutil import atomic_write_json, atomic_write_text
from paths import arbeit_dir, bausteine_path, guide_content_path, project_dir from paths import arbeit_dir, bausteine_path, guide_content_path, project_dir
_semaphore = asyncio.Semaphore(MAX_CONCURRENT_GENERATIONS) _semaphore = asyncio.Semaphore(MAX_CONCURRENT_GENERATIONS)
@@ -580,10 +581,7 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
# Titel eindeutig machen und unsortiertes Inventar schreiben # Titel eindeutig machen und unsortiertes Inventar schreiben
entries = _eindeutige_titel(entries) entries = _eindeutige_titel(entries)
final_path.write_text( atomic_write_text(final_path, "\n".join(f"{i}. {t}" for i, t in entries.items()) + "\n")
"\n".join(f"{i}. {t}" for i, t in entries.items()) + "\n",
encoding="utf-8",
)
except Exception as e: except Exception as e:
log.exception("[%s] Bausteine-Generierung fehlgeschlagen", topic) log.exception("[%s] Bausteine-Generierung fehlgeschlagen", topic)
_bausteine_errors[topic] = str(e)[:2000] _bausteine_errors[topic] = str(e)[:2000]
@@ -937,10 +935,7 @@ async def _generate_onepager(
return None return None
if res is None: if res is None:
_log(topic, "OnePager-Fix ungültig — ursprüngliche Karten bleiben") _log(topic, "OnePager-Fix ungültig — ursprüngliche Karten bleiben")
karten_path.write_text( atomic_write_json(karten_path, {"karten": {k["key"]: {"titel": k["titel"], "md": k["md"]} for k in karten}})
json.dumps({"karten": {k["key"]: {"titel": k["titel"], "md": k["md"]} for k in karten}}, ensure_ascii=False),
encoding="utf-8",
)
else: else:
karten = res[0] karten = res[0]
@@ -1041,7 +1036,7 @@ async def _generate_sections(
return None return None
if res is None: if res is None:
_log(topic, "Auswahl-Fix ungültig — ursprüngliche Auswahl bleibt") _log(topic, "Auswahl-Fix ungültig — ursprüngliche Auswahl bleibt")
files["auswahl"].write_text(auswahl_json(), encoding="utf-8") atomic_write_text(files["auswahl"], auswahl_json())
else: else:
auswahl = res[0] auswahl = res[0]
@@ -1122,7 +1117,7 @@ async def _generate_sections(
return None return None
if res is None: if res is None:
_log(topic, "Gliederungs-Fix ungültig — ursprüngliche Gliederung bleibt") _log(topic, "Gliederungs-Fix ungültig — ursprüngliche Gliederung bleibt")
files["gliederung"].write_text(gliederung_json(), encoding="utf-8") atomic_write_text(files["gliederung"], gliederung_json())
else: else:
plan = res[0] plan = res[0]
@@ -1310,10 +1305,7 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
if chapters is None or guide_id in _cancelled: if chapters is None or guide_id in _cancelled:
return return
content_path.write_text( atomic_write_json(content_path, {"topic": topic, "format": format_name, "chapters": chapters}, indent=1)
json.dumps({"topic": topic, "format": format_name, "chapters": chapters}, ensure_ascii=False, indent=1),
encoding="utf-8",
)
now = datetime.now(timezone.utc).isoformat() now = datetime.now(timezone.utc).isoformat()
await update_guide(guide_id, status="done", progress=None, step=None, updated_at=now) await update_guide(guide_id, status="done", progress=None, step=None, updated_at=now)