Adiciona mecanismo de expiração periódica para jobs que ficam presos nos status received ou processing além de um threshold configurável. - models: novo status StatusTimeout - store: migration v2 com índice em jobs(status); método ExpireStuck que faz UPDATE atômico para os status alvo anteriores ao threshold - expirer: novo pacote com goroutine periódica que chama ExpireStuck e loga WARN quando algum job é expirado - config: campos JobTimeoutMinutes (default 120) e ExpirerIntervalMinutes (default 10), lidos via env vars JOB_TIMEOUT_MINUTES e EXPIRER_INTERVAL_MINUTES - main: instancia e inicia o Expirer no startup - dashboard: card Timeout com contagem; badge neutral na tabela de jobs
58 lines
1.1 KiB
Go
58 lines
1.1 KiB
Go
package expirer
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"coda/internal/models"
|
|
)
|
|
|
|
type JobExpirer interface {
|
|
ExpireStuck(ctx context.Context, statuses []models.JobStatus, olderThan time.Duration) (int, error)
|
|
}
|
|
|
|
type Expirer struct {
|
|
jobs JobExpirer
|
|
timeout time.Duration
|
|
interval time.Duration
|
|
}
|
|
|
|
func New(jobs JobExpirer, timeout, interval time.Duration) *Expirer {
|
|
return &Expirer{
|
|
jobs: jobs,
|
|
timeout: timeout,
|
|
interval: interval,
|
|
}
|
|
}
|
|
|
|
func (e *Expirer) Start(ctx context.Context) {
|
|
go e.run(ctx)
|
|
}
|
|
|
|
func (e *Expirer) run(ctx context.Context) {
|
|
ticker := time.NewTicker(e.interval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
e.expire(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *Expirer) expire(ctx context.Context) {
|
|
statuses := []models.JobStatus{models.StatusReceived, models.StatusProcessing}
|
|
n, err := e.jobs.ExpireStuck(ctx, statuses, e.timeout)
|
|
if err != nil {
|
|
slog.Error("expirer: failed to expire stuck jobs", "error", err)
|
|
return
|
|
}
|
|
if n > 0 {
|
|
slog.Warn("expirer: expired stuck jobs", "count", n, "timeout", e.timeout)
|
|
}
|
|
}
|