From 731fbdb17322a76ae466ae70a9f8bcaa666d718e Mon Sep 17 00:00:00 2001 From: Yi LIU Date: Mon, 16 Feb 2026 13:30:12 +0800 Subject: [PATCH] Fix data race in compile watch debounce timer The modifiedFiles map is written by the main goroutine on fsnotify events and concurrently read/replaced by the time.AfterFunc callback goroutine, with no synchronization. This causes a fatal "concurrent map read and map write" panic during rapid file saves. Add a sync.Mutex to protect modifiedFiles across both goroutines. The lock is held briefly for map operations and released before the potentially long-running compilation step. --- pkg/cli/compile_watch.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/cli/compile_watch.go b/pkg/cli/compile_watch.go index 73bfd3d8c59..d7f85d2cf77 100644 --- a/pkg/cli/compile_watch.go +++ b/pkg/cli/compile_watch.go @@ -7,6 +7,7 @@ import ( "path/filepath" "runtime" "strings" + "sync" "syscall" "time" @@ -113,6 +114,7 @@ func watchAndCompileWorkflows(markdownFile string, compiler *workflow.Compiler, // Debouncing setup const debounceDelay = 300 * time.Millisecond var debounceTimer *time.Timer + var debounceMu sync.Mutex modifiedFiles := make(map[string]struct{}) // Compile initially if no specific file provided @@ -187,6 +189,7 @@ func watchAndCompileWorkflows(markdownFile string, compiler *workflow.Compiler, depGraph.RemoveWorkflow(event.Name) case event.Has(fsnotify.Write) || event.Has(fsnotify.Create): // Handle file modification or creation - add to debounced compilation + debounceMu.Lock() modifiedFiles[event.Name] = struct{}{} // Reset debounce timer @@ -194,16 +197,19 @@ func watchAndCompileWorkflows(markdownFile string, compiler *workflow.Compiler, debounceTimer.Stop() } debounceTimer = time.AfterFunc(debounceDelay, func() { + debounceMu.Lock() filesToCompile := make([]string, 0, len(modifiedFiles)) for file := range modifiedFiles { filesToCompile = append(filesToCompile, file) } // Clear the modifiedFiles map modifiedFiles = make(map[string]struct{}) + debounceMu.Unlock() // Compile the modified files using dependency graph compileModifiedFilesWithDependencies(compiler, depGraph, filesToCompile, verbose) }) + debounceMu.Unlock() } case err, ok := <-watcher.Errors: