diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 08323be125..19ffecdf96 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -377,6 +377,34 @@ else: DATABASE_ENABLE_SQLITE_WAL = os.environ.get('DATABASE_ENABLE_SQLITE_WAL', 'True').lower() == 'true' +# SQLite PRAGMA tuning — these defaults are optimised for WAL-mode web-server +# workloads. Each can be overridden via its environment variable. +# Set any value to an empty string to skip that PRAGMA entirely. + +# PRAGMA synchronous: NORMAL (1) is safe with WAL and avoids an fsync per +# transaction. Valid values: OFF (0), NORMAL (1), FULL (2), EXTRA (3). +DATABASE_SQLITE_PRAGMA_SYNCHRONOUS = os.environ.get('DATABASE_SQLITE_PRAGMA_SYNCHRONOUS', 'NORMAL') + +# PRAGMA busy_timeout (ms): how long a connection waits for a write lock +# before raising SQLITE_BUSY. +DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT = os.environ.get('DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT', '5000') + +# PRAGMA cache_size: negative value = KiB. -65536 ≈ 64 MB page cache. +DATABASE_SQLITE_PRAGMA_CACHE_SIZE = os.environ.get('DATABASE_SQLITE_PRAGMA_CACHE_SIZE', '-65536') + +# PRAGMA temp_store: MEMORY (2) keeps temp tables and indices in RAM. +# Valid values: DEFAULT (0), FILE (1), MEMORY (2). +DATABASE_SQLITE_PRAGMA_TEMP_STORE = os.environ.get('DATABASE_SQLITE_PRAGMA_TEMP_STORE', 'MEMORY') + +# PRAGMA mmap_size (bytes): memory-mapped I/O size. 268435456 ≈ 256 MB. +# Set to 0 to disable mmap. +DATABASE_SQLITE_PRAGMA_MMAP_SIZE = os.environ.get('DATABASE_SQLITE_PRAGMA_MMAP_SIZE', '268435456') + +# PRAGMA journal_size_limit (bytes): caps the WAL file size after checkpoint. +# Without this the WAL grows unbounded during write bursts and is never +# truncated. 67108864 ≈ 64 MB. Set to -1 for no limit (SQLite default). +DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT = os.environ.get('DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT', '67108864') + DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = os.environ.get('DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL', None) if DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL is not None: try: diff --git a/backend/open_webui/internal/db.py b/backend/open_webui/internal/db.py index 3818543fc7..d75ca11d04 100644 --- a/backend/open_webui/internal/db.py +++ b/backend/open_webui/internal/db.py @@ -15,6 +15,12 @@ from open_webui.env import ( DATABASE_POOL_TIMEOUT, DATABASE_ENABLE_SQLITE_WAL, DATABASE_ENABLE_SESSION_SHARING, + DATABASE_SQLITE_PRAGMA_SYNCHRONOUS, + DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT, + DATABASE_SQLITE_PRAGMA_CACHE_SIZE, + DATABASE_SQLITE_PRAGMA_TEMP_STORE, + DATABASE_SQLITE_PRAGMA_MMAP_SIZE, + DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT, ENABLE_DB_MIGRATIONS, ) from peewee_migrate import Router @@ -155,14 +161,32 @@ if SQLALCHEMY_DATABASE_URL.startswith('sqlite+sqlcipher://'): elif 'sqlite' in SQLALCHEMY_DATABASE_URL: engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={'check_same_thread': False}) - def on_connect(dbapi_connection, connection_record): + def _apply_sqlite_pragmas(dbapi_connection): + """Apply all configured SQLite PRAGMAs to a raw DBAPI connection.""" cursor = dbapi_connection.cursor() if DATABASE_ENABLE_SQLITE_WAL: cursor.execute('PRAGMA journal_mode=WAL') else: cursor.execute('PRAGMA journal_mode=DELETE') + + # Each PRAGMA is skipped when its env var is empty, allowing opt-out. + if DATABASE_SQLITE_PRAGMA_SYNCHRONOUS: + cursor.execute(f'PRAGMA synchronous={DATABASE_SQLITE_PRAGMA_SYNCHRONOUS}') + if DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT: + cursor.execute(f'PRAGMA busy_timeout={DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT}') + if DATABASE_SQLITE_PRAGMA_CACHE_SIZE: + cursor.execute(f'PRAGMA cache_size={DATABASE_SQLITE_PRAGMA_CACHE_SIZE}') + if DATABASE_SQLITE_PRAGMA_TEMP_STORE: + cursor.execute(f'PRAGMA temp_store={DATABASE_SQLITE_PRAGMA_TEMP_STORE}') + if DATABASE_SQLITE_PRAGMA_MMAP_SIZE: + cursor.execute(f'PRAGMA mmap_size={DATABASE_SQLITE_PRAGMA_MMAP_SIZE}') + if DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT: + cursor.execute(f'PRAGMA journal_size_limit={DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT}') cursor.close() + def on_connect(dbapi_connection, connection_record): + _apply_sqlite_pragmas(dbapi_connection) + event.listen(engine, 'connect', on_connect) else: if isinstance(DATABASE_POOL_SIZE, int): @@ -213,13 +237,9 @@ if 'sqlite' in ASYNC_SQLALCHEMY_DATABASE_URL: connect_args={'check_same_thread': False}, ) - if DATABASE_ENABLE_SQLITE_WAL: - - @event.listens_for(async_engine.sync_engine, 'connect') - def _set_sqlite_wal(dbapi_connection, connection_record): - cursor = dbapi_connection.cursor() - cursor.execute('PRAGMA journal_mode=WAL') - cursor.close() + @event.listens_for(async_engine.sync_engine, 'connect') + def _set_sqlite_pragmas(dbapi_connection, connection_record): + _apply_sqlite_pragmas(dbapi_connection) else: if isinstance(DATABASE_POOL_SIZE, int): if DATABASE_POOL_SIZE > 0: