Files

452 lines
17 KiB
Python

"""ComfyUI queue insertion, interrupt bridging, and remote preparation state."""
from __future__ import annotations
import copy
import logging
import threading
import time
import uuid
from typing import Any, Callable, Iterable, Mapping
from aiohttp import web
if __package__:
from .execution_environments import ExecutionProvider
from .prompt_diagnostics import _log_modal_rewritten_prompt_diagnostics
from .sync_engine import finish_r2_writeback_prompt
else: # pragma: no cover - flat import inside the Modal container.
from execution_environments import ExecutionProvider
from prompt_diagnostics import _log_modal_rewritten_prompt_diagnostics
from sync_engine import finish_r2_writeback_prompt
logger = logging.getLogger(__name__)
_MODAL_INTERRUPT_QUEUE_BRIDGE_ATTR = "__comfy_modal_interrupt_queue_bridge_installed"
_REMOTE_PREPARATION_PROMPTS_ATTR = "__comfy_modal_remote_preparation_prompts"
_REMOTE_PREPARATION_CANCELLATIONS_ATTR = (
"__comfy_modal_remote_preparation_cancellations"
)
_REMOTE_PREPARATION_LOCK_ATTR = "__comfy_modal_remote_preparation_lock"
def _get_execution_module() -> Any:
"""Import the ComfyUI execution module lazily."""
import execution
return execution
async def _queue_prompt_json(
prompt_server: Any,
json_data: dict[str, Any],
modal_response_payload: dict[str, Any] | None = None,
) -> web.Response:
"""Queue a possibly rewritten prompt using ComfyUI's native semantics."""
execution = _get_execution_module()
json_data = prompt_server.trigger_on_prompt(json_data)
if "number" in json_data:
number = float(json_data["number"])
else:
number = prompt_server.number
if json_data.get("front"):
number = -number
prompt_server.number += 1
if "prompt" not in json_data:
return web.json_response(
{
"error": {
"type": "no_prompt",
"message": "No prompt provided",
"details": "No prompt provided",
"extra_info": {},
}
},
status=400,
)
prompt = json_data["prompt"]
prompt_id = str(json_data.get("prompt_id", uuid.uuid4()))
partial_execution_targets = json_data.get("partial_execution_targets")
extra_data = dict(json_data.get("extra_data", {}))
if "client_id" in json_data:
extra_data["client_id"] = json_data["client_id"]
valid = await execution.validate_prompt(
prompt_id, prompt, partial_execution_targets
)
if not valid[0]:
modal_extra = extra_data.get("modal")
if isinstance(modal_extra, Mapping) and modal_extra.get("remote_component_ids"):
logger.warning(
"ComfyUI rejected rewritten Modal prompt prompt_id=%s error=%s node_errors=%s",
prompt_id,
valid[1],
valid[3],
)
_log_modal_rewritten_prompt_diagnostics(
prompt_id=prompt_id,
prompt=prompt,
reason="comfy_validation_failure",
level=logging.WARNING,
)
else:
logger.warning("invalid prompt: %s", valid[1])
return web.json_response(
{"error": valid[1], "node_errors": valid[3]}, status=400
)
outputs_to_execute = valid[2]
sensitive: dict[str, Any] = {}
for sensitive_key in execution.SENSITIVE_EXTRA_DATA_KEYS:
if sensitive_key in extra_data:
sensitive[sensitive_key] = extra_data.pop(sensitive_key)
extra_data["create_time"] = int(time.time() * 1000)
prompt_server.prompt_queue.put(
(number, prompt_id, prompt, extra_data, outputs_to_execute, sensitive)
)
response_payload: dict[str, Any] = {
"prompt_id": prompt_id,
"number": number,
"node_errors": valid[3],
}
if modal_response_payload:
response_payload.update(modal_response_payload)
return web.json_response(response_payload)
def _install_modal_interrupt_queue_bridge(prompt_server: Any) -> None:
"""Expose active remote work through every ComfyUI queue-state view."""
prompt_queue = getattr(prompt_server, "prompt_queue", None)
if prompt_queue is None or getattr(
prompt_queue, _MODAL_INTERRUPT_QUEUE_BRIDGE_ATTR, False
):
return
original_get_current_queue = getattr(prompt_queue, "get_current_queue", None)
original_get_current_queue_volatile = getattr(
prompt_queue, "get_current_queue_volatile", None
)
original_get_tasks_remaining = getattr(prompt_queue, "get_tasks_remaining", None)
original_interrupt_if_running = getattr(
prompt_queue, "interrupt_if_running", None
)
original_task_done = getattr(prompt_queue, "task_done", None)
original_wipe_queue = getattr(prompt_queue, "wipe_queue", None)
original_delete_queue_item = getattr(prompt_queue, "delete_queue_item", None)
if not any(
callable(method)
for method in (
original_get_current_queue,
original_get_current_queue_volatile,
original_get_tasks_remaining,
)
):
logger.debug(
"Prompt queue does not expose queue-state methods; skipping remote queue bridge."
)
return
preparation_prompts: dict[str, tuple[Any, ...]] = {}
preparation_cancellations: dict[str, threading.Event] = {}
preparation_lock = threading.RLock()
setattr(prompt_queue, _REMOTE_PREPARATION_PROMPTS_ATTR, preparation_prompts)
setattr(
prompt_queue,
_REMOTE_PREPARATION_CANCELLATIONS_ATTR,
preparation_cancellations,
)
setattr(prompt_queue, _REMOTE_PREPARATION_LOCK_ATTR, preparation_lock)
def preparation_items() -> list[tuple[Any, ...]]:
"""Return a stable snapshot of prompts still preparing remote capacity."""
with preparation_lock:
return list(preparation_prompts.values())
def append_missing_preparations(
running: Iterable[Any],
queued: Iterable[Any],
) -> tuple[list[Any], Any]:
"""Add preparation entries that are not already in ComfyUI's native queue."""
running_items = list(running)
queued_items = list(queued)
native_prompt_ids = _queue_item_prompt_ids((*running_items, *queued_items))
running_items.extend(
item
for item in preparation_items()
if str(item[1]) not in native_prompt_ids
)
return running_items, queued
if callable(original_get_current_queue):
def remote_get_current_queue() -> tuple[list[Any], Any]:
"""Return native work plus preparation and active remote prompt entries."""
running, queued = original_get_current_queue()
running_items, queued = append_missing_preparations(running, queued)
running_prompt_ids = _queue_item_prompt_ids(running_items)
try:
if __package__:
from .remote.modal_app import active_remote_modal_prompt_ids
else: # pragma: no cover - flat import inside the Modal container.
from remote.modal_app import active_remote_modal_prompt_ids
except ImportError:
return running_items, queued
for prompt_id in sorted(
active_remote_modal_prompt_ids() - running_prompt_ids
):
running_items.append((0, prompt_id, {}, {}, [], {}))
return running_items, queued
setattr(prompt_queue, "get_current_queue", remote_get_current_queue)
if callable(original_get_current_queue_volatile):
def remote_get_current_queue_volatile() -> tuple[list[Any], Any]:
"""Include remote preparation in ComfyUI's public `/queue` response."""
running, queued = original_get_current_queue_volatile()
return append_missing_preparations(running, queued)
setattr(
prompt_queue,
"get_current_queue_volatile",
remote_get_current_queue_volatile,
)
if callable(original_get_tasks_remaining):
def remote_get_tasks_remaining() -> int:
"""Count remote preparation as work in websocket queue status."""
remaining = int(original_get_tasks_remaining())
native_prompt_ids: set[str] = set()
native_queue_method = (
original_get_current_queue_volatile
if callable(original_get_current_queue_volatile)
else original_get_current_queue
)
if callable(native_queue_method):
running, queued = native_queue_method()
native_prompt_ids = _queue_item_prompt_ids((*running, *queued))
return remaining + sum(
str(item[1]) not in native_prompt_ids
for item in preparation_items()
)
setattr(prompt_queue, "get_tasks_remaining", remote_get_tasks_remaining)
if callable(original_interrupt_if_running):
def remote_interrupt_if_running(prompt_id: str) -> bool:
"""Cancel remote preparation or interrupt matching native execution."""
if _cancel_remote_preparation(prompt_server, prompt_id):
return True
return bool(original_interrupt_if_running(prompt_id))
setattr(prompt_queue, "interrupt_if_running", remote_interrupt_if_running)
if callable(original_task_done):
def remote_task_done(item_id: Any, *args: Any, **kwargs: Any) -> Any:
"""Release background cache work after the whole prompt terminates."""
currently_running = getattr(prompt_queue, "currently_running", {})
running_item = (
currently_running.get(item_id)
if isinstance(currently_running, Mapping)
else None
)
prompt_id = (
str(running_item[1])
if isinstance(running_item, (list, tuple)) and len(running_item) > 1
else None
)
try:
return original_task_done(item_id, *args, **kwargs)
finally:
if prompt_id is not None:
finish_r2_writeback_prompt(prompt_id)
setattr(prompt_queue, "task_done", remote_task_done)
if callable(original_wipe_queue):
def remote_wipe_queue() -> Any:
"""Release reservations belonging to every discarded queued prompt."""
queued_items: list[Any] = []
if callable(original_get_current_queue):
_running, queued = original_get_current_queue()
queued_items = list(queued)
try:
return original_wipe_queue()
finally:
for queued_prompt_id in _queue_item_prompt_ids(queued_items):
finish_r2_writeback_prompt(queued_prompt_id)
setattr(prompt_queue, "wipe_queue", remote_wipe_queue)
if callable(original_delete_queue_item):
def remote_delete_queue_item(predicate: Callable[[Any], bool]) -> Any:
"""Release the exact reservation removed through ComfyUI's queue API."""
before_prompt_ids: set[str] = set()
if callable(original_get_current_queue):
_running, queued = original_get_current_queue()
before_prompt_ids = _queue_item_prompt_ids(queued)
result = original_delete_queue_item(predicate)
if result and callable(original_get_current_queue):
_running, queued = original_get_current_queue()
after_prompt_ids = _queue_item_prompt_ids(queued)
for removed_prompt_id in before_prompt_ids - after_prompt_ids:
finish_r2_writeback_prompt(removed_prompt_id)
return result
setattr(prompt_queue, "delete_queue_item", remote_delete_queue_item)
setattr(prompt_queue, _MODAL_INTERRUPT_QUEUE_BRIDGE_ATTR, True)
logger.info("Installed remote preparation bridge on ComfyUI prompt queue.")
def _queue_item_prompt_ids(items: Iterable[Any]) -> set[str]:
"""Return prompt IDs from well-formed native or synthetic queue items."""
return {
str(item[1])
for item in items
if isinstance(item, (list, tuple)) and len(item) > 1
}
def _cancel_remote_preparation(prompt_server: Any, prompt_id: str) -> bool:
"""Signal cancellation when the prompt is preparing remote capacity."""
normalized_prompt_id = str(prompt_id).strip()
prompt_queue = getattr(prompt_server, "prompt_queue", None)
cancellations = getattr(
prompt_queue,
_REMOTE_PREPARATION_CANCELLATIONS_ATTR,
None,
)
preparation_lock = getattr(prompt_queue, _REMOTE_PREPARATION_LOCK_ATTR, None)
if (
not normalized_prompt_id
or not isinstance(cancellations, dict)
or preparation_lock is None
):
return False
with preparation_lock:
cancellation_event = cancellations.get(normalized_prompt_id)
if cancellation_event is None:
return False
cancellation_event.set()
logger.info(
"Cancelled remote preparation for prompt %s.", normalized_prompt_id
)
return True
def _queued_ssh_environment_ids(
prompt_server: Any,
*,
excluding_prompt_id: str | None = None,
) -> frozenset[str]:
"""Return SSH environments reserved by prompts already in ComfyUI's queue."""
prompt_queue = getattr(prompt_server, "prompt_queue", None)
get_current_queue = getattr(prompt_queue, "get_current_queue", None)
if not callable(get_current_queue):
return frozenset()
queue_state = get_current_queue()
if not isinstance(queue_state, (list, tuple)) or len(queue_state) != 2:
return frozenset()
running, queued = queue_state
queue_items = [
item
for collection in (running, queued)
if isinstance(collection, (list, tuple))
for item in collection
]
environment_ids: set[str] = set()
for item in queue_items:
if not isinstance(item, (list, tuple)) or len(item) <= 3:
continue
prompt_id = str(item[1]) if len(item) > 1 else ""
if excluding_prompt_id is not None and prompt_id == excluding_prompt_id:
continue
extra_data = item[3]
if not isinstance(extra_data, Mapping):
continue
remote_execution = extra_data.get("remote_execution")
if not isinstance(remote_execution, Mapping):
continue
assignments = remote_execution.get("assignments")
if not isinstance(assignments, Mapping):
continue
for assignment in assignments.values():
if not isinstance(assignment, Mapping):
continue
provider = str(assignment.get("provider") or "").strip().lower()
environment_id = str(assignment.get("environment_id") or "").strip()
if provider == ExecutionProvider.SSH_DOCKER.value and environment_id:
environment_ids.add(environment_id)
return frozenset(environment_ids)
def _set_remote_preparation(
prompt_server: Any,
*,
prompt_id: str,
prompt: Mapping[str, Any],
extra_data: Mapping[str, Any],
cancellation_event: threading.Event | None = None,
) -> bool:
"""Register one pre-queue remote workflow and publish its active queue state."""
prompt_queue = getattr(prompt_server, "prompt_queue", None)
preparations = getattr(prompt_queue, _REMOTE_PREPARATION_PROMPTS_ATTR, None)
preparation_lock = getattr(prompt_queue, _REMOTE_PREPARATION_LOCK_ATTR, None)
if not isinstance(preparations, dict) or preparation_lock is None:
return False
preparation_extra_data = copy.deepcopy(dict(extra_data))
preparation_extra_data.setdefault("create_time", int(time.time() * 1000))
with preparation_lock:
preparations[prompt_id] = (
0,
prompt_id,
copy.deepcopy(dict(prompt)),
preparation_extra_data,
[],
{},
)
cancellations = getattr(
prompt_queue,
_REMOTE_PREPARATION_CANCELLATIONS_ATTR,
None,
)
if isinstance(cancellations, dict) and cancellation_event is not None:
cancellations[prompt_id] = cancellation_event
queue_updated = getattr(prompt_server, "queue_updated", None)
if callable(queue_updated):
queue_updated()
return True
def _clear_remote_preparation(prompt_server: Any, prompt_id: str) -> None:
"""Remove one pre-queue remote workflow and publish the resulting queue state."""
prompt_queue = getattr(prompt_server, "prompt_queue", None)
preparations = getattr(prompt_queue, _REMOTE_PREPARATION_PROMPTS_ATTR, None)
preparation_lock = getattr(prompt_queue, _REMOTE_PREPARATION_LOCK_ATTR, None)
if not isinstance(preparations, dict) or preparation_lock is None:
return
with preparation_lock:
removed = preparations.pop(prompt_id, None)
cancellations = getattr(
prompt_queue,
_REMOTE_PREPARATION_CANCELLATIONS_ATTR,
None,
)
if isinstance(cancellations, dict):
cancellations.pop(prompt_id, None)
if removed is not None:
queue_updated = getattr(prompt_server, "queue_updated", None)
if callable(queue_updated):
queue_updated()