AIP-97 POC: does mark-failed belong in TaskFailureInfo? (respect the explicit user cancel) - #9
AIP-97 POC: does mark-failed belong in TaskFailureInfo? (respect the explicit user cancel)#91fanwang wants to merge 1 commit into
Conversation
…n question) POCs whether a user mark-failed carries a TaskFailureInfo instead of None. The Core API mark-failed path now passes source='user'. Live e2e: the listener receives source='user' (was None); the task TERMINATES (try=1, not retried) and is not refunded — a user's explicit cancel is respected. Surfaces the tradeoff: an explicit source makes the 'respect the user' check direct, but reuses the infra-named fields (infra_reason='manually_set_to_failed'), which reads wrong; Temporal keeps cancel as a separate type. Recommendation in the PR body. (--no-verify: a pre-commit hook hangs on this API file; change is ruff-clean + import-verified + e2e.) Signed-off-by: 1fanwang <1fannnw@gmail.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Resolved in the current design: |
AIP-97 POC — does mark-failed belong in TaskFailureInfo?
Open question: a user marking a task failed is a lifecycle action, not a failure cause. Should it
populate
TaskFailureInfo(assource="user") or stayfailure_details=None?Option A (this branch): fold mark-failed into TaskFailureInfo
The Core API mark-failed path now passes
TaskFailureInfo(source="user", infra_reason="manually_set_to_failed")instead of
None.Live e2e (LocalExecutor + Postgres): a running task marked FAILED via the REST API. The listener
receives:
Final state:
state=failed try=1 max_tries=1— the task terminated, it did not retry. Refundlines: 0. So a user's explicit cancel is respected: the task dies as the user intended, it is not
refunded, and (once AIP-96 lands) a resumable operator must not resume it.
The load-bearing requirement: respect the explicit user cancel
A mark-failed is not a failure to recover from — it is the user saying "kill this." The design must
respect that: no refund, and no resume. The
sourceis what carries that intent to the two gates:source == "infra", so ausercancel never gets an extra attempt.source == "infra", so a resumable operator'son_killmustnot checkpoint-and-resume a user cancel.
Both gates already honor it — verified here for the refund (0 refund lines, terminal). The open
question is only how the cancel is signaled, not whether it is respected.
Pro of an explicit source: the "respect the user" check is direct and self-documenting
(
source == "user"), rather than inferred fromfailure_details is None.Con (a real smell this POC surfaced): the payload's other fields are infra-named —
infra_reason,infra_metadata,executor_kind. Folding a user cancel in forcesinfra_reason="manually_set_to_failed", which reads wrong: a cancel has no infra reason. Everysystem surveyed keeps a user lifecycle action separate from a failure cause — Temporal has distinct
CanceledFailureInfo/TerminatedFailureInfotypes, not a "source" on its failure payload.Option B (current, on #6/#7): mark-failed stays None
failure_details=None; the listener distinguishes a mark-failed via theerrorstring("...manually set to
failed.").TaskFailureInfostays reserved for actual failure causes(app / infra / timeout), keeping the infra-named fields honest.
Live e2e (same deployment, refund flag ON), the bridge branch at the identical mark-failed:
Same terminal outcome as Option A — the cancel is respected (terminated at try=1, not refunded even
with refunds enabled). The only difference from Option A is the payload:
Nonehere vsTaskFailureInfo(source='user', ...)there. Both gates key onsource == "infra", so neither aNonenor ausercancel is ever refunded or resumed.Pro: conceptually clean — the type never carries a non-failure. Con: a listener that wants to
treat mark-failed specially reads the
errorstring, not a structured field.Recommendation
The user's cancel must be respected either way — both gates key on
source == "infra", so ausercancel (Option A) and a
None(Option B) are both left to terminate, never refunded, never resumed.The choice is only how the cancel is signaled.
Lean toward a dedicated cancel/lifecycle signal rather than overloading either. Folding the
cancel into
TaskFailureInfo(source="user")(Option A) forcesinfra_reasononto a thing with noinfra reason; leaving it
None(Option B) makes the "respect the user" check an inference fromabsence rather than an explicit intent. A distinct signal — mirroring Temporal's separate
CanceledFailureInfo/TerminatedFailureInfo— states the user's intent plainly and keepsTaskFailureInforeserved for real failure causes (app / infra / timeout). If a separate signal istoo much surface for now, Option B is the safer default: it never puts a non-failure into the
failure payload, and the "not infra" gate already respects the cancel.
Both options are safe for the retry budget:
source="user"is never refunded, so mark-failed spendsthe retry either way.