-
Notifications
You must be signed in to change notification settings - Fork 177
Lookout pruner: repair lease-returned/lease-expired zombie jobs #5058
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
a947243
edfbc16
f8be81e
9cb36e4
56948ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,22 @@ import ( | |
| // parameters) is safe because they are compile-time constants from | ||
| // internal/common/database/lookout, not user input. It also lets PostgreSQL | ||
| // infer mapping.new_state as smallint without explicit casts. | ||
| // | ||
| // LEASE_RETURNED and LEASE_EXPIRED are mapped to job FAILED rather than left | ||
| // unhandled. Ordinarily a lease-returned/expired run is followed by either a | ||
| // JobRequeued or a terminal JobErrors event, so the run itself is not a | ||
| // reliable terminal signal for the job. But if that follow-up event is lost | ||
| // (e.g. dropped by the ingester), the job is stuck showing a non-terminal | ||
| // state forever with no other event to correct it. FAILED is a conservative | ||
| // choice: it may mislabel a job that was actually still being retried, but it | ||
| // stops a permanently stuck job from being reported as active indefinitely. | ||
| // | ||
| // LEASE_RETURNED/LEASE_EXPIRED use a separate, longer cutoff ($2) than the | ||
| // other four run states ($1): unlike the other four, which are unconditionally | ||
| // terminal for the job the moment the run reaches them, a lease-returned or | ||
| // lease-expired run is normally expected to be followed by a legitimate | ||
| // scheduler decision (retry or fail) that can take substantially longer than | ||
| // ordinary ingester lag to arrive. | ||
| var reconcileZombiesQuery = fmt.Sprintf(` | ||
| UPDATE job | ||
| SET state = mapping.new_state, | ||
|
|
@@ -32,10 +48,12 @@ var reconcileZombiesQuery = fmt.Sprintf(` | |
| j.state AS old_state, | ||
| j.latest_run_id AS run_id, | ||
| CASE r.job_run_state | ||
| WHEN %[1]d THEN %[2]d -- run succeeded -> job succeeded | ||
| WHEN %[3]d THEN %[4]d -- run failed -> job failed | ||
| WHEN %[5]d THEN %[6]d -- run cancelled -> job cancelled | ||
| WHEN %[7]d THEN %[8]d -- run preempted -> job preempted | ||
| WHEN %[1]d THEN %[2]d -- run succeeded -> job succeeded | ||
| WHEN %[3]d THEN %[4]d -- run failed -> job failed | ||
| WHEN %[5]d THEN %[6]d -- run cancelled -> job cancelled | ||
| WHEN %[7]d THEN %[8]d -- run preempted -> job preempted | ||
| WHEN %[13]d THEN %[4]d -- run lease returned -> job failed | ||
| WHEN %[14]d THEN %[4]d -- run lease expired -> job failed | ||
| ELSE j.state -- defensive: a job_run_state that | ||
| -- passes the IN filter but is not | ||
| -- listed above (e.g. after a future | ||
|
|
@@ -47,10 +65,13 @@ var reconcileZombiesQuery = fmt.Sprintf(` | |
| FROM job j | ||
| JOIN job_run r ON r.run_id = j.latest_run_id | ||
| WHERE j.state IN (%[9]d, %[10]d, %[11]d, %[12]d) | ||
| AND r.job_run_state IN (%[1]d, %[3]d, %[5]d, %[7]d) | ||
| AND r.finished IS NOT NULL | ||
| AND r.finished < $1 | ||
| LIMIT $2 | ||
| AND ( | ||
| (r.job_run_state IN (%[1]d, %[3]d, %[5]d, %[7]d) AND r.finished < $1) | ||
| OR | ||
| (r.job_run_state IN (%[13]d, %[14]d) AND r.finished < $2) | ||
| ) | ||
| LIMIT $3 | ||
| ) AS mapping | ||
| WHERE job.job_id = mapping.job_id | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an executor timestamps a lease return ahead of the scheduler clock used for the subsequent legitimate Knowledge Base Used: |
||
| -- Filter out ELSE-branch rows so they don't (a) get last_transition_time | ||
|
|
@@ -66,6 +87,8 @@ var reconcileZombiesQuery = fmt.Sprintf(` | |
| lookout.JobLeasedOrdinal, | ||
| lookout.JobPendingOrdinal, | ||
| lookout.JobRunningOrdinal, | ||
| lookout.JobRunLeaseReturnedOrdinal, | ||
| lookout.JobRunLeaseExpiredOrdinal, | ||
| ) | ||
|
|
||
| // countZombiesWithNullFinishedQuery counts jobs that match the zombie shape | ||
|
|
@@ -76,34 +99,54 @@ var countZombiesWithNullFinishedQuery = fmt.Sprintf(` | |
| SELECT COUNT(*) | ||
| FROM job j | ||
| JOIN job_run r ON r.run_id = j.latest_run_id | ||
| WHERE j.state IN (%[5]d, %[6]d, %[7]d, %[8]d) | ||
| AND r.job_run_state IN (%[1]d, %[2]d, %[3]d, %[4]d) | ||
| WHERE j.state IN (%[7]d, %[8]d, %[9]d, %[10]d) | ||
| AND r.job_run_state IN (%[1]d, %[2]d, %[3]d, %[4]d, %[5]d, %[6]d) | ||
| AND r.finished IS NULL`, | ||
| lookout.JobRunSucceededOrdinal, | ||
| lookout.JobRunFailedOrdinal, | ||
| lookout.JobRunCancelledOrdinal, | ||
| lookout.JobRunPreemptedOrdinal, | ||
| lookout.JobRunLeaseReturnedOrdinal, | ||
| lookout.JobRunLeaseExpiredOrdinal, | ||
| lookout.JobQueuedOrdinal, | ||
| lookout.JobLeasedOrdinal, | ||
| lookout.JobPendingOrdinal, | ||
| lookout.JobRunningOrdinal, | ||
| ) | ||
|
|
||
| // ReconcileZombieJobs finds jobs whose state column is non-terminal but whose | ||
| // latest run is in a terminal state and finished more than zombieRepairThreshold | ||
| // ago, and updates job.state (and last_transition_time) to match the run. | ||
| // Returns the number of jobs repaired. | ||
| // latest run is in a terminal state, or in a lease-returned/lease-expired | ||
| // state that never received its expected follow-up event, and updates | ||
| // job.state (and last_transition_time) to match the run. The two cases use | ||
| // separate grace periods: zombieRepairThreshold for the unconditionally | ||
| // terminal run states, and leaseReturnedZombieRepairThreshold (normally much | ||
| // longer) for lease-returned/lease-expired, since those are legitimately | ||
| // followed by a scheduler retry-or-fail decision that can take a while to | ||
| // arrive. A zero threshold disables reconciliation for its run-state group | ||
| // independently of the other. Returns the number of jobs repaired. | ||
| func ReconcileZombieJobs( | ||
| ctx *armadacontext.Context, | ||
| db *pgx.Conn, | ||
| zombieRepairThreshold time.Duration, | ||
| leaseReturnedZombieRepairThreshold time.Duration, | ||
| batchLimit int, | ||
| clock clock.Clock, | ||
| ) (int, error) { | ||
| cutOffTime := clock.Now().Add(-zombieRepairThreshold) | ||
| // A zero threshold disables reconciliation for that run-state group. Using | ||
| // the zero time.Time (year 1) as the cutoff, rather than clock.Now(), | ||
| // ensures "r.finished < cutoff" can never match instead of matching | ||
| // everything already finished. | ||
| cutOffTime := time.Time{} | ||
| if zombieRepairThreshold > 0 { | ||
| cutOffTime = clock.Now().Add(-zombieRepairThreshold) | ||
| } | ||
| leaseReturnedCutOffTime := time.Time{} | ||
| if leaseReturnedZombieRepairThreshold > 0 { | ||
| leaseReturnedCutOffTime = clock.Now().Add(-leaseReturnedZombieRepairThreshold) | ||
| } | ||
| totalRepaired := 0 | ||
| for { | ||
| batchRepaired, err := reconcileZombieBatch(ctx, db, cutOffTime, batchLimit) | ||
| batchRepaired, err := reconcileZombieBatch(ctx, db, cutOffTime, leaseReturnedCutOffTime, batchLimit) | ||
| if err != nil { | ||
| return totalRepaired, err | ||
| } | ||
|
|
@@ -151,6 +194,7 @@ func reconcileZombieBatch( | |
| ctx *armadacontext.Context, | ||
| db *pgx.Conn, | ||
| cutOffTime time.Time, | ||
| leaseReturnedCutOffTime time.Time, | ||
| batchLimit int, | ||
| ) (int, error) { | ||
| type repair struct { | ||
|
|
@@ -168,7 +212,7 @@ func reconcileZombieBatch( | |
| IsoLevel: pgx.ReadCommitted, | ||
| AccessMode: pgx.ReadWrite, | ||
| }, func(tx pgx.Tx) error { | ||
| rows, err := tx.Query(ctx, reconcileZombiesQuery, cutOffTime, batchLimit) | ||
| rows, err := tx.Query(ctx, reconcileZombiesQuery, cutOffTime, leaseReturnedCutOffTime, batchLimit) | ||
| if err != nil { | ||
| return errors.WithStack(err) | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.