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,
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user