fix: serialize decision ledger writes

This commit is contained in:
User
2026-07-14 15:30:06 +02:00
parent 333874a755
commit 9d4e057426
5 changed files with 77 additions and 6 deletions
@@ -1,4 +1,7 @@
import fcntl
import json
import multiprocessing
import time
import pytest
from typer.testing import CliRunner
@@ -8,6 +11,16 @@ from tht.decisions import append_decision, append_decisions, list_decisions
from tht.phase import current_phase
def _concurrent_append_worker(session, subject, start, ready, done):
ready.put(subject)
start.wait()
try:
append_decision(session, type="concept_clarified", subject=subject)
done.put((subject, None))
except Exception as error: # pragma: no cover - surfaced through the parent assertion
done.put((subject, repr(error)))
def _walk_to_phase(session, target):
while current_phase(session) < target:
append_decision(session, type="phase_approved", subject=f"phase:{current_phase(session)}")
@@ -91,3 +104,41 @@ def test_atomic_replace_failure_keeps_the_original_ledger(tmp_path, monkeypatch)
)
assert (session / "review_decisions.jsonl").read_text() == original
def test_concurrent_appends_wait_for_the_session_lock_and_keep_both_records(tmp_path):
session = tmp_path / "s1"
session.mkdir()
context = multiprocessing.get_context("fork")
start = context.Event()
ready = context.Queue()
done = context.Queue()
processes = [
context.Process(
target=_concurrent_append_worker,
args=(session, subject, start, ready, done),
)
for subject in ("first", "second")
]
for process in processes:
process.start()
assert {ready.get(timeout=5), ready.get(timeout=5)} == {"first", "second"}
lock_path = session / ".review_decisions.lock"
with lock_path.open("a+") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
start.set()
time.sleep(0.2)
writers_waited = not (session / "review_decisions.jsonl").exists()
fcntl.flock(lock, fcntl.LOCK_UN)
results = [done.get(timeout=5), done.get(timeout=5)]
for process in processes:
process.join(timeout=5)
assert process.exitcode == 0
assert writers_waited
assert all(error is None for _subject, error in results)
records = list_decisions(session)
assert {record.subject for record in records} == {"first", "second"}
assert [record.seq for record in records] == [1, 2]