fix: keep the step key in a cached task's identity

This commit is contained in:
Ruben Fiszel
2026-09-16 08:49:18 +02:00
parent 9d7001ddc0
commit 35c349696d
15 changed files with 156 additions and 78 deletions
@@ -1863,3 +1863,28 @@ class TestTaskFingerprint:
)
assert [s["key"] for s in second["steps"]] == ["add_one", "add_one_2"]
assert second["steps"][0]["fn_id"] != first["steps"][0]["fn_id"]
def test_a_builtin_task_still_decorates_and_dispatches(self):
"""The fingerprint is taken for every task, cached or not, so a callable
with neither source nor code object must not break the decorator."""
builtin_task = task(pow)
@workflow
async def wf():
return await builtin_task(2, 3)
result = _run_workflow(wf, {}, {})
assert result["type"] == "dispatch"
assert result["steps"][0]["key"] == "pow"
def test_two_lambdas_on_one_line_are_told_apart(self):
"""``inspect.getsource`` gives each the whole line, so the code's shape is
what separates them, and it must not depend on where the line sits."""
first, second = task(lambda x: x + 1), task(lambda x: x + 2)
@workflow
async def wf():
return await asyncio.gather(first(x=1), second(x=1))
result = _run_workflow(wf, {}, {})
assert result["steps"][0]["fn_id"] != result["steps"][1]["fn_id"]
+20 -10
View File
@@ -3291,14 +3291,23 @@ def _fn_fingerprint(func) -> str:
tasks called at the same position, so neither can tell them apart."""
import hashlib
import inspect
import marshal
# Runs for every task at decoration, cached or not, so it must never raise: a
# builtin has neither source nor code object. Source alone cannot separate two
# lambdas written on one line, so the code's shape goes in as well, without the
# line numbers that would move whenever the file is edited above it.
parts = []
try:
src = inspect.getsource(func).encode()
except (OSError, TypeError):
# No source on disk: the whole code object, constants and names included.
src = marshal.dumps(func.__code__)
return hashlib.sha256(src).hexdigest()
parts.append(inspect.getsource(func).encode())
except Exception:
pass
code = getattr(func, "__code__", None)
if code is not None:
consts = tuple(c for c in code.co_consts if not isinstance(c, type(code)))
parts.append(repr((code.co_code, code.co_names, code.co_varnames, consts)).encode())
if not parts:
parts.append(repr(func).encode())
return hashlib.sha256(b"\x1f".join(parts)).hexdigest()
def task(
@@ -3344,10 +3353,11 @@ def task(
no ``delay`` all go out in a single round.
``cache_ttl`` serves a previous result of the task for that many seconds
instead of running it again. The result is keyed on the task and the
arguments it is called with, so anything a cached task reads from its
closure must be passed in as an argument. It has no effect on a
``task_flow`` target, which keeps its flow's own cache policy.
instead of running it again. The result is keyed on the task, the step it
runs as and the arguments it is called with, so anything a cached task reads
from its closure, the receiver of a bound method included, must be passed in
as an argument. It has no effect on a ``task_flow`` target, which keeps its
flow's own cache policy.
Usage::