some fixes again

This commit is contained in:
Ayzen
2026-06-05 17:50:59 +03:00
parent bbea744459
commit 3c30a12d4a
24 changed files with 209 additions and 157 deletions
+17 -11
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
from contextlib import suppress
import json
import os
from pathlib import Path
@@ -42,13 +43,12 @@ class ConfigWriter:
asset.bundle_path = str(bundle_path)
def write(self, config: RunConfigModel, output_path: Path) -> Path:
"""Atomically write run configuration JSON file.
"""Atomically write the run configuration JSON file.
Mirrors ProcessingLiveConfigWriter: dump to a sibling .tmp, flush+fsync to
durably commit the bytes, then os.replace() onto the destination. The replace
is atomic, so a C++ consumer can never observe a half-written config (which
would abort it with an opaque JSON parse error), even across a crash or power
loss mid-write on the SD-card-backed Pi.
Dump to a sibling ``.tmp``, flush + ``fsync`` to durably commit the bytes on
the SD-card-backed Pi, then ``os.replace()`` atomically onto the destination.
A C++ consumer can therefore never observe a half-written config (which would
abort it with an opaque JSON parse error), even across a crash or power loss.
"""
output_path.parent.mkdir(parents=True, exist_ok=True)
# allow_nan=False: a stray NaN/Infinity must fail loudly here in Python
@@ -56,11 +56,17 @@ class ConfigWriter:
# consumer at startup with an opaque JSON parse error.
serialized = json.dumps(config.to_dict(), indent=2, allow_nan=False)
tmp_path = output_path.with_suffix(output_path.suffix + ".tmp")
with open(tmp_path, "w", encoding="utf-8") as handle:
handle.write(serialized)
handle.flush()
os.fsync(handle.fileno())
os.replace(tmp_path, output_path)
try:
with open(tmp_path, "w", encoding="utf-8") as handle:
handle.write(serialized)
handle.flush()
os.fsync(handle.fileno())
os.replace(tmp_path, output_path)
except Exception:
# Never leave a half-written .tmp behind on a write/fsync failure.
with suppress(OSError):
tmp_path.unlink()
raise
return output_path
+2 -15
View File
@@ -72,7 +72,6 @@ class ShmRingWriter:
self._mmap.close()
self._file.close()
self._unlink_if_present()
created = not self._path.exists()
fd = os.open(self._path, os.O_RDWR | os.O_CREAT, 0o660)
self._file = os.fdopen(fd, "r+b", buffering=0)
self._file.truncate(self._mapped_size)
@@ -135,23 +134,11 @@ class ShmRingWriter:
self._write_u64(32, 0)
self._write_u64(40, 0)
def _validate_header(self) -> None:
magic = self._mmap[:8]
version = self._read_u32(8)
capacity = self._read_u32(12)
slot_size_bytes = self._read_u32(16)
if magic != _MAGIC:
raise RuntimeError(f"Shared memory ring magic mismatch for {self._ring_name}")
if version != _VERSION:
raise RuntimeError(f"Shared memory ring version mismatch for {self._ring_name}")
if capacity != self._capacity or slot_size_bytes != self._slot_size_bytes:
raise RuntimeError(f"Shared memory ring geometry mismatch for {self._ring_name}")
def _header_matches(self) -> bool:
"""Return whether the existing segment's header matches this ring's geometry.
Non-throwing counterpart of `_validate_header` used by the owner to decide
whether a same-sized pre-existing segment can be reused or must be recreated.
Used by the owner to decide whether a same-sized pre-existing segment can be
reused as-is or must be unlinked and recreated.
"""
return (
self._mmap[:8] == _MAGIC