some fixes again
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user