feat: namespace Modal apps per ComfyUI instance
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
!/api_intercept.py
|
||||
!/comfyui_modal_sync_cloud.py
|
||||
!/durable_state.py
|
||||
!/instance_identity.py
|
||||
!/modal_executor_node.py
|
||||
!/modal_sdk.py
|
||||
!/runtime_environment.py
|
||||
|
||||
@@ -52,6 +52,8 @@ When ComfyUI starts in remote mode, Modal-Sync checks whether the supported Moda
|
||||
|
||||
Modal authentication remains user-managed. Run `<comfyui-venv>/bin/python -m modal setup` when the current user does not already have working credentials. To install the SDK manually before startup, run `uv pip install --python <comfyui-venv>/bin/python "modal==1.4.2"`.
|
||||
|
||||
Each ComfyUI environment receives its own Modal app name on first startup. Modal-Sync generates 64 random bits, persists them as lowercase hexadecimal in `<ComfyUI user directory>/.comfy-modal-sync-instance-id`, and uses an unpadded URL-safe Base64 encoding in names such as `comfy-modal-sync-R7uqpQZ6S1A`. The identity file is published atomically, so concurrent first startups converge on one name. It lives in ComfyUI's effective user directory and therefore follows `--user-directory`; keep the file when upgrading if the deployment and its state should remain associated with the same ComfyUI environment. `COMFY_MODAL_APP_NAME` remains an authoritative override for deliberately shared or externally managed deployments.
|
||||
|
||||
For repository development, `uv sync --extra remote --group test` installs the same pinned SDK. Remote mode uses the stable cloud entrypoint in [`comfyui_modal_sync_cloud.py`](comfyui_modal_sync_cloud.py). On first use, Modal-Sync can auto-deploy the configured Modal app if it does not exist.
|
||||
|
||||
The deployed image uses Python 3.11 plus an exact ComfyUI support and CUDA package set. Its local build context is limited to the ComfyUI source packages, top-level Python modules, and runtime configuration needed by the headless worker; model directories, custom nodes, caches, tests, virtual environments, user data, and unknown top-level directories stay out of the image snapshot. Before every process's first remote invocation, Modal-Sync compares the deployed worker's runtime fingerprint with the local source, ComfyUI source, custom-node requirements, and runtime-shaping settings. A missing or mismatched fingerprint is treated as stale and replaced automatically when `COMFY_MODAL_AUTO_DEPLOY=true`.
|
||||
@@ -218,7 +220,8 @@ Boolean values accept `1`, `true`, `yes`, `on`, `0`, `false`, `no`, and `off`.
|
||||
| Variable | Default | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `COMFY_MODAL_EXECUTION_MODE` | `local` | Set to `remote` for Modal-backed execution. |
|
||||
| `COMFY_MODAL_APP_NAME` | `comfy-modal-sync` | Modal app name. |
|
||||
| `COMFY_MODAL_APP_NAME` | `comfy-modal-sync-<instance_id>` | Explicit Modal app name override; otherwise derived from the persistent per-ComfyUI identity. |
|
||||
| `COMFY_MODAL_INSTANCE_ID_PATH` | `<ComfyUI user directory>/.comfy-modal-sync-instance-id` | Override the persistent 64-bit identity file location. |
|
||||
| `COMFY_MODAL_VOLUME_NAME` | `comfy-universal-storage` | Modal volume name for synced assets and bundles. |
|
||||
| `COMFY_MODAL_AUTO_DEPLOY` | `true` | Deploy or replace the configured app when lookup fails or its runtime fingerprint is stale. |
|
||||
| `COMFY_MODAL_ALLOW_EPHEMERAL_FALLBACK` | `false` | Allow the older temporary `app.run()` fallback when deployed lookup fails. |
|
||||
|
||||
+16
-20
@@ -6104,6 +6104,21 @@ def _execute_loader_prewarm_plans(
|
||||
)
|
||||
|
||||
|
||||
def _modal_image_environment(settings: Any, runtime_fingerprint: str) -> dict[str, str]:
|
||||
"""Return environment values that keep the worker aligned with local settings."""
|
||||
return {
|
||||
"COMFY_MODAL_APP_NAME": settings.app_name,
|
||||
"COMFY_MODAL_RUNTIME_FINGERPRINT": runtime_fingerprint,
|
||||
"COMFY_MODAL_STREAM_EVENT_QUEUE_MAXSIZE": str(settings.stream_event_queue_maxsize),
|
||||
"COMFY_MODAL_BRIDGE_INLINE_MAX_BYTES": str(settings.bridge_inline_max_bytes),
|
||||
"COMFY_MODAL_INVOCATION_RESULT_INLINE_MAX_BYTES": str(
|
||||
settings.invocation_result_inline_max_bytes
|
||||
),
|
||||
"COMFY_MODAL_EXECUTION_TIMEOUT_SECONDS": str(settings.execution_timeout_seconds),
|
||||
"COMFY_MODAL_STARTUP_TIMEOUT_SECONDS": str(settings.startup_timeout_seconds),
|
||||
}
|
||||
|
||||
|
||||
if modal is not None: # pragma: no branch - remote entrypoint configuration.
|
||||
settings = get_settings()
|
||||
_guard_against_existing_modal_app(settings, modal)
|
||||
@@ -6145,26 +6160,7 @@ if modal is not None: # pragma: no branch - remote entrypoint configuration.
|
||||
image = (
|
||||
modal.Image.debian_slim(python_version=REMOTE_PYTHON_VERSION)
|
||||
.apt_install(*_comfyui_apt_packages())
|
||||
.env(
|
||||
{
|
||||
"COMFY_MODAL_RUNTIME_FINGERPRINT": runtime_identity.fingerprint,
|
||||
"COMFY_MODAL_STREAM_EVENT_QUEUE_MAXSIZE": str(
|
||||
settings.stream_event_queue_maxsize
|
||||
),
|
||||
"COMFY_MODAL_BRIDGE_INLINE_MAX_BYTES": str(
|
||||
settings.bridge_inline_max_bytes
|
||||
),
|
||||
"COMFY_MODAL_INVOCATION_RESULT_INLINE_MAX_BYTES": str(
|
||||
settings.invocation_result_inline_max_bytes
|
||||
),
|
||||
"COMFY_MODAL_EXECUTION_TIMEOUT_SECONDS": str(
|
||||
settings.execution_timeout_seconds
|
||||
),
|
||||
"COMFY_MODAL_STARTUP_TIMEOUT_SECONDS": str(
|
||||
settings.startup_timeout_seconds
|
||||
),
|
||||
}
|
||||
)
|
||||
.env(_modal_image_environment(settings, runtime_identity.fingerprint))
|
||||
.pip_install(*_comfyui_runtime_packages())
|
||||
)
|
||||
if custom_node_packages:
|
||||
|
||||
@@ -305,3 +305,4 @@
|
||||
- Pinning `opencv-python-headless` in the core Modal runtime does not guarantee that `cv2` stays headless. Custom-node requirements can install `opencv-python` directly, and dependencies such as RMBG's `transparent-background` can pull it transitively. The Modal image therefore needs `libgl1` and `libglib2.0-0` so GUI-enabled OpenCV remains importable for custom nodes such as RMBG and Inspire Pack.
|
||||
- Modal can retry a generator input after an internal delivery failure even while the original container's daemon compute thread is still running. A durable invocation guard should prevent duplicate GPU work, but a raw `already running` error turns that protection into a prompt failure. On stream abandonment, cancel the original compute and mark its record failed without committing pending Volume objects; overlapping delivery should briefly wait for the record to become completed or retryable before deciding whether to replay or start the next attempt.
|
||||
- A fresh uv-created ComfyUI virtual environment may intentionally have no `pip` module, so startup dependency repair cannot assume `sys.executable -m pip` works. For remote mode, detect the missing Modal SDK before importing the rest of the extension, prefer `uv pip install --python sys.executable` with the exact supported pin, fall back to the interpreter's pip module when available, and retry the import in the same process after invalidating import caches.
|
||||
- A fixed default Modal app name makes independent ComfyUI installations collide inside one Modal workspace and also shares every app-derived Dict namespace. Persist 64 random bits in the effective ComfyUI user directory, publish the fully written identity atomically so concurrent first starts agree, encode it with unpadded URL-safe Base64 for the app suffix, preserve explicit name overrides, and inject the resolved app name into the remote image so workers never generate a second identity.
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Persistent per-ComfyUI identity for Modal resource namespacing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
INSTANCE_ID_BYTES = 8
|
||||
INSTANCE_ID_ENTROPY_BITS = INSTANCE_ID_BYTES * 8
|
||||
INSTANCE_ID_FILENAME = ".comfy-modal-sync-instance-id"
|
||||
MODAL_APP_PREFIX = "comfy-modal-sync"
|
||||
|
||||
|
||||
class ComfyInstanceIdentityError(RuntimeError):
|
||||
"""Raised when a persistent ComfyUI instance identity cannot be used safely."""
|
||||
|
||||
|
||||
def _read_instance_id(identity_path: Path) -> bytes:
|
||||
"""Read and validate one persisted hexadecimal instance identifier."""
|
||||
try:
|
||||
encoded_identity = identity_path.read_text(encoding="ascii").strip()
|
||||
except FileNotFoundError:
|
||||
raise
|
||||
except OSError as exc:
|
||||
raise ComfyInstanceIdentityError(
|
||||
f"Unable to read ComfyUI instance identity from {identity_path}: {exc}"
|
||||
) from exc
|
||||
|
||||
try:
|
||||
instance_id = bytes.fromhex(encoded_identity)
|
||||
except ValueError as exc:
|
||||
raise ComfyInstanceIdentityError(
|
||||
f"ComfyUI instance identity at {identity_path} is not valid hexadecimal data."
|
||||
) from exc
|
||||
if len(instance_id) != INSTANCE_ID_BYTES or encoded_identity != instance_id.hex():
|
||||
raise ComfyInstanceIdentityError(
|
||||
f"ComfyUI instance identity at {identity_path} must contain exactly "
|
||||
f"{INSTANCE_ID_BYTES} bytes encoded as lowercase hexadecimal."
|
||||
)
|
||||
return instance_id
|
||||
|
||||
|
||||
def _remove_temporary_identity_file(temporary_path: Path) -> None:
|
||||
"""Remove an unpublished identity candidate without masking the primary result."""
|
||||
try:
|
||||
temporary_path.unlink()
|
||||
except FileNotFoundError:
|
||||
return
|
||||
except OSError as exc:
|
||||
logger.warning("Unable to remove temporary instance identity %s: %s", temporary_path, exc)
|
||||
|
||||
|
||||
def _publish_instance_id(identity_path: Path, instance_id: bytes) -> bool:
|
||||
"""Atomically publish an identifier and return whether this process won creation."""
|
||||
try:
|
||||
identity_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as exc:
|
||||
raise ComfyInstanceIdentityError(
|
||||
f"Unable to create instance identity directory {identity_path.parent}: {exc}"
|
||||
) from exc
|
||||
|
||||
temporary_path: Path | None = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
encoding="ascii",
|
||||
dir=identity_path.parent,
|
||||
prefix=f".{identity_path.name}.",
|
||||
suffix=".tmp",
|
||||
delete=False,
|
||||
) as temporary_file:
|
||||
temporary_path = Path(temporary_file.name)
|
||||
temporary_file.write(f"{instance_id.hex()}\n")
|
||||
temporary_file.flush()
|
||||
os.fsync(temporary_file.fileno())
|
||||
try:
|
||||
os.link(temporary_path, identity_path)
|
||||
except FileExistsError:
|
||||
return False
|
||||
except OSError as exc:
|
||||
raise ComfyInstanceIdentityError(
|
||||
f"Unable to publish ComfyUI instance identity at {identity_path}: {exc}"
|
||||
) from exc
|
||||
return True
|
||||
finally:
|
||||
if temporary_path is not None:
|
||||
_remove_temporary_identity_file(temporary_path)
|
||||
|
||||
|
||||
def load_or_create_instance_id(identity_path: Path) -> bytes:
|
||||
"""Return the stable 64-bit identifier stored at the requested path."""
|
||||
try:
|
||||
return _read_instance_id(identity_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
candidate = secrets.token_bytes(INSTANCE_ID_BYTES)
|
||||
if _publish_instance_id(identity_path, candidate):
|
||||
logger.info(
|
||||
"Created %d-bit ComfyUI instance identity at %s.",
|
||||
INSTANCE_ID_ENTROPY_BITS,
|
||||
identity_path,
|
||||
)
|
||||
return candidate
|
||||
return _read_instance_id(identity_path)
|
||||
|
||||
|
||||
def instance_id_base64(instance_id: bytes) -> str:
|
||||
"""Encode a 64-bit instance identifier as unpadded URL-safe Base64."""
|
||||
if len(instance_id) != INSTANCE_ID_BYTES:
|
||||
raise ValueError(f"Instance identifier must contain exactly {INSTANCE_ID_BYTES} bytes.")
|
||||
return base64.urlsafe_b64encode(instance_id).decode("ascii").rstrip("=")
|
||||
|
||||
|
||||
def modal_app_name_for_instance(instance_id: bytes) -> str:
|
||||
"""Return the Modal app name namespaced to one persistent ComfyUI instance."""
|
||||
return f"{MODAL_APP_PREFIX}-{instance_id_base64(instance_id)}"
|
||||
+62
-1
@@ -2,12 +2,26 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
if __package__:
|
||||
from .instance_identity import (
|
||||
INSTANCE_ID_FILENAME,
|
||||
load_or_create_instance_id,
|
||||
modal_app_name_for_instance,
|
||||
)
|
||||
else: # pragma: no cover - the stable cloud entrypoint imports this module top-level.
|
||||
from instance_identity import (
|
||||
INSTANCE_ID_FILENAME,
|
||||
load_or_create_instance_id,
|
||||
modal_app_name_for_instance,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SETTINGS_ENV_KEYS = (
|
||||
@@ -18,6 +32,7 @@ _SETTINGS_ENV_KEYS = (
|
||||
"COMFY_MODAL_SYNC_CUSTOM_NODES",
|
||||
"COMFY_MODAL_LOCAL_STORAGE_ROOT",
|
||||
"COMFY_MODAL_APP_NAME",
|
||||
"COMFY_MODAL_INSTANCE_ID_PATH",
|
||||
"COMFY_MODAL_AUTO_DEPLOY",
|
||||
"COMFY_MODAL_ALLOW_EPHEMERAL_FALLBACK",
|
||||
"COMFY_MODAL_ENABLE_MEMORY_SNAPSHOT",
|
||||
@@ -206,6 +221,51 @@ def _discover_custom_nodes_dir(repo_root: Path, comfyui_root: Path | None) -> Pa
|
||||
return None
|
||||
|
||||
|
||||
def _discover_comfyui_user_directory(comfyui_root: Path | None) -> Path | None:
|
||||
"""Return ComfyUI's effective user directory when it can be resolved."""
|
||||
try:
|
||||
folder_paths = importlib.import_module("folder_paths")
|
||||
except ModuleNotFoundError as exc:
|
||||
if exc.name != "folder_paths":
|
||||
logger.debug("Unable to import folder_paths while resolving the user directory: %s", exc)
|
||||
else:
|
||||
get_user_directory = getattr(folder_paths, "get_user_directory", None)
|
||||
if callable(get_user_directory):
|
||||
return Path(get_user_directory()).expanduser().resolve()
|
||||
if comfyui_root is None:
|
||||
return None
|
||||
return (comfyui_root / "user").resolve()
|
||||
|
||||
|
||||
def _resolve_modal_app_name(comfyui_root: Path | None, execution_mode: str) -> str:
|
||||
"""Return the explicit or persistent per-ComfyUI Modal app name."""
|
||||
configured_name = os.getenv("COMFY_MODAL_APP_NAME")
|
||||
if configured_name is not None:
|
||||
resolved_name = configured_name.strip()
|
||||
if not resolved_name:
|
||||
raise ValueError("COMFY_MODAL_APP_NAME must not be empty.")
|
||||
return resolved_name
|
||||
|
||||
identity_path = _read_path_env("COMFY_MODAL_INSTANCE_ID_PATH")
|
||||
if identity_path is None:
|
||||
user_directory = _discover_comfyui_user_directory(comfyui_root)
|
||||
if user_directory is not None:
|
||||
identity_path = user_directory / INSTANCE_ID_FILENAME
|
||||
if identity_path is None:
|
||||
if execution_mode == "remote":
|
||||
raise RuntimeError(
|
||||
"Remote execution requires a persistent ComfyUI instance identity, but the "
|
||||
"ComfyUI user directory could not be resolved. Set COMFY_MODAL_COMFYUI_ROOT, "
|
||||
"COMFY_MODAL_INSTANCE_ID_PATH, or COMFY_MODAL_APP_NAME explicitly."
|
||||
)
|
||||
logger.debug("Using the legacy local app name because no ComfyUI user directory was found.")
|
||||
return "comfy-modal-sync"
|
||||
|
||||
app_name = modal_app_name_for_instance(load_or_create_instance_id(identity_path))
|
||||
logger.info("Using per-ComfyUI Modal app %s from identity file %s.", app_name, identity_path)
|
||||
return app_name
|
||||
|
||||
|
||||
def _settings_env_signature() -> tuple[tuple[str, str | None], ...]:
|
||||
"""Return the environment values that affect resolved Modal-Sync settings."""
|
||||
return tuple((key, os.getenv(key)) for key in _SETTINGS_ENV_KEYS)
|
||||
@@ -221,6 +281,7 @@ def _get_settings_cached(
|
||||
comfyui_root = _discover_comfyui_root(repo_root)
|
||||
custom_nodes_dir = _discover_custom_nodes_dir(repo_root, comfyui_root)
|
||||
execution_mode = os.getenv("COMFY_MODAL_EXECUTION_MODE", "local").strip().lower()
|
||||
app_name = _resolve_modal_app_name(comfyui_root, execution_mode)
|
||||
sync_custom_nodes = _read_bool_env("COMFY_MODAL_SYNC_CUSTOM_NODES")
|
||||
if sync_custom_nodes is None:
|
||||
sync_custom_nodes = execution_mode != "local"
|
||||
@@ -230,7 +291,7 @@ def _get_settings_cached(
|
||||
)
|
||||
|
||||
settings = ModalSyncSettings(
|
||||
app_name=(app_name := os.getenv("COMFY_MODAL_APP_NAME", "comfy-modal-sync")),
|
||||
app_name=app_name,
|
||||
auto_deploy=_read_bool_env("COMFY_MODAL_AUTO_DEPLOY") is not False,
|
||||
allow_ephemeral_fallback=_read_bool_env("COMFY_MODAL_ALLOW_EPHEMERAL_FALLBACK") or False,
|
||||
enable_memory_snapshot=_read_bool_env("COMFY_MODAL_ENABLE_MEMORY_SNAPSHOT") is not False,
|
||||
|
||||
@@ -14,6 +14,7 @@ import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
PACKAGE_NAME = "comfyui_modal_sync_under_test"
|
||||
os.environ["COMFY_MODAL_APP_NAME"] = "comfy-modal-sync"
|
||||
|
||||
|
||||
def _comfyui_root() -> Path:
|
||||
@@ -94,6 +95,12 @@ def modal_sdk_module(extension_package: object) -> object:
|
||||
return importlib.import_module(f"{PACKAGE_NAME}.modal_sdk")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def instance_identity_module(extension_package: object) -> object:
|
||||
"""Return the persistent ComfyUI instance identity module."""
|
||||
return importlib.import_module(f"{PACKAGE_NAME}.instance_identity")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def api_intercept_module(extension_package: object) -> object:
|
||||
"""Return the prompt interception module."""
|
||||
@@ -159,7 +166,9 @@ def modal_cloud_module() -> object:
|
||||
def reset_modal_environment(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
"""Isolate Modal-Sync environment variables between tests."""
|
||||
monkeypatch.setenv("COMFY_MODAL_EXECUTION_MODE", "local")
|
||||
monkeypatch.setenv("COMFY_MODAL_APP_NAME", "comfy-modal-sync")
|
||||
monkeypatch.setenv("COMFY_MODAL_LOCAL_STORAGE_ROOT", str(tmp_path / "storage"))
|
||||
monkeypatch.delenv("COMFY_MODAL_INSTANCE_ID_PATH", raising=False)
|
||||
monkeypatch.delenv("COMFY_MODAL_AUTO_DEPLOY", raising=False)
|
||||
monkeypatch.delenv("COMFY_MODAL_ALLOW_EPHEMERAL_FALLBACK", raising=False)
|
||||
monkeypatch.delenv("COMFY_MODAL_CUSTOM_NODES_DIR", raising=False)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Tests for persistent per-ComfyUI Modal app identities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_instance_identity_is_persisted_and_reused(
|
||||
instance_identity_module: Any,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""A generated 64-bit identifier should remain stable across later loads."""
|
||||
identity_path = tmp_path / "user" / instance_identity_module.INSTANCE_ID_FILENAME
|
||||
|
||||
first_identity = instance_identity_module.load_or_create_instance_id(identity_path)
|
||||
second_identity = instance_identity_module.load_or_create_instance_id(identity_path)
|
||||
|
||||
assert first_identity == second_identity
|
||||
assert len(first_identity) == instance_identity_module.INSTANCE_ID_BYTES
|
||||
assert identity_path.read_text(encoding="ascii").strip() == first_identity.hex()
|
||||
|
||||
|
||||
def test_instance_identity_uses_unpadded_url_safe_base64(
|
||||
instance_identity_module: Any,
|
||||
) -> None:
|
||||
"""The app suffix should encode all 64 bits into eleven URL-safe characters."""
|
||||
instance_id = bytes.fromhex("0001020304050607")
|
||||
|
||||
app_name = instance_identity_module.modal_app_name_for_instance(instance_id)
|
||||
|
||||
assert app_name == "comfy-modal-sync-AAECAwQFBgc"
|
||||
|
||||
|
||||
def test_concurrent_identity_creation_publishes_one_winner(
|
||||
instance_identity_module: Any,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Concurrent first startups should all observe the same atomically published identity."""
|
||||
identity_path = tmp_path / "user" / instance_identity_module.INSTANCE_ID_FILENAME
|
||||
|
||||
with ThreadPoolExecutor(max_workers=16) as executor:
|
||||
identities = list(
|
||||
executor.map(
|
||||
lambda _: instance_identity_module.load_or_create_instance_id(identity_path),
|
||||
range(64),
|
||||
)
|
||||
)
|
||||
|
||||
assert len(set(identities)) == 1
|
||||
assert list(identity_path.parent.glob(f".{identity_path.name}.*.tmp")) == []
|
||||
|
||||
|
||||
def test_corrupt_instance_identity_is_not_silently_replaced(
|
||||
instance_identity_module: Any,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Malformed persistent state should fail rather than silently select a new Modal app."""
|
||||
identity_path = tmp_path / instance_identity_module.INSTANCE_ID_FILENAME
|
||||
identity_path.write_text("not-an-identity\n", encoding="ascii")
|
||||
|
||||
with pytest.raises(instance_identity_module.ComfyInstanceIdentityError):
|
||||
instance_identity_module.load_or_create_instance_id(identity_path)
|
||||
|
||||
assert identity_path.read_text(encoding="ascii") == "not-an-identity\n"
|
||||
@@ -1321,6 +1321,25 @@ def test_stable_modal_cloud_entry_imports_without_modal_sdk(
|
||||
assert hasattr(modal_cloud_module, "RemoteEngine")
|
||||
|
||||
|
||||
def test_modal_cloud_image_environment_preserves_unique_app_name(
|
||||
modal_cloud_module: Any,
|
||||
) -> None:
|
||||
"""Remote workers should receive the same per-ComfyUI app name resolved locally."""
|
||||
settings = types.SimpleNamespace(
|
||||
app_name="comfy-modal-sync-AAECAwQFBgc",
|
||||
stream_event_queue_maxsize=256,
|
||||
bridge_inline_max_bytes=1024,
|
||||
invocation_result_inline_max_bytes=2048,
|
||||
execution_timeout_seconds=3600,
|
||||
startup_timeout_seconds=900,
|
||||
)
|
||||
|
||||
image_environment = modal_cloud_module._modal_image_environment(settings, "fingerprint-1")
|
||||
|
||||
assert image_environment["COMFY_MODAL_APP_NAME"] == "comfy-modal-sync-AAECAwQFBgc"
|
||||
assert image_environment["COMFY_MODAL_RUNTIME_FINGERPRINT"] == "fingerprint-1"
|
||||
|
||||
|
||||
def test_modal_cloud_installs_timestamped_logger_handler(
|
||||
modal_cloud_module: Any,
|
||||
) -> None:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -59,6 +60,49 @@ def test_settings_reads_modal_gpu_override(
|
||||
assert settings.modal_gpu == "L40S"
|
||||
|
||||
|
||||
def test_settings_generates_stable_per_comfyui_app_name(
|
||||
settings_module: Any,
|
||||
monkeypatch: Any,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Default remote app names should be stable and namespaced by a persisted 64-bit identity."""
|
||||
identity_path = tmp_path / "instance-id"
|
||||
monkeypatch.delenv("COMFY_MODAL_APP_NAME", raising=False)
|
||||
monkeypatch.setenv("COMFY_MODAL_INSTANCE_ID_PATH", str(identity_path))
|
||||
settings_module.get_settings.cache_clear()
|
||||
try:
|
||||
first_settings = settings_module.get_settings()
|
||||
settings_module.get_settings.cache_clear()
|
||||
second_settings = settings_module.get_settings()
|
||||
finally:
|
||||
settings_module.get_settings.cache_clear()
|
||||
|
||||
assert first_settings.app_name == second_settings.app_name
|
||||
assert re.fullmatch(r"comfy-modal-sync-[A-Za-z0-9_-]{11}", first_settings.app_name)
|
||||
assert len(bytes.fromhex(identity_path.read_text(encoding="ascii").strip())) == 8
|
||||
assert first_settings.interrupt_dict_name == f"{first_settings.app_name}-interrupts"
|
||||
assert first_settings.invocation_dict_name == f"{first_settings.app_name}-invocations"
|
||||
|
||||
|
||||
def test_settings_app_name_override_bypasses_instance_identity(
|
||||
settings_module: Any,
|
||||
monkeypatch: Any,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""An explicit app name should remain authoritative and require no identity file."""
|
||||
identity_path = tmp_path / "instance-id"
|
||||
monkeypatch.setenv("COMFY_MODAL_APP_NAME", "shared-explicit-app")
|
||||
monkeypatch.setenv("COMFY_MODAL_INSTANCE_ID_PATH", str(identity_path))
|
||||
settings_module.get_settings.cache_clear()
|
||||
try:
|
||||
settings = settings_module.get_settings()
|
||||
finally:
|
||||
settings_module.get_settings.cache_clear()
|
||||
|
||||
assert settings.app_name == "shared-explicit-app"
|
||||
assert not identity_path.exists()
|
||||
|
||||
|
||||
def test_settings_cache_tracks_execution_mode_env_changes(
|
||||
settings_module: Any,
|
||||
monkeypatch: Any,
|
||||
|
||||
Reference in New Issue
Block a user