seanghaeli opened a new pull request, #69499:
URL: https://github.com/apache/airflow/pull/69499

   ## Why
   
   `RedshiftDeleteClusterOperator` retries on `InvalidClusterStateFault` when 
the cluster is mid-transition (e.g. pausing/resuming/resizing), but the retry 
budget was **hardcoded** to `10 attempts * 15s = 2.5 minutes`. A cluster 
transition routinely takes much longer than that (a pause is ~2–15 min, a 
classic resize can be many minutes), so the loop exhausts and re-raises long 
before the cluster reaches a deletable state. The task fails and the cluster is 
**leaked** — it keeps running and billing until someone deletes it manually.
   
   The real AWS error the operator hits mid-transition:
   
   ```
   InvalidClusterState: There is an operation running on the Cluster.
   Please try to delete it at a later time.
   ```
   
   (and, once resizing: `Unable to delete the cluster ... You can only delete 
clusters with PAUSED, ..., ACTIVE, ... lifecycles.`)
   
   ## What
   
   Promote the two hardcoded constants to `__init__` parameters and raise the 
default so the retry outlasts a real transition:
   
   - `busy_retry_attempts: int = 60`
   - `busy_retry_interval: int = 15`
   
   Default is now `60 * 15s = 15 minutes`, and both are overridable. 
`poll_interval` / `max_attempts` (the completion waiter) are unchanged. The 
existing fail-loud behavior is preserved: once the budget is exhausted the 
operator still re-raises rather than swallowing the error.
   
   ## Behavior change
   
   - Default busy-retry window: **2.5 min → 15 min**. A delete against a 
transitioning cluster now rides through the transition instead of failing.
   - The private attributes `self._attempts` / `self._attempt_interval` are 
replaced by the new public params. They were underscore-private, so external 
dependence is unlikely.
   
   ## Scope / follow-up
   
   This retry loop is **synchronous** and runs before the `deferrable` branch, 
so in deferrable mode it still blocks the worker for the retry window. This PR 
fixes the synchronous path (the reported leak). The correct async fix for 
deferrable mode — defer immediately on `InvalidClusterStateFault`, poll until 
the cluster settles via a trigger, then re-issue the delete (mirroring the 
re-defer pattern in `EksCreateClusterOperator.deferrable_create_cluster_next`) 
— is a larger change that adds a new serialized trigger, so it will be a 
**separate follow-up PR** to keep this one small and focused.
   
   ## Testing
   
   Unit tests (added/updated): default values, custom values, retry-then-raise 
after a custom budget, and succeed-on-second-attempt. `16 passed` locally.
   
   ### Real-AWS end-to-end proof (before/after)
   
   Verified against a real Redshift cluster in a dev account, using an 
identical DAG and an identical ~6.5-minute transient (a classic resize) as a 
controlled before/after. Same cluster, same transient window.
   
   **BEFORE (stock `main`, 2.5-min budget) — task FAILS, cluster leaks:**
   
   ```
   20:38:51  Cluster in resizing state, unable to delete. 9 attempts remaining.
   20:39:06  ... 8 attempts remaining.
      ... (15s sleeps) ...
   20:40:54  ... 1 attempts remaining.
   botocore.errorfactory.InvalidClusterStateFault: An error occurred 
(InvalidClusterState)
     when calling the DeleteCluster operation: Unable to delete the cluster ...
   DagRun ... state=failed   TASK_EXIT=1
   ```
   
   **AFTER (this PR, 15-min budget) — task SUCCEEDS, cluster deleted:**
   
   ```
   20:42:27  Cluster in resizing state, unable to delete. 59 attempts remaining.
   20:42:43  ... 58 attempts remaining.
   20:43:29  ... 55 attempts remaining.      <-- already past where stock main 
died
      ...
   20:44:46  ... 50 attempts remaining.
   20:45:01  Task instance state updated ... new_state=success   <-- delete 
accepted once resize settled
   DagRun ... state=success   TASK_EXIT=0
   ```
   
   AWS-side status timeline confirms the sequence: `resizing` throughout both 
runs → BEFORE fails mid-resize at 20:41 → AFTER's delete is accepted at 20:45 
(`deleting`) → `ClusterNotFound` at 20:48 (cluster fully torn down, no leak).
   
   > Note: UI screenshots were not captured (headless environment); the task 
logs above are the primary artifact. A classic resize was used as the transient 
because pause/resume settled too quickly (~100–170s) to reliably exceed stock 
`main`'s 150s budget — the transition *type* is immaterial, any 
`InvalidClusterStateFault`-producing operation exercises the same retry loop.
   
   ---
   Generated-by: Claude Code (Opus)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to