mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Make doc_modeler resilient to SQLite "database is locked"
A refresh on a large library aborted the whole doc_modeler run on the
first lock error, so the ~20k stranded docs never got modeled.
doc_modeler held one write transaction across a whole batch of 10 docs
while doing slow per-doc extraction (a PDF/epub can be tens of MB). When
another connection committed mid-batch, the deferred transaction failed
to upgrade to a write with SQLITE_BUSY_SNAPSHOT ("database is locked",
which busy_timeout does not absorb -- the same trap that motivated the
short-transaction rewrite of apply_indexers). The per-item error handler
then did `logger.error(f'... {file_group}')`, which lazy-loaded an
attribute on the now-rolled-back session and raised a *second* exception,
escaping the handler and aborting the entire modeler.
Restructure:
- Pick each batch of candidate ids in a short read-only transaction, then
model each doc in its own short write transaction. A lock/error on one
doc rolls back only that doc and cannot poison the rest of the run.
- Retry a doc a few times on "database is locked" -- a fresh transaction
gets a fresh snapshot and succeeds.
- Log the plain file_group id on failure; never touch the ORM object on a
possibly rolled-back session.
- Track only *failed* ids for the termination guard (successes are
excluded by the Doc.id IS NULL gate), keeping the notin_ set small and
well under SQLite's bound-variable limit.
Adds test_doc_modeler_survives_database_locked.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
80cc758e6c
commit
9310fe801f
2 changed files with 100 additions and 34 deletions
|
|
@ -1,10 +1,12 @@
|
|||
"""Tests for the doc_modeler."""
|
||||
import asyncio
|
||||
import shutil
|
||||
import sqlite3
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
from modules.docs import doc_modeler
|
||||
from modules.docs.models import Doc, DocSection
|
||||
|
|
@ -173,6 +175,41 @@ async def test_doc_modeler_terminates_on_persistent_failure(async_client, test_s
|
|||
assert test_session.query(Doc).count() == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_doc_modeler_survives_database_locked(async_client, test_session, test_directory, refresh_files):
|
||||
"""A transient SQLite "database is locked" on one doc must not abort the whole modeler run.
|
||||
|
||||
Regression: doc_modeler held one write transaction across a whole batch of slow extractions.
|
||||
When another connection committed mid-batch, the deferred transaction failed to upgrade with
|
||||
SQLITE_BUSY_SNAPSHOT ("database is locked"), the error handler crashed on the rolled-back
|
||||
session, and the entire run aborted -- so no further docs were modeled. Each doc now runs in
|
||||
its own short transaction and a lock is retried.
|
||||
"""
|
||||
ebook_dir = test_directory / 'ebooks'
|
||||
ebook_dir.mkdir(parents=True)
|
||||
for i in range(3):
|
||||
shutil.copy(PROJECT_DIR / 'test/ebook example.epub', ebook_dir / f'book_{i:03d}.epub')
|
||||
|
||||
from modules.docs import _model_doc as real_model_doc
|
||||
|
||||
calls = {'n': 0}
|
||||
|
||||
def flaky_model_doc(file_group, session):
|
||||
# Fail the very first modeling attempt with a lock error, then behave normally. Proves the
|
||||
# run recovers (retries the doc, keeps modeling the rest) instead of aborting.
|
||||
calls['n'] += 1
|
||||
if calls['n'] == 1:
|
||||
raise OperationalError('stmt', {}, sqlite3.OperationalError('database is locked'))
|
||||
return real_model_doc(file_group, session)
|
||||
|
||||
with patch('modules.docs._model_doc', side_effect=flaky_model_doc):
|
||||
await refresh_files()
|
||||
|
||||
# All three ebooks were modeled despite the initial lock error.
|
||||
assert test_session.query(Doc).count() == 3
|
||||
assert calls['n'] >= 4 # 1 failed attempt + 1 retry + 2 more docs.
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_doc_modeler_processes_more_than_batch_limit(async_client, test_session, test_directory):
|
||||
"""Doc modeler processes more than one batch of files (limit=10)."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue