Watches for jobs that may have become stuck. i.e. They've run longer than their job timeout (plus a small margin) and don't appear to be responding to context cancellation (unfortunately, quite an easy error to make in Go). Producers use stuck-job notifications for periodic stats and optional user
(ctx context.Context, jobTimeout time.Duration)
| 275 | // Producers use stuck-job notifications for periodic stats and optional user |
| 276 | // handlers. |
| 277 | func (e *JobExecutor) watchStuck(ctx context.Context, jobTimeout time.Duration) context.CancelFunc { |
| 278 | // We add a WithoutCancel here so that this inner goroutine becomes |
| 279 | // immune to all context cancellations _except_ the one where it's |
| 280 | // cancelled because we leave JobExecutor.execute. |
| 281 | // |
| 282 | // This shadows the context outside the e.ClientJobTimeout > 0 check. |
| 283 | ctx, cancel := context.WithCancel(context.WithoutCancel(ctx)) |
| 284 | |
| 285 | go func() { |
| 286 | const stuckThresholdDefault = 5 * time.Second |
| 287 | |
| 288 | select { |
| 289 | case <-ctx.Done(): |
| 290 | // context cancelled as we leave JobExecutor.execute |
| 291 | |
| 292 | case <-time.After(jobTimeout + cmp.Or(e.StuckThresholdOverride, stuckThresholdDefault)): |
| 293 | e.ProducerCallbacks.Stuck(ctx, e.JobRow) |
| 294 | |
| 295 | e.Logger.WarnContext(ctx, e.Name+": Job appears to be stuck", |
| 296 | slog.Int64("job_id", e.JobRow.ID), |
| 297 | slog.String("kind", e.JobRow.Kind), |
| 298 | slog.Duration("timeout", e.ClientJobTimeout), |
| 299 | ) |
| 300 | |
| 301 | // context cancelled as we leave JobExecutor.execute |
| 302 | <-ctx.Done() |
| 303 | |
| 304 | // In case the executor ever becomes unstuck, inform the |
| 305 | // producer. However, if we got all the way here there's a good |
| 306 | // chance this will never happen (the worker is really stuck and |
| 307 | // will never return). |
| 308 | defer e.ProducerCallbacks.Unstuck() |
| 309 | |
| 310 | defer func() { |
| 311 | e.Logger.InfoContext(ctx, e.Name+": Job became unstuck", |
| 312 | slog.Duration("duration", time.Since(e.start)), |
| 313 | slog.Int64("job_id", e.JobRow.ID), |
| 314 | slog.String("kind", e.JobRow.Kind), |
| 315 | ) |
| 316 | }() |
| 317 | } |
| 318 | }() |
| 319 | |
| 320 | return cancel |
| 321 | } |
| 322 | |
| 323 | func (e *JobExecutor) invokeErrorHandler(ctx context.Context, res *jobExecutorResult) bool { |
| 324 | invokeAndHandlePanic := func(funcName string, errorHandler func() *ErrorHandlerResult) *ErrorHandlerResult { |