refactor: Datei-Scope-Serialisierung entfernen

ScopeGlobs auf Tasks und SerializeOnFileOverlap auf der Listen-Config waren
ungenutzt: der Scope wurde nie befuellt, also hat der Queue-Picker nie
serialisiert. ScopeOverlap, das Picker-Gate, die DTO-Felder, die UI-Option und
die Spalten fallen weg (Migration DropFileScopeSerialization).
This commit is contained in:
mika kuns
2026-08-27 16:43:14 +02:00
parent 4e55f9161c
commit 84219a9f88
25 changed files with 1051 additions and 482 deletions
-73
View File
@@ -18,22 +18,6 @@ public sealed class QueuePicker : IQueuePicker
var nowStr = now.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss.fffffff");
var startedAtStr = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fffffff");
var serializingListIds = await ctx.ListConfigs
.Where(c => c.SerializeOnFileOverlap)
.Select(c => c.ListId)
.ToListAsync(ct);
// Fast path: no list has opted into file-scope serialization, so behavior and cost stay
// identical to before that option existed -- single atomic UPDATE...RETURNING.
if (serializingListIds.Count == 0)
return await ClaimTopEligibleAsync(ctx, nowStr, startedAtStr, ct);
return await ClaimRespectingScopeAsync(ctx, now, startedAtStr, serializingListIds.ToHashSet(StringComparer.Ordinal), ct);
}
private static async Task<TaskEntity?> ClaimTopEligibleAsync(
ClaudeDoDbContext ctx, string nowStr, string startedAtStr, CancellationToken ct)
{
// 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
@@ -62,61 +46,4 @@ public sealed class QueuePicker : IQueuePicker
return rows.FirstOrDefault();
}
// At least one list wants overlapping-scope tasks serialized: walk eligible candidates in the
// usual order and skip any whose declared scope overlaps a running or awaiting-merge sibling
// in the same list. A candidate with no declared scope, or belonging to a list not in
// serializingListIds, is claimed exactly as before -- there is no attempt to predict a scope
// that was never declared and no finished sibling to infer it from.
private async Task<TaskEntity?> ClaimRespectingScopeAsync(
ClaudeDoDbContext ctx, DateTime now, string startedAtStr, HashSet<string> serializingListIds, CancellationToken ct)
{
var candidates = await ctx.Tasks
.AsNoTracking()
.Where(t => t.Status == TaskStatus.Queued
&& t.BlockedByTaskId == null
&& !t.IsManual
&& (t.ScheduledFor == null || t.ScheduledFor <= now))
.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;
var rows = await ctx.Tasks.FromSqlRaw("""
UPDATE tasks SET status = 'running', started_at = {1}
WHERE id = {0} AND status = 'queued'
RETURNING *
""", candidate.Id, startedAtStr).ToListAsync(ct);
var claimed = rows.FirstOrDefault();
if (claimed is not null) return claimed;
// Lost the race for this row to a concurrent picker -- try the next candidate.
}
return null;
}
}