mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-03 13:19:23 +08:00
A node whose FUNCTION points at a method that does not exist (e.g. a typo in a custom node) raised an AttributeError inside the scheduling heuristic (ux_friendly_pick_node -> is_async). That exception escaped stage_node_execution() and the prompt worker's error handling, silently killing the worker thread with nothing reported to the client. - is_async() now treats a node whose FUNCTION does not resolve to a method as non-async, so scheduling proceeds and the missing-method error is raised and reported through the normal execution path. - stage_node_execution() wraps node picking so any unexpected scheduling error is returned as an execution error (attributed to an available node) rather than propagating and killing the worker thread. Add regression tests covering both paths.
69 lines
2.2 KiB
Python
69 lines
2.2 KiB
Python
"""Regression tests for scheduler resilience to malformed nodes.
|
|
|
|
A node whose FUNCTION points at a method that does not exist (e.g. a typo in a
|
|
custom node) used to raise inside the scheduling heuristic, escaping the prompt
|
|
worker's error handling and silently killing the worker thread. Scheduling must
|
|
instead either proceed (so the error surfaces through normal execution) or report
|
|
the failure as an execution error.
|
|
"""
|
|
import asyncio
|
|
|
|
import nodes
|
|
from comfy_execution.graph import DynamicPrompt, ExecutionList
|
|
|
|
|
|
class _MalformedV1Node:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {}}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "invert" # the actual method below is misspelled
|
|
OUTPUT_NODE = True
|
|
CATEGORY = "Test"
|
|
|
|
def invvert(self):
|
|
return (None,)
|
|
|
|
|
|
class _FakeOutputCache:
|
|
def all_node_ids(self):
|
|
return set()
|
|
|
|
async def get(self, node_id):
|
|
return None
|
|
|
|
|
|
def _make_execution_list(class_type, class_def):
|
|
nodes.NODE_CLASS_MAPPINGS[class_type] = class_def
|
|
prompt = {"1": {"class_type": class_type, "inputs": {}}}
|
|
execution_list = ExecutionList(DynamicPrompt(prompt), _FakeOutputCache())
|
|
execution_list.add_node("1")
|
|
return execution_list
|
|
|
|
|
|
def test_malformed_function_does_not_crash_scheduler():
|
|
"""A FUNCTION-typo node schedules without raising; the error surfaces later."""
|
|
execution_list = _make_execution_list("MalformedV1Node", _MalformedV1Node)
|
|
node_id, error, ex = asyncio.run(execution_list.stage_node_execution())
|
|
assert ex is None
|
|
assert error is None
|
|
assert node_id == "1"
|
|
|
|
|
|
def test_pick_node_failure_is_reported_not_raised():
|
|
"""An unexpected scheduling error is returned as an error, not raised."""
|
|
execution_list = _make_execution_list("MalformedV1Node", _MalformedV1Node)
|
|
|
|
def raise_on_pick(_available):
|
|
raise RuntimeError("boom")
|
|
|
|
execution_list.ux_friendly_pick_node = raise_on_pick
|
|
node_id, error, ex = asyncio.run(execution_list.stage_node_execution())
|
|
assert node_id is None
|
|
assert isinstance(ex, RuntimeError)
|
|
assert error["node_id"] == "1"
|
|
assert error["exception_type"] == "RuntimeError"
|
|
assert error["exception_message"] == "boom"
|
|
assert error["traceback"]
|