on main help sign in
fix: request MaximizeOpenFileLimit for unix
d401aa3Raphael Amorim
internal/lsp/watcher/global_watcher.go+27
@@ -2,12 +2,14 @@ package watcher
2
2
3import (
3import (
4 "context"
4 "context"
5+ "errors"
5 "fmt"
6 "fmt"
6 "log/slog"
7 "log/slog"
7 "os"
8 "os"
8 "path/filepath"
9 "path/filepath"
9 "sync"
10 "sync"
10 "sync/atomic"
11 "sync/atomic"
12+ "syscall"
11 "time"
13 "time"
12
14
13 "github.com/charmbracelet/crush/internal/config"
15 "github.com/charmbracelet/crush/internal/config"
@@ -113,8 +115,24 @@ func Start() error {
113 events := notify.Create | notify.Write | notify.Remove | notify.Rename
115 events := notify.Create | notify.Write | notify.Remove | notify.Rename
114
116
115 if err := notify.Watch(watchPath, gw.events, events); err != nil {
117 if err := notify.Watch(watchPath, gw.events, events); err != nil {
118+ // Check if the error might be due to file descriptor limits
119+ if isFileLimitError(err) {
120+ slog.Warn("lsp watcher: Hit file descriptor limit, attempting to increase", "error", err)
121+ if newLimit, rlimitErr := MaximizeOpenFileLimit(); rlimitErr == nil {
122+ slog.Info("lsp watcher: Increased file descriptor limit", "limit", newLimit)
123+ // Retry the watch operation
124+ if err = notify.Watch(watchPath, gw.events, events); err == nil {
125+ slog.Info("lsp watcher: Successfully set up watch after increasing limit")
126+ goto watchSuccess
127+ }
128+ err = fmt.Errorf("still failed after increasing limit: %w", err)
129+ } else {
130+ slog.Warn("lsp watcher: Failed to increase file descriptor limit", "error", rlimitErr)
131+ }
132+ }
116 return fmt.Errorf("lsp watcher: error setting up recursive watch on %s: %w", root, err)
133 return fmt.Errorf("lsp watcher: error setting up recursive watch on %s: %w", root, err)
117 }
134 }
135+watchSuccess:
118
136
119 slog.Info("lsp watcher: Started recursive watching", "root", root)
137 slog.Info("lsp watcher: Started recursive watching", "root", root)
120 return nil
138 return nil
@@ -335,3 +353,12 @@ func (gw *global) shutdown() {
335func Shutdown() {
353func Shutdown() {
336 instance().shutdown()
354 instance().shutdown()
337}
355}
356+
357+// isFileLimitError checks if an error is related to file descriptor limits
358+func isFileLimitError(err error) bool {
359+ if err == nil {
360+ return false
361+ }
362+ // Check for common file limit errors
363+ return errors.Is(err, syscall.EMFILE) || errors.Is(err, syscall.ENFILE)
364+}
internal/lsp/watcher/rlimit_stub.gonew file+12
@@ -0,0 +1,12 @@
1+//go:build !unix
2+
3+package watcher
4+
5+// MaximizeOpenFileLimit is a no-op on non-Unix systems.
6+// Returns a high value to indicate no practical limit.
7+func MaximizeOpenFileLimit() (int, error) {
8+ // Windows and other non-Unix systems don't have file descriptor limits
9+ // in the same way Unix systems do. Return a high value to indicate
10+ // there's no practical limit to worry about.
11+ return 1<<20, nil // 1M, effectively unlimited
12+}
internal/lsp/watcher/rlimit_unix.gonew file+57
@@ -0,0 +1,57 @@
1+//go:build unix
2+
3+// This file contains code inspired by Syncthing's rlimit implementation
4+// Syncthing is licensed under the Mozilla Public License Version 2.0
5+// See: https://github.com/syncthing/syncthing/blob/main/LICENSE
6+
7+package watcher
8+
9+import (
10+ "runtime"
11+ "syscall"
12+)
13+
14+const (
15+ // macOS has a specific limit for RLIMIT_NOFILE
16+ darwinOpenMax = 10240
17+)
18+
19+// MaximizeOpenFileLimit tries to set the resource limit RLIMIT_NOFILE (number
20+// of open file descriptors) to the max (hard limit), if the current (soft
21+// limit) is below the max. Returns the new (though possibly unchanged) limit,
22+// or an error if it could not be changed.
23+func MaximizeOpenFileLimit() (int, error) {
24+ // Get the current limit on number of open files.
25+ var lim syscall.Rlimit
26+ if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
27+ return 0, err
28+ }
29+
30+ // If we're already at max, there's no need to try to raise the limit.
31+ if lim.Cur >= lim.Max {
32+ return int(lim.Cur), nil
33+ }
34+
35+ // macOS doesn't like a soft limit greater than OPEN_MAX
36+ if runtime.GOOS == "darwin" && lim.Max > darwinOpenMax {
37+ lim.Max = darwinOpenMax
38+ }
39+
40+ // Try to increase the limit to the max.
41+ oldLimit := lim.Cur
42+ lim.Cur = lim.Max
43+ if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
44+ return int(oldLimit), err
45+ }
46+
47+ // If the set succeeded, perform a new get to see what happened. We might
48+ // have gotten a value lower than the one in lim.Max, if lim.Max was
49+ // something that indicated "unlimited" (i.e. intmax).
50+ if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
51+ // We don't really know the correct value here since Getrlimit
52+ // mysteriously failed after working once... Shouldn't ever happen.
53+ return 0, err
54+ }
55+
56+ return int(lim.Cur), nil
57+}