Detect and alert on stuck jobs
CI / Test (rest) (pull_request) Successful in 39s
CI / Test (store) (pull_request) Successful in 40s
CI / Lint (pull_request) Failing after 11s
CI / Build (linux/amd64) (pull_request) Successful in 8s
CI / Build (linux/arm64) (pull_request) Successful in 7s
CI / Build (windows/amd64) (pull_request) Successful in 25s
CI / Test (server-http) (pull_request) Successful in 1m39s
e2e / Playwright vs docker-compose (pull_request) Successful in 1m16s
CI / Test (rest) (pull_request) Successful in 39s
CI / Test (store) (pull_request) Successful in 40s
CI / Lint (pull_request) Failing after 11s
CI / Build (linux/amd64) (pull_request) Successful in 8s
CI / Build (linux/arm64) (pull_request) Successful in 7s
CI / Build (windows/amd64) (pull_request) Successful in 25s
CI / Test (server-http) (pull_request) Successful in 1m39s
e2e / Playwright vs docker-compose (pull_request) Successful in 1m16s
This commit is contained in:
@@ -27,6 +27,52 @@ type Job struct {
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// RunningJobActivity is the persisted activity summary used to detect jobs
|
||||
// whose terminal message was lost. LastActivity is the newer of started_at and
|
||||
// the most recent persisted log line; ephemeral progress events deliberately
|
||||
// do not extend it.
|
||||
type RunningJobActivity struct {
|
||||
JobID string
|
||||
HostID string
|
||||
Kind string
|
||||
StartedAt time.Time
|
||||
LastActivity time.Time
|
||||
}
|
||||
|
||||
// ListRunningJobActivity returns every running job with its latest persisted
|
||||
// activity. Jobs without started_at are excluded because they have not actually
|
||||
// entered the running state coherently and cannot be aged safely here.
|
||||
func (s *Store) ListRunningJobActivity(ctx context.Context) ([]RunningJobActivity, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT j.id, j.host_id, j.kind, j.started_at,
|
||||
COALESCE(MAX(l.ts), j.started_at) AS last_activity
|
||||
FROM jobs j
|
||||
LEFT JOIN job_logs l ON l.job_id = j.id
|
||||
WHERE j.status = 'running' AND j.started_at IS NOT NULL
|
||||
GROUP BY j.id, j.host_id, j.kind, j.started_at
|
||||
ORDER BY j.started_at`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: list running job activity: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var out []RunningJobActivity
|
||||
for rows.Next() {
|
||||
var item RunningJobActivity
|
||||
var started, activity string
|
||||
if err := rows.Scan(&item.JobID, &item.HostID, &item.Kind, &started, &activity); err != nil {
|
||||
return nil, fmt.Errorf("store: scan running job activity: %w", err)
|
||||
}
|
||||
item.StartedAt, _ = time.Parse(time.RFC3339Nano, started)
|
||||
item.LastActivity, _ = time.Parse(time.RFC3339Nano, activity)
|
||||
out = append(out, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("store: iterate running job activity: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CreateJob inserts a queued job. The agent will mark it running
|
||||
// when it actually starts work. ScheduledID is set when the job
|
||||
// originates from a cron fire (actor_kind="schedule"); nil for
|
||||
|
||||
Reference in New Issue
Block a user