Files
hermes-agent/tests/agent/test_trajectory_file_locking.py
shafdev bd70d7387a fix(agent): lock save_trajectory() appends so concurrent processes cannot corrupt the JSONL
Gateway sessions and batch workers append to the same default
trajectory_samples.jsonl / failed_trajectories.jsonl with a plain open("a") +
write(); concurrent writers interleaved mid-object and the file stopped
parsing (#12684). The append now holds an exclusive lock for write+flush:
flock on POSIX, a 1-byte msvcrt.locking range on Windows.

Tests: a foreign process holding the lock must block the append (red on
base); six processes appending oversized entries all land parseable.

Fixes #12684. Salvaged from #12685 by @shafdev; Windows arm and test trim ours.
2026-09-12 08:25:14 -07:00

62 lines
2.6 KiB
Python

"""``save_trajectory()`` appends must be serialized across processes (#12684)."""
import json
import subprocess
import sys
import textwrap
import time
from pathlib import Path
from agent.trajectory import save_trajectory
_REPO_ROOT = str(Path(__file__).resolve().parents[2])
def test_concurrent_process_appends_stay_parseable(tmp_path):
"""Payloads far larger than one atomic write() from several processes: every line parses."""
target = tmp_path / "trajectory_samples.jsonl"
script = textwrap.dedent(f"""
import sys; sys.path.insert(0, {_REPO_ROOT!r})
from agent.trajectory import save_trajectory
big = "x" * 300_000
for i in range(5):
save_trajectory([{{"from": "human", "value": f"P{{sys.argv[1]}}-{{i}} " + big}}],
model="m", completed=True, filename={str(target)!r})
""")
procs = [subprocess.Popen([sys.executable, "-c", script, str(n)], stdin=subprocess.DEVNULL) for n in range(6)]
for p in procs:
assert p.wait(timeout=120) == 0
lines = target.read_text(encoding="utf-8").splitlines()
assert len(lines) == 30
tags = {json.loads(ln)["conversations"][0]["value"].split(" ", 1)[0] for ln in lines}
assert tags == {f"P{n}-{i}" for n in range(6) for i in range(5)}
def test_append_honours_a_foreign_exclusive_lock(tmp_path):
"""While another process holds the file lock, save_trajectory() blocks instead of writing through."""
target = tmp_path / "failed_trajectories.jsonl"
target.write_text("", encoding="utf-8")
holder = subprocess.Popen(
[sys.executable, "-c", textwrap.dedent(f"""
import os, sys, time
f = open({str(target)!r}, "a")
if os.name == "nt":
import msvcrt; f.write(" "); f.flush(); f.seek(0); msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, 1)
else:
import fcntl; fcntl.flock(f.fileno(), fcntl.LOCK_EX)
print("locked", flush=True)
time.sleep(1.5)
""")],
stdout=subprocess.PIPE, stdin=subprocess.DEVNULL, text=True,
)
try:
assert holder.stdout.readline().strip() == "locked" # type: ignore[union-attr]
started = time.monotonic()
save_trajectory([{"from": "human", "value": "hi"}], model="m", completed=False, filename=str(target))
waited = time.monotonic() - started
finally:
holder.kill()
holder.wait()
assert waited >= 1.0, f"append went through a held lock after {waited:.2f}s"
assert json.loads(target.read_text(encoding="utf-8").strip().splitlines()[-1])["completed"] is False