<@ULVA73B9P> Does task caching survive a parent-fl...
# ask-marvin
g
@Marvin Does task caching survive a parent-flow SIGTERM under default LAZY commit? Prefect 3.6.22, K8s worker. Long-running parent flow with subflows; inside subflow, a cached task submits an OpenAI batch: @task(persist_result=True, result_storage=GcsBucket(...), cache_policy=Inputs(exclude=["client"])) def submit_batch_task(client, jsonl_uri, config: BatchConfig, ...): ... K8s SIGTERMs the pod mid-poll_status_task sleep. Prefect reschedules (same flow_run_id). The new attempt re-submits a fresh batch. Every submit_batch task run shows cache_key=null and state.name="RolledBack" ("Task rolled back as part of transaction") — including tasks that completed successfully before the crash. Verified Inputs(...).compute_key(inputs={...}) returns a stable hash in isolation, so the policy works. My read of prefect/transactions.py: task transactions inherit commit_mode=LAZY from parent (L194-195), and on exit defer commit to the parent (L285-287). Parent flow only commits at end of run. So if the parent process dies before completion, no in-flight transaction commits → cache_key never persisted → no cache hit on rerun. Fix I applied: wrap the call site to force EAGER: with transaction(commit_mode=CommitMode.EAGER): batch_id = submit_batch_task(...) Questions: 1. Is that diagnosis correct? Under default LAZY, does a completed task inside an unfinished flow run truly fail to persist its cache record? 2. Is transaction(commit_mode=EAGER) at the call site the idiomatic way to make task caching survive a parent crash, or is there a more native pattern (per-task option, flow-level setting) I missed? 3. Any sharp edges with EAGER child inside LAZY parent (rollback hooks, state transitions, isolation)? 4. For long-running flows needing crash-recovery caching — is the official answer "use external idempotency, not Prefect cache"?
m
thought for 184.7 seconds
Short answer: your diagnosis is right, and your fix is the recommended pattern. Here’s a point-by-point response: 1) Does a completed task inside an unfinished flow fail to persist its cache under default LAZY? - Yes. In Prefect 3.x, transactions default to LAZY and child transactions inherit the parent’s commit mode. Task result writes and cache records are staged and only persisted when the parent transaction commits. If the parent flow process dies (SIGTERM) before the parent transaction exits successfully, child task transactions never commit, so: - Those task runs end up with state.name="RolledBack" - No cache record is written (hence
cache_key=null
) - This matches what you’re seeing. References: - Transactions - Caching - States (RolledBack) 2) Is using transaction(commit_mode=EAGER) at the call site idiomatic? - Yes. Wrapping the critical call(s) in an EAGER transaction is the intended way to force early persistence of task results and cache records, independent of the parent’s eventual success/failure. Example:
Copy code
from prefect.transactions import transaction, CommitMode

with transaction(commit_mode=CommitMode.EAGER):
    batch_id = submit_batch_task(...)
- You can scope this narrowly around just the side-effectful or expensive step(s) whose cache/result you need to survive a crash. There are no per-task or flow-level decorator options to set commit mode. Commit mode is controlled via explicit transaction contexts. 3) Any sharp edges with an EAGER child inside a LAZY parent? - Expected/normal behavior: - The EAGER child commits immediately on exiting its context; those results and caches persist even if the parent later rolls back or crashes. - Caveats to be aware of: - Partial commit: if the parent later fails, you can end up with some steps persisted (the EAGER ones) and others not. Use EAGER only where you want that durability. - Process-kill window: if the pod is killed before the EAGER block exits, the commit still won’t occur. Keeping the EAGER block small reduces this window. - Rollback hooks on the parent will not undo already-committed EAGER results or external side effects. - Make sure the EAGER’d tasks are idempotent or harmless to re-use via cache on retry. 4) For long-running flows needing crash-recovery caching — should I rely on external idempotency instead? - Prefer a combination: - Use provider-level idempotency for external side effects (e.g., OpenAI idempotency keys) so duplicates are naturally rejected. - Use Prefect task caching with
persist_result=True
plus a stable
cache_policy
, and wrap critical steps in EAGER transactions so the cache persists mid-run. - Optionally restructure long workflows into smaller, independently-executing flow runs (e.g., subflows as separate deployments) to narrow transactional blast radius and commit durable boundaries more frequently. Practical patterns: - Wrap only the submission step in EAGER:
Copy code
with transaction(commit_mode=CommitMode.EAGER):
    batch_id = submit_batch_task(...)
- Persist the submission receipt (e.g.,
batch_id
) to a durable store (GCS/DB) that you can re-read on retry to avoid re-submission. - Use OpenAI (or other API) idempotency tokens keyed on your inputs so a double-submit is a no-op server-side. If you want, I can help sketch a minimal example showing: - First attempt submits once under EAGER (persisted) - Parent crashes - Second attempt reads the cache or external receipt and skips resubmission Docs for further reading: - Transactions - Caching - Cache workflow steps - States (including RolledBack)