feat(worker): chain a normal parent's children on WaitingForChildren

A standalone parent (PlanningPhase.None) with existing Idle/Queued children
now runs itself first, then sequences its children instead of flat-enqueuing
all of them at once: SubmitForChildrenAsync calls
PlanningChainCoordinator.SetupChainAsync(enqueue: true) right after the
Running -> WaitingForChildren transition, mirroring the planning path but
firing immediately instead of waiting for an explicit "Queue plan". Planning
parents are left untouched (they queue via QueuePlanAsync), and a child
already Running makes SetupChainAsync throw, which is now caught and logged
so it can't fail the parent's own transition.
This commit is contained in:
mika kuns
2026-09-10 13:36:08 +02:00
parent 897f287ee1
commit d2648f965c
4 changed files with 109 additions and 12 deletions
+33 -9
View File
@@ -159,16 +159,40 @@ public sealed class TaskStateService : ITaskStateService
public async Task<TransitionResult> SubmitForChildrenAsync(string taskId, DateTime finishedAt, string? result, CancellationToken ct)
{
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
var affected = await ctx.Tasks
.Where(t => t.Id == taskId && t.Status == TaskStatus.Running)
.ExecuteUpdateAsync(s => s
.SetProperty(t => t.Status, TaskStatus.WaitingForChildren)
.SetProperty(t => t.FinishedAt, finishedAt)
.SetProperty(t => t.Result, result), ct);
PlanningPhase planningPhase;
await using (var ctx = await _dbFactory.CreateDbContextAsync(ct))
{
var affected = await ctx.Tasks
.Where(t => t.Id == taskId && t.Status == TaskStatus.Running)
.ExecuteUpdateAsync(s => s
.SetProperty(t => t.Status, TaskStatus.WaitingForChildren)
.SetProperty(t => t.FinishedAt, finishedAt)
.SetProperty(t => t.Result, result), ct);
if (affected == 0)
return new TransitionResult(false, "Task not running; cannot submit for children.");
if (affected == 0)
return new TransitionResult(false, "Task not running; cannot submit for children.");
planningPhase = await ctx.Tasks.AsNoTracking()
.Where(t => t.Id == taskId)
.Select(t => t.PlanningPhase)
.FirstAsync(ct);
}
// A normal (non-planning) parent queues its own children as a sequential chain right
// here; a planning parent's children wait for the user's explicit "Queue plan" action
// (PlanningChainCoordinator.QueuePlanAsync), so leave those untouched. A child that's
// already Running makes SetupChainAsync throw — log it rather than fail this transition.
if (planningPhase == PlanningPhase.None)
{
try
{
await _chain.SetupChainAsync(taskId, enqueue: true, ct);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "SetupChainAsync failed for parent {TaskId}", taskId);
}
}
await NotifyAsync(taskId);
return new TransitionResult(true, null);