Merge pull request #2768 from langbot-app/fix/sqlite-startup-wal-checkpoint

fix(sqlite): reclaim startup WAL and ship patched SQLite
This commit is contained in:
彼方
2026-10-24 23:45:56 +08:00
committed by GitHub
7 changed files with 271 additions and 8 deletions
@@ -6,6 +6,19 @@ import sqlalchemy.ext.asyncio as sqlalchemy_asyncio
from .. import database
WAL_RETAIN_BYTES = 64 * 1024 * 1024
def _configure_connection(dbapi_connection, _connection_record):
# This limit applies when a WAL resets, not while a transaction or reader
# still needs its frames. Keep SQLite's automatic checkpoint policy.
cursor = dbapi_connection.cursor()
try:
cursor.execute(f'PRAGMA journal_size_limit={WAL_RETAIN_BYTES}')
finally:
cursor.close()
@database.manager_class('sqlite')
class SQLiteDatabaseManager(database.BaseDatabaseManager):
"""SQLite database manager"""
@@ -14,6 +27,7 @@ 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)
sqlalchemy.event.listen(self.engine.sync_engine, 'connect', _configure_connection)
# 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:
+6
View File
@@ -198,6 +198,12 @@ class PersistenceManager:
return
await self._initialize_managed_schema()
from .sqlite_startup import checkpoint_after_migration
if engine.dialect.name == 'sqlite':
self.ap.logger.info('Completing SQLite startup WAL checkpoint before starting producers...')
await checkpoint_after_migration(engine)
self.ap.logger.info('SQLite startup WAL checkpoint completed.')
await self._enable_sqlite_foreign_keys_after_migration()
from .resource_urls import install
@@ -0,0 +1,27 @@
"""Finish SQLite WAL maintenance before application producers start."""
STARTUP_BUSY_TIMEOUT_MS = 60_000
async def checkpoint_after_migration(engine):
"""Require a complete WAL checkpoint, including on a retry at schema head."""
if engine.dialect.name != 'sqlite':
return
async with engine.connect() as connection:
connection = await connection.execution_options(isolation_level='AUTOCOMMIT')
if (await connection.exec_driver_sql('PRAGMA journal_mode')).scalar_one().lower() != 'wal':
return
timeout = (await connection.exec_driver_sql('PRAGMA busy_timeout')).scalar_one()
try:
await connection.exec_driver_sql(f'PRAGMA busy_timeout={STARTUP_BUSY_TIMEOUT_MS}')
busy, frames, checkpointed = (
await connection.exec_driver_sql('PRAGMA main.wal_checkpoint(TRUNCATE)')
).one()
if (busy, frames, checkpointed) != (0, 0, 0):
raise RuntimeError(
'SQLite startup WAL checkpoint did not complete: '
f'busy={busy}, frames={frames}, checkpointed={checkpointed}. '
'Stop other database users and restart LangBot to retry.'
)
finally:
await connection.exec_driver_sql(f'PRAGMA busy_timeout={timeout}')