fix(worker): prevent PlanningMergeOrchestrator double-drain race and orphaned state

This commit is contained in:
mika kuns
2026-04-24 18:12:21 +02:00
parent 3142ba203f
commit ef070ddab5

View File

@@ -56,11 +56,9 @@ public sealed class PlanningMergeOrchestrator
.Where(c => c.Worktree is not null && c.Worktree.State != WorktreeState.Merged) .Where(c => c.Worktree is not null && c.Worktree.State != WorktreeState.Merged)
.Select(c => c.Id)); .Select(c => c.Id));
_states[planningTaskId] = new State var state = new State { TargetBranch = targetBranch, RemainingSubtaskIds = queue };
{ if (!_states.TryAdd(planningTaskId, state))
TargetBranch = targetBranch, throw new InvalidOperationException($"Merge already in progress for {planningTaskId}.");
RemainingSubtaskIds = queue,
};
await _broadcaster.PlanningMergeStarted(planningTaskId, targetBranch); await _broadcaster.PlanningMergeStarted(planningTaskId, targetBranch);
await DrainAsync(planningTaskId, ct); await DrainAsync(planningTaskId, ct);
@@ -70,38 +68,47 @@ public sealed class PlanningMergeOrchestrator
{ {
if (!_states.TryGetValue(planningTaskId, out var state)) return; if (!_states.TryGetValue(planningTaskId, out var state)) return;
while (state.RemainingSubtaskIds.TryDequeue(out var subtaskId)) var keepState = false;
try
{ {
state.CurrentSubtaskId = subtaskId; while (state.RemainingSubtaskIds.TryDequeue(out var subtaskId))
var result = await _merge.MergeAsync(
subtaskId,
state.TargetBranch,
removeWorktree: true,
commitMessage: "Merge subtask",
leaveConflictsInTree: true,
ct);
if (result.Status == TaskMergeService.StatusConflict)
{ {
await _broadcaster.PlanningMergeConflict(planningTaskId, subtaskId, result.ConflictFiles); state.CurrentSubtaskId = subtaskId;
return; var result = await _merge.MergeAsync(
subtaskId,
state.TargetBranch,
removeWorktree: true,
commitMessage: "Merge subtask",
leaveConflictsInTree: true,
ct);
if (result.Status == TaskMergeService.StatusConflict)
{
await _broadcaster.PlanningMergeConflict(planningTaskId, subtaskId, result.ConflictFiles);
keepState = true;
return;
}
if (result.Status != TaskMergeService.StatusMerged)
{
await _broadcaster.PlanningMergeConflict(
planningTaskId, subtaskId,
new[] { result.ErrorMessage ?? "merge blocked" });
keepState = true;
return;
}
await _broadcaster.PlanningSubtaskMerged(planningTaskId, subtaskId);
} }
if (result.Status != TaskMergeService.StatusMerged) state.CurrentSubtaskId = null;
{ await FinalizePlanningDoneAsync(planningTaskId, ct);
await _broadcaster.PlanningMergeConflict( await _broadcaster.PlanningCompleted(planningTaskId);
planningTaskId, subtaskId, }
new[] { result.ErrorMessage ?? "merge blocked" }); finally
return; {
} if (!keepState) _states.TryRemove(planningTaskId, out _);
await _broadcaster.PlanningSubtaskMerged(planningTaskId, subtaskId);
} }
state.CurrentSubtaskId = null;
await FinalizePlanningDoneAsync(planningTaskId, ct);
_states.TryRemove(planningTaskId, out _);
await _broadcaster.PlanningCompleted(planningTaskId);
} }
private async Task FinalizePlanningDoneAsync(string planningTaskId, CancellationToken ct) private async Task FinalizePlanningDoneAsync(string planningTaskId, CancellationToken ct)