mirror of
https://github.com/Comfy-Org/ComfyUI.git
synced 2026-09-30 01:48:20 -05:00
* Take the SQLite write lock up front for scan and output-registration writes The scanner's seeding and reference sync, and executed-output registration, write through a separate engine whose transactions open with BEGIN IMMEDIATE, and the database runs in WAL mode. On those paths stat, hashing and metadata extraction now happen before the write transaction opens, and reference-sync results are applied only to rows unchanged since they were observed. busy_timeout stays at pysqlite's 5s default. A fast-scan batch now commits as one transaction, so an unexpected error partway through discards the whole batch; the next scan recreates it. Enrichment, verification, uploads and tagging still write through the existing sessions. Migration backups use SQLite's backup API, and a legacy database is checkpointed before it is relocated, since in WAL mode committed pages can live in the -wal file that a plain file copy misses. * Skip relocating a legacy database whose WAL cannot be checkpointed The checkpoint can report busy without raising; moving the file then would leave committed pages behind in the -wal. Also keep the source's file mode on SQLite backups, as the plain copy did. * Replace run_write_txn with a create_write_session factory Write paths open the write engine's session the same way the rest of the code opens create_session(), and commit explicitly. Executed-output registration reads the new record's fields before committing, so expiry does not reload them in a second write transaction. * Document the scanner's pre-transaction observation types * Document that write sessions must not nest * Warn that a nested write session looks like lock contention * Seed a hashed spec with the stat its hash was verified against * Bound the SQLite backup so a locked destination cannot hang startup * Time out a backup only while it is blocked
82 lines
2.8 KiB
Python
82 lines
2.8 KiB
Python
import sqlite3
|
|
|
|
import pytest
|
|
from sqlalchemy import text
|
|
|
|
from app.database import db as db_module
|
|
|
|
|
|
@pytest.fixture
|
|
def file_db(tmp_path, monkeypatch):
|
|
db_path = str(tmp_path / "comfyui.db")
|
|
monkeypatch.setattr(db_module.args, "database_url", f"sqlite:///{db_path}")
|
|
monkeypatch.setattr(db_module, "Session", None)
|
|
monkeypatch.setattr(db_module, "WriteSession", None)
|
|
monkeypatch.setattr(db_module, "_db_lock", None)
|
|
db_module._init_file_db(db_module.args.database_url)
|
|
yield db_path
|
|
db_module.Session.kw["bind"].dispose()
|
|
db_module.WriteSession.kw["bind"].dispose()
|
|
db_module._db_lock.release(force=True)
|
|
|
|
|
|
def _other_writer_can_begin(db_path):
|
|
other = sqlite3.connect(db_path, timeout=0, isolation_level=None)
|
|
try:
|
|
other.execute("BEGIN IMMEDIATE")
|
|
other.execute("ROLLBACK")
|
|
return True
|
|
except sqlite3.OperationalError:
|
|
return False
|
|
finally:
|
|
other.close()
|
|
|
|
|
|
def test_file_db_uses_wal(file_db):
|
|
with db_module.create_session() as session:
|
|
assert session.execute(text("PRAGMA journal_mode")).scalar_one() == "wal"
|
|
|
|
|
|
def test_write_session_takes_the_write_lock_before_its_first_write(file_db):
|
|
with db_module.create_write_session() as session:
|
|
session.execute(text("SELECT 1")).scalar_one()
|
|
assert _other_writer_can_begin(file_db) is False
|
|
|
|
|
|
def test_read_session_does_not_take_the_write_lock(file_db):
|
|
with db_module.create_session() as session:
|
|
session.execute(text("SELECT 1")).scalar_one()
|
|
assert _other_writer_can_begin(file_db) is True
|
|
|
|
|
|
def test_backup_gives_up_when_the_destination_stays_locked(tmp_path, monkeypatch):
|
|
source = tmp_path / "source.db"
|
|
destination = tmp_path / "destination.db"
|
|
with sqlite3.connect(source) as conn:
|
|
conn.execute("CREATE TABLE t (x)")
|
|
with sqlite3.connect(destination) as conn:
|
|
conn.execute("CREATE TABLE u (y)")
|
|
holder = sqlite3.connect(destination, isolation_level=None, timeout=0)
|
|
holder.execute("BEGIN IMMEDIATE")
|
|
monkeypatch.setattr(db_module, "_BACKUP_TIMEOUT_SECONDS", 0.0)
|
|
try:
|
|
with pytest.raises(TimeoutError):
|
|
db_module._backup_database(str(source), str(destination))
|
|
finally:
|
|
holder.execute("ROLLBACK")
|
|
holder.close()
|
|
|
|
|
|
def test_backup_past_its_deadline_still_completes_when_nothing_blocks_it(tmp_path, monkeypatch):
|
|
source = tmp_path / "source.db"
|
|
destination = tmp_path / "destination.db"
|
|
with sqlite3.connect(source) as conn:
|
|
conn.execute("CREATE TABLE t (x)")
|
|
conn.execute("INSERT INTO t VALUES (1)")
|
|
monkeypatch.setattr(db_module, "_BACKUP_TIMEOUT_SECONDS", -1.0)
|
|
|
|
db_module._backup_database(str(source), str(destination))
|
|
|
|
with sqlite3.connect(destination) as conn:
|
|
assert conn.execute("SELECT x FROM t").fetchall() == [(1,)]
|