* refactor: remove dead and test-only code and the write-only prompt registry (#620) Removes helpers nothing calls, test-only wrappers and their tests, the unused config writer, three unused images, and the prompt registry the nodes wrote to but nothing read, which grew with every run. * fix: network-share readers, model unload off the loop, bad save requests (#620) Every read-only SQLite open now uses one URI helper, so a library on a Windows network share works in backups, restores, moves and history reads. Unloading a model no longer blocks the server. A malformed /save body gets a 400, not a 500. * feat(library): add Recently used, Most used and Name Z-A sorting (#620) The old Dashboard's sorts the community asked for, on the Prompt library, in saved views, and with paging through the whole list. * feat(workspace): stop linking the old pages (#620) They show a removal notice since 3.5.1. System no longer links them, the load failure offers a reload and ComfyUI instead, and the README says they are going away. The pages still work at their URLs.
80 lines
2.7 KiB
Python
80 lines
2.7 KiB
Python
"""Restore publication checks for snapshot-owned history reads."""
|
|
|
|
import os
|
|
import sqlite3
|
|
import threading
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from ..database.operations import PromptDatabase
|
|
|
|
try:
|
|
from ..database.sqlite_uri import readonly_uri
|
|
from ..database.execution_history import (
|
|
HistoryEpochChanged,
|
|
subscribe_history_restore,
|
|
)
|
|
except ImportError:
|
|
from database.sqlite_uri import readonly_uri
|
|
from database.execution_history import (
|
|
HistoryEpochChanged,
|
|
subscribe_history_restore,
|
|
)
|
|
|
|
|
|
class HistoryReadFence:
|
|
"""Combine a fresh storage epoch with process-local restore notifications."""
|
|
|
|
def __init__(self, db: "PromptDatabase") -> None:
|
|
self._path = os.path.normcase(os.path.realpath(db.model.db_path))
|
|
self._lock = threading.Lock()
|
|
self._generation = 0
|
|
self._closed = False
|
|
self._unsubscribe = subscribe_history_restore(db.model.db_path, self._restored)
|
|
|
|
def _restored(self) -> None:
|
|
with self._lock:
|
|
if not self._closed:
|
|
self._generation += 1
|
|
|
|
def _check(self, generation: int | None = None, epoch: str = "") -> int:
|
|
if self._closed:
|
|
raise sqlite3.OperationalError("History reads are closed")
|
|
if generation is not None and generation != self._generation:
|
|
raise HistoryEpochChanged(epoch)
|
|
return self._generation
|
|
|
|
def begin(self) -> int:
|
|
with self._lock:
|
|
return self._check()
|
|
|
|
def validate(self, epoch: str, generation: int) -> None:
|
|
"""Validate after the caller's read transaction and response work ended."""
|
|
with self._lock:
|
|
self._check(generation, epoch)
|
|
# An API restore retires its previous model. Never reopen that owner,
|
|
# and never reuse the projection transaction's view of the epoch.
|
|
conn = sqlite3.connect(readonly_uri(self._path), uri=True, timeout=0.25)
|
|
try:
|
|
row = conn.execute(
|
|
"SELECT storage_epoch FROM execution_history_metadata "
|
|
"WHERE singleton_id=1"
|
|
).fetchone()
|
|
if row is None or not isinstance(row[0], str):
|
|
raise sqlite3.DatabaseError("Missing history storage epoch")
|
|
current_epoch = row[0]
|
|
finally:
|
|
conn.close()
|
|
with self._lock:
|
|
self._check(generation, epoch)
|
|
if current_epoch != epoch:
|
|
raise HistoryEpochChanged(current_epoch)
|
|
|
|
def close(self) -> None:
|
|
with self._lock:
|
|
if self._closed:
|
|
return
|
|
self._closed = True
|
|
self._generation += 1
|
|
self._unsubscribe()
|