Enforce execution_timeout in deferrable KubernetesPodOperator - #67229

Merged
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout
May 30, 2026
Merged

Enforce execution_timeout in deferrable KubernetesPodOperator#67229
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout

Conversation

@paultmathew

Copy link
Copy Markdown
Contributor

Why + What

KubernetesPodOperator(deferrable=True) does not enforce execution_timeout. Once the operator defers, the synchronous execute() returns and the signal.alarm-based timeout context wrapping it exits cleanly — there is no further execution_timeout enforcement for the lifetime of the deferral. Pods continue running well past execution_timeout, bounded only by active_deadline_seconds (which defaults to ~1h or whatever the operator passed).

The framework gap is acknowledged by # TODO: handle timeout in case of deferral at task-sdk/.../task_runner.py:1782.

This PR fixes the symptom for KubernetesPodOperator, mirroring the pattern already merged for AirbyteTriggerSyncOperator (PR #64051) and DbtCloudRunJobOperator (PR #66449).

Approach

  1. Operator (pod.py): in invoke_defer_method, translate execution_timeout into an absolute deadline anchored on ti.start_date:

    • execution_deadline = ti.start_date.timestamp() + execution_timeout.total_seconds()
    • Pass execution_deadline to KubernetesPodTrigger.
    • Pass timeout=remaining (timedelta) to self.defer() so the framework's trigger_timeout also bounds the trigger lifetime as a backstop.
    • Anchoring on ti.start_date keeps the deadline stable across re-deferrals (e.g. logging_interval re-entries), since Airflow preserves the original start_date when a task resumes from defer.
    • Re-pass context from trigger_reentryinvoke_defer_method so the deadline is recomputed correctly on each re-defer.
  2. Trigger (pod.py): at the top of _wait_for_container_completion, check time.time() >= execution_deadline and emit a status="timeout" event when the deadline is crossed. The operator's existing trigger_reentry terminal-event path already handles status in ("error", "failed", "timeout", "success") — the operator fails the task and _clean() runs on_finish_action (default: delete pod).

Impact

  • Existing behaviour preserved: execution_timeout was previously a no-op for deferred KPO tasks, and remains a no-op when not set. Tasks without execution_timeout see no behaviour change (execution_deadline=None, defer.timeout=None).
  • No public API changes: the new execution_deadline parameter on KubernetesPodTrigger is keyword-only with a None default. Trigger serialization adds the field but defaults preserve back-compat for existing serialized triggers (the trigger's __init__ accepts the kwarg as optional).
  • Pod cleanup: the existing on_finish_action path handles pod deletion (default delete_pod) when the operator fails on a timeout event. _clean() already special-cases event["status"] == "timeout" to skip await_pod_completion (the pod may hang on ErrImagePull/ContainerCreating).

Tests

  • Trigger (tests/unit/cncf/kubernetes/triggers/test_pod.py):
    • Updated test_serialize to include the new execution_deadline key.
    • Added test_serialize_with_execution_deadline — round-trips a non-None deadline.
    • Added test_run_loop_emits_timeout_event_when_execution_deadline_reached — past-deadline → first iteration emits status="timeout" event.
    • Added test_run_loop_does_not_emit_timeout_when_execution_deadline_not_reached — far-future deadline → trigger keeps polling normally.
  • Operator (tests/unit/cncf/kubernetes/operators/test_pod.py):
    • Added test_invoke_defer_method_passes_execution_deadline_when_execution_timeout_set — operator with execution_timeout=300s passes a deadline ≈ ti.start_date + 300s to the trigger; defer.timeout is set.
    • Added test_invoke_defer_method_passes_no_deadline_when_execution_timeout_not_set — operator without execution_timeout passes None (no enforcement, no behaviour change).

Backwards Compatibility

No public API changes. New execution_deadline parameter on KubernetesPodTrigger is optional with default None. Behaviour change: execution_timeout-equipped deferred KPO tasks now actually fail at the configured timeout instead of running indefinitely; this is the documented contract.

Closes

Closes: #67227

@boring-cyborgboring-cyborgBot added area:providers provider:cncf-kubernetes Kubernetes (k8s) provider related issues labels May 20, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 4eb5810 to fb5e3fbCompareMay 20, 2026 14:18
@paultmathew
paultmathew marked this pull request as ready for review May 20, 2026 15:27
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 5151d22 to fb5e3fbCompareMay 20, 2026 15:57

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the extension, looks good to me. Except some comments.

Comment threadproviders/amazon/src/airflow/providers/amazon/aws/triggers/eks.py Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enforces execution_timeout for KubernetesPodOperator(deferrable=True) by translating the timeout into an absolute deadline, passing it to KubernetesPodTrigger, and adding trigger-side logic to emit a terminal timeout event when the deadline is exceeded (with accompanying unit tests). It also updates the EKS-specific trigger subclass to forward the new parameter.

Changes:

  • Add execution_deadline plumbing from KubernetesPodOperator.invoke_defer_method() to KubernetesPodTrigger and pass a timeout= to defer() based on remaining budget.
  • Add trigger-side deadline enforcement that emits a status="timeout" event once the deadline is crossed.
  • Extend/adjust unit tests for trigger serialization and timeout behavior, plus operator deferral plumbing.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/pod.pyCompute an absolute execution deadline from ti.start_date and execution_timeout, pass it to the trigger, and set defer(timeout=…).
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/triggers/pod.pyAdd execution_deadline to trigger init/serialization and emit a timeout TriggerEvent when the deadline is exceeded.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.pyAdd tests asserting the operator passes execution_deadline (or None) into the trigger and sets defer.timeout appropriately.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.pyUpdate serialization expectations and add trigger run-loop tests for deadline timeout vs. continued polling.
providers/amazon/src/airflow/providers/amazon/aws/triggers/eks.pyForward the new execution_deadline parameter through EksPodTrigger to the base Kubernetes trigger.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from fb5e3fb to 8dfa1afCompareMay 20, 2026 23:45
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch 3 times, most recently from 2da1d91 to 6eec8cfCompareMay 22, 2026 18:08
@jscheffl

Copy link
Copy Markdown
Contributor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

Can you please resolve the comments addressed? And in the ones not being addressed reply a comment?

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 6eec8cf to ad266d8CompareMay 24, 2026 20:01

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine overall. I have left some comments.

Also, I would urge you to verify your implementation using a live cluster to ensure that it fixes the issue. Passing a non-None value as an argument for the timeout parameter in the defer method resulted in the exact same bug remaining unresolved in the DBT Cloud and Airbyte providers. Arguably, your approach is more robust but it still needs live verification to be sure.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 26, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from ad266d8 to 972a05fCompareMay 26, 2026 02:41
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl@SameerMesiah97 thank you both for the reviews.

Pulled the **kwargs revert and any new surface on KubernetesPodTrigger. The deadline now rides inside the existing trigger_kwargs dict under the reserved key _execution_deadline. The leading-underscore convention follows the _redefer_count precedent in the same file.

@SameerMesiah97 you were right — the previous 60s minimum clamp didn't account for poll_interval. With execution_timeout=30s, poll_interval=60s, the framework would have cancelled the trigger at T+60 before its next deadline check at T+65.

The latest commit guarantees defer.timeout always covers remaining + ≥ 2 poll cycles. New unit test pins it: test_invoke_defer_method_pads_defer_timeout_for_slow_poll_interval verifies defer.timeout = 30 + 120 = 150s for a poll_interval=60 setup. I also did a smoke test against a live Kubernetes cluster (EKS) in our sandbox environment.

Smoke test summary

TaskConfigurationResultWhat it proved
t1 happy pathexecution_timeout=5m, sleep 20s✓ success at ~26s, pod deletedDeferred path works end-to-end on real K8s; deadline doesn't trip prematurely
t2 timeout default pollexecution_timeout=30s, poll_interval=2s, sleep 600s✓ failed at ~64s with "Execution deadline reached for pod ... emitting timeout event", pod deletedNew code path fires; _clean() runs; trigger emits soft-timeout event
t3 timeout slow pollexecution_timeout=30s, poll_interval=30s, sleep 600s✓ failed at ~70s, timeout event at +38s, pod deletedThe reviewer's exact concern — slow poll doesn't break cleanup; new poll_buffer = max(60, poll_interval * 2) gives the trigger sufficient runway
t4 no timeoutno execution_timeout, sleep 20s✓ success at ~26s, no deadline messagesOpt-out path preserved; tasks without execution_timeout continue to work as before

Compute the deadline operator-side from ti.start_date + execution_timeout
and plumb it to KubernetesPodTrigger via trigger_kwargs["_execution_deadline"]
(an existing dict already accepted and serialized by every subclass) so
the trigger can short-circuit and emit its own status="timeout" event.
This routes timeout through trigger_reentry → _clean() → pod deletion +
final log capture, matching the success/failure event path.
defer.timeout is set to the remaining budget with a 60s minimum so the
trigger has runway to emit its own timeout event before the framework
backstop fires (which would otherwise short-circuit to TaskDeferralTimeout
and skip the operator's cleanup).
Following the leading-underscore convention established by _redefer_count
in the same file: trigger_kwargs is the only existing operator -> trigger
plumbing that's a generic, untyped, dict-shaped, fully-serialized bag and
already accepted by every KubernetesPodTrigger subclass. Using it avoids
adding a new __init__ kwarg or serialize() field on KubernetesPodTrigger,
keeping cross-version compatibility with subclasses (e.g. EksPodTrigger,
GKEStartPodTrigger) released independently.
Closes: apache#67227
Co-authored-by: Cursor <cursoragent@cursor.com>
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 972a05f to d762e65CompareMay 26, 2026 03:23

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. @SameerMesiah97 another pass of review or any other maintainer feedback?

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. I must mention that your approach to add a buffer is a good compromise (which I will implement myself going forwards). Unfortunately, there is a gap at the framework level regarding the implementation of execution timeouts that has not been addressed as of yet so we must use temporary workarounds like this.

@jscheffl
jscheffl merged commit 3d3e79d into apache:mainMay 30, 2026
113 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:cncf-kubernetesKubernetes (k8s) provider related issuesready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KubernetesPodOperator does not enforce execution_timeout semantics in Deferrable mode

5 participants

@paultmathew@jscheffl@SameerMesiah97@potiuk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Enforce execution_timeout in deferrable KubernetesPodOperator - #67229

Merged
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout
May 30, 2026
Merged

Enforce execution_timeout in deferrable KubernetesPodOperator#67229
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout

Conversation

@paultmathew

Copy link
Copy Markdown
Contributor

Why + What

KubernetesPodOperator(deferrable=True) does not enforce execution_timeout. Once the operator defers, the synchronous execute() returns and the signal.alarm-based timeout context wrapping it exits cleanly — there is no further execution_timeout enforcement for the lifetime of the deferral. Pods continue running well past execution_timeout, bounded only by active_deadline_seconds (which defaults to ~1h or whatever the operator passed).

The framework gap is acknowledged by # TODO: handle timeout in case of deferral at task-sdk/.../task_runner.py:1782.

This PR fixes the symptom for KubernetesPodOperator, mirroring the pattern already merged for AirbyteTriggerSyncOperator (PR #64051) and DbtCloudRunJobOperator (PR #66449).

Approach

  1. Operator (pod.py): in invoke_defer_method, translate execution_timeout into an absolute deadline anchored on ti.start_date:

    • execution_deadline = ti.start_date.timestamp() + execution_timeout.total_seconds()
    • Pass execution_deadline to KubernetesPodTrigger.
    • Pass timeout=remaining (timedelta) to self.defer() so the framework's trigger_timeout also bounds the trigger lifetime as a backstop.
    • Anchoring on ti.start_date keeps the deadline stable across re-deferrals (e.g. logging_interval re-entries), since Airflow preserves the original start_date when a task resumes from defer.
    • Re-pass context from trigger_reentryinvoke_defer_method so the deadline is recomputed correctly on each re-defer.
  2. Trigger (pod.py): at the top of _wait_for_container_completion, check time.time() >= execution_deadline and emit a status="timeout" event when the deadline is crossed. The operator's existing trigger_reentry terminal-event path already handles status in ("error", "failed", "timeout", "success") — the operator fails the task and _clean() runs on_finish_action (default: delete pod).

Impact

  • Existing behaviour preserved: execution_timeout was previously a no-op for deferred KPO tasks, and remains a no-op when not set. Tasks without execution_timeout see no behaviour change (execution_deadline=None, defer.timeout=None).
  • No public API changes: the new execution_deadline parameter on KubernetesPodTrigger is keyword-only with a None default. Trigger serialization adds the field but defaults preserve back-compat for existing serialized triggers (the trigger's __init__ accepts the kwarg as optional).
  • Pod cleanup: the existing on_finish_action path handles pod deletion (default delete_pod) when the operator fails on a timeout event. _clean() already special-cases event["status"] == "timeout" to skip await_pod_completion (the pod may hang on ErrImagePull/ContainerCreating).

Tests

  • Trigger (tests/unit/cncf/kubernetes/triggers/test_pod.py):
    • Updated test_serialize to include the new execution_deadline key.
    • Added test_serialize_with_execution_deadline — round-trips a non-None deadline.
    • Added test_run_loop_emits_timeout_event_when_execution_deadline_reached — past-deadline → first iteration emits status="timeout" event.
    • Added test_run_loop_does_not_emit_timeout_when_execution_deadline_not_reached — far-future deadline → trigger keeps polling normally.
  • Operator (tests/unit/cncf/kubernetes/operators/test_pod.py):
    • Added test_invoke_defer_method_passes_execution_deadline_when_execution_timeout_set — operator with execution_timeout=300s passes a deadline ≈ ti.start_date + 300s to the trigger; defer.timeout is set.
    • Added test_invoke_defer_method_passes_no_deadline_when_execution_timeout_not_set — operator without execution_timeout passes None (no enforcement, no behaviour change).

Backwards Compatibility

No public API changes. New execution_deadline parameter on KubernetesPodTrigger is optional with default None. Behaviour change: execution_timeout-equipped deferred KPO tasks now actually fail at the configured timeout instead of running indefinitely; this is the documented contract.

Closes

Closes: #67227

@boring-cyborgboring-cyborgBot added area:providers provider:cncf-kubernetes Kubernetes (k8s) provider related issues labels May 20, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 4eb5810 to fb5e3fbCompareMay 20, 2026 14:18
@paultmathew
paultmathew marked this pull request as ready for review May 20, 2026 15:27
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 5151d22 to fb5e3fbCompareMay 20, 2026 15:57

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the extension, looks good to me. Except some comments.

Comment threadproviders/amazon/src/airflow/providers/amazon/aws/triggers/eks.py Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enforces execution_timeout for KubernetesPodOperator(deferrable=True) by translating the timeout into an absolute deadline, passing it to KubernetesPodTrigger, and adding trigger-side logic to emit a terminal timeout event when the deadline is exceeded (with accompanying unit tests). It also updates the EKS-specific trigger subclass to forward the new parameter.

Changes:

  • Add execution_deadline plumbing from KubernetesPodOperator.invoke_defer_method() to KubernetesPodTrigger and pass a timeout= to defer() based on remaining budget.
  • Add trigger-side deadline enforcement that emits a status="timeout" event once the deadline is crossed.
  • Extend/adjust unit tests for trigger serialization and timeout behavior, plus operator deferral plumbing.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/pod.pyCompute an absolute execution deadline from ti.start_date and execution_timeout, pass it to the trigger, and set defer(timeout=…).
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/triggers/pod.pyAdd execution_deadline to trigger init/serialization and emit a timeout TriggerEvent when the deadline is exceeded.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.pyAdd tests asserting the operator passes execution_deadline (or None) into the trigger and sets defer.timeout appropriately.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.pyUpdate serialization expectations and add trigger run-loop tests for deadline timeout vs. continued polling.
providers/amazon/src/airflow/providers/amazon/aws/triggers/eks.pyForward the new execution_deadline parameter through EksPodTrigger to the base Kubernetes trigger.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from fb5e3fb to 8dfa1afCompareMay 20, 2026 23:45
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch 3 times, most recently from 2da1d91 to 6eec8cfCompareMay 22, 2026 18:08
@jscheffl

Copy link
Copy Markdown
Contributor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

Can you please resolve the comments addressed? And in the ones not being addressed reply a comment?

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 6eec8cf to ad266d8CompareMay 24, 2026 20:01

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine overall. I have left some comments.

Also, I would urge you to verify your implementation using a live cluster to ensure that it fixes the issue. Passing a non-None value as an argument for the timeout parameter in the defer method resulted in the exact same bug remaining unresolved in the DBT Cloud and Airbyte providers. Arguably, your approach is more robust but it still needs live verification to be sure.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 26, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from ad266d8 to 972a05fCompareMay 26, 2026 02:41
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl@SameerMesiah97 thank you both for the reviews.

Pulled the **kwargs revert and any new surface on KubernetesPodTrigger. The deadline now rides inside the existing trigger_kwargs dict under the reserved key _execution_deadline. The leading-underscore convention follows the _redefer_count precedent in the same file.

@SameerMesiah97 you were right — the previous 60s minimum clamp didn't account for poll_interval. With execution_timeout=30s, poll_interval=60s, the framework would have cancelled the trigger at T+60 before its next deadline check at T+65.

The latest commit guarantees defer.timeout always covers remaining + ≥ 2 poll cycles. New unit test pins it: test_invoke_defer_method_pads_defer_timeout_for_slow_poll_interval verifies defer.timeout = 30 + 120 = 150s for a poll_interval=60 setup. I also did a smoke test against a live Kubernetes cluster (EKS) in our sandbox environment.

Smoke test summary

TaskConfigurationResultWhat it proved
t1 happy pathexecution_timeout=5m, sleep 20s✓ success at ~26s, pod deletedDeferred path works end-to-end on real K8s; deadline doesn't trip prematurely
t2 timeout default pollexecution_timeout=30s, poll_interval=2s, sleep 600s✓ failed at ~64s with "Execution deadline reached for pod ... emitting timeout event", pod deletedNew code path fires; _clean() runs; trigger emits soft-timeout event
t3 timeout slow pollexecution_timeout=30s, poll_interval=30s, sleep 600s✓ failed at ~70s, timeout event at +38s, pod deletedThe reviewer's exact concern — slow poll doesn't break cleanup; new poll_buffer = max(60, poll_interval * 2) gives the trigger sufficient runway
t4 no timeoutno execution_timeout, sleep 20s✓ success at ~26s, no deadline messagesOpt-out path preserved; tasks without execution_timeout continue to work as before

Compute the deadline operator-side from ti.start_date + execution_timeout
and plumb it to KubernetesPodTrigger via trigger_kwargs["_execution_deadline"]
(an existing dict already accepted and serialized by every subclass) so
the trigger can short-circuit and emit its own status="timeout" event.
This routes timeout through trigger_reentry → _clean() → pod deletion +
final log capture, matching the success/failure event path.
defer.timeout is set to the remaining budget with a 60s minimum so the
trigger has runway to emit its own timeout event before the framework
backstop fires (which would otherwise short-circuit to TaskDeferralTimeout
and skip the operator's cleanup).
Following the leading-underscore convention established by _redefer_count
in the same file: trigger_kwargs is the only existing operator -> trigger
plumbing that's a generic, untyped, dict-shaped, fully-serialized bag and
already accepted by every KubernetesPodTrigger subclass. Using it avoids
adding a new __init__ kwarg or serialize() field on KubernetesPodTrigger,
keeping cross-version compatibility with subclasses (e.g. EksPodTrigger,
GKEStartPodTrigger) released independently.
Closes: apache#67227
Co-authored-by: Cursor <cursoragent@cursor.com>
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 972a05f to d762e65CompareMay 26, 2026 03:23

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. @SameerMesiah97 another pass of review or any other maintainer feedback?

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. I must mention that your approach to add a buffer is a good compromise (which I will implement myself going forwards). Unfortunately, there is a gap at the framework level regarding the implementation of execution timeouts that has not been addressed as of yet so we must use temporary workarounds like this.

@jscheffl
jscheffl merged commit 3d3e79d into apache:mainMay 30, 2026
113 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:cncf-kubernetesKubernetes (k8s) provider related issuesready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KubernetesPodOperator does not enforce execution_timeout semantics in Deferrable mode

5 participants

@paultmathew@jscheffl@SameerMesiah97@potiuk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Enforce execution_timeout in deferrable KubernetesPodOperator - #67229

Merged
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout
May 30, 2026
Merged

Enforce execution_timeout in deferrable KubernetesPodOperator#67229
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout

Conversation

@paultmathew

Copy link
Copy Markdown
Contributor

Why + What

KubernetesPodOperator(deferrable=True) does not enforce execution_timeout. Once the operator defers, the synchronous execute() returns and the signal.alarm-based timeout context wrapping it exits cleanly — there is no further execution_timeout enforcement for the lifetime of the deferral. Pods continue running well past execution_timeout, bounded only by active_deadline_seconds (which defaults to ~1h or whatever the operator passed).

The framework gap is acknowledged by # TODO: handle timeout in case of deferral at task-sdk/.../task_runner.py:1782.

This PR fixes the symptom for KubernetesPodOperator, mirroring the pattern already merged for AirbyteTriggerSyncOperator (PR #64051) and DbtCloudRunJobOperator (PR #66449).

Approach

  1. Operator (pod.py): in invoke_defer_method, translate execution_timeout into an absolute deadline anchored on ti.start_date:

    • execution_deadline = ti.start_date.timestamp() + execution_timeout.total_seconds()
    • Pass execution_deadline to KubernetesPodTrigger.
    • Pass timeout=remaining (timedelta) to self.defer() so the framework's trigger_timeout also bounds the trigger lifetime as a backstop.
    • Anchoring on ti.start_date keeps the deadline stable across re-deferrals (e.g. logging_interval re-entries), since Airflow preserves the original start_date when a task resumes from defer.
    • Re-pass context from trigger_reentryinvoke_defer_method so the deadline is recomputed correctly on each re-defer.
  2. Trigger (pod.py): at the top of _wait_for_container_completion, check time.time() >= execution_deadline and emit a status="timeout" event when the deadline is crossed. The operator's existing trigger_reentry terminal-event path already handles status in ("error", "failed", "timeout", "success") — the operator fails the task and _clean() runs on_finish_action (default: delete pod).

Impact

  • Existing behaviour preserved: execution_timeout was previously a no-op for deferred KPO tasks, and remains a no-op when not set. Tasks without execution_timeout see no behaviour change (execution_deadline=None, defer.timeout=None).
  • No public API changes: the new execution_deadline parameter on KubernetesPodTrigger is keyword-only with a None default. Trigger serialization adds the field but defaults preserve back-compat for existing serialized triggers (the trigger's __init__ accepts the kwarg as optional).
  • Pod cleanup: the existing on_finish_action path handles pod deletion (default delete_pod) when the operator fails on a timeout event. _clean() already special-cases event["status"] == "timeout" to skip await_pod_completion (the pod may hang on ErrImagePull/ContainerCreating).

Tests

  • Trigger (tests/unit/cncf/kubernetes/triggers/test_pod.py):
    • Updated test_serialize to include the new execution_deadline key.
    • Added test_serialize_with_execution_deadline — round-trips a non-None deadline.
    • Added test_run_loop_emits_timeout_event_when_execution_deadline_reached — past-deadline → first iteration emits status="timeout" event.
    • Added test_run_loop_does_not_emit_timeout_when_execution_deadline_not_reached — far-future deadline → trigger keeps polling normally.
  • Operator (tests/unit/cncf/kubernetes/operators/test_pod.py):
    • Added test_invoke_defer_method_passes_execution_deadline_when_execution_timeout_set — operator with execution_timeout=300s passes a deadline ≈ ti.start_date + 300s to the trigger; defer.timeout is set.
    • Added test_invoke_defer_method_passes_no_deadline_when_execution_timeout_not_set — operator without execution_timeout passes None (no enforcement, no behaviour change).

Backwards Compatibility

No public API changes. New execution_deadline parameter on KubernetesPodTrigger is optional with default None. Behaviour change: execution_timeout-equipped deferred KPO tasks now actually fail at the configured timeout instead of running indefinitely; this is the documented contract.

Closes

Closes: #67227

@boring-cyborgboring-cyborgBot added area:providers provider:cncf-kubernetes Kubernetes (k8s) provider related issues labels May 20, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 4eb5810 to fb5e3fbCompareMay 20, 2026 14:18
@paultmathew
paultmathew marked this pull request as ready for review May 20, 2026 15:27
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 5151d22 to fb5e3fbCompareMay 20, 2026 15:57

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the extension, looks good to me. Except some comments.

Comment threadproviders/amazon/src/airflow/providers/amazon/aws/triggers/eks.py Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enforces execution_timeout for KubernetesPodOperator(deferrable=True) by translating the timeout into an absolute deadline, passing it to KubernetesPodTrigger, and adding trigger-side logic to emit a terminal timeout event when the deadline is exceeded (with accompanying unit tests). It also updates the EKS-specific trigger subclass to forward the new parameter.

Changes:

  • Add execution_deadline plumbing from KubernetesPodOperator.invoke_defer_method() to KubernetesPodTrigger and pass a timeout= to defer() based on remaining budget.
  • Add trigger-side deadline enforcement that emits a status="timeout" event once the deadline is crossed.
  • Extend/adjust unit tests for trigger serialization and timeout behavior, plus operator deferral plumbing.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/pod.pyCompute an absolute execution deadline from ti.start_date and execution_timeout, pass it to the trigger, and set defer(timeout=…).
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/triggers/pod.pyAdd execution_deadline to trigger init/serialization and emit a timeout TriggerEvent when the deadline is exceeded.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.pyAdd tests asserting the operator passes execution_deadline (or None) into the trigger and sets defer.timeout appropriately.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.pyUpdate serialization expectations and add trigger run-loop tests for deadline timeout vs. continued polling.
providers/amazon/src/airflow/providers/amazon/aws/triggers/eks.pyForward the new execution_deadline parameter through EksPodTrigger to the base Kubernetes trigger.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from fb5e3fb to 8dfa1afCompareMay 20, 2026 23:45
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch 3 times, most recently from 2da1d91 to 6eec8cfCompareMay 22, 2026 18:08
@jscheffl

Copy link
Copy Markdown
Contributor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

Can you please resolve the comments addressed? And in the ones not being addressed reply a comment?

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 6eec8cf to ad266d8CompareMay 24, 2026 20:01

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine overall. I have left some comments.

Also, I would urge you to verify your implementation using a live cluster to ensure that it fixes the issue. Passing a non-None value as an argument for the timeout parameter in the defer method resulted in the exact same bug remaining unresolved in the DBT Cloud and Airbyte providers. Arguably, your approach is more robust but it still needs live verification to be sure.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 26, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from ad266d8 to 972a05fCompareMay 26, 2026 02:41
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl@SameerMesiah97 thank you both for the reviews.

Pulled the **kwargs revert and any new surface on KubernetesPodTrigger. The deadline now rides inside the existing trigger_kwargs dict under the reserved key _execution_deadline. The leading-underscore convention follows the _redefer_count precedent in the same file.

@SameerMesiah97 you were right — the previous 60s minimum clamp didn't account for poll_interval. With execution_timeout=30s, poll_interval=60s, the framework would have cancelled the trigger at T+60 before its next deadline check at T+65.

The latest commit guarantees defer.timeout always covers remaining + ≥ 2 poll cycles. New unit test pins it: test_invoke_defer_method_pads_defer_timeout_for_slow_poll_interval verifies defer.timeout = 30 + 120 = 150s for a poll_interval=60 setup. I also did a smoke test against a live Kubernetes cluster (EKS) in our sandbox environment.

Smoke test summary

TaskConfigurationResultWhat it proved
t1 happy pathexecution_timeout=5m, sleep 20s✓ success at ~26s, pod deletedDeferred path works end-to-end on real K8s; deadline doesn't trip prematurely
t2 timeout default pollexecution_timeout=30s, poll_interval=2s, sleep 600s✓ failed at ~64s with "Execution deadline reached for pod ... emitting timeout event", pod deletedNew code path fires; _clean() runs; trigger emits soft-timeout event
t3 timeout slow pollexecution_timeout=30s, poll_interval=30s, sleep 600s✓ failed at ~70s, timeout event at +38s, pod deletedThe reviewer's exact concern — slow poll doesn't break cleanup; new poll_buffer = max(60, poll_interval * 2) gives the trigger sufficient runway
t4 no timeoutno execution_timeout, sleep 20s✓ success at ~26s, no deadline messagesOpt-out path preserved; tasks without execution_timeout continue to work as before

Compute the deadline operator-side from ti.start_date + execution_timeout
and plumb it to KubernetesPodTrigger via trigger_kwargs["_execution_deadline"]
(an existing dict already accepted and serialized by every subclass) so
the trigger can short-circuit and emit its own status="timeout" event.
This routes timeout through trigger_reentry → _clean() → pod deletion +
final log capture, matching the success/failure event path.
defer.timeout is set to the remaining budget with a 60s minimum so the
trigger has runway to emit its own timeout event before the framework
backstop fires (which would otherwise short-circuit to TaskDeferralTimeout
and skip the operator's cleanup).
Following the leading-underscore convention established by _redefer_count
in the same file: trigger_kwargs is the only existing operator -> trigger
plumbing that's a generic, untyped, dict-shaped, fully-serialized bag and
already accepted by every KubernetesPodTrigger subclass. Using it avoids
adding a new __init__ kwarg or serialize() field on KubernetesPodTrigger,
keeping cross-version compatibility with subclasses (e.g. EksPodTrigger,
GKEStartPodTrigger) released independently.
Closes: apache#67227
Co-authored-by: Cursor <cursoragent@cursor.com>
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 972a05f to d762e65CompareMay 26, 2026 03:23

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. @SameerMesiah97 another pass of review or any other maintainer feedback?

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. I must mention that your approach to add a buffer is a good compromise (which I will implement myself going forwards). Unfortunately, there is a gap at the framework level regarding the implementation of execution timeouts that has not been addressed as of yet so we must use temporary workarounds like this.

@jscheffl
jscheffl merged commit 3d3e79d into apache:mainMay 30, 2026
113 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:cncf-kubernetesKubernetes (k8s) provider related issuesready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KubernetesPodOperator does not enforce execution_timeout semantics in Deferrable mode

5 participants

@paultmathew@jscheffl@SameerMesiah97@potiuk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Enforce execution_timeout in deferrable KubernetesPodOperator - #67229

Merged
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout
May 30, 2026
Merged

Enforce execution_timeout in deferrable KubernetesPodOperator#67229
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout

Conversation

@paultmathew

Copy link
Copy Markdown
Contributor

Why + What

KubernetesPodOperator(deferrable=True) does not enforce execution_timeout. Once the operator defers, the synchronous execute() returns and the signal.alarm-based timeout context wrapping it exits cleanly — there is no further execution_timeout enforcement for the lifetime of the deferral. Pods continue running well past execution_timeout, bounded only by active_deadline_seconds (which defaults to ~1h or whatever the operator passed).

The framework gap is acknowledged by # TODO: handle timeout in case of deferral at task-sdk/.../task_runner.py:1782.

This PR fixes the symptom for KubernetesPodOperator, mirroring the pattern already merged for AirbyteTriggerSyncOperator (PR #64051) and DbtCloudRunJobOperator (PR #66449).

Approach

  1. Operator (pod.py): in invoke_defer_method, translate execution_timeout into an absolute deadline anchored on ti.start_date:

    • execution_deadline = ti.start_date.timestamp() + execution_timeout.total_seconds()
    • Pass execution_deadline to KubernetesPodTrigger.
    • Pass timeout=remaining (timedelta) to self.defer() so the framework's trigger_timeout also bounds the trigger lifetime as a backstop.
    • Anchoring on ti.start_date keeps the deadline stable across re-deferrals (e.g. logging_interval re-entries), since Airflow preserves the original start_date when a task resumes from defer.
    • Re-pass context from trigger_reentryinvoke_defer_method so the deadline is recomputed correctly on each re-defer.
  2. Trigger (pod.py): at the top of _wait_for_container_completion, check time.time() >= execution_deadline and emit a status="timeout" event when the deadline is crossed. The operator's existing trigger_reentry terminal-event path already handles status in ("error", "failed", "timeout", "success") — the operator fails the task and _clean() runs on_finish_action (default: delete pod).

Impact

  • Existing behaviour preserved: execution_timeout was previously a no-op for deferred KPO tasks, and remains a no-op when not set. Tasks without execution_timeout see no behaviour change (execution_deadline=None, defer.timeout=None).
  • No public API changes: the new execution_deadline parameter on KubernetesPodTrigger is keyword-only with a None default. Trigger serialization adds the field but defaults preserve back-compat for existing serialized triggers (the trigger's __init__ accepts the kwarg as optional).
  • Pod cleanup: the existing on_finish_action path handles pod deletion (default delete_pod) when the operator fails on a timeout event. _clean() already special-cases event["status"] == "timeout" to skip await_pod_completion (the pod may hang on ErrImagePull/ContainerCreating).

Tests

  • Trigger (tests/unit/cncf/kubernetes/triggers/test_pod.py):
    • Updated test_serialize to include the new execution_deadline key.
    • Added test_serialize_with_execution_deadline — round-trips a non-None deadline.
    • Added test_run_loop_emits_timeout_event_when_execution_deadline_reached — past-deadline → first iteration emits status="timeout" event.
    • Added test_run_loop_does_not_emit_timeout_when_execution_deadline_not_reached — far-future deadline → trigger keeps polling normally.
  • Operator (tests/unit/cncf/kubernetes/operators/test_pod.py):
    • Added test_invoke_defer_method_passes_execution_deadline_when_execution_timeout_set — operator with execution_timeout=300s passes a deadline ≈ ti.start_date + 300s to the trigger; defer.timeout is set.
    • Added test_invoke_defer_method_passes_no_deadline_when_execution_timeout_not_set — operator without execution_timeout passes None (no enforcement, no behaviour change).

Backwards Compatibility

No public API changes. New execution_deadline parameter on KubernetesPodTrigger is optional with default None. Behaviour change: execution_timeout-equipped deferred KPO tasks now actually fail at the configured timeout instead of running indefinitely; this is the documented contract.

Closes

Closes: #67227

@boring-cyborgboring-cyborgBot added area:providers provider:cncf-kubernetes Kubernetes (k8s) provider related issues labels May 20, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 4eb5810 to fb5e3fbCompareMay 20, 2026 14:18
@paultmathew
paultmathew marked this pull request as ready for review May 20, 2026 15:27
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 5151d22 to fb5e3fbCompareMay 20, 2026 15:57

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the extension, looks good to me. Except some comments.

Comment threadproviders/amazon/src/airflow/providers/amazon/aws/triggers/eks.py Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enforces execution_timeout for KubernetesPodOperator(deferrable=True) by translating the timeout into an absolute deadline, passing it to KubernetesPodTrigger, and adding trigger-side logic to emit a terminal timeout event when the deadline is exceeded (with accompanying unit tests). It also updates the EKS-specific trigger subclass to forward the new parameter.

Changes:

  • Add execution_deadline plumbing from KubernetesPodOperator.invoke_defer_method() to KubernetesPodTrigger and pass a timeout= to defer() based on remaining budget.
  • Add trigger-side deadline enforcement that emits a status="timeout" event once the deadline is crossed.
  • Extend/adjust unit tests for trigger serialization and timeout behavior, plus operator deferral plumbing.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/pod.pyCompute an absolute execution deadline from ti.start_date and execution_timeout, pass it to the trigger, and set defer(timeout=…).
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/triggers/pod.pyAdd execution_deadline to trigger init/serialization and emit a timeout TriggerEvent when the deadline is exceeded.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.pyAdd tests asserting the operator passes execution_deadline (or None) into the trigger and sets defer.timeout appropriately.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.pyUpdate serialization expectations and add trigger run-loop tests for deadline timeout vs. continued polling.
providers/amazon/src/airflow/providers/amazon/aws/triggers/eks.pyForward the new execution_deadline parameter through EksPodTrigger to the base Kubernetes trigger.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from fb5e3fb to 8dfa1afCompareMay 20, 2026 23:45
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch 3 times, most recently from 2da1d91 to 6eec8cfCompareMay 22, 2026 18:08
@jscheffl

Copy link
Copy Markdown
Contributor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

Can you please resolve the comments addressed? And in the ones not being addressed reply a comment?

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 6eec8cf to ad266d8CompareMay 24, 2026 20:01

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine overall. I have left some comments.

Also, I would urge you to verify your implementation using a live cluster to ensure that it fixes the issue. Passing a non-None value as an argument for the timeout parameter in the defer method resulted in the exact same bug remaining unresolved in the DBT Cloud and Airbyte providers. Arguably, your approach is more robust but it still needs live verification to be sure.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 26, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from ad266d8 to 972a05fCompareMay 26, 2026 02:41
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl@SameerMesiah97 thank you both for the reviews.

Pulled the **kwargs revert and any new surface on KubernetesPodTrigger. The deadline now rides inside the existing trigger_kwargs dict under the reserved key _execution_deadline. The leading-underscore convention follows the _redefer_count precedent in the same file.

@SameerMesiah97 you were right — the previous 60s minimum clamp didn't account for poll_interval. With execution_timeout=30s, poll_interval=60s, the framework would have cancelled the trigger at T+60 before its next deadline check at T+65.

The latest commit guarantees defer.timeout always covers remaining + ≥ 2 poll cycles. New unit test pins it: test_invoke_defer_method_pads_defer_timeout_for_slow_poll_interval verifies defer.timeout = 30 + 120 = 150s for a poll_interval=60 setup. I also did a smoke test against a live Kubernetes cluster (EKS) in our sandbox environment.

Smoke test summary

TaskConfigurationResultWhat it proved
t1 happy pathexecution_timeout=5m, sleep 20s✓ success at ~26s, pod deletedDeferred path works end-to-end on real K8s; deadline doesn't trip prematurely
t2 timeout default pollexecution_timeout=30s, poll_interval=2s, sleep 600s✓ failed at ~64s with "Execution deadline reached for pod ... emitting timeout event", pod deletedNew code path fires; _clean() runs; trigger emits soft-timeout event
t3 timeout slow pollexecution_timeout=30s, poll_interval=30s, sleep 600s✓ failed at ~70s, timeout event at +38s, pod deletedThe reviewer's exact concern — slow poll doesn't break cleanup; new poll_buffer = max(60, poll_interval * 2) gives the trigger sufficient runway
t4 no timeoutno execution_timeout, sleep 20s✓ success at ~26s, no deadline messagesOpt-out path preserved; tasks without execution_timeout continue to work as before

Compute the deadline operator-side from ti.start_date + execution_timeout
and plumb it to KubernetesPodTrigger via trigger_kwargs["_execution_deadline"]
(an existing dict already accepted and serialized by every subclass) so
the trigger can short-circuit and emit its own status="timeout" event.
This routes timeout through trigger_reentry → _clean() → pod deletion +
final log capture, matching the success/failure event path.
defer.timeout is set to the remaining budget with a 60s minimum so the
trigger has runway to emit its own timeout event before the framework
backstop fires (which would otherwise short-circuit to TaskDeferralTimeout
and skip the operator's cleanup).
Following the leading-underscore convention established by _redefer_count
in the same file: trigger_kwargs is the only existing operator -> trigger
plumbing that's a generic, untyped, dict-shaped, fully-serialized bag and
already accepted by every KubernetesPodTrigger subclass. Using it avoids
adding a new __init__ kwarg or serialize() field on KubernetesPodTrigger,
keeping cross-version compatibility with subclasses (e.g. EksPodTrigger,
GKEStartPodTrigger) released independently.
Closes: apache#67227
Co-authored-by: Cursor <cursoragent@cursor.com>
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 972a05f to d762e65CompareMay 26, 2026 03:23

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. @SameerMesiah97 another pass of review or any other maintainer feedback?

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. I must mention that your approach to add a buffer is a good compromise (which I will implement myself going forwards). Unfortunately, there is a gap at the framework level regarding the implementation of execution timeouts that has not been addressed as of yet so we must use temporary workarounds like this.

@jscheffl
jscheffl merged commit 3d3e79d into apache:mainMay 30, 2026
113 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:cncf-kubernetesKubernetes (k8s) provider related issuesready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KubernetesPodOperator does not enforce execution_timeout semantics in Deferrable mode

5 participants

@paultmathew@jscheffl@SameerMesiah97@potiuk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Enforce execution_timeout in deferrable KubernetesPodOperator - #67229

Merged
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout
May 30, 2026
Merged

Enforce execution_timeout in deferrable KubernetesPodOperator#67229
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout

Conversation

@paultmathew

Copy link
Copy Markdown
Contributor

Why + What

KubernetesPodOperator(deferrable=True) does not enforce execution_timeout. Once the operator defers, the synchronous execute() returns and the signal.alarm-based timeout context wrapping it exits cleanly — there is no further execution_timeout enforcement for the lifetime of the deferral. Pods continue running well past execution_timeout, bounded only by active_deadline_seconds (which defaults to ~1h or whatever the operator passed).

The framework gap is acknowledged by # TODO: handle timeout in case of deferral at task-sdk/.../task_runner.py:1782.

This PR fixes the symptom for KubernetesPodOperator, mirroring the pattern already merged for AirbyteTriggerSyncOperator (PR #64051) and DbtCloudRunJobOperator (PR #66449).

Approach

  1. Operator (pod.py): in invoke_defer_method, translate execution_timeout into an absolute deadline anchored on ti.start_date:

    • execution_deadline = ti.start_date.timestamp() + execution_timeout.total_seconds()
    • Pass execution_deadline to KubernetesPodTrigger.
    • Pass timeout=remaining (timedelta) to self.defer() so the framework's trigger_timeout also bounds the trigger lifetime as a backstop.
    • Anchoring on ti.start_date keeps the deadline stable across re-deferrals (e.g. logging_interval re-entries), since Airflow preserves the original start_date when a task resumes from defer.
    • Re-pass context from trigger_reentryinvoke_defer_method so the deadline is recomputed correctly on each re-defer.
  2. Trigger (pod.py): at the top of _wait_for_container_completion, check time.time() >= execution_deadline and emit a status="timeout" event when the deadline is crossed. The operator's existing trigger_reentry terminal-event path already handles status in ("error", "failed", "timeout", "success") — the operator fails the task and _clean() runs on_finish_action (default: delete pod).

Impact

  • Existing behaviour preserved: execution_timeout was previously a no-op for deferred KPO tasks, and remains a no-op when not set. Tasks without execution_timeout see no behaviour change (execution_deadline=None, defer.timeout=None).
  • No public API changes: the new execution_deadline parameter on KubernetesPodTrigger is keyword-only with a None default. Trigger serialization adds the field but defaults preserve back-compat for existing serialized triggers (the trigger's __init__ accepts the kwarg as optional).
  • Pod cleanup: the existing on_finish_action path handles pod deletion (default delete_pod) when the operator fails on a timeout event. _clean() already special-cases event["status"] == "timeout" to skip await_pod_completion (the pod may hang on ErrImagePull/ContainerCreating).

Tests

  • Trigger (tests/unit/cncf/kubernetes/triggers/test_pod.py):
    • Updated test_serialize to include the new execution_deadline key.
    • Added test_serialize_with_execution_deadline — round-trips a non-None deadline.
    • Added test_run_loop_emits_timeout_event_when_execution_deadline_reached — past-deadline → first iteration emits status="timeout" event.
    • Added test_run_loop_does_not_emit_timeout_when_execution_deadline_not_reached — far-future deadline → trigger keeps polling normally.
  • Operator (tests/unit/cncf/kubernetes/operators/test_pod.py):
    • Added test_invoke_defer_method_passes_execution_deadline_when_execution_timeout_set — operator with execution_timeout=300s passes a deadline ≈ ti.start_date + 300s to the trigger; defer.timeout is set.
    • Added test_invoke_defer_method_passes_no_deadline_when_execution_timeout_not_set — operator without execution_timeout passes None (no enforcement, no behaviour change).

Backwards Compatibility

No public API changes. New execution_deadline parameter on KubernetesPodTrigger is optional with default None. Behaviour change: execution_timeout-equipped deferred KPO tasks now actually fail at the configured timeout instead of running indefinitely; this is the documented contract.

Closes

Closes: #67227

@boring-cyborgboring-cyborgBot added area:providers provider:cncf-kubernetes Kubernetes (k8s) provider related issues labels May 20, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 4eb5810 to fb5e3fbCompareMay 20, 2026 14:18
@paultmathew
paultmathew marked this pull request as ready for review May 20, 2026 15:27
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 5151d22 to fb5e3fbCompareMay 20, 2026 15:57

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the extension, looks good to me. Except some comments.

Comment threadproviders/amazon/src/airflow/providers/amazon/aws/triggers/eks.py Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enforces execution_timeout for KubernetesPodOperator(deferrable=True) by translating the timeout into an absolute deadline, passing it to KubernetesPodTrigger, and adding trigger-side logic to emit a terminal timeout event when the deadline is exceeded (with accompanying unit tests). It also updates the EKS-specific trigger subclass to forward the new parameter.

Changes:

  • Add execution_deadline plumbing from KubernetesPodOperator.invoke_defer_method() to KubernetesPodTrigger and pass a timeout= to defer() based on remaining budget.
  • Add trigger-side deadline enforcement that emits a status="timeout" event once the deadline is crossed.
  • Extend/adjust unit tests for trigger serialization and timeout behavior, plus operator deferral plumbing.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/pod.pyCompute an absolute execution deadline from ti.start_date and execution_timeout, pass it to the trigger, and set defer(timeout=…).
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/triggers/pod.pyAdd execution_deadline to trigger init/serialization and emit a timeout TriggerEvent when the deadline is exceeded.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.pyAdd tests asserting the operator passes execution_deadline (or None) into the trigger and sets defer.timeout appropriately.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.pyUpdate serialization expectations and add trigger run-loop tests for deadline timeout vs. continued polling.
providers/amazon/src/airflow/providers/amazon/aws/triggers/eks.pyForward the new execution_deadline parameter through EksPodTrigger to the base Kubernetes trigger.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from fb5e3fb to 8dfa1afCompareMay 20, 2026 23:45
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch 3 times, most recently from 2da1d91 to 6eec8cfCompareMay 22, 2026 18:08
@jscheffl

Copy link
Copy Markdown
Contributor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

Can you please resolve the comments addressed? And in the ones not being addressed reply a comment?

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 6eec8cf to ad266d8CompareMay 24, 2026 20:01

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine overall. I have left some comments.

Also, I would urge you to verify your implementation using a live cluster to ensure that it fixes the issue. Passing a non-None value as an argument for the timeout parameter in the defer method resulted in the exact same bug remaining unresolved in the DBT Cloud and Airbyte providers. Arguably, your approach is more robust but it still needs live verification to be sure.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 26, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from ad266d8 to 972a05fCompareMay 26, 2026 02:41
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl@SameerMesiah97 thank you both for the reviews.

Pulled the **kwargs revert and any new surface on KubernetesPodTrigger. The deadline now rides inside the existing trigger_kwargs dict under the reserved key _execution_deadline. The leading-underscore convention follows the _redefer_count precedent in the same file.

@SameerMesiah97 you were right — the previous 60s minimum clamp didn't account for poll_interval. With execution_timeout=30s, poll_interval=60s, the framework would have cancelled the trigger at T+60 before its next deadline check at T+65.

The latest commit guarantees defer.timeout always covers remaining + ≥ 2 poll cycles. New unit test pins it: test_invoke_defer_method_pads_defer_timeout_for_slow_poll_interval verifies defer.timeout = 30 + 120 = 150s for a poll_interval=60 setup. I also did a smoke test against a live Kubernetes cluster (EKS) in our sandbox environment.

Smoke test summary

TaskConfigurationResultWhat it proved
t1 happy pathexecution_timeout=5m, sleep 20s✓ success at ~26s, pod deletedDeferred path works end-to-end on real K8s; deadline doesn't trip prematurely
t2 timeout default pollexecution_timeout=30s, poll_interval=2s, sleep 600s✓ failed at ~64s with "Execution deadline reached for pod ... emitting timeout event", pod deletedNew code path fires; _clean() runs; trigger emits soft-timeout event
t3 timeout slow pollexecution_timeout=30s, poll_interval=30s, sleep 600s✓ failed at ~70s, timeout event at +38s, pod deletedThe reviewer's exact concern — slow poll doesn't break cleanup; new poll_buffer = max(60, poll_interval * 2) gives the trigger sufficient runway
t4 no timeoutno execution_timeout, sleep 20s✓ success at ~26s, no deadline messagesOpt-out path preserved; tasks without execution_timeout continue to work as before

Compute the deadline operator-side from ti.start_date + execution_timeout
and plumb it to KubernetesPodTrigger via trigger_kwargs["_execution_deadline"]
(an existing dict already accepted and serialized by every subclass) so
the trigger can short-circuit and emit its own status="timeout" event.
This routes timeout through trigger_reentry → _clean() → pod deletion +
final log capture, matching the success/failure event path.
defer.timeout is set to the remaining budget with a 60s minimum so the
trigger has runway to emit its own timeout event before the framework
backstop fires (which would otherwise short-circuit to TaskDeferralTimeout
and skip the operator's cleanup).
Following the leading-underscore convention established by _redefer_count
in the same file: trigger_kwargs is the only existing operator -> trigger
plumbing that's a generic, untyped, dict-shaped, fully-serialized bag and
already accepted by every KubernetesPodTrigger subclass. Using it avoids
adding a new __init__ kwarg or serialize() field on KubernetesPodTrigger,
keeping cross-version compatibility with subclasses (e.g. EksPodTrigger,
GKEStartPodTrigger) released independently.
Closes: apache#67227
Co-authored-by: Cursor <cursoragent@cursor.com>
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 972a05f to d762e65CompareMay 26, 2026 03:23

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. @SameerMesiah97 another pass of review or any other maintainer feedback?

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. I must mention that your approach to add a buffer is a good compromise (which I will implement myself going forwards). Unfortunately, there is a gap at the framework level regarding the implementation of execution timeouts that has not been addressed as of yet so we must use temporary workarounds like this.

@jscheffl
jscheffl merged commit 3d3e79d into apache:mainMay 30, 2026
113 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:cncf-kubernetesKubernetes (k8s) provider related issuesready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KubernetesPodOperator does not enforce execution_timeout semantics in Deferrable mode

5 participants

@paultmathew@jscheffl@SameerMesiah97@potiuk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Enforce execution_timeout in deferrable KubernetesPodOperator - #67229

Merged
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout
May 30, 2026
Merged

Enforce execution_timeout in deferrable KubernetesPodOperator#67229
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout

Conversation

@paultmathew

Copy link
Copy Markdown
Contributor

Why + What

KubernetesPodOperator(deferrable=True) does not enforce execution_timeout. Once the operator defers, the synchronous execute() returns and the signal.alarm-based timeout context wrapping it exits cleanly — there is no further execution_timeout enforcement for the lifetime of the deferral. Pods continue running well past execution_timeout, bounded only by active_deadline_seconds (which defaults to ~1h or whatever the operator passed).

The framework gap is acknowledged by # TODO: handle timeout in case of deferral at task-sdk/.../task_runner.py:1782.

This PR fixes the symptom for KubernetesPodOperator, mirroring the pattern already merged for AirbyteTriggerSyncOperator (PR #64051) and DbtCloudRunJobOperator (PR #66449).

Approach

  1. Operator (pod.py): in invoke_defer_method, translate execution_timeout into an absolute deadline anchored on ti.start_date:

    • execution_deadline = ti.start_date.timestamp() + execution_timeout.total_seconds()
    • Pass execution_deadline to KubernetesPodTrigger.
    • Pass timeout=remaining (timedelta) to self.defer() so the framework's trigger_timeout also bounds the trigger lifetime as a backstop.
    • Anchoring on ti.start_date keeps the deadline stable across re-deferrals (e.g. logging_interval re-entries), since Airflow preserves the original start_date when a task resumes from defer.
    • Re-pass context from trigger_reentryinvoke_defer_method so the deadline is recomputed correctly on each re-defer.
  2. Trigger (pod.py): at the top of _wait_for_container_completion, check time.time() >= execution_deadline and emit a status="timeout" event when the deadline is crossed. The operator's existing trigger_reentry terminal-event path already handles status in ("error", "failed", "timeout", "success") — the operator fails the task and _clean() runs on_finish_action (default: delete pod).

Impact

  • Existing behaviour preserved: execution_timeout was previously a no-op for deferred KPO tasks, and remains a no-op when not set. Tasks without execution_timeout see no behaviour change (execution_deadline=None, defer.timeout=None).
  • No public API changes: the new execution_deadline parameter on KubernetesPodTrigger is keyword-only with a None default. Trigger serialization adds the field but defaults preserve back-compat for existing serialized triggers (the trigger's __init__ accepts the kwarg as optional).
  • Pod cleanup: the existing on_finish_action path handles pod deletion (default delete_pod) when the operator fails on a timeout event. _clean() already special-cases event["status"] == "timeout" to skip await_pod_completion (the pod may hang on ErrImagePull/ContainerCreating).

Tests

  • Trigger (tests/unit/cncf/kubernetes/triggers/test_pod.py):
    • Updated test_serialize to include the new execution_deadline key.
    • Added test_serialize_with_execution_deadline — round-trips a non-None deadline.
    • Added test_run_loop_emits_timeout_event_when_execution_deadline_reached — past-deadline → first iteration emits status="timeout" event.
    • Added test_run_loop_does_not_emit_timeout_when_execution_deadline_not_reached — far-future deadline → trigger keeps polling normally.
  • Operator (tests/unit/cncf/kubernetes/operators/test_pod.py):
    • Added test_invoke_defer_method_passes_execution_deadline_when_execution_timeout_set — operator with execution_timeout=300s passes a deadline ≈ ti.start_date + 300s to the trigger; defer.timeout is set.
    • Added test_invoke_defer_method_passes_no_deadline_when_execution_timeout_not_set — operator without execution_timeout passes None (no enforcement, no behaviour change).

Backwards Compatibility

No public API changes. New execution_deadline parameter on KubernetesPodTrigger is optional with default None. Behaviour change: execution_timeout-equipped deferred KPO tasks now actually fail at the configured timeout instead of running indefinitely; this is the documented contract.

Closes

Closes: #67227

@boring-cyborgboring-cyborgBot added area:providers provider:cncf-kubernetes Kubernetes (k8s) provider related issues labels May 20, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 4eb5810 to fb5e3fbCompareMay 20, 2026 14:18
@paultmathew
paultmathew marked this pull request as ready for review May 20, 2026 15:27
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 5151d22 to fb5e3fbCompareMay 20, 2026 15:57

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the extension, looks good to me. Except some comments.

Comment threadproviders/amazon/src/airflow/providers/amazon/aws/triggers/eks.py Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enforces execution_timeout for KubernetesPodOperator(deferrable=True) by translating the timeout into an absolute deadline, passing it to KubernetesPodTrigger, and adding trigger-side logic to emit a terminal timeout event when the deadline is exceeded (with accompanying unit tests). It also updates the EKS-specific trigger subclass to forward the new parameter.

Changes:

  • Add execution_deadline plumbing from KubernetesPodOperator.invoke_defer_method() to KubernetesPodTrigger and pass a timeout= to defer() based on remaining budget.
  • Add trigger-side deadline enforcement that emits a status="timeout" event once the deadline is crossed.
  • Extend/adjust unit tests for trigger serialization and timeout behavior, plus operator deferral plumbing.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/pod.pyCompute an absolute execution deadline from ti.start_date and execution_timeout, pass it to the trigger, and set defer(timeout=…).
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/triggers/pod.pyAdd execution_deadline to trigger init/serialization and emit a timeout TriggerEvent when the deadline is exceeded.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.pyAdd tests asserting the operator passes execution_deadline (or None) into the trigger and sets defer.timeout appropriately.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.pyUpdate serialization expectations and add trigger run-loop tests for deadline timeout vs. continued polling.
providers/amazon/src/airflow/providers/amazon/aws/triggers/eks.pyForward the new execution_deadline parameter through EksPodTrigger to the base Kubernetes trigger.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from fb5e3fb to 8dfa1afCompareMay 20, 2026 23:45
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch 3 times, most recently from 2da1d91 to 6eec8cfCompareMay 22, 2026 18:08
@jscheffl

Copy link
Copy Markdown
Contributor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

Can you please resolve the comments addressed? And in the ones not being addressed reply a comment?

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 6eec8cf to ad266d8CompareMay 24, 2026 20:01

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine overall. I have left some comments.

Also, I would urge you to verify your implementation using a live cluster to ensure that it fixes the issue. Passing a non-None value as an argument for the timeout parameter in the defer method resulted in the exact same bug remaining unresolved in the DBT Cloud and Airbyte providers. Arguably, your approach is more robust but it still needs live verification to be sure.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 26, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from ad266d8 to 972a05fCompareMay 26, 2026 02:41
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl@SameerMesiah97 thank you both for the reviews.

Pulled the **kwargs revert and any new surface on KubernetesPodTrigger. The deadline now rides inside the existing trigger_kwargs dict under the reserved key _execution_deadline. The leading-underscore convention follows the _redefer_count precedent in the same file.

@SameerMesiah97 you were right — the previous 60s minimum clamp didn't account for poll_interval. With execution_timeout=30s, poll_interval=60s, the framework would have cancelled the trigger at T+60 before its next deadline check at T+65.

The latest commit guarantees defer.timeout always covers remaining + ≥ 2 poll cycles. New unit test pins it: test_invoke_defer_method_pads_defer_timeout_for_slow_poll_interval verifies defer.timeout = 30 + 120 = 150s for a poll_interval=60 setup. I also did a smoke test against a live Kubernetes cluster (EKS) in our sandbox environment.

Smoke test summary

TaskConfigurationResultWhat it proved
t1 happy pathexecution_timeout=5m, sleep 20s✓ success at ~26s, pod deletedDeferred path works end-to-end on real K8s; deadline doesn't trip prematurely
t2 timeout default pollexecution_timeout=30s, poll_interval=2s, sleep 600s✓ failed at ~64s with "Execution deadline reached for pod ... emitting timeout event", pod deletedNew code path fires; _clean() runs; trigger emits soft-timeout event
t3 timeout slow pollexecution_timeout=30s, poll_interval=30s, sleep 600s✓ failed at ~70s, timeout event at +38s, pod deletedThe reviewer's exact concern — slow poll doesn't break cleanup; new poll_buffer = max(60, poll_interval * 2) gives the trigger sufficient runway
t4 no timeoutno execution_timeout, sleep 20s✓ success at ~26s, no deadline messagesOpt-out path preserved; tasks without execution_timeout continue to work as before

Compute the deadline operator-side from ti.start_date + execution_timeout
and plumb it to KubernetesPodTrigger via trigger_kwargs["_execution_deadline"]
(an existing dict already accepted and serialized by every subclass) so
the trigger can short-circuit and emit its own status="timeout" event.
This routes timeout through trigger_reentry → _clean() → pod deletion +
final log capture, matching the success/failure event path.
defer.timeout is set to the remaining budget with a 60s minimum so the
trigger has runway to emit its own timeout event before the framework
backstop fires (which would otherwise short-circuit to TaskDeferralTimeout
and skip the operator's cleanup).
Following the leading-underscore convention established by _redefer_count
in the same file: trigger_kwargs is the only existing operator -> trigger
plumbing that's a generic, untyped, dict-shaped, fully-serialized bag and
already accepted by every KubernetesPodTrigger subclass. Using it avoids
adding a new __init__ kwarg or serialize() field on KubernetesPodTrigger,
keeping cross-version compatibility with subclasses (e.g. EksPodTrigger,
GKEStartPodTrigger) released independently.
Closes: apache#67227
Co-authored-by: Cursor <cursoragent@cursor.com>
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 972a05f to d762e65CompareMay 26, 2026 03:23

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. @SameerMesiah97 another pass of review or any other maintainer feedback?

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. I must mention that your approach to add a buffer is a good compromise (which I will implement myself going forwards). Unfortunately, there is a gap at the framework level regarding the implementation of execution timeouts that has not been addressed as of yet so we must use temporary workarounds like this.

@jscheffl
jscheffl merged commit 3d3e79d into apache:mainMay 30, 2026
113 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:cncf-kubernetesKubernetes (k8s) provider related issuesready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KubernetesPodOperator does not enforce execution_timeout semantics in Deferrable mode

5 participants

@paultmathew@jscheffl@SameerMesiah97@potiuk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Enforce execution_timeout in deferrable KubernetesPodOperator - #67229

Merged
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout
May 30, 2026
Merged

Enforce execution_timeout in deferrable KubernetesPodOperator#67229
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout

Conversation

@paultmathew

Copy link
Copy Markdown
Contributor

Why + What

KubernetesPodOperator(deferrable=True) does not enforce execution_timeout. Once the operator defers, the synchronous execute() returns and the signal.alarm-based timeout context wrapping it exits cleanly — there is no further execution_timeout enforcement for the lifetime of the deferral. Pods continue running well past execution_timeout, bounded only by active_deadline_seconds (which defaults to ~1h or whatever the operator passed).

The framework gap is acknowledged by # TODO: handle timeout in case of deferral at task-sdk/.../task_runner.py:1782.

This PR fixes the symptom for KubernetesPodOperator, mirroring the pattern already merged for AirbyteTriggerSyncOperator (PR #64051) and DbtCloudRunJobOperator (PR #66449).

Approach

  1. Operator (pod.py): in invoke_defer_method, translate execution_timeout into an absolute deadline anchored on ti.start_date:

    • execution_deadline = ti.start_date.timestamp() + execution_timeout.total_seconds()
    • Pass execution_deadline to KubernetesPodTrigger.
    • Pass timeout=remaining (timedelta) to self.defer() so the framework's trigger_timeout also bounds the trigger lifetime as a backstop.
    • Anchoring on ti.start_date keeps the deadline stable across re-deferrals (e.g. logging_interval re-entries), since Airflow preserves the original start_date when a task resumes from defer.
    • Re-pass context from trigger_reentryinvoke_defer_method so the deadline is recomputed correctly on each re-defer.
  2. Trigger (pod.py): at the top of _wait_for_container_completion, check time.time() >= execution_deadline and emit a status="timeout" event when the deadline is crossed. The operator's existing trigger_reentry terminal-event path already handles status in ("error", "failed", "timeout", "success") — the operator fails the task and _clean() runs on_finish_action (default: delete pod).

Impact

  • Existing behaviour preserved: execution_timeout was previously a no-op for deferred KPO tasks, and remains a no-op when not set. Tasks without execution_timeout see no behaviour change (execution_deadline=None, defer.timeout=None).
  • No public API changes: the new execution_deadline parameter on KubernetesPodTrigger is keyword-only with a None default. Trigger serialization adds the field but defaults preserve back-compat for existing serialized triggers (the trigger's __init__ accepts the kwarg as optional).
  • Pod cleanup: the existing on_finish_action path handles pod deletion (default delete_pod) when the operator fails on a timeout event. _clean() already special-cases event["status"] == "timeout" to skip await_pod_completion (the pod may hang on ErrImagePull/ContainerCreating).

Tests

  • Trigger (tests/unit/cncf/kubernetes/triggers/test_pod.py):
    • Updated test_serialize to include the new execution_deadline key.
    • Added test_serialize_with_execution_deadline — round-trips a non-None deadline.
    • Added test_run_loop_emits_timeout_event_when_execution_deadline_reached — past-deadline → first iteration emits status="timeout" event.
    • Added test_run_loop_does_not_emit_timeout_when_execution_deadline_not_reached — far-future deadline → trigger keeps polling normally.
  • Operator (tests/unit/cncf/kubernetes/operators/test_pod.py):
    • Added test_invoke_defer_method_passes_execution_deadline_when_execution_timeout_set — operator with execution_timeout=300s passes a deadline ≈ ti.start_date + 300s to the trigger; defer.timeout is set.
    • Added test_invoke_defer_method_passes_no_deadline_when_execution_timeout_not_set — operator without execution_timeout passes None (no enforcement, no behaviour change).

Backwards Compatibility

No public API changes. New execution_deadline parameter on KubernetesPodTrigger is optional with default None. Behaviour change: execution_timeout-equipped deferred KPO tasks now actually fail at the configured timeout instead of running indefinitely; this is the documented contract.

Closes

Closes: #67227

@boring-cyborgboring-cyborgBot added area:providers provider:cncf-kubernetes Kubernetes (k8s) provider related issues labels May 20, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 4eb5810 to fb5e3fbCompareMay 20, 2026 14:18
@paultmathew
paultmathew marked this pull request as ready for review May 20, 2026 15:27
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 5151d22 to fb5e3fbCompareMay 20, 2026 15:57

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the extension, looks good to me. Except some comments.

Comment threadproviders/amazon/src/airflow/providers/amazon/aws/triggers/eks.py Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enforces execution_timeout for KubernetesPodOperator(deferrable=True) by translating the timeout into an absolute deadline, passing it to KubernetesPodTrigger, and adding trigger-side logic to emit a terminal timeout event when the deadline is exceeded (with accompanying unit tests). It also updates the EKS-specific trigger subclass to forward the new parameter.

Changes:

  • Add execution_deadline plumbing from KubernetesPodOperator.invoke_defer_method() to KubernetesPodTrigger and pass a timeout= to defer() based on remaining budget.
  • Add trigger-side deadline enforcement that emits a status="timeout" event once the deadline is crossed.
  • Extend/adjust unit tests for trigger serialization and timeout behavior, plus operator deferral plumbing.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/pod.pyCompute an absolute execution deadline from ti.start_date and execution_timeout, pass it to the trigger, and set defer(timeout=…).
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/triggers/pod.pyAdd execution_deadline to trigger init/serialization and emit a timeout TriggerEvent when the deadline is exceeded.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.pyAdd tests asserting the operator passes execution_deadline (or None) into the trigger and sets defer.timeout appropriately.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.pyUpdate serialization expectations and add trigger run-loop tests for deadline timeout vs. continued polling.
providers/amazon/src/airflow/providers/amazon/aws/triggers/eks.pyForward the new execution_deadline parameter through EksPodTrigger to the base Kubernetes trigger.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from fb5e3fb to 8dfa1afCompareMay 20, 2026 23:45
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch 3 times, most recently from 2da1d91 to 6eec8cfCompareMay 22, 2026 18:08
@jscheffl

Copy link
Copy Markdown
Contributor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

Can you please resolve the comments addressed? And in the ones not being addressed reply a comment?

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 6eec8cf to ad266d8CompareMay 24, 2026 20:01

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine overall. I have left some comments.

Also, I would urge you to verify your implementation using a live cluster to ensure that it fixes the issue. Passing a non-None value as an argument for the timeout parameter in the defer method resulted in the exact same bug remaining unresolved in the DBT Cloud and Airbyte providers. Arguably, your approach is more robust but it still needs live verification to be sure.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 26, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from ad266d8 to 972a05fCompareMay 26, 2026 02:41
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl@SameerMesiah97 thank you both for the reviews.

Pulled the **kwargs revert and any new surface on KubernetesPodTrigger. The deadline now rides inside the existing trigger_kwargs dict under the reserved key _execution_deadline. The leading-underscore convention follows the _redefer_count precedent in the same file.

@SameerMesiah97 you were right — the previous 60s minimum clamp didn't account for poll_interval. With execution_timeout=30s, poll_interval=60s, the framework would have cancelled the trigger at T+60 before its next deadline check at T+65.

The latest commit guarantees defer.timeout always covers remaining + ≥ 2 poll cycles. New unit test pins it: test_invoke_defer_method_pads_defer_timeout_for_slow_poll_interval verifies defer.timeout = 30 + 120 = 150s for a poll_interval=60 setup. I also did a smoke test against a live Kubernetes cluster (EKS) in our sandbox environment.

Smoke test summary

TaskConfigurationResultWhat it proved
t1 happy pathexecution_timeout=5m, sleep 20s✓ success at ~26s, pod deletedDeferred path works end-to-end on real K8s; deadline doesn't trip prematurely
t2 timeout default pollexecution_timeout=30s, poll_interval=2s, sleep 600s✓ failed at ~64s with "Execution deadline reached for pod ... emitting timeout event", pod deletedNew code path fires; _clean() runs; trigger emits soft-timeout event
t3 timeout slow pollexecution_timeout=30s, poll_interval=30s, sleep 600s✓ failed at ~70s, timeout event at +38s, pod deletedThe reviewer's exact concern — slow poll doesn't break cleanup; new poll_buffer = max(60, poll_interval * 2) gives the trigger sufficient runway
t4 no timeoutno execution_timeout, sleep 20s✓ success at ~26s, no deadline messagesOpt-out path preserved; tasks without execution_timeout continue to work as before

Compute the deadline operator-side from ti.start_date + execution_timeout
and plumb it to KubernetesPodTrigger via trigger_kwargs["_execution_deadline"]
(an existing dict already accepted and serialized by every subclass) so
the trigger can short-circuit and emit its own status="timeout" event.
This routes timeout through trigger_reentry → _clean() → pod deletion +
final log capture, matching the success/failure event path.
defer.timeout is set to the remaining budget with a 60s minimum so the
trigger has runway to emit its own timeout event before the framework
backstop fires (which would otherwise short-circuit to TaskDeferralTimeout
and skip the operator's cleanup).
Following the leading-underscore convention established by _redefer_count
in the same file: trigger_kwargs is the only existing operator -> trigger
plumbing that's a generic, untyped, dict-shaped, fully-serialized bag and
already accepted by every KubernetesPodTrigger subclass. Using it avoids
adding a new __init__ kwarg or serialize() field on KubernetesPodTrigger,
keeping cross-version compatibility with subclasses (e.g. EksPodTrigger,
GKEStartPodTrigger) released independently.
Closes: apache#67227
Co-authored-by: Cursor <cursoragent@cursor.com>
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 972a05f to d762e65CompareMay 26, 2026 03:23

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. @SameerMesiah97 another pass of review or any other maintainer feedback?

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. I must mention that your approach to add a buffer is a good compromise (which I will implement myself going forwards). Unfortunately, there is a gap at the framework level regarding the implementation of execution timeouts that has not been addressed as of yet so we must use temporary workarounds like this.

@jscheffl
jscheffl merged commit 3d3e79d into apache:mainMay 30, 2026
113 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:cncf-kubernetesKubernetes (k8s) provider related issuesready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KubernetesPodOperator does not enforce execution_timeout semantics in Deferrable mode

5 participants

@paultmathew@jscheffl@SameerMesiah97@potiuk
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Enforce execution_timeout in deferrable KubernetesPodOperator - #67229

Merged
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout
May 30, 2026
Merged

Enforce execution_timeout in deferrable KubernetesPodOperator#67229
jscheffl merged 1 commit into
apache:mainfrom
paultmathew:fix/67227-kpo-deferrable-execution-timeout

Conversation

@paultmathew

Copy link
Copy Markdown
Contributor

Why + What

KubernetesPodOperator(deferrable=True) does not enforce execution_timeout. Once the operator defers, the synchronous execute() returns and the signal.alarm-based timeout context wrapping it exits cleanly — there is no further execution_timeout enforcement for the lifetime of the deferral. Pods continue running well past execution_timeout, bounded only by active_deadline_seconds (which defaults to ~1h or whatever the operator passed).

The framework gap is acknowledged by # TODO: handle timeout in case of deferral at task-sdk/.../task_runner.py:1782.

This PR fixes the symptom for KubernetesPodOperator, mirroring the pattern already merged for AirbyteTriggerSyncOperator (PR #64051) and DbtCloudRunJobOperator (PR #66449).

Approach

  1. Operator (pod.py): in invoke_defer_method, translate execution_timeout into an absolute deadline anchored on ti.start_date:

    • execution_deadline = ti.start_date.timestamp() + execution_timeout.total_seconds()
    • Pass execution_deadline to KubernetesPodTrigger.
    • Pass timeout=remaining (timedelta) to self.defer() so the framework's trigger_timeout also bounds the trigger lifetime as a backstop.
    • Anchoring on ti.start_date keeps the deadline stable across re-deferrals (e.g. logging_interval re-entries), since Airflow preserves the original start_date when a task resumes from defer.
    • Re-pass context from trigger_reentryinvoke_defer_method so the deadline is recomputed correctly on each re-defer.
  2. Trigger (pod.py): at the top of _wait_for_container_completion, check time.time() >= execution_deadline and emit a status="timeout" event when the deadline is crossed. The operator's existing trigger_reentry terminal-event path already handles status in ("error", "failed", "timeout", "success") — the operator fails the task and _clean() runs on_finish_action (default: delete pod).

Impact

  • Existing behaviour preserved: execution_timeout was previously a no-op for deferred KPO tasks, and remains a no-op when not set. Tasks without execution_timeout see no behaviour change (execution_deadline=None, defer.timeout=None).
  • No public API changes: the new execution_deadline parameter on KubernetesPodTrigger is keyword-only with a None default. Trigger serialization adds the field but defaults preserve back-compat for existing serialized triggers (the trigger's __init__ accepts the kwarg as optional).
  • Pod cleanup: the existing on_finish_action path handles pod deletion (default delete_pod) when the operator fails on a timeout event. _clean() already special-cases event["status"] == "timeout" to skip await_pod_completion (the pod may hang on ErrImagePull/ContainerCreating).

Tests

  • Trigger (tests/unit/cncf/kubernetes/triggers/test_pod.py):
    • Updated test_serialize to include the new execution_deadline key.
    • Added test_serialize_with_execution_deadline — round-trips a non-None deadline.
    • Added test_run_loop_emits_timeout_event_when_execution_deadline_reached — past-deadline → first iteration emits status="timeout" event.
    • Added test_run_loop_does_not_emit_timeout_when_execution_deadline_not_reached — far-future deadline → trigger keeps polling normally.
  • Operator (tests/unit/cncf/kubernetes/operators/test_pod.py):
    • Added test_invoke_defer_method_passes_execution_deadline_when_execution_timeout_set — operator with execution_timeout=300s passes a deadline ≈ ti.start_date + 300s to the trigger; defer.timeout is set.
    • Added test_invoke_defer_method_passes_no_deadline_when_execution_timeout_not_set — operator without execution_timeout passes None (no enforcement, no behaviour change).

Backwards Compatibility

No public API changes. New execution_deadline parameter on KubernetesPodTrigger is optional with default None. Behaviour change: execution_timeout-equipped deferred KPO tasks now actually fail at the configured timeout instead of running indefinitely; this is the documented contract.

Closes

Closes: #67227

@boring-cyborgboring-cyborgBot added area:providers provider:cncf-kubernetes Kubernetes (k8s) provider related issues labels May 20, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 4eb5810 to fb5e3fbCompareMay 20, 2026 14:18
@paultmathew
paultmathew marked this pull request as ready for review May 20, 2026 15:27
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 5151d22 to fb5e3fbCompareMay 20, 2026 15:57

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the extension, looks good to me. Except some comments.

Comment threadproviders/amazon/src/airflow/providers/amazon/aws/triggers/eks.py Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enforces execution_timeout for KubernetesPodOperator(deferrable=True) by translating the timeout into an absolute deadline, passing it to KubernetesPodTrigger, and adding trigger-side logic to emit a terminal timeout event when the deadline is exceeded (with accompanying unit tests). It also updates the EKS-specific trigger subclass to forward the new parameter.

Changes:

  • Add execution_deadline plumbing from KubernetesPodOperator.invoke_defer_method() to KubernetesPodTrigger and pass a timeout= to defer() based on remaining budget.
  • Add trigger-side deadline enforcement that emits a status="timeout" event once the deadline is crossed.
  • Extend/adjust unit tests for trigger serialization and timeout behavior, plus operator deferral plumbing.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/pod.pyCompute an absolute execution deadline from ti.start_date and execution_timeout, pass it to the trigger, and set defer(timeout=…).
providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/triggers/pod.pyAdd execution_deadline to trigger init/serialization and emit a timeout TriggerEvent when the deadline is exceeded.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.pyAdd tests asserting the operator passes execution_deadline (or None) into the trigger and sets defer.timeout appropriately.
providers/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.pyUpdate serialization expectations and add trigger run-loop tests for deadline timeout vs. continued polling.
providers/amazon/src/airflow/providers/amazon/aws/triggers/eks.pyForward the new execution_deadline parameter through EksPodTrigger to the base Kubernetes trigger.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from fb5e3fb to 8dfa1afCompareMay 20, 2026 23:45
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch 3 times, most recently from 2da1d91 to 6eec8cfCompareMay 22, 2026 18:08
@jscheffl

Copy link
Copy Markdown
Contributor

@jscheffl Thanks for the review. I pushed a change and addressed the comments.

Can you please resolve the comments addressed? And in the ones not being addressed reply a comment?

@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 6eec8cf to ad266d8CompareMay 24, 2026 20:01

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine overall. I have left some comments.

Also, I would urge you to verify your implementation using a live cluster to ensure that it fixes the issue. Passing a non-None value as an argument for the timeout parameter in the defer method resulted in the exact same bug remaining unresolved in the DBT Cloud and Airbyte providers. Arguably, your approach is more robust but it still needs live verification to be sure.

Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/operators/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
Comment threadproviders/cncf/kubernetes/tests/unit/cncf/kubernetes/triggers/test_pod.py Outdated
@potiukpotiuk added the ready for maintainer review Set after triaging when all criteria pass. label May 26, 2026
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from ad266d8 to 972a05fCompareMay 26, 2026 02:41
@paultmathew

Copy link
Copy Markdown
ContributorAuthor

@jscheffl@SameerMesiah97 thank you both for the reviews.

Pulled the **kwargs revert and any new surface on KubernetesPodTrigger. The deadline now rides inside the existing trigger_kwargs dict under the reserved key _execution_deadline. The leading-underscore convention follows the _redefer_count precedent in the same file.

@SameerMesiah97 you were right — the previous 60s minimum clamp didn't account for poll_interval. With execution_timeout=30s, poll_interval=60s, the framework would have cancelled the trigger at T+60 before its next deadline check at T+65.

The latest commit guarantees defer.timeout always covers remaining + ≥ 2 poll cycles. New unit test pins it: test_invoke_defer_method_pads_defer_timeout_for_slow_poll_interval verifies defer.timeout = 30 + 120 = 150s for a poll_interval=60 setup. I also did a smoke test against a live Kubernetes cluster (EKS) in our sandbox environment.

Smoke test summary

TaskConfigurationResultWhat it proved
t1 happy pathexecution_timeout=5m, sleep 20s✓ success at ~26s, pod deletedDeferred path works end-to-end on real K8s; deadline doesn't trip prematurely
t2 timeout default pollexecution_timeout=30s, poll_interval=2s, sleep 600s✓ failed at ~64s with "Execution deadline reached for pod ... emitting timeout event", pod deletedNew code path fires; _clean() runs; trigger emits soft-timeout event
t3 timeout slow pollexecution_timeout=30s, poll_interval=30s, sleep 600s✓ failed at ~70s, timeout event at +38s, pod deletedThe reviewer's exact concern — slow poll doesn't break cleanup; new poll_buffer = max(60, poll_interval * 2) gives the trigger sufficient runway
t4 no timeoutno execution_timeout, sleep 20s✓ success at ~26s, no deadline messagesOpt-out path preserved; tasks without execution_timeout continue to work as before

Compute the deadline operator-side from ti.start_date + execution_timeout
and plumb it to KubernetesPodTrigger via trigger_kwargs["_execution_deadline"]
(an existing dict already accepted and serialized by every subclass) so
the trigger can short-circuit and emit its own status="timeout" event.
This routes timeout through trigger_reentry → _clean() → pod deletion +
final log capture, matching the success/failure event path.
defer.timeout is set to the remaining budget with a 60s minimum so the
trigger has runway to emit its own timeout event before the framework
backstop fires (which would otherwise short-circuit to TaskDeferralTimeout
and skip the operator's cleanup).
Following the leading-underscore convention established by _redefer_count
in the same file: trigger_kwargs is the only existing operator -> trigger
plumbing that's a generic, untyped, dict-shaped, fully-serialized bag and
already accepted by every KubernetesPodTrigger subclass. Using it avoids
adding a new __init__ kwarg or serialize() field on KubernetesPodTrigger,
keeping cross-version compatibility with subclasses (e.g. EksPodTrigger,
GKEStartPodTrigger) released independently.
Closes: apache#67227
Co-authored-by: Cursor <cursoragent@cursor.com>
@paultmathew
paultmathewforce-pushed the fix/67227-kpo-deferrable-execution-timeout branch from 972a05f to d762e65CompareMay 26, 2026 03:23

@jscheffljscheffl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. @SameerMesiah97 another pass of review or any other maintainer feedback?

@SameerMesiah97SameerMesiah97 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. I must mention that your approach to add a buffer is a good compromise (which I will implement myself going forwards). Unfortunately, there is a gap at the framework level regarding the implementation of execution timeouts that has not been addressed as of yet so we must use temporary workarounds like this.

@jscheffl
jscheffl merged commit 3d3e79d into apache:mainMay 30, 2026
113 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:providersprovider:cncf-kubernetesKubernetes (k8s) provider related issuesready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KubernetesPodOperator does not enforce execution_timeout semantics in Deferrable mode

5 participants

@paultmathew@jscheffl@SameerMesiah97@potiuk