From c1ce00287cb4613e53b2c3a15362242c1e3058ac Mon Sep 17 00:00:00 2001 From: xmarre Date: Mon, 16 Mar 2026 19:21:24 +0100 Subject: [PATCH] Stop requeueing live containers --- comfy_execution/caching.py | 41 +++++++++++------------ tests-unit/execution_test/caching_test.py | 20 +++++++++++ 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/comfy_execution/caching.py b/comfy_execution/caching.py index 00a3444ac..c971a29f2 100644 --- a/comfy_execution/caching.py +++ b/comfy_execution/caching.py @@ -311,26 +311,34 @@ def to_hashable(obj, max_nodes=_MAX_SIGNATURE_CONTAINER_VISITS): return (container_tag, tuple(value for _, value in ordered_items)) while work_stack: - current, expanded = work_stack.pop() - current_type = type(current) + entry = work_stack.pop() + if len(entry) == 3: + _, current_id, current_type = entry + current = None + expanded = True + else: + current, expanded = entry + current_type = type(current) + current_id = id(current) - if current_type in _PRIMITIVE_SIGNATURE_TYPES or current_type is Unhashable: + if not expanded and (current_type in _PRIMITIVE_SIGNATURE_TYPES or current_type is Unhashable): continue - if current_type not in _CONTAINER_SIGNATURE_TYPES: - memo[id(current)] = Unhashable() + if not expanded and current_type not in _CONTAINER_SIGNATURE_TYPES: + memo[current_id] = Unhashable() continue - current_id = id(current) if current_id in memo: continue if expanded: active.discard(current_id) try: + items = snapshots.pop(current_id, None) + if items is None: + memo[current_id] = Unhashable() + continue + if current_type is dict: - items = snapshots.pop(current_id, None) - if items is None: - items = list(current.items()) ordered_items = [ (_sanitized_sort_key(k, memo=sort_memo), k, resolve_value(v)) for k, v in items @@ -349,32 +357,20 @@ def to_hashable(obj, max_nodes=_MAX_SIGNATURE_CONTAINER_VISITS): tuple((key, value) for _, key, value in ordered_items), ) elif current_type is list: - items = snapshots.pop(current_id, None) - if items is None: - items = list(current) resolved_items = tuple(resolve_value(item) for item in items) if any(is_failed(item) for item in resolved_items): memo[current_id] = Unhashable() else: memo[current_id] = ("list", resolved_items) elif current_type is tuple: - items = snapshots.pop(current_id, None) - if items is None: - items = list(current) resolved_items = tuple(resolve_value(item) for item in items) if any(is_failed(item) for item in resolved_items): memo[current_id] = Unhashable() else: memo[current_id] = ("tuple", resolved_items) elif current_type is set: - items = snapshots.pop(current_id, None) - if items is None: - items = list(current) memo[current_id] = resolve_unordered_values(items, "set") else: - items = snapshots.pop(current_id, None) - if items is None: - items = list(current) memo[current_id] = resolve_unordered_values(items, "frozenset") except RuntimeError: memo[current_id] = Unhashable() @@ -389,7 +385,6 @@ def to_hashable(obj, max_nodes=_MAX_SIGNATURE_CONTAINER_VISITS): return Unhashable() active.add(current_id) - work_stack.append((current, True)) if current_type is dict: try: items = list(current.items()) @@ -405,6 +400,7 @@ def to_hashable(obj, max_nodes=_MAX_SIGNATURE_CONTAINER_VISITS): active.discard(current_id) break else: + work_stack.append(("EXPANDED", current_id, current_type)) for _, value in reversed(items): work_stack.append((value, False)) continue @@ -417,6 +413,7 @@ def to_hashable(obj, max_nodes=_MAX_SIGNATURE_CONTAINER_VISITS): memo[current_id] = Unhashable() active.discard(current_id) continue + work_stack.append(("EXPANDED", current_id, current_type)) for item in reversed(items): work_stack.append((item, False)) diff --git a/tests-unit/execution_test/caching_test.py b/tests-unit/execution_test/caching_test.py index 36d5b0688..e11e01285 100644 --- a/tests-unit/execution_test/caching_test.py +++ b/tests-unit/execution_test/caching_test.py @@ -205,6 +205,26 @@ def test_to_hashable_handles_shared_builtin_substructures(caching_module): assert hashable[1][0][0] == "list" +def test_to_hashable_uses_parent_snapshot_during_expanded_phase(caching_module, monkeypatch): + """Expanded-phase assembly should not reread a live parent container after snapshotting.""" + caching, _ = caching_module + original_sort_key = caching._sanitized_sort_key + outer = [{"marker"}, 2] + + def mutating_sort_key(obj, *args, **kwargs): + """Mutate the live parent while a child container is being canonicalized.""" + if obj == "marker": + outer[1] = 3 + return original_sort_key(obj, *args, **kwargs) + + monkeypatch.setattr(caching, "_sanitized_sort_key", mutating_sort_key) + + hashable = caching.to_hashable(outer) + + assert hashable == ("list", (("set", ("marker",)), 2)) + assert outer[1] == 3 + + def test_to_hashable_fails_closed_for_ordered_container_with_opaque_child(caching_module): """Ordered containers should fail closed when a child cannot be canonicalized.""" caching, _ = caching_module