Merge task branch for: fix(worker): planning-chain cascade stalls at an Idle middle link
This commit is contained in:
@@ -92,14 +92,7 @@ public sealed class PlanningChainCoordinator
|
||||
public async Task<string?> OnChildFinishedAsync(
|
||||
string childTaskId, TaskStatus finalStatus, CancellationToken ct = default)
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
// The successor is whichever sibling explicitly blocks on this child.
|
||||
var nextId = await ctx.Tasks
|
||||
.AsNoTracking()
|
||||
.Where(t => t.BlockedByTaskId == childTaskId)
|
||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||
.Select(t => t.Id)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
var nextId = await FindSuccessorAsync(childTaskId, ct);
|
||||
if (nextId is null) return null;
|
||||
|
||||
if (finalStatus == TaskStatus.Done)
|
||||
@@ -108,10 +101,33 @@ public sealed class PlanningChainCoordinator
|
||||
return nextId;
|
||||
}
|
||||
|
||||
// Child failed or was cancelled: cancel the immediate successor so the chain
|
||||
// is not left wedged. CancelAsync triggers OnChildTerminalAsync → OnChildFinishedAsync
|
||||
// for that successor, cascading cancellation through the rest of the chain.
|
||||
await _state().CancelAsync(nextId, DateTime.UtcNow, ct);
|
||||
return null;
|
||||
// Child failed or was cancelled: cancel the immediate successor so the chain is
|
||||
// not left wedged. If it's cancellable, CancelAsync's own OnChildTerminalAsync
|
||||
// callback recurses into this method for its successor, cascading cancellation
|
||||
// through the rest of the chain. If it's not (e.g. it was parked back to Idle
|
||||
// out of band), CancelAsync is a no-op and nothing will ever call back for it —
|
||||
// keep walking the chain ourselves so the tail isn't left wedged forever.
|
||||
var predecessorId = nextId;
|
||||
while (true)
|
||||
{
|
||||
var result = await _state().CancelAsync(predecessorId, DateTime.UtcNow, ct);
|
||||
if (result.Ok) return null;
|
||||
|
||||
var following = await FindSuccessorAsync(predecessorId, ct);
|
||||
if (following is null) return null;
|
||||
predecessorId = following;
|
||||
}
|
||||
}
|
||||
|
||||
// The successor is whichever sibling explicitly blocks on this task.
|
||||
private async Task<string?> FindSuccessorAsync(string taskId, CancellationToken ct)
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
return await ctx.Tasks
|
||||
.AsNoTracking()
|
||||
.Where(t => t.BlockedByTaskId == taskId)
|
||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||
.Select(t => t.Id)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user