From 58a0e0b4e81019746b52d68cd0ca487cd39455f1 Mon Sep 17 00:00:00 2001 From: Dominik Bargiel Date: Thu, 10 Sep 2026 11:22:48 +0200 Subject: [PATCH] Allow distributed worker registration preflight --- README.md | 1 + plugins/ComfyUI/ComfyUI.py | 15 ++++++++++++++- pyproject.toml | 2 +- tests/test_headless_preflight.py | 27 +++++++++++++++++++++++++++ tests/test_reuse_endpoint.py | 1 + 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f996a6f..48ba2a6 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ Every variation is checked on the selected Deadline worker after input staging, - removes fixed `ImpactSwitch`, `LatentSwitch`, and `SEGSSwitch` pass-through nodes by reconnecting consumers to the selected upstream input; - sends the complete submitted `extra_pnginfo.workflow` to metadata-aware nodes; - verifies that ComfyUI history reports the expected output nodes and that every reported file exists; +- permits the exact `DeadlineWorkerRegistration` control prompt when both worker and distributed modes are enabled; this prompt completes through history and intentionally produces no file; - turns prompt-validation errors, execution exceptions, early process exits, HTTP timeouts, missing output, and Deadline task timeouts into task failures. Dynamic/connected switch selections are not rewritten. Browser-driven chooser/picker/preview-bridge nodes (`FL_ImagePicker`, `easy imageChooser`, `ImageChooser`, `PreviewChooser`, `PreviewBridge`, and `ImpactPreviewBridge`) are rejected because a farm render has no user to answer them. Other nodes using `PROMPT`, `DYNPROMPT`, `UNIQUE_ID`, or `EXTRA_PNGINFO` remain supported when the worker has the node and the submission includes full workflow metadata. Unknown third-party interactive nodes cannot be identified from `/object_info` alone; add their exact `class_type` to `KNOWN_UI_DEPENDENT_NODE_TYPES` after confirming that they wait for frontend state. diff --git a/plugins/ComfyUI/ComfyUI.py b/plugins/ComfyUI/ComfyUI.py index 676588a..28a5292 100644 --- a/plugins/ComfyUI/ComfyUI.py +++ b/plugins/ComfyUI/ComfyUI.py @@ -1606,11 +1606,24 @@ sys.exit(0 if result.get('success') else 1) if definition.get("output_node") and re.search(r"save|preview|video|image|audio", class_type, re.I): expected_outputs.append(str(node_id)) - if not expected_outputs: + worker_mode, distributed_mode, _ = get_distributed_config_for_plugin(self) + is_registration_prompt = ( + worker_mode + and distributed_mode + and len(prompt) == 1 + and next(iter(prompt.values())).get("class_type") == "DeadlineWorkerRegistration" + ) + if not expected_outputs and not is_registration_prompt: raise ComfyUIError( f"Headless preflight found no file-producing output node on worker {self.GetSlaveName()}." ) self.current_prompt_payload = prompt + if is_registration_prompt: + self.LogInfo( + f"Headless preflight accepted the distributed worker registration prompt on " + f"{self.GetSlaveName()}; registration completes through ComfyUI history and does not write a file." + ) + return prompt, expected_outputs self.LogInfo( f"Headless preflight passed for {len(prompt)} node(s) on worker {self.GetSlaveName()}; " f"expected output nodes: {', '.join(expected_outputs)}" diff --git a/pyproject.toml b/pyproject.toml index 39b2a80..574672f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "comfyui-deadline-plugin" -version = "1.0.8" +version = "1.0.9" description = "ComfyUI plugin for submitting workflows to Thinkbox Deadline for distributed rendering" readme = "README.md" license = {text = "MIT"} diff --git a/tests/test_headless_preflight.py b/tests/test_headless_preflight.py index e7fdb25..d310238 100644 --- a/tests/test_headless_preflight.py +++ b/tests/test_headless_preflight.py @@ -33,6 +33,10 @@ def object_info(): }, "FL_ImagePicker": {"input": {"required": {}, "hidden": {"unique_id": "UNIQUE_ID"}}, "output_node": False}, "Explode": {"input": {"required": {}, "hidden": {}}, "output_node": False}, + "DeadlineWorkerRegistration": { + "input": {"required": {}, "optional": {}, "hidden": {}}, + "output_node": True, + }, } @@ -93,6 +97,29 @@ class HeadlessPreflightTests(unittest.TestCase): "2": {"class_type": "SaveImage", "inputs": {"images": ["7", 0]}}, }, WORKFLOW) + def test_distributed_worker_registration_does_not_require_a_file_output(self): + with tempfile.TemporaryDirectory() as temp: + plugin = self.make_plugin(Path(temp)) + flags = {"WorkerMode": True, "DistributedMode": True} + plugin.GetBooleanPluginInfoEntryWithDefault = lambda key, default: flags.get(key, default) + prompt = { + "1": {"class_type": "DeadlineWorkerRegistration", "inputs": {}}, + } + result, expected = plugin._preflight_worker_payload(prompt) + self.assertEqual(result, prompt) + self.assertEqual(expected, []) + + def test_distributed_non_registration_prompt_still_requires_file_output(self): + with tempfile.TemporaryDirectory() as temp: + plugin = self.make_plugin(Path(temp)) + flags = {"WorkerMode": True, "DistributedMode": True} + plugin.GetBooleanPluginInfoEntryWithDefault = lambda key, default: flags.get(key, default) + with self.assertRaisesRegex(ComfyUIError, "no file-producing output node"): + plugin._preflight_worker_payload({ + "1": {"class_type": "DeadlineWorkerRegistration", "inputs": {}}, + "3": {"class_type": "Explode", "inputs": {}}, + }) + def test_invalid_staged_path_is_rejected_before_queue(self): with tempfile.TemporaryDirectory() as temp: plugin = self.make_plugin(Path(temp)) diff --git a/tests/test_reuse_endpoint.py b/tests/test_reuse_endpoint.py index 9815297..c8c9e1d 100644 --- a/tests/test_reuse_endpoint.py +++ b/tests/test_reuse_endpoint.py @@ -103,6 +103,7 @@ def new_plugin(**attrs): plugin.standard_workflow = None plugin.GetSlaveName = lambda: "M21" plugin.GetPluginInfoEntryWithDefault = lambda key, default: default + plugin.GetBooleanPluginInfoEntryWithDefault = lambda key, default: default plugin.LogInfo = lambda message: plugin.logs.append(("info", message)) plugin.LogWarning = lambda message: plugin.logs.append(("warning", message)) for key, value in attrs.items():