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:
22
backend/fsutil.py
Normal file
22
backend/fsutil.py
Normal 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))
|
||||
@@ -18,6 +18,7 @@ from config import (
|
||||
MAX_CONCURRENT_GENERATIONS,
|
||||
)
|
||||
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
|
||||
|
||||
_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
|
||||
entries = _eindeutige_titel(entries)
|
||||
final_path.write_text(
|
||||
"\n".join(f"{i}. {t}" for i, t in entries.items()) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
atomic_write_text(final_path, "\n".join(f"{i}. {t}" for i, t in entries.items()) + "\n")
|
||||
except Exception as e:
|
||||
log.exception("[%s] Bausteine-Generierung fehlgeschlagen", topic)
|
||||
_bausteine_errors[topic] = str(e)[:2000]
|
||||
@@ -937,10 +935,7 @@ async def _generate_onepager(
|
||||
return None
|
||||
if res is None:
|
||||
_log(topic, "OnePager-Fix ungültig — ursprüngliche Karten bleiben")
|
||||
karten_path.write_text(
|
||||
json.dumps({"karten": {k["key"]: {"titel": k["titel"], "md": k["md"]} for k in karten}}, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
atomic_write_json(karten_path, {"karten": {k["key"]: {"titel": k["titel"], "md": k["md"]} for k in karten}})
|
||||
else:
|
||||
karten = res[0]
|
||||
|
||||
@@ -1041,7 +1036,7 @@ async def _generate_sections(
|
||||
return None
|
||||
if res is None:
|
||||
_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:
|
||||
auswahl = res[0]
|
||||
|
||||
@@ -1122,7 +1117,7 @@ async def _generate_sections(
|
||||
return None
|
||||
if res is None:
|
||||
_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:
|
||||
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:
|
||||
return
|
||||
|
||||
content_path.write_text(
|
||||
json.dumps({"topic": topic, "format": format_name, "chapters": chapters}, ensure_ascii=False, indent=1),
|
||||
encoding="utf-8",
|
||||
)
|
||||
atomic_write_json(content_path, {"topic": topic, "format": format_name, "chapters": chapters}, indent=1)
|
||||
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
await update_guide(guide_id, status="done", progress=None, step=None, updated_at=now)
|
||||
|
||||
Reference in New Issue
Block a user