You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG] v2: run absent from UI but its TaskAction CR reconciles forever, recreating the task pod #7926
A v2 run entered a state where the control plane, the backend worker, and the TaskAction CRs disagreed about whether it exists:
The run no longer appeared as running in the console, so there was no way to abort it from the UI.
Its root TaskAction CR (<run>-a0, flyte.org/is-root: "true") remained in the cluster with condition Progressing / reason=Executing for 17 days after creation, never reaching a terminal phase and never being GC'd. Its status phase history cycles Queued → Initializing → Executing repeatedly.
The taskaction-controller kept the task pod alive indefinitely: deleting the pod (exit 137) just had it recreated within seconds.
Meanwhile the backend worker was stuck in a retry loop on a sibling action of the same run:
[flyte] ERROR Failed to launch action: <action-id>, Code: Code.INTERNAL, Details failed to create TaskAction CR <run>-<action-id>: taskactions.flyte.org "<run>-<action-id>" already exists backing off...
[flyte] WARNING [worker-0] Backing off for 10.0 [retry 6/100] on action <action-id> due to error: ...
[<run>][a0] [flyte] WARNING Informer watch retrying, attempt 4/40
[<run>][a0] [flyte] ERROR RPC error: <run>.a0, Error reading content: request or response body error: error reading a body from connection: unexpected EOF during chunk size line
and the controller's events were rejected:
ERROR events Server rejected event (will not retry!) ... Reason:FailedPluginHandle ... Note:Plugin "pod" system error: [Interrupted] Pod failed. No message received from kubernetes.
[<run>-a0-0] terminated with exit code (137). Reason [Error].
The practical impact for us: the orphaned pod carried karpenter.sh/do-not-disrupt, so it blocked a node drain for hours until we manually kubectl delete taskactions -l flyte.org/run=<run> (which also stopped the worker's retry loop).
Expected behavior
When a run is terminal (or no longer known to the control plane), its TaskAction CRs should converge to a terminal phase and be garbage-collected, and the controller should stop recreating pods. At minimum, a run in this state should be visible/abortable so the CRs can be cleaned up through the API rather than kubectl.
Steps to reproduce
Unknown trigger — the run was launched from a schedule and the desync was noticed 17 days later. The informer-watch errors in the logs suggest a dropped watch may have started it. Happy to provide the full TaskAction/pod YAML and larger log excerpts.
It could be possible that this issue arises from a desynchronization between the control plane (Run Service), the TaskAction controller, and the backend worker, especially when the run is no longer present in the control plane but TaskAction CRs persist and are continually reconciled. According to the Flyte v2 architecture, when a run is terminal or deleted, all in-progress descendant actions (TaskAction CRs) should be automatically aborted and garbage-collected, and the controller should not keep recreating pods. However, if the informer or event stream between the control plane and the controller is interrupted or the run is missing from the control plane, the TaskAction controller may not receive the signal to finalize or abort the CR, leading to indefinite reconciliation and pod recreation. This is hinted at in the life of a run and abort/cancel actions documentation.
Though I'm not entirely certain, it might be that the controller should detect when the root run is missing or terminal in the control plane and then mark all associated TaskAction CRs as terminal (e.g., ABORTED or FAILED), triggering garbage collection and stopping pod recreation. The current behavior you describe—where the TaskAction CR cycles phases and the pod is recreated—suggests this detection or reconciliation is not happening as expected, possibly due to a dropped informer watch or a missed event. There is no explicit mention in the data sources of a built-in mechanism for the controller to forcibly finalize orphaned TaskActions when the run is missing, but this would be a reasonable enhancement or bugfix for the controller logic.
Would you like a step-by-step breakdown of how Flyte's run and action lifecycle is supposed to work, or more detail on possible controller-side mitigations?
What the PR does: when a driver pod gets killed and restarts, it can try to re-create child actions that already exist. The task path in Enqueue returned INTERNAL for that instead of ALREADY_EXISTS, so the SDK kept retrying for ~17 min and then marked the child as failed. The PR makes it a no-op, same as the condition path already does.
One thing I noticed while reproducing: this doesn't happen on every restart. On my local devbox I killed the driver, and the pod came back in the same attempt as expected, but the new driver got the existing children from the watch snapshot and never re-enqueued them, so the run just finished fine. It looks like you only hit the already exists error if the watch also fails around that time, which would match the Informer watch retrying / unexpected EOF lines in your logs. I couldn't reproduce that part locally yet, so I'll keep looking into it.
Not covered yet, and I'll follow up separately:
the pod being recreated forever after SIGKILL (the system retry counter keeps resetting)
why the run disappeared from the UI while the CR was still running
@earlybard if you still have it, the CR's full status and the a0 row from the actions table would really help with the last one.
Follow-up on the pod being recreated forever. I reproduced it locally and found the cause. I'd like to agree on the approach before opening a PR, since it changes how system failures are handled.
(Correction to my last comment: #8172 now returns ALREADY_EXISTS to the SDK instead of a no-op.)
Repro
A task that sleeps for an hour, no children. I deleted its pod 5 times with kubectl delete pod --force, waiting for Running before each kill:
Every kill was recorded as a system error (UnexpectedObjectDeletion), but the pod was recreated each time and the task never failed, even after maxSystemFailures (3).
Cause
A pod deletion (plugin_manager.go#L278) or SIGKILL (pod_helper.go#L1657) is a system-retryable failure, so the controller relaunches the pod in place (taskaction_controller.go#L882-L884): SystemFailures goes 0 → 1 and the plugin state is cleared. On the next reconcile, the plugin launches a new pod and returns Queued. That is not a system error, so #L897 resets SystemFailures to 0.
SystemRetries keeps growing but nothing checks it, and Attempts doesn't change, so nothing bounds the loop.
Problem 1: the counter resets before any progress
The #8070 test (#L1522) says "the progress in between puts the consecutive failure count back to zero". But the reset happens on the relaunch's own Queued, so even a pod killed before it ever runs is relaunched forever.
Fix: only reset SystemFailures once the attempt reaches Running or later.
Side effect: transient errors from Plugin.Handle before Running would also start to accumulate. We could apply the new rule everywhere (A), or only after a relaunch (B).
Problem 2: no bound when the pod makes progress between kills
That is my repro and the original report, so Problem 1 alone doesn't fix it. #8070 seems to allow repeated non-consecutive failures on purpose (e.g. Kueue preemptions), so this is a policy question.
Possible fix: a separate maxSystemRetries on total in-place relaunches, configurable, possibly not counting preemptions.
Questions
Do you want a total bound on in-place relaunches (Problem 2)? If so, what default, and should preemptions count?
For Problem 1, is Running the right definition of progress, and A or B?
Hi @earlybard, I opened #8237 for the pod being recreated forever.
For the last part (the run disappearing from the UI while the TaskAction CR was still running), I can't reproduce it locally yet. A few quick questions would help narrow it down, even without logs:
When you say the run disappeared, was it missing from the runs list, or did opening the run's URL directly also fail? If it opened, what status did it show?
Was the run (or any action in it) aborted from the UI/CLI before it disappeared?
If the cluster is still around, the output of: kubectl get taskaction <name> -n <ns> -o jsonpath='{.metadata.labels}{"\n"}{.status.conditions}'
Thanks for digging into this. The TaskAction CRs are gone (I deleted them by run label to unblock the node drain), so I can't give you (3). But the control plane still has the run and our Loki still has that day's logs, and together they answer most of it.
1. It didn't disappear. That was my mistake in the report. The run still opens by URL / flyte get run, and it shows ACTION_PHASE_ABORTED. It had dropped out of the running view, which is where I was looking.
2. Nobody aborted it, as far as I can tell.a0 finished normally, and the pod that ran for 17 days was a re-run of a0, started right after a backend restart. Timeline in UTC, 2026-08-10 unless noted, from flyte get run and Loki:
time
source
event
04:07:32
a0 container
Flyte runtime started for action a0 (schedule trigger s2_every_3h, SDK 2.5.17, backend v2.0.42)
04:42:04
a0 container
Flyte runtime completed for action a0
04:42:06.284
run record
attempt 1 → SUCCEEDED
04:42:06.330
run record
run → ABORTED, abortInfo: {}
05:17:56, 05:47:56
backend
Reconciler error … taskactions.flyte.org "<run>-<action>" not found for ~90 of the run's CRs, including <run>-a0
05:47:57
run record (cluster event)
aborted
06:03:46
backend
backend pod shuts down for a deploy
06:03:58
k8s events
<run>-a0-0 created again, on a new node
06:04:06
a0 container
Flyte runtime started for action a0: the task body runs again from the start. WatchForUpdates/Enqueue get connection refused while the new backend comes up, then Informer cache sync timed out
06:30:15
a0 container
started a third time
→ 08-26
still running until I deleted the CRs
What looks wrong:
The run went ABORTED with an empty abortInfo 2 s after a0 completed on its own, with no abort from us.
After the restart, a0's CR came back even though it had already been deleted (not found at 05:47), and the task re-ran for a run that was already terminal.
The re-run asked for its children again under the same deterministic IDs (e.g. 4324w5siuqbm5tvgxi79re0in ran in the first pass), which is where the already exists loop in my report came from. The informer-watch errors were the re-run losing the backend during the restart, not the cause.
So the trigger may be "restart the backend shortly after a run finishes" rather than a dropped watch. I haven't tried to reproduce it that way yet.
Happy to share the full get run output or more log excerpts if it helps.
Activity
runllm commentedon Aug 27, 2026
It could be possible that this issue arises from a desynchronization between the control plane (Run Service), the TaskAction controller, and the backend worker, especially when the run is no longer present in the control plane but TaskAction CRs persist and are continually reconciled. According to the Flyte v2 architecture, when a run is terminal or deleted, all in-progress descendant actions (TaskAction CRs) should be automatically aborted and garbage-collected, and the controller should not keep recreating pods. However, if the informer or event stream between the control plane and the controller is interrupted or the run is missing from the control plane, the TaskAction controller may not receive the signal to finalize or abort the CR, leading to indefinite reconciliation and pod recreation. This is hinted at in the life of a run and abort/cancel actions documentation.
Though I'm not entirely certain, it might be that the controller should detect when the root run is missing or terminal in the control plane and then mark all associated TaskAction CRs as terminal (e.g., ABORTED or FAILED), triggering garbage collection and stopping pod recreation. The current behavior you describe—where the TaskAction CR cycles phases and the pod is recreated—suggests this detection or reconciliation is not happening as expected, possibly due to a dropped informer watch or a missed event. There is no explicit mention in the data sources of a built-in mechanism for the controller to forcibly finalize orphaned TaskActions when the run is missing, but this would be a reasonable enhancement or bugfix for the controller logic.
Would you like a step-by-step breakdown of how Flyte's run and action lifecycle is supposed to work, or more detail on possible controller-side mitigations?
Sources:
This is an AI-generated response and your feedback is appreciated! Please leave a 👍 if this is helpful and 👎 if it is not.
Mention @Herald to ask follow-up questions. See runllm.com for more!
ChickenBenny commentedon Oct 13, 2026
Hi, I'd like to work on this. I'll start by trying to reproduce it locally and will share what I find here.
ChickenBenny commentedon Oct 13, 2026
I dug into this and opened #8172 as a first fix.
What the PR does: when a driver pod gets killed and restarts, it can try to re-create child actions that already exist. The task path in
EnqueuereturnedINTERNALfor that instead ofALREADY_EXISTS, so the SDK kept retrying for ~17 min and then marked the child as failed. The PR makes it a no-op, same as the condition path already does.One thing I noticed while reproducing: this doesn't happen on every restart. On my local devbox I killed the driver, and the pod came back in the same attempt as expected, but the new driver got the existing children from the watch snapshot and never re-enqueued them, so the run just finished fine. It looks like you only hit the
already existserror if the watch also fails around that time, which would match theInformer watch retrying/unexpected EOFlines in your logs. I couldn't reproduce that part locally yet, so I'll keep looking into it.Not covered yet, and I'll follow up separately:
@earlybard if you still have it, the CR's full
statusand thea0row from theactionstable would really help with the last one.ChickenBenny commentedon Oct 14, 2026
Follow-up on the pod being recreated forever. I reproduced it locally and found the cause. I'd like to agree on the approach before opening a PR, since it changes how system failures are handled.
(Correction to my last comment: #8172 now returns
ALREADY_EXISTSto the SDK instead of a no-op.)Repro
A task that sleeps for an hour, no children. I deleted its pod 5 times with
kubectl delete pod --force, waiting for Running before each kill:Every kill was recorded as a system error (
UnexpectedObjectDeletion), but the pod was recreated each time and the task never failed, even aftermaxSystemFailures(3).Cause
A pod deletion (
plugin_manager.go#L278) or SIGKILL (pod_helper.go#L1657) is a system-retryable failure, so the controller relaunches the pod in place (taskaction_controller.go#L882-L884):SystemFailuresgoes 0 → 1 and the plugin state is cleared. On the next reconcile, the plugin launches a new pod and returnsQueued. That is not a system error, so#L897resetsSystemFailuresto 0.SystemRetrieskeeps growing but nothing checks it, andAttemptsdoesn't change, so nothing bounds the loop.Problem 1: the counter resets before any progress
The #8070 test (
#L1522) says "the progress in between puts the consecutive failure count back to zero". But the reset happens on the relaunch's ownQueued, so even a pod killed before it ever runs is relaunched forever.Fix: only reset
SystemFailuresonce the attempt reachesRunningor later.Side effect: transient errors from
Plugin.HandlebeforeRunningwould also start to accumulate. We could apply the new rule everywhere (A), or only after a relaunch (B).Problem 2: no bound when the pod makes progress between kills
That is my repro and the original report, so Problem 1 alone doesn't fix it. #8070 seems to allow repeated non-consecutive failures on purpose (e.g. Kueue preemptions), so this is a policy question.
Possible fix: a separate
maxSystemRetrieson total in-place relaunches, configurable, possibly not counting preemptions.Questions
Runningthe right definition of progress, and A or B?cc @AdilFayyaz and @Sovietaced. Would appreciate your thoughts.
ChickenBenny commentedon Oct 24, 2026
Hi @earlybard, I opened #8237 for the pod being recreated forever.
For the last part (the run disappearing from the UI while the TaskAction CR was still running), I can't reproduce it locally yet. A few quick questions would help narrow it down, even without logs:
kubectl get taskaction <name> -n <ns> -o jsonpath='{.metadata.labels}{"\n"}{.status.conditions}'earlybard commentedon Oct 29, 2026
Thanks for digging into this. The TaskAction CRs are gone (I deleted them by run label to unblock the node drain), so I can't give you (3). But the control plane still has the run and our Loki still has that day's logs, and together they answer most of it.
1. It didn't disappear. That was my mistake in the report. The run still opens by URL /
flyte get run, and it showsACTION_PHASE_ABORTED. It had dropped out of the running view, which is where I was looking.2. Nobody aborted it, as far as I can tell.
a0finished normally, and the pod that ran for 17 days was a re-run ofa0, started right after a backend restart. Timeline in UTC, 2026-08-10 unless noted, fromflyte get runand Loki:Flyte runtime started for action a0(schedule triggers2_every_3h, SDK 2.5.17, backend v2.0.42)Flyte runtime completed for action a0SUCCEEDEDABORTED,abortInfo: {}Reconciler error … taskactions.flyte.org "<run>-<action>" not foundfor ~90 of the run's CRs, including<run>-a0aborted<run>-a0-0created again, on a new nodeFlyte runtime started for action a0: the task body runs again from the start.WatchForUpdates/Enqueueget connection refused while the new backend comes up, thenInformer cache sync timed outWhat looks wrong:
ABORTEDwith an emptyabortInfo2 s aftera0completed on its own, with no abort from us.a0's CR came back even though it had already been deleted (not foundat 05:47), and the task re-ran for a run that was already terminal.4324w5siuqbm5tvgxi79re0inran in the first pass), which is where thealready existsloop in my report came from. The informer-watch errors were the re-run losing the backend during the restart, not the cause.So the trigger may be "restart the backend shortly after a run finishes" rather than a dropped watch. I haven't tried to reproduce it that way yet.
Happy to share the full
get runoutput or more log excerpts if it helps.