95 lines
3.0 KiB
Python
95 lines
3.0 KiB
Python
"""Append-only JSONL primitives for the Org OS state kernel."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from contextlib import nullcontext
|
|
from typing import Any, Callable, ContextManager, Iterable
|
|
|
|
|
|
def read_jsonl(path: str | None, on_error: Callable[[str], None] | None = None) -> list[dict[str, Any]]:
|
|
if not path or not os.path.exists(path):
|
|
return []
|
|
rows: list[dict[str, Any]] = []
|
|
try:
|
|
with open(path, encoding="utf-8") as handle:
|
|
for line in handle:
|
|
try:
|
|
value = json.loads(line)
|
|
except Exception:
|
|
continue
|
|
if isinstance(value, dict):
|
|
rows.append(value)
|
|
except Exception as exc:
|
|
if on_error:
|
|
on_error(f"event 원장 읽기 실패({path}): {exc}")
|
|
return rows
|
|
|
|
|
|
def append_jsonl(
|
|
path: str | None,
|
|
event: dict[str, Any],
|
|
*,
|
|
file_lock: bool = False,
|
|
on_error: Callable[[str], None] | None = None,
|
|
) -> bool:
|
|
if not path:
|
|
return False
|
|
try:
|
|
with open(path, "a", encoding="utf-8") as handle:
|
|
if file_lock:
|
|
try:
|
|
import fcntl
|
|
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
except Exception:
|
|
pass
|
|
handle.write(json.dumps(event, ensure_ascii=False) + "\n")
|
|
handle.flush()
|
|
os.fsync(handle.fileno())
|
|
return True
|
|
except Exception as exc:
|
|
if on_error:
|
|
on_error(f"event append 실패({path}): {exc}")
|
|
return False
|
|
|
|
|
|
def atomic_append(
|
|
entries: Iterable[tuple[str | None, dict[str, Any]]],
|
|
*,
|
|
transaction_lock: ContextManager[Any] | None = None,
|
|
) -> None:
|
|
"""Append a group of events and truncate every participating tail on failure."""
|
|
normalized = list(entries)
|
|
handles: list[tuple[Any, int]] = []
|
|
with (transaction_lock or nullcontext()):
|
|
try:
|
|
for path, _event in normalized:
|
|
if not path:
|
|
raise OSError("event path 해석 실패")
|
|
handle = open(path, "a+", encoding="utf-8")
|
|
try:
|
|
import fcntl
|
|
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
except Exception:
|
|
pass
|
|
handle.seek(0, os.SEEK_END)
|
|
handles.append((handle, handle.tell()))
|
|
try:
|
|
for (handle, _offset), (_path, event) in zip(handles, normalized):
|
|
handle.write(json.dumps(event, ensure_ascii=False) + "\n")
|
|
handle.flush()
|
|
os.fsync(handle.fileno())
|
|
except Exception:
|
|
for handle, offset in handles:
|
|
handle.seek(offset)
|
|
handle.truncate()
|
|
handle.flush()
|
|
raise
|
|
finally:
|
|
for handle, _offset in handles:
|
|
try:
|
|
handle.close()
|
|
except Exception:
|
|
pass
|
|
|