on main help sign in
fix: request MaximizeOpenFileLimit for unix
commitd401aa3a4a21de982db8752fae5ad9a4d1745b68 authorRaphael Amorim <rapha850@gmail.com> dateMon 15 Sep 2025, 14:19 (1 year ago) parentfd6b617
3 files changed+96
internal/lsp/watcher/global_watcher.go+27 internal/lsp/watcher/rlimit_stub.go+12 internal/lsp/watcher/rlimit_unix.go+57
internal/lsp/watcher/global_watcher.go+27
@@ -2,12 +2,14 @@ package watcher
22
33import (
44 "context"
5+ "errors"
56 "fmt"
67 "log/slog"
78 "os"
89 "path/filepath"
910 "sync"
1011 "sync/atomic"
12+ "syscall"
1113 "time"
1214
1315 "github.com/charmbracelet/crush/internal/config"
@@ -113,8 +115,24 @@ func Start() error {
113115 events := notify.Create | notify.Write | notify.Remove | notify.Rename
114116
115117 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+ }
116133 return fmt.Errorf("lsp watcher: error setting up recursive watch on %s: %w", root, err)
117134 }
135+watchSuccess:
118136
119137 slog.Info("lsp watcher: Started recursive watching", "root", root)
120138 return nil
@@ -335,3 +353,12 @@ func (gw *global) shutdown() {
335353func Shutdown() {
336354 instance().shutdown()
337355}
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+}