import asyncio from types import SimpleNamespace import torch import nodes from comfy_execution.caching import BasicCache, CacheKeySetID from comfy_execution.graph import DynamicPrompt from comfy_execution.progress import WebUIProgressHandler, get_progress_state, reset_progress_state from comfy_execution.utils import get_current_client_id, has_current_client_id, reset_current_client_id, set_current_client_id from execution import CacheSet, PromptExecutor, _send_cached_ui class Server: def __init__(self): self.client_id = "shared-server-value" self.messages = [] self.last_node_id = None def send_sync(self, event, data, client_id): self.messages.append((event, data, client_id)) def test_prompt_executor_messages_use_executor_client_id(): server = Server() first = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0}) second = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0}) first.client_id = "first-client" second.client_id = "second-client" first.add_message("event", {"prompt_id": "first"}, broadcast=False) second.add_message("event", {"prompt_id": "second"}, broadcast=False) assert [message[2] for message in server.messages] == ["first-client", "second-client"] assert server.client_id == "shared-server-value" def test_cached_ui_is_recorded_without_a_connected_client(): server = Server() ui_outputs = {} cached = SimpleNamespace(ui={"output": {"images": []}, "meta": {"node_id": "node"}}) _send_cached_ui(server, None, "node", "node", cached, "prompt", ui_outputs) assert ui_outputs == {"node": cached.ui} assert server.messages == [] def test_progress_registry_is_isolated_between_async_prompt_tasks(): async def prompt(prompt_id, ready, release): reset_progress_state(prompt_id, DynamicPrompt({})) ready.set() await release.wait() return get_progress_state().prompt_id async def run(): first_ready = asyncio.Event() second_ready = asyncio.Event() release = asyncio.Event() first = asyncio.create_task(prompt("first", first_ready, release)) second = asyncio.create_task(prompt("second", second_ready, release)) await asyncio.gather(first_ready.wait(), second_ready.wait()) release.set() return await asyncio.gather(first, second) assert asyncio.run(run()) == ["first", "second"] def test_webui_progress_handler_uses_prompt_client_id(): server = Server() first = WebUIProgressHandler(server, "first-client") second = WebUIProgressHandler(server, "second-client") first._send_progress_state("first", {}) second._send_progress_state("second", {}) assert [message[2] for message in server.messages] == ["first-client", "second-client"] def test_explicit_anonymous_client_does_not_fall_back_to_stale_server_client(): server = Server() handler = WebUIProgressHandler(server, None) handler._send_progress_state("anonymous", {}) assert server.messages[0][2] is None assert not has_current_client_id() token = set_current_client_id(None) try: assert has_current_client_id() assert get_current_client_id() is None finally: reset_current_client_id(token) def test_shared_cache_uses_task_local_prompt_signatures(): async def run(): cache = BasicCache(CacheKeySetID) both_ready = asyncio.Event() ready_count = 0 async def prompt(class_type, value): nonlocal ready_count dynprompt = DynamicPrompt({"same-id": {"class_type": class_type, "inputs": {}}}) await cache.set_prompt(dynprompt, ["same-id"], None) cache.set_local("same-id", value) ready_count += 1 if ready_count == 2: both_ready.set() await both_ready.wait() result = cache.get_local("same-id") cache.release_prompt() return result return await asyncio.gather(prompt("First", "first"), prompt("Second", "second")) assert asyncio.run(run()) == ["first", "second"] def test_cooperative_executors_do_not_share_stateful_node_instances(): instances = [] both_ready = None ready = 0 class StatefulProbe: def __init__(self): instances.append(self) @classmethod def INPUT_TYPES(cls): return {"required": {}} RETURN_TYPES = () FUNCTION = "run" async def run(self): nonlocal ready ready += 1 if ready == 2: both_ready.set() await both_ready.wait() return () async def run(): nonlocal both_ready both_ready = asyncio.Event() server = Server() server.prompt_queue = SimpleNamespace(cooperative=True, is_cancelled=lambda prompt_id: False) cache_args = {"lru": 0, "ram": 0, "ram_inactive": 0} shared_outputs = CacheSet(cache_args=cache_args).outputs prompt = {"same-id": {"class_type": "StatefulInterleaveProbe", "inputs": {}}} first = PromptExecutor(server, cache_args=cache_args, shared_outputs=shared_outputs) second = PromptExecutor(server, cache_args=cache_args, shared_outputs=shared_outputs) with torch.inference_mode(): await asyncio.gather( first.execute_async(prompt, "first", {}, ["same-id"]), second.execute_async(prompt, "second", {}, ["same-id"]), ) nodes.NODE_CLASS_MAPPINGS["StatefulInterleaveProbe"] = StatefulProbe try: asyncio.run(run()) finally: del nodes.NODE_CLASS_MAPPINGS["StatefulInterleaveProbe"] assert len(instances) == 2 assert instances[0] is not instances[1] def test_concurrent_cooperative_executors_do_not_clear_global_interrupt(monkeypatch): interrupt_calls = [] monkeypatch.setattr(nodes, "interrupt_processing", lambda value=True: interrupt_calls.append(value)) async def run(): server = Server() server.prompt_queue = SimpleNamespace(cooperative=True, is_cancelled=lambda prompt_id: False) first = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0}) second = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0}) with torch.inference_mode(): await asyncio.gather( first.execute_async({}, "first", {}, []), second.execute_async({}, "second", {}, []), ) asyncio.run(run()) assert interrupt_calls == [] def test_inference_mode_stays_enabled_across_interleaved_cooperative_tasks(): observations = [] both_ready = None ready = 0 class Probe: @classmethod def INPUT_TYPES(cls): return {"required": {}} RETURN_TYPES = () FUNCTION = "run" async def run(self): nonlocal ready observations.append(torch.is_inference_mode_enabled()) ready += 1 if ready == 2: both_ready.set() await both_ready.wait() observations.append(torch.is_inference_mode_enabled()) return () async def run(): nonlocal both_ready both_ready = asyncio.Event() server = Server() server.prompt_queue = SimpleNamespace(cooperative=True, is_cancelled=lambda prompt_id: False) prompt = {"probe": {"class_type": "InferenceModeInterleaveProbe", "inputs": {}}} first = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0}) second = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0}) with torch.inference_mode(): await asyncio.gather( first.execute_async(prompt, "first", {}, ["probe"]), second.execute_async(prompt, "second", {}, ["probe"]), ) assert torch.is_inference_mode_enabled() nodes.NODE_CLASS_MAPPINGS["InferenceModeInterleaveProbe"] = Probe try: asyncio.run(run()) finally: del nodes.NODE_CLASS_MAPPINGS["InferenceModeInterleaveProbe"] assert observations == [True, True, True, True] assert not torch.is_inference_mode_enabled() def test_serial_prompt_executor_still_owns_inference_mode(): observations = [] class Probe: @classmethod def INPUT_TYPES(cls): return {"required": {}} RETURN_TYPES = () FUNCTION = "run" def run(self): observations.append(torch.is_inference_mode_enabled()) return () async def run(): server = Server() server.prompt_queue = SimpleNamespace(cooperative=False) prompt = {"probe": {"class_type": "InferenceModeSerialProbe", "inputs": {}}} executor = PromptExecutor(server, cache_args={"lru": 0, "ram": 0, "ram_inactive": 0}) await executor.execute_async(prompt, "serial", {}, ["probe"]) nodes.NODE_CLASS_MAPPINGS["InferenceModeSerialProbe"] = Probe try: asyncio.run(run()) finally: del nodes.NODE_CLASS_MAPPINGS["InferenceModeSerialProbe"] assert observations == [True] assert not torch.is_inference_mode_enabled()