Merge branch 'claudedo/5c80a0aba5d9468a96cf809fc7a6e7a1'
This commit is contained in:
@@ -36,7 +36,10 @@ public sealed class QueuePicker : IQueuePicker
|
||||
{
|
||||
// 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, not manual, and due (or unscheduled).
|
||||
// Eligible task must be Queued, unblocked (chain and depends-on), not manual, and due
|
||||
// (or unscheduled). depends_on_task_id 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.
|
||||
// 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}
|
||||
@@ -46,6 +49,11 @@ public sealed class QueuePicker : IQueuePicker
|
||||
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 (t.depends_on_task_id IS NULL
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM tasks d
|
||||
WHERE d.id = t.depends_on_task_id AND d.status = 'done'
|
||||
))
|
||||
ORDER BY t.sort_order ASC, t.created_at ASC
|
||||
LIMIT 1
|
||||
)
|
||||
@@ -72,8 +80,28 @@ public sealed class QueuePicker : IQueuePicker
|
||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
// The same depends-on gate ClaimTopEligibleAsync applies in SQL: a declared dependency must
|
||||
// be Done. Resolved as one extra query over just the referenced ids (rather than repeating
|
||||
// the correlated subquery) so both claim paths agree -- without it, opting a list into
|
||||
// scope serialization would silently stop enforcing dependencies.
|
||||
var dependencyIds = candidates
|
||||
.Where(t => t.DependsOnTaskId != null)
|
||||
.Select(t => t.DependsOnTaskId!)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var doneDependencyIds = dependencyIds.Count == 0
|
||||
? new HashSet<string>(StringComparer.Ordinal)
|
||||
: (await ctx.Tasks.AsNoTracking()
|
||||
.Where(d => dependencyIds.Contains(d.Id) && d.Status == TaskStatus.Done)
|
||||
.Select(d => d.Id)
|
||||
.ToListAsync(ct))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (candidate.DependsOnTaskId is { } dependsOn && !doneDependencyIds.Contains(dependsOn))
|
||||
continue;
|
||||
|
||||
if (serializingListIds.Contains(candidate.ListId)
|
||||
&& await ScopeOverlap.FindBlockingSiblingAsync(ctx, candidate, ct) is not null)
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user