Files
ComfyAssets-ComfyUI_PromptM…/utils/history_read_fence.py
Vito ecd21165a7 feat(history): add chronological runs and exact output inspection (#227)
* feat(history): add bounded chronological read projections (#209)

* fix(history): validate internal continuation positions (#209)

* feat(history): expose fenced history windows and run details (#209)

* fix(history): release retired projection connections (#209)

* feat(history): resolve reported outputs through current authorization (#209)

* fix(history): reject invalid inspection targets (#209)

* feat(history): add scoped history state and current-file adapter (#209)

* fix(history): preserve slideshow and clear stale reentry actions (#209)

* feat(history): render recorded executions and frozen run details (#209)

* fix(history): recover receipts and refresh capture evidence (#209)

* test(history): verify chronological history across restore and host modes (#209)

* fix(history): enforce chronological marker visual parity (#209)

* fix(history): resolve final chronological review findings

Address #209 recovery, bounded visit restoration, exact receipt availability, and portable regression coverage.

* test(history): repair Windows fixtures and review feedback

Address #209 and PR #227 native path, cleanup, media and review-comment expectations. Document Windows portability preflight without changing production behavior.

* ci: raise the viewerjs job budget for the larger browser suite

The cross-browser workflows suite grew from 396 to 496 tests on this
branch, which pushed the whole viewerjs job to about 25.5 minutes
against the 25 minute cap set in #181. Two exact-head runs on this
branch timed out without any test failing: run 39173019283 was killed
on test 474 of 496, and run 39175125474 printed "496 passed (12.8m)"
and was then cancelled during Playwright shutdown.

Raise only the job budget. Browser projects, workers and per-test
timeouts are unchanged. Splitting the browsers into parallel jobs is
tracked separately.
2026-10-18 17:17:05 -07:00

81 lines
2.6 KiB
Python

"""Restore publication checks for snapshot-owned history reads."""
import os
from pathlib import Path
import sqlite3
import threading
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..database.operations import PromptDatabase
try:
from ..database.execution_history import (
HistoryEpochChanged,
subscribe_history_restore,
)
except ImportError:
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(
Path(self._path).as_uri() + "?mode=ro", 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()