fix(worker): prevent PlanningMergeOrchestrator double-drain race and orphaned state
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user