From b6009f0f7aa5f3bdc5e1a091e78bf231ef5b70f7 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Tue, 18 Aug 2026 06:50:02 +0200 Subject: [PATCH] Reserve toolsrv process slots atomically --- cmd/toolsrv/internal/server/proc.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/cmd/toolsrv/internal/server/proc.go b/cmd/toolsrv/internal/server/proc.go index ea075bd..d48bfbd 100644 --- a/cmd/toolsrv/internal/server/proc.go +++ b/cmd/toolsrv/internal/server/proc.go @@ -38,10 +38,11 @@ type State struct { pathLocks pathLockTable // Process management - procMu sync.Mutex - procs map[int]*Proc - nextID int - procLimit int + procMu sync.Mutex + procs map[int]*Proc + nextID int + procLimit int + procReserved int // Callbacks OnToolsChanged func() @@ -223,13 +224,23 @@ func (st *State) NewProc(ctx context.Context, payload string, background bool) ( return "", 0, fmt.Errorf("missing 'tool' in payload") } - // Check proc limit + // Reserve a process slot before setup so concurrent callers cannot all pass + // the limit check while their processes are still being initialized. st.procMu.Lock() - if len(st.procs) >= st.procLimit { + if len(st.procs)+st.procReserved >= st.procLimit { st.procMu.Unlock() return "", 0, fmt.Errorf("process limit reached (%d)", st.procLimit) } + st.procReserved++ st.procMu.Unlock() + reserved := true + defer func() { + if reserved { + st.procMu.Lock() + st.procReserved-- + st.procMu.Unlock() + } + }() // Look up tool st.mu.RLock() @@ -289,7 +300,9 @@ func (st *State) NewProc(ctx context.Context, payload string, background bool) ( st.procMu.Lock() st.procs[proc.ID] = proc + st.procReserved-- st.procMu.Unlock() + reserved = false // For background procs, stream output directly into the proc buffer. // For foreground, use nil (local buffer in exec path).