SqliteEventLog._ensure_open at src/arcp/_store/eventlog.py:74 checks if self._db is not None: return self._db and then performs an await aiosqlite.connect(...), an executescript(schema), and a commit() before storing the connection on self._db. There is no lock around the lazy-init: if two coroutines call append/read_since_seq/latest_seq for the first time concurrently (entirely possible because the dispatch table allows session.list_jobs and job.submit to interleave on the same session), both pass the None check, both open a connection, and both run the schema. The second aiosqlite.connect succeeds, both executescript calls run against their own connection so the schema is reapplied (idempotent thanks to IF NOT EXISTS, but wasteful), and the first connection object is silently discarded — closed only when Python collects it. On a slow or contended disk this race extends the open window enough to be reproducible in tests, and the abandoned connection holds a file handle until GC.
Fix prompt: Wrap the lazy initialization in an asyncio.Lock. Add self._open_lock = asyncio.Lock() to SqliteEventLog.__init__ at src/arcp/_store/eventlog.py:70, then in _ensure_open re-check self._db is not None after acquiring the lock to keep the fast path uncontended. As a small additional improvement, set PRAGMA journal_mode=WAL once after executescript so concurrent readers and writers do not block each other. Add a focused test that calls _ensure_open from two asyncio.gather-ed coroutines and asserts that only one connection was opened (by stubbing aiosqlite.connect with a counter).
SqliteEventLog._ensure_openatsrc/arcp/_store/eventlog.py:74checksif self._db is not None: return self._dband then performs anawait aiosqlite.connect(...), anexecutescript(schema), and acommit()before storing the connection onself._db. There is no lock around the lazy-init: if two coroutines callappend/read_since_seq/latest_seqfor the first time concurrently (entirely possible because the dispatch table allowssession.list_jobsandjob.submitto interleave on the same session), both pass theNonecheck, both open a connection, and both run the schema. The secondaiosqlite.connectsucceeds, bothexecutescriptcalls run against their own connection so the schema is reapplied (idempotent thanks toIF NOT EXISTS, but wasteful), and the first connection object is silently discarded — closed only when Python collects it. On a slow or contended disk this race extends the open window enough to be reproducible in tests, and the abandoned connection holds a file handle until GC.Fix prompt: Wrap the lazy initialization in an
asyncio.Lock. Addself._open_lock = asyncio.Lock()toSqliteEventLog.__init__atsrc/arcp/_store/eventlog.py:70, then in_ensure_openre-checkself._db is not Noneafter acquiring the lock to keep the fast path uncontended. As a small additional improvement, setPRAGMA journal_mode=WALonce afterexecutescriptso concurrent readers and writers do not block each other. Add a focused test that calls_ensure_openfrom twoasyncio.gather-ed coroutines and asserts that only one connection was opened (by stubbingaiosqlite.connectwith a counter).