coda/internal/store/jobs.go

199 lines
5.2 KiB
Go

package store
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"coda/internal/models"
"github.com/google/uuid"
)
type ListJobsParams struct {
Status *models.JobStatus
Limit int
Offset int
}
type JobRepository struct {
db *sql.DB
}
func (r *JobRepository) Create(ctx context.Context, job *models.Job) error {
if job.ID == "" {
job.ID = uuid.New().String()
}
if job.CreatedAt.IsZero() {
job.CreatedAt = time.Now().UTC()
}
_, err := r.db.ExecContext(ctx,
`INSERT INTO jobs (id, artist, album, path, status, source, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
job.ID, job.Artist, job.Album, job.Path, job.Status, job.Source, job.CreatedAt,
)
if err != nil {
return fmt.Errorf("insert job: %w", err)
}
return nil
}
func (r *JobRepository) Get(ctx context.Context, id string) (*models.Job, error) {
row := r.db.QueryRowContext(ctx,
`SELECT id, artist, album, path, status, source, created_at, finished_at, duration_ms, error_msg, files_total, files_processed
FROM jobs WHERE id = ?`, id)
var job models.Job
err := row.Scan(&job.ID, &job.Artist, &job.Album, &job.Path, &job.Status, &job.Source,
&job.CreatedAt, &job.FinishedAt, &job.DurationMs, &job.ErrorMsg, &job.FilesTotal, &job.FilesProcessed)
if err == sql.ErrNoRows {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("get job: %w", err)
}
return &job, nil
}
func (r *JobRepository) List(ctx context.Context, params ListJobsParams) ([]models.Job, error) {
query := `SELECT id, artist, album, path, status, source, created_at, finished_at, duration_ms, error_msg, files_total, files_processed
FROM jobs`
args := []any{}
if params.Status != nil {
query += ` WHERE status = ?`
args = append(args, *params.Status)
}
query += ` ORDER BY created_at DESC`
if params.Limit > 0 {
query += ` LIMIT ?`
args = append(args, params.Limit)
}
if params.Offset > 0 {
query += ` OFFSET ?`
args = append(args, params.Offset)
}
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("list jobs: %w", err)
}
defer rows.Close()
var jobs []models.Job
for rows.Next() {
var job models.Job
if err := rows.Scan(&job.ID, &job.Artist, &job.Album, &job.Path, &job.Status, &job.Source,
&job.CreatedAt, &job.FinishedAt, &job.DurationMs, &job.ErrorMsg, &job.FilesTotal, &job.FilesProcessed); err != nil {
return nil, fmt.Errorf("scan job: %w", err)
}
jobs = append(jobs, job)
}
return jobs, rows.Err()
}
func (r *JobRepository) Count(ctx context.Context, status *models.JobStatus) (int, error) {
query := `SELECT COUNT(*) FROM jobs`
args := []any{}
if status != nil {
query += ` WHERE status = ?`
args = append(args, *status)
}
var count int
err := r.db.QueryRowContext(ctx, query, args...).Scan(&count)
if err != nil {
return 0, fmt.Errorf("count jobs: %w", err)
}
return count, nil
}
func (r *JobRepository) UpdateProgress(ctx context.Context, id string, filesProcessed, filesTotal int) error {
result, err := r.db.ExecContext(ctx,
`UPDATE jobs SET files_processed = ?, files_total = ? WHERE id = ?`,
filesProcessed, filesTotal, id,
)
if err != nil {
return fmt.Errorf("update job progress: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("rows affected: %w", err)
}
if rows == 0 {
return ErrNotFound
}
return nil
}
func (r *JobRepository) UpdateStatus(ctx context.Context, id string, status models.JobStatus) error {
result, err := r.db.ExecContext(ctx, `UPDATE jobs SET status = ? WHERE id = ?`, status, id)
if err != nil {
return fmt.Errorf("update job status: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("rows affected: %w", err)
}
if rows == 0 {
return ErrNotFound
}
return nil
}
func (r *JobRepository) Finish(ctx context.Context, id string, status models.JobStatus, durationMs int64, errMsg string) error {
result, err := r.db.ExecContext(ctx,
`UPDATE jobs SET status = ?, finished_at = ?, duration_ms = ?, error_msg = ? WHERE id = ?`,
status, time.Now().UTC(), durationMs, errMsg, id,
)
if err != nil {
return fmt.Errorf("finish job: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("rows affected: %w", err)
}
if rows == 0 {
return ErrNotFound
}
return nil
}
func (r *JobRepository) ExpireStuck(ctx context.Context, statuses []models.JobStatus, olderThan time.Duration) (int, error) {
if len(statuses) == 0 {
return 0, nil
}
placeholders := make([]string, len(statuses))
args := make([]any, 0, len(statuses)+1)
for i, status := range statuses {
placeholders[i] = "?"
args = append(args, status)
}
args = append(args, time.Now().UTC().Add(-olderThan))
query := fmt.Sprintf(
`UPDATE jobs SET status = ?, finished_at = ?, error_msg = ? WHERE status IN (%s) AND created_at < ?`,
strings.Join(placeholders, ","),
)
finalArgs := []any{models.StatusTimeout, time.Now().UTC(), "job timed out"}
finalArgs = append(finalArgs, args...)
result, err := r.db.ExecContext(ctx, query, finalArgs...)
if err != nil {
return 0, fmt.Errorf("expire stuck jobs: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("rows affected: %w", err)
}
return int(rows), nil
}