fix(worker): address concurrency, cancellation, and resource issues
- claude process: run stdout/stderr reads without ct; rely on kill-on-cancel closing the pipes to unblock them — previously ReadLineAsync(ct) could hang, stalling task slots and shutdown - task runner: terminal db writes (task_runs, MarkDone, MarkFailed, SetLogPath) now use CancellationToken.None; RunOnceAsync catches OCE and finalizes the run row so ContinueAsync can resume - task repository: GetNextQueuedAgentTaskAsync is now a single UPDATE ... RETURNING statement — closes TOCTOU window where two loop iterations could dispatch the same queued task - queue service: dispose CancellationTokenSource in slot-completion ContinueWith to stop leaking wait handles - git service: register ct.Kill(processTree), drain reads without ct, always reap via WaitForExitAsync(None) — no more git zombies on cancelled worktree ops - worktree manager: branch name uses full task id (dashes stripped) instead of 8-char prefix, eliminating collision risk Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
fc9029de97
commit
d3b85f2234
@@ -45,6 +45,9 @@ public sealed class ClaudeProcess : IClaudeProcess
|
||||
var analyzer = new StreamAnalyzer();
|
||||
var lastStderr = new StringBuilder();
|
||||
|
||||
// On cancellation: kill the tree. Killing closes the redirected pipes,
|
||||
// which unblocks the ReadLineAsync loops below (which run without ct
|
||||
// so they reliably drain instead of hanging on cancellation).
|
||||
await using var ctr = ct.Register(() =>
|
||||
{
|
||||
try { process.Kill(entireProcessTree: true); }
|
||||
@@ -53,26 +56,30 @@ public sealed class ClaudeProcess : IClaudeProcess
|
||||
|
||||
var stdoutTask = Task.Run(async () =>
|
||||
{
|
||||
while (await process.StandardOutput.ReadLineAsync(ct) is { } line)
|
||||
while (await process.StandardOutput.ReadLineAsync() is { } line)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
await onStdoutLine(line);
|
||||
analyzer.ProcessLine(line);
|
||||
}
|
||||
}, ct);
|
||||
});
|
||||
|
||||
var stderrTask = Task.Run(async () =>
|
||||
{
|
||||
while (await process.StandardError.ReadLineAsync(ct) is { } line)
|
||||
while (await process.StandardError.ReadLineAsync() is { } line)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
lastStderr.AppendLine(line);
|
||||
await onStdoutLine($"[stderr] {line}");
|
||||
}
|
||||
}, ct);
|
||||
});
|
||||
|
||||
await Task.WhenAll(stdoutTask, stderrTask);
|
||||
await process.WaitForExitAsync(ct);
|
||||
await process.WaitForExitAsync(CancellationToken.None);
|
||||
|
||||
// If we were asked to cancel, surface that to the caller now that
|
||||
// the process is fully reaped.
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var exitCode = process.ExitCode;
|
||||
var streamResult = analyzer.GetResult();
|
||||
|
||||
Reference in New Issue
Block a user