Merge PR #7751 (contributor head 68a068c2e5)

This commit is contained in:
nesquena-hermes committed 2026-09-23 06:22:20 +00:00
commit 071710354f
3 files changed
+94 -8

No files matched your search

+3 -8
View File
@@ -451,8 +451,6 @@ class RunJournalWriter:
self.session_id = _validate_id(session_id, "session_id")
self.run_id = _validate_id(run_id, "run_id")
self.session_dir = Path(session_dir) if session_dir is not None else None
self._path = _run_path(self.session_id, self.run_id, session_dir=self.session_dir)
self._lock = _lock_for(self._path)
def append_sse_event(self, event_name: str, payload=None) -> dict | None:
# Live-UI-only telemetry (metering) has no recovery value in the journal:
@@ -464,18 +462,15 @@ class RunJournalWriter:
# the offline-gap coverage and replay-cursor contiguity checks rely on.
if str(event_name or "").strip() in REPLAY_SKIPPED_SSE_EVENTS:
return None
# Draw from the shared module-level seq cache under the per-path lock so
# this writer and any direct append_run_event() call on the same path
# agree on one monotonic, gapless sequence.
with self._lock:
seq = _reserve_next_seq(self._path)
# Allocate the sequence inside the same per-path transaction that writes
# the row. Reserving here, then releasing the lock before append, lets a
# concurrent writer put a higher sequence on disk first.
return append_run_event(
self.session_id,
self.run_id,
event_name,
payload or {},
session_dir=self.session_dir,
seq=seq,
)
@@ -30,6 +30,16 @@ This RFC defines a consistency contract for those layers. It complements the
larger run adapter direction in #1925 by documenting what must remain coherent
while WebUI still has multiple overlapping state stores.
## Run-journal sequence publication
Within one WebUI process, auto-numbered appends to the same journal allocate
sequence numbers and write their rows under the same per-path lock.
`RunJournalWriter` delegates both operations to `append_run_event`; it must not
reserve a sequence and release the lock before the physical append. Otherwise
individually valid rows can reach disk out of order and the session replay
reader must reject them as noncontiguous. This does not change caller-supplied
sequence semantics, cross-process ownership, or failed-write recovery.
## Inactive compression continuation recovery
The Agent profile's SQLite compression lineage owns the canonical continuation,
+81
View File
@@ -0,0 +1,81 @@
"""Sequence allocation and physical append must share one journal lock.
Only scheduling at the real append boundary is controlled. Writers, sequence
allocation, on-disk JSONL and the session replay reader are production code.
"""
from concurrent.futures import ThreadPoolExecutor
import threading
import pytest
from api import run_journal
@pytest.mark.parametrize("competitor", ["same_writer", "other_writer", "free_function"])
def test_writer_sequence_follows_physical_append_order(tmp_path, monkeypatch, competitor):
sid, rid = "writer_order_session", "writer_order_run"
writer = run_journal.RunJournalWriter(sid, rid, session_dir=tmp_path)
other = run_journal.RunJournalWriter(sid, rid, session_dir=tmp_path)
reached = threading.Event()
release = threading.Event()
real_append = run_journal.append_run_event
def pause_first_append(session_id, run_id, event_name, payload=None, **kwargs):
if payload == {"text": "paused"}:
reached.set()
assert release.wait(5), "test did not release the paused append"
return real_append(session_id, run_id, event_name, payload, **kwargs)
monkeypatch.setattr(run_journal, "append_run_event", pause_first_append)
def append_competitor():
if competitor == "free_function":
return run_journal.append_run_event(
sid, rid, "token", {"text": "overtaking"}, session_dir=tmp_path
)
target = writer if competitor == "same_writer" else other
return target.append_sse_event("token", {"text": "overtaking"})
with ThreadPoolExecutor(max_workers=2) as pool:
first = pool.submit(writer.append_sse_event, "token", {"text": "paused"})
try:
assert reached.wait(5), "writer did not reach the append boundary"
second = pool.submit(append_competitor).result(timeout=5)
finally:
release.set()
first_result = first.result(timeout=5)
journal = run_journal.read_run_events(sid, rid, session_dir=tmp_path)
# Deliberately do not sort: the reader requires physical contiguous order.
assert [row["seq"] for row in journal["events"]] == [1, 2]
assert [row["payload"]["text"] for row in journal["events"]] == ["overtaking", "paused"]
assert second["event_id"] == f"{rid}:1"
assert first_result["event_id"] == f"{rid}:2"
replay = run_journal.read_session_run_events(
sid, after_event_id=second["event_id"], session_dir=tmp_path
)
assert replay["status"] == "ok"
assert [row["event_id"] for row in replay["events"]] == [first_result["event_id"]]
def test_writer_rejected_empty_name_does_not_reserve_a_sequence(tmp_path):
writer = run_journal.RunJournalWriter("name_session", "name_run", session_dir=tmp_path)
with pytest.raises(ValueError, match="event_name is required"):
writer.append_sse_event(" ", {"text": "rejected"})
event = writer.append_sse_event("token", {"text": "accepted"})
assert event["seq"] == 1
assert event["event_id"] == "name_run:1"
def test_metering_skip_still_does_not_reserve_or_write(tmp_path):
writer = run_journal.RunJournalWriter("meter_session", "meter_run", session_dir=tmp_path)
assert writer.append_sse_event("metering", {"tps": 10}) is None
assert not (tmp_path / "_run_journal" / "meter_session" / "meter_run.jsonl").exists()
assert writer.append_sse_event("token", {"text": "kept"})["seq"] == 1
def test_unused_writer_does_not_allocate_a_registry_lock(tmp_path):
parent = str(tmp_path / run_journal.RUN_JOURNAL_DIR_NAME / "unused_session")
writer = run_journal.RunJournalWriter("unused_session", "unused_run", session_dir=tmp_path)
assert writer.append_sse_event("metering", {"tps": 10}) is None
assert not any(key[0] == parent for key in run_journal._WRITER_LOCKS)