fix(state): serialize replaced/generation probe with close() in _execute_write
A lock-free _raise_if_db_replaced() probe at the top of the _execute_write retry loop raced a concurrent close(). close() runs under the same _lock and ends the WAL generation: it checkpoints, closes the connection (SQLite unlinks the -wal/-shm sidecars), nulls _conn and clears _db_sidecar_identity. The probe could observe the mid-teardown state — sidecars already unlinked while _db_sidecar_identity was not yet cleared — and misclassify this process's OWN clean close as an externally deleted WAL generation, raising a sticky DeletedWalGenerationError that permanently refused every later write on that handle (#105567). Move the live probe inside the lock, ahead of the close-race reopen decision, so it only ever observes the stable post-close state (identity cleared -> the existing adopt/reopen path). The corrupt flag check stays on the lock-free fast path; external file/generation replacement detection is unchanged, just serialized with teardown. Synthetic repro (100 rounds x 40 writes, direct SessionDB handles): before ~9 failing rounds / ~360 DeletedWalGenerationError; after 0 failures, 4000/4000 writes persisted across repeated runs. tests/state (181) plus the generation/replaced/corrupt guard suites (55) pass. Fixes #105567
This commit is contained in:
@@ -810,10 +810,20 @@ class SessionDB(
|
||||
ioerr_begin_retried = False
|
||||
while True:
|
||||
self._raise_if_db_corrupt()
|
||||
self._raise_if_db_replaced()
|
||||
# NOTE: the replaced/generation live probe runs INSIDE the lock below,
|
||||
# not here. close() mutates _conn and _db_sidecar_identity under that
|
||||
# same lock, ending the WAL generation (SQLite unlinks the -wal/-shm
|
||||
# sidecars on a clean close). A lock-free probe that races close() can
|
||||
# observe the mid-teardown state — sidecars already unlinked while
|
||||
# _db_sidecar_identity is not yet cleared — and misclassify this
|
||||
# process's OWN clean close as an externally deleted generation,
|
||||
# raising a sticky DeletedWalGenerationError that permanently refuses
|
||||
# later writes (#105567). Inside the lock the probe only ever sees the
|
||||
# stable post-close state (identity cleared → adopt / reopen path).
|
||||
fn_started = False
|
||||
try:
|
||||
with self._lock:
|
||||
self._raise_if_db_replaced()
|
||||
if self._conn is None: # close() raced this writer
|
||||
self._reopen_after_close_locked(context="write")
|
||||
self._conn.execute("BEGIN IMMEDIATE")
|
||||
|
||||
Reference in New Issue
Block a user