github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/filewatch/watcher.go (about) 1 package filewatch 2 3 import ( 4 "context" 5 "sync" 6 "time" 7 8 "k8s.io/apimachinery/pkg/types" 9 ctrl "sigs.k8s.io/controller-runtime" 10 11 "github.com/jonboulle/clockwork" 12 13 "github.com/tilt-dev/tilt/internal/watch" 14 "github.com/tilt-dev/tilt/pkg/apis" 15 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 16 "github.com/tilt-dev/tilt/pkg/logger" 17 ) 18 19 // MaxFileEventHistory is the maximum number of file events that will be retained on the FileWatch status. 20 const MaxFileEventHistory = 20 21 22 const maxRestartBackoff = 5 * time.Minute 23 24 const DetectedOverflowErrMsg = `It looks like the inotify event queue has overflowed. Check these instructions for how to raise the queue limit: https://facebook.github.io/watchman/docs/install#system-specific-preparation` 25 26 type watcher struct { 27 clock clockwork.Clock 28 name types.NamespacedName 29 spec v1alpha1.FileWatchSpec 30 status *v1alpha1.FileWatchStatus 31 mu sync.Mutex 32 restartBackoff time.Duration 33 doneAt time.Time 34 done bool 35 notify watch.Notify 36 cancel func() 37 } 38 39 // Whether we need to restart the watcher. 40 func (w *watcher) shouldRestart() (bool, ctrl.Result) { 41 w.mu.Lock() 42 defer w.mu.Unlock() 43 if !w.done { 44 return false, ctrl.Result{} 45 } 46 47 if w.clock.Since(w.doneAt) < w.restartBackoff { 48 return false, ctrl.Result{RequeueAfter: w.restartBackoff - w.clock.Since(w.doneAt)} 49 } 50 return true, ctrl.Result{} 51 } 52 53 // cleanupWatch stops watching for changes and frees up resources. 54 func (w *watcher) cleanupWatch(ctx context.Context) { 55 w.mu.Lock() 56 defer w.mu.Unlock() 57 if w.done { 58 return 59 } 60 61 if w.notify != nil { 62 if err := w.notify.Close(); err != nil { 63 logger.Get(ctx).Debugf("Failed to close notifier for %q: %v", w.name.String(), err) 64 } 65 } 66 67 w.restartBackoff *= 2 68 if w.restartBackoff > maxRestartBackoff { 69 w.restartBackoff = maxRestartBackoff 70 } 71 w.doneAt = w.clock.Now() 72 if ctx.Err() == nil && w.status.Error == "" { 73 w.status.Error = "unexpected close" 74 } 75 76 w.cancel() 77 w.done = true 78 } 79 80 func (w *watcher) copyStatus() *v1alpha1.FileWatchStatus { 81 w.mu.Lock() 82 defer w.mu.Unlock() 83 return w.status.DeepCopy() 84 } 85 86 func (w *watcher) recordError(err error) { 87 w.mu.Lock() 88 defer w.mu.Unlock() 89 if err == nil { 90 w.status.Error = "" 91 } else { 92 w.status.Error = err.Error() 93 } 94 } 95 96 func (w *watcher) recordEvent(fsEvents []watch.FileEvent) { 97 now := apis.NowMicro() 98 w.mu.Lock() 99 defer w.mu.Unlock() 100 event := v1alpha1.FileEvent{Time: *now.DeepCopy()} 101 for _, fsEvent := range fsEvents { 102 event.SeenFiles = append(event.SeenFiles, fsEvent.Path()) 103 } 104 if len(event.SeenFiles) != 0 { 105 w.status.LastEventTime = *now.DeepCopy() 106 w.status.FileEvents = append(w.status.FileEvents, event) 107 if len(w.status.FileEvents) > MaxFileEventHistory { 108 w.status.FileEvents = w.status.FileEvents[len(w.status.FileEvents)-MaxFileEventHistory:] 109 } 110 w.status.Error = "" 111 } 112 }