Skip to content

Commit 394c80e

Browse files
committed
fix: race condition in dual indexing causing system freeze
ResumeIndexingOnConnect and DetectContext auto-trigger could both call StartIndexingAsync for the same workspace simultaneously, bypassing the LoadOrStore dedup guard via TOCTOU race window. Changes: - ResumeIndexingOnConnect now marks connectTriggered before StartIndexingAsync - Removed redundant indexingJobs.Load check from DetectContext (TOCTOU) - Changed 'go e.StartIndexingAsync(...)' to direct call (goroutine created internally) Fixes system freeze when indexing large workspaces (~5000+ files).
1 parent 2e5cc25 commit 394c80e

1 file changed

Lines changed: 12 additions & 5 deletions

File tree

internal/service/engine/engine.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,14 @@ func (e *Engine) DetectContext(ctx context.Context, path string) (*WorkspaceCont
282282
// recreate=false ensures incremental indexing — only new/changed files are processed.
283283
if e.config == nil || e.config.Workspace.AutoIndex {
284284
if _, triggered := e.connectTriggered.LoadOrStore(wctx.ID, true); !triggered {
285-
if _, alreadyRunning := e.indexingJobs.Load(wctx.ID); !alreadyRunning {
286-
logger.Instance.Info("[DAEMON] [WS-DETECT] Auto-triggering incremental index for workspace: %s", wctx.Root)
287-
go e.StartIndexingAsync(wctx.Root, wctx.ID, nil, false)
288-
}
285+
// Do NOT check indexingJobs.Load here — that is a TOCTOU race window.
286+
// StartIndexingAsync uses indexingJobs.LoadOrStore atomically and is
287+
// already idempotent: if a job is running it returns immediately.
288+
// Calling it directly (not via `go`) avoids the extra scheduling delay
289+
// that previously widened the race window between ResumeIndexingOnConnect
290+
// and DetectContext. StartIndexingAsync spawns its own goroutine internally.
291+
logger.Instance.Info("[DAEMON] [WS-DETECT] Auto-triggering incremental index for workspace: %s", wctx.Root)
292+
e.StartIndexingAsync(wctx.Root, wctx.ID, nil, false)
289293
}
290294
}
291295

@@ -363,7 +367,10 @@ func (e *Engine) ResumeIndexingOnConnect() {
363367
bestRoot := indexer.GetLastInterruptedWorkspace(roots)
364368
if bestRoot != "" {
365369
logger.Instance.Info("[DAEMON] Resuming incomplete indexing for workspace: %s", filepath.Base(bestRoot))
366-
// trigger indexing incrementally
370+
// Mark as triggered BEFORE calling StartIndexingAsync so that DetectContext's
371+
// auto-trigger (connectTriggered.LoadOrStore) sees this workspace as already
372+
// handled and does NOT start a second concurrent indexing job.
373+
e.connectTriggered.Store(idMap[bestRoot], true)
367374
e.StartIndexingAsync(bestRoot, idMap[bestRoot], nil, false)
368375
} else {
369376
logger.Instance.Debug("[DAEMON] ResumeIndexingOnConnect: no incomplete indexing jobs found")

0 commit comments

Comments
 (0)