using ClaudeDo.Data; using ClaudeDo.Data.Models; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Queue; public sealed class QueuePicker : IQueuePicker { private readonly IDbContextFactory _dbFactory; public QueuePicker(IDbContextFactory dbFactory) => _dbFactory = dbFactory; public async Task ClaimNextAsync(DateTime now, CancellationToken ct) { await using var ctx = await _dbFactory.CreateDbContextAsync(ct); var nowStr = now.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss.fffffff"); var startedAtStr = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fffffff"); // Atomic queue claim: UPDATE + RETURNING in a single statement prevents TOCTOU races. // Raw SQL because EF cannot express UPDATE...RETURNING. // Eligible task must be Queued, unblocked (chain and depends-on), not manual, and due // (or unscheduled). A task_dependencies edge only gates on the dependency's Status='done' // -- unlike blocked_by_task_id there is no cascade on the dependency failing, so a task // with a Failed dependency simply stays here, skipped, until someone intervenes. The // depends-on clause is doubly negated on purpose: "no edge whose predecessor isn't done" // means a task with zero edges is eligible, ALL edges must be done, and an edge pointing // at a vanished task keeps blocking (same as the old single-column form did). // EF SQLite stores DateTime as "yyyy-MM-dd HH:mm:ss.fffffff" — same format used here for comparison. var rows = await ctx.Tasks.FromSqlRaw(""" UPDATE tasks SET status = 'running', started_at = {1} WHERE id = ( SELECT t.id FROM tasks t WHERE t.status = 'queued' AND t.blocked_by_task_id IS NULL AND t.is_manual = 0 AND (t.scheduled_for IS NULL OR t.scheduled_for <= {0}) AND NOT EXISTS ( SELECT 1 FROM task_dependencies td WHERE td.task_id = t.id AND NOT EXISTS ( SELECT 1 FROM tasks d WHERE d.id = td.depends_on_task_id AND d.status = 'done' ) ) ORDER BY t.sort_order ASC, t.created_at ASC LIMIT 1 ) RETURNING * """, nowStr, startedAtStr).ToListAsync(ct); return rows.FirstOrDefault(); } }