Files
ComfyAssets-ComfyUI_PromptM…/tests/test_execution_history_writer.py
T
Vito 763e6b37b1 feat(history): record durable ComfyUI execution attempts (#212)
* feat(history): normalize bounded execution evidence (#209)

* fix(history): bound lifecycle kind checks before hashing (#209)

* feat(history): persist execution attempts with restore fencing (#209)

* test(api): bootstrap standalone backup restore fixtures (#209)

* feat(history): isolate bounded asynchronous ledger writes (#209)

* feat(history): observe actual queue attempts without altering execution (#209)

* fix(database): honor package-scoped configuration (#211)

* test(history): exercise real cached and headless executions (#209)

* fix(history): reject unsupported uniqueness and isolate package fixtures (#209)

* test(history): support Python 3.10 package fixture cleanup (#209)
2026-10-17 16:42:07 -07:00

487 lines
18 KiB
Python

"""Real-thread, disposable-database coverage for the bounded history writer."""
from dataclasses import FrozenInstanceError, replace
import logging
import os
import sqlite3
import tempfile
import threading
import time
from types import SimpleNamespace
import unittest
from unittest.mock import patch
from uuid import uuid4
from database import PromptDatabase
from utils.execution_history_events import (
LifecycleEvent,
RunFinish,
TerminalEvidence,
make_finish,
make_start,
)
from utils.execution_history_writer import HistoryWriter
class ManualClock:
def __init__(self):
self.value = 0.0
self.lock = threading.Lock()
def __call__(self):
with self.lock:
return self.value
def advance(self, seconds):
with self.lock:
self.value += seconds
class HistoryWriterTests(unittest.TestCase):
def setUp(self):
logger = logging.getLogger("prompt_manager.execution_history_writer")
handlers, propagate, level = logger.handlers, logger.propagate, logger.level
logger.handlers = [logging.NullHandler()]
logger.propagate = False
logger.setLevel(logging.WARNING)
def restore_logging():
logger.handlers, logger.propagate, logger.level = handlers, propagate, level
self.addCleanup(restore_logging)
self.directory = tempfile.TemporaryDirectory()
self.addCleanup(self.directory.cleanup)
self.db = PromptDatabase(os.path.join(self.directory.name, "history.db"))
self.addCleanup(self.db.model.reset_schema_cache)
self.addCleanup(self.db.model.close_all)
self.clock = ManualClock()
self.session = str(uuid4())
self.job = str(uuid4())
self.releases = []
self.writers = []
self.addCleanup(self.cleanup_workers)
def cleanup_workers(self):
for release in self.releases:
release.set()
for writer in self.writers:
self.assertTrue(writer.close(3), "test-created worker leaked")
def wait_for(self, predicate):
deadline = time.monotonic() + 3
while time.monotonic() < deadline:
if predicate():
return
threading.Event().wait(0.005)
self.fail("finite deadline expired waiting for worker behavior")
def writer(self, factory=None, ready=True):
writer = HistoryWriter(factory or (lambda: self.db), clock=self.clock)
self.writers.append(writer)
writer.start()
if ready:
self.wait_for(lambda: writer.status().accepting)
return writer
def start_command(self, item=1, epoch=None):
return make_start(
self.session,
((0, self.job), item),
epoch or self.db.get_execution_epoch(),
100,
)
def finish_command(self, start):
return make_finish(
start.session_id,
start.queue_item_id,
start.storage_epoch,
SimpleNamespace(messages=[("execution_success", {"prompt_id": self.job})]),
200,
)
def block(self):
entered, release = threading.Event(), threading.Event()
self.releases.append(release)
return entered, release
def test_fifo_single_worker_and_thread_local_connection_cleanup(self):
caller_conn = self.db.model.get_connection()
threads, timeouts = [], []
original = self.db.write_execution_start
def write(command):
threads.append(threading.get_ident())
timeouts.append(
self.db.model.get_connection()
.execute("PRAGMA busy_timeout")
.fetchone()[0]
)
return original(command)
with patch.object(self.db, "write_execution_start", write):
writer = self.writer()
writer.start()
start = self.start_command()
self.assertTrue(writer.submit(start))
self.assertTrue(writer.submit(self.finish_command(start)))
self.wait_for(
lambda: (self.db.get_execution_run(start.run_id) or {}).get("outcome")
== "succeeded"
)
self.assertTrue(writer.submit(self.start_command(2)))
self.assertTrue(writer.close(2))
self.assertEqual(len(set(threads)), 1)
self.assertNotEqual(threads[0], threading.get_ident())
self.assertEqual(timeouts, [250, 250])
self.assertFalse(writer.status().alive)
self.assertFalse(writer.status().accepting)
self.assertEqual(writer.status().pending, 0)
self.assertEqual(
caller_conn.execute("PRAGMA busy_timeout").fetchone()[0], 30000
)
self.assertEqual(len(self.db.model._connections), 1)
with self.assertRaises(FrozenInstanceError):
writer.status().epoch = "changed"
self.assertFalse(writer.submit(start))
def test_blocked_initialization_never_accepts_or_blocks_producers(self):
entered, release = self.block()
def factory():
entered.set()
release.wait(3)
return self.db
writer = self.writer(factory, ready=False)
self.assertTrue(entered.wait(1))
before = time.monotonic()
self.assertFalse(writer.submit(self.start_command()))
self.assertIsNone(writer.status().epoch)
self.assertLess(time.monotonic() - before, 0.1)
self.assertIsNone(self.db.get_execution_run(self.start_command().run_id))
before = time.monotonic()
self.assertFalse(writer.close(0.03))
self.assertLess(time.monotonic() - before, 0.2)
release.set()
self.wait_for(lambda: not writer.status().alive)
self.assertFalse(writer.status().accepting)
self.assertEqual(len(self.db.model._connections), 1)
def test_saturation_bounds_all_pending_commands_and_coalesces_diagnostics(self):
entered, release = self.block()
original = self.db.write_execution_start
def write(command):
entered.set()
release.wait(3)
return original(command)
with patch.object(self.db, "write_execution_start", write):
writer = self.writer()
command = self.start_command()
self.assertTrue(writer.submit(command))
self.assertTrue(entered.wait(1))
for _ in range(1023):
self.assertTrue(writer.submit(command))
for _ in range(100):
self.assertFalse(writer.submit(command))
self.assertEqual(writer.status().pending, 1024)
self.assertEqual(writer.status().recording_gaps, 100)
with self.assertLogs(
"prompt_manager.execution_history_writer", "WARNING"
) as logs:
release.set()
self.wait_for(lambda: writer.status().pending == 0)
self.assertTrue(writer.close(2))
self.assertEqual(len(logs.records), 1)
def test_rejects_oversized_invalid_and_wrong_epoch_commands(self):
writer = self.writer()
start = self.start_command()
oversized = RunFinish(
self.session,
1,
start.storage_epoch,
200,
TerminalEvidence(
tuple(
LifecycleEvent(
"execution_error", self.job, i, "n" * 128, None, None
)
for i in range(20)
),
False,
False,
False,
),
)
for command in (
oversized,
object(),
replace(start, storage_epoch=str(uuid4())),
):
self.assertFalse(writer.submit(command))
self.assertEqual(writer.status().pending, 0)
self.assertEqual(writer.status().recording_gaps, 3)
self.assertIsNone(self.db.get_execution_run(start.run_id))
def test_failed_command_once_discards_backlog_and_waits_five_seconds(self):
entered, release = self.block()
probes, writes = [], []
original_epoch = self.db.get_execution_epoch
original_write = self.db.write_execution_start
def epoch():
probes.append(self.clock())
return original_epoch()
def write(command):
writes.append(command.queue_item_id)
if command.queue_item_id == 1:
entered.set()
release.wait(3)
raise sqlite3.OperationalError("private failure text")
return original_write(command)
start, queued, fresh = (self.start_command(i) for i in (1, 2, 3))
with (
patch.object(self.db, "get_execution_epoch", epoch),
patch.object(self.db, "write_execution_start", write),
):
writer = self.writer()
self.assertTrue(writer.submit(start))
self.assertTrue(entered.wait(1))
self.assertTrue(writer.submit(queued))
release.set()
self.wait_for(lambda: not writer.status().accepting)
self.assertFalse(writer.submit(fresh))
self.clock.advance(4.99)
threading.Event().wait(0.08)
self.assertEqual(probes, [0])
self.clock.advance(0.01)
self.wait_for(lambda: writer.status().accepting)
self.assertEqual(probes, [0, 5])
self.assertTrue(writer.submit(fresh))
self.wait_for(lambda: self.db.get_execution_run(fresh.run_id) is not None)
self.assertEqual(writes, [1, 3])
self.assertIsNone(self.db.get_execution_run(start.run_id))
self.assertIsNone(self.db.get_execution_run(queued.run_id))
def test_factory_failure_recovery_probes_are_bounded(self):
calls = []
def factory():
calls.append(self.clock())
if len(calls) < 3:
raise sqlite3.OperationalError("unavailable")
return self.db
writer = self.writer(factory, ready=False)
self.wait_for(lambda: len(calls) == 1)
self.clock.advance(4.99)
threading.Event().wait(0.08)
self.assertEqual(calls, [0])
self.clock.advance(0.01)
self.wait_for(lambda: len(calls) == 2)
self.clock.advance(4.99)
threading.Event().wait(0.08)
self.assertEqual(calls, [0, 5])
self.clock.advance(0.01)
self.wait_for(lambda: writer.status().accepting)
self.assertEqual(calls, [0, 5, 10])
command = self.start_command()
self.assertTrue(writer.submit(command))
self.wait_for(lambda: self.db.get_execution_run(command.run_id) is not None)
def test_failed_epoch_probe_closes_only_worker_connection_and_recovers(self):
command = self.start_command()
original = self.db.get_execution_epoch
failed = threading.Event()
def epoch():
if not failed.is_set():
failed.set()
raise sqlite3.DatabaseError("metadata unavailable")
return original()
with patch.object(self.db, "get_execution_epoch", epoch):
writer = self.writer(ready=False)
self.wait_for(lambda: writer.status().recording_gaps == 1)
self.assertIsNone(writer.status().epoch)
self.assertFalse(writer.submit(command))
self.assertEqual(len(self.db.model._connections), 1)
self.clock.advance(5)
self.wait_for(lambda: writer.status().accepting)
self.assertTrue(writer.submit(command))
self.wait_for(lambda: self.db.get_execution_run(command.run_id) is not None)
def test_invalidation_during_epoch_probe_cannot_publish_stale_readiness(self):
entered, release = self.block()
original = self.db.get_execution_epoch
stale_epoch = original()
fresh_epoch = str(uuid4())
def epoch():
result = original()
if not entered.is_set():
entered.set()
release.wait(3)
return result
with patch.object(self.db, "get_execution_epoch", epoch):
writer = self.writer(ready=False)
self.assertTrue(entered.wait(1))
with self.db.model.get_connection() as conn:
conn.execute(
"UPDATE execution_history_metadata SET storage_epoch=?",
(fresh_epoch,),
)
writer.invalidate()
release.set()
threading.Event().wait(0.08)
self.assertFalse(writer.status().accepting)
self.assertIsNone(writer.status().epoch)
self.assertFalse(writer.submit(self.start_command(epoch=stale_epoch)))
self.clock.advance(5)
self.wait_for(lambda: writer.status().epoch == fresh_epoch)
command = self.start_command(epoch=fresh_epoch)
self.assertTrue(writer.submit(command))
self.wait_for(lambda: self.db.get_execution_run(command.run_id) is not None)
def test_held_sqlite_lock_does_not_block_submit_and_is_not_retried(self):
writer = self.writer()
command = self.start_command()
lock = sqlite3.connect(self.db.model.db_path)
self.addCleanup(lock.close)
lock.execute("BEGIN IMMEDIATE")
before = time.monotonic()
self.assertTrue(writer.submit(command))
self.assertLess(time.monotonic() - before, 0.1)
self.wait_for(lambda: not writer.status().accepting)
lock.rollback()
self.clock.advance(5)
self.wait_for(lambda: writer.status().accepting)
self.assertIsNone(self.db.get_execution_run(command.run_id))
fresh = self.start_command(2)
self.assertTrue(writer.submit(fresh))
self.wait_for(lambda: self.db.get_execution_run(fresh.run_id) is not None)
def test_external_epoch_rotation_drops_stale_commands_then_records_fresh(self):
writer = self.writer()
command = self.start_command()
new_epoch = str(uuid4())
with self.db.model.get_connection() as conn:
conn.execute(
"UPDATE execution_history_metadata SET storage_epoch=?", (new_epoch,)
)
self.assertTrue(writer.submit(command))
self.wait_for(lambda: not writer.status().accepting)
self.assertIsNone(writer.status().epoch)
self.clock.advance(5)
self.wait_for(lambda: writer.status().epoch == new_epoch)
self.assertFalse(writer.submit(command))
fresh = self.start_command(2)
self.assertTrue(writer.submit(fresh))
self.wait_for(lambda: self.db.get_execution_run(fresh.run_id) is not None)
self.assertIsNone(self.db.get_execution_run(command.run_id))
def test_restore_invalidates_queued_and_inflight_commands(self):
backup = os.path.join(self.directory.name, "backup.db")
command = self.start_command()
self.db.write_execution_start(command)
self.assertTrue(self.db.model.backup_database(backup))
writer = self.writer()
entered, release = self.block()
original = self.db.write_execution_start
def write(command):
entered.set()
release.wait(3)
return original(command)
with patch.object(self.db, "write_execution_start", write):
self.assertTrue(writer.submit(command))
self.assertTrue(entered.wait(1))
self.assertTrue(writer.submit(self.finish_command(command)))
self.db.model.restore_from_file(backup)
self.assertFalse(writer.status().accepting)
self.assertIsNone(writer.status().epoch)
self.assertFalse(writer.submit(command))
release.set()
self.wait_for(lambda: writer.status().pending == 0)
self.clock.advance(5)
self.wait_for(lambda: writer.status().accepting)
self.assertNotEqual(writer.status().epoch, command.storage_epoch)
self.assertEqual(
self.db.get_execution_run(command.run_id)["outcome"], "observed"
)
fresh = self.start_command(2)
timeouts = []
def write_fresh(command):
timeouts.append(
self.db.model.get_connection()
.execute("PRAGMA busy_timeout")
.fetchone()[0]
)
return original(command)
with patch.object(self.db, "write_execution_start", write_fresh):
self.assertTrue(writer.submit(fresh))
self.assertTrue(writer.submit(self.finish_command(fresh)))
self.wait_for(
lambda: (self.db.get_execution_run(fresh.run_id) or {}).get("outcome")
== "succeeded"
)
self.assertEqual(timeouts, [250])
def test_bounded_close_discards_backlog_and_disposes_after_blocked_write(self):
entered, release = self.block()
original = self.db.write_execution_start
def write(command):
entered.set()
release.wait(3)
return original(command)
with patch.object(self.db, "write_execution_start", write):
writer = self.writer()
first, queued = self.start_command(), self.start_command(2)
self.assertTrue(writer.submit(first))
self.assertTrue(entered.wait(1))
self.assertTrue(writer.submit(queued))
before = time.monotonic()
self.assertFalse(writer.close(0.03))
self.assertLess(time.monotonic() - before, 0.2)
release.set()
self.wait_for(lambda: not writer.status().alive)
self.assertIsNone(self.db.get_execution_run(queued.run_id))
self.assertEqual(writer.status().pending, 0)
self.assertEqual(len(self.db.model._connections), 1)
def test_close_before_start_is_terminal(self):
writer = HistoryWriter(lambda: self.fail("closed writer initialized database"))
self.assertTrue(writer.close())
writer.start()
self.assertFalse(writer.status().alive)
self.assertFalse(writer.submit(self.start_command()))
def test_external_capture_gaps_are_nonblocking_and_saturate_all_rejections(self):
writer = HistoryWriter(lambda: self.fail("producer accessed database"))
self.assertTrue(callable(getattr(writer, "record_gap", None)))
writer.record_gap()
self.assertEqual(writer.status().recording_gaps, 1)
with writer._lock:
writer._gaps = 2**63 - 2
writer.record_gap()
writer.record_gap()
writer.submit(self.start_command())
writer.submit(object())
self.assertEqual(writer.status().recording_gaps, 2**63 - 1)
self.assertTrue(writer.close())
if __name__ == "__main__":
unittest.main()