Files
ComfyUI-Lora-Manager/py/utils/cache_db.py
T
Will Miao fd40cd952b fix(cache): close the connection the schema init opened
`with sqlite3.Connection` ends the transaction but keeps the file handle open,
so every cache class leaked one handle per instance until the garbage collector
happened to run. On Windows that makes the database file undeletable while the
process lives (WinError 32), which broke 30 cache tests in teardown and would
also block moving or clearing a cache at runtime.

All eight `with <connection>` sites now go through `closing_connection()`, and a
regression test asserts the schema init leaves no usable connection behind.
2026-10-12 11:35:14 +08:00

99 lines
3.4 KiB
Python

"""Shared SQLite connection setup for LoRA Manager cache databases.
Cache databases live under the settings directory (``cache/model/<library>.sqlite``,
``cache/recipe/<library>.sqlite``, ``cache/fts/*.sqlite``). With portable mode or a
pinned ``LORA_MANAGER_SETTINGS_DIR`` off, that directory is shared by every ComfyUI
instance on the machine, so two processes can open the same cache file at once.
SQLite serializes writers, but the default ``timeout`` is 5 seconds: a second
instance that writes while the first is mid-transaction fails with "database is
locked". These settings make concurrent access wait instead of failing, and keep
the write path in WAL so readers are never blocked by a writer.
"""
from __future__ import annotations
import sqlite3
from contextlib import contextmanager
from typing import Any, Iterator
# How long a connection waits for a competing writer before raising.
CONCURRENT_TIMEOUT_SECONDS = 30.0
# PRAGMAs applied to every cache connection.
#
# ``busy_timeout`` mirrors the connection timeout so a busy database is retried
# inside SQLite rather than surfacing as an immediate error. ``synchronous=NORMAL``
# is the documented companion of WAL: still crash-safe, far fewer fsyncs.
_TUNING_PRAGMAS = (
"PRAGMA busy_timeout = 30000",
"PRAGMA synchronous = NORMAL",
)
def connect_cache_db(
path: str,
*,
readonly: bool = False,
uri: bool = False,
detect_types: int = 0,
row_factory: Any = None,
) -> sqlite3.Connection:
"""Open a cache database with multi-instance-friendly settings.
Args:
path: Database path, or a ``file:`` URI when *uri* is True.
readonly: Open through a read-only URI. Callers still pass the
plain path; the ``mode=ro`` suffix is added here. The
write-oriented tuning pragmas are skipped in that case so a
read-only connection never attempts to change the file.
uri: Treat *path* as a SQLite URI.
detect_types: Forwarded to :func:`sqlite3.connect`.
row_factory: Optional ``row_factory`` for the connection.
Returns:
A configured :class:`sqlite3.Connection`.
"""
if readonly:
if not uri and not path.startswith("file:"):
path = f"file:{path}?mode=ro"
uri = True
conn = sqlite3.connect(
path,
check_same_thread=False,
uri=uri,
detect_types=detect_types,
timeout=CONCURRENT_TIMEOUT_SECONDS,
)
if row_factory is not None:
conn.row_factory = row_factory
try:
for pragma in _TUNING_PRAGMAS:
# A read-only connection may reject write PRAGMAs; they are not
# needed there anyway.
conn.execute(pragma)
except sqlite3.Error:
# Tuning is best-effort: a connection that cannot set pragmas still
# works, just without the concurrency headroom.
pass
return conn
@contextmanager
def closing_connection(conn: sqlite3.Connection) -> Iterator[sqlite3.Connection]:
"""Yield *conn* and close it on exit.
``with sqlite3.Connection`` only ends the transaction: it leaves the file
handle open, and a live handle keeps the cache database undeletable on
Windows (``WinError 32``) until the garbage collector happens to run. Every
``with`` over a cache connection goes through here instead.
"""
try:
yield conn
finally:
conn.close()