From 48c6a1f536bab421cbaab98a1e880fe767034463 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Tue, 13 Oct 2026 11:40:55 +0000 Subject: [PATCH] fix(sqlite): enable WAL for concurrent plugin callback commits --- Dockerfile.acceptance | 2 ++ .../pkg/persistence/databases/sqlite.py | 6 ++++ .../persistence/test_sqlite_concurrency.py | 34 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 Dockerfile.acceptance create mode 100644 tests/integration/persistence/test_sqlite_concurrency.py diff --git a/Dockerfile.acceptance b/Dockerfile.acceptance new file mode 100644 index 000000000..3fa1250a2 --- /dev/null +++ b/Dockerfile.acceptance @@ -0,0 +1,2 @@ +FROM local/langbot-oss-411:688e7761 +COPY src/langbot/pkg/persistence/databases/sqlite.py /app/src/langbot/pkg/persistence/databases/sqlite.py diff --git a/src/langbot/pkg/persistence/databases/sqlite.py b/src/langbot/pkg/persistence/databases/sqlite.py index ed096040f..8080d20d8 100644 --- a/src/langbot/pkg/persistence/databases/sqlite.py +++ b/src/langbot/pkg/persistence/databases/sqlite.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sqlalchemy import sqlalchemy.ext.asyncio as sqlalchemy_asyncio from .. import database @@ -13,3 +14,8 @@ class SQLiteDatabaseManager(database.BaseDatabaseManager): db_file_path = self.ap.instance_config.data.get('database', {}).get('sqlite', {}).get('path', 'data/langbot.db') engine_url = f'sqlite+aiosqlite:///{db_file_path}' self.engine = sqlalchemy_asyncio.create_async_engine(engine_url) + # Reader transactions must not block independent plugin callback commits. + # Configure the persistent journal before serving requests, not per write. + async with self.engine.connect() as connection: + await connection.execute(sqlalchemy.text('PRAGMA journal_mode=WAL')) + await connection.commit() diff --git a/tests/integration/persistence/test_sqlite_concurrency.py b/tests/integration/persistence/test_sqlite_concurrency.py new file mode 100644 index 000000000..fbf3355e5 --- /dev/null +++ b/tests/integration/persistence/test_sqlite_concurrency.py @@ -0,0 +1,34 @@ +from types import SimpleNamespace + +import pytest +import sqlalchemy as sa + +from langbot.pkg.core import app # noqa: F401 - initialize manager registration before database imports +from langbot.pkg.persistence.databases.sqlite import SQLiteDatabaseManager + +pytestmark = [pytest.mark.integration, pytest.mark.asyncio] + + +async def test_writer_commits_while_independent_reader_is_open(tmp_path): + application = SimpleNamespace( + instance_config=SimpleNamespace(data={'database': {'sqlite': {'path': str(tmp_path / 'concurrency.db')}}}) + ) + manager = SQLiteDatabaseManager(application) + await manager.initialize() + engine = manager.get_engine() + try: + async with engine.begin() as connection: + await connection.execute(sa.text('CREATE TABLE probe (id INTEGER PRIMARY KEY, value TEXT)')) + await connection.execute(sa.text("INSERT INTO probe VALUES (1, 'old')")) + async with engine.connect() as reader: + await reader.execute(sa.text('BEGIN')) + assert (await reader.execute(sa.text('SELECT value FROM probe'))).scalar_one() == 'old' + async with engine.connect() as writer: + await writer.execute(sa.text('PRAGMA busy_timeout=250')) + await writer.execute(sa.text("UPDATE probe SET value='new' WHERE id=1")) + await writer.commit() + await reader.rollback() + async with engine.connect() as connection: + assert (await connection.execute(sa.text('SELECT value FROM probe'))).scalar_one() == 'new' + finally: + await engine.dispose()