watch: don't watch each individual file (#1613)

This commit is contained in:
Dan Bentley 2019-05-14 15:05:03 -04:00 committed by Nicolas De loof
parent 9c7f7bc0fa
commit f82e2de57e
4 changed files with 242 additions and 72 deletions

View File

@ -1,27 +0,0 @@
package watch
import (
"os"
"strconv"
)
const CheckLimitKey = "WM_CHECK_LIMIT"
var limitChecksEnabled = true
// Allows limit checks to be disabled for testing.
func SetLimitChecksEnabled(enabled bool) {
limitChecksEnabled = enabled
}
func LimitChecksEnabled() bool {
env, ok := os.LookupEnv(CheckLimitKey)
if ok {
enabled, err := strconv.ParseBool(env)
if err == nil {
return enabled
}
}
return limitChecksEnabled
}

View File

@ -5,6 +5,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -329,7 +330,7 @@ func TestWatchBothDirAndFile(t *testing.T) {
f.assertEvents(fileB) f.assertEvents(fileB)
} }
func TestWatchNonexistentDirectory(t *testing.T) { func TestWatchNonexistentFileInNonexistentDirectoryCreatedSimultaneously(t *testing.T) {
f := newNotifyFixture(t) f := newNotifyFixture(t)
defer f.tearDown() defer f.tearDown()
@ -347,6 +348,69 @@ func TestWatchNonexistentDirectory(t *testing.T) {
f.assertEvents(file) f.assertEvents(file)
} }
func TestWatchNonexistentDirectory(t *testing.T) {
f := newNotifyFixture(t)
defer f.tearDown()
root := f.JoinPath("root")
err := os.Mkdir(root, 0777)
if err != nil {
t.Fatal(err)
}
parent := f.JoinPath("parent")
file := f.JoinPath("parent", "a")
f.watch(parent)
f.fsync()
f.events = nil
err = os.Mkdir(parent, 0777)
if err != nil {
t.Fatal(err)
}
if runtime.GOOS == "darwin" {
// for directories that were the root of an Add, we don't report creation, cf. watcher_darwin.go
f.assertEvents()
} else {
f.assertEvents(parent)
}
f.WriteFile(file, "hello")
if runtime.GOOS == "darwin" {
// mac doesn't return the dir change as part of file creation
f.assertEvents(file)
} else {
f.assertEvents(parent, file)
}
}
// doesn't work on linux
// func TestWatchNonexistentFileInNonexistentDirectory(t *testing.T) {
// f := newNotifyFixture(t)
// defer f.tearDown()
// root := f.JoinPath("root")
// err := os.Mkdir(root, 0777)
// if err != nil {
// t.Fatal(err)
// }
// parent := f.JoinPath("parent")
// file := f.JoinPath("parent", "a")
// f.watch(file)
// f.assertEvents()
// err = os.Mkdir(parent, 0777)
// if err != nil {
// t.Fatal(err)
// }
// f.assertEvents()
// f.WriteFile(file, "hello")
// f.assertEvents(file)
// }
type notifyFixture struct { type notifyFixture struct {
*tempdir.TempDirFixture *tempdir.TempDirFixture
notify Notify notify Notify
@ -355,7 +419,6 @@ type notifyFixture struct {
} }
func newNotifyFixture(t *testing.T) *notifyFixture { func newNotifyFixture(t *testing.T) *notifyFixture {
SetLimitChecksEnabled(false)
notify, err := NewWatcher() notify, err := NewWatcher()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -434,12 +497,20 @@ F:
} }
func (f *notifyFixture) tearDown() { func (f *notifyFixture) tearDown() {
SetLimitChecksEnabled(true)
err := f.notify.Close() err := f.notify.Close()
if err != nil { if err != nil {
f.T().Fatal(err) f.T().Fatal(err)
} }
// drain channels from watcher
go func() {
for _ = range f.notify.Events() {
}
}()
go func() {
for _ = range f.notify.Errors() {
}
}()
f.TempDirFixture.TearDown() f.TempDirFixture.TearDown()
} }

View File

@ -7,6 +7,7 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/windmilleng/fsnotify" "github.com/windmilleng/fsnotify"
@ -24,6 +25,8 @@ type naiveNotify struct {
wrappedEvents chan FileEvent wrappedEvents chan FileEvent
errors chan error errors chan error
mu sync.Mutex
// Paths that we're watching that should be passed up to the caller. // Paths that we're watching that should be passed up to the caller.
// Note that we may have to watch ancestors of these paths // Note that we may have to watch ancestors of these paths
// in order to fulfill the API promise. // in order to fulfill the API promise.
@ -48,11 +51,14 @@ func (d *naiveNotify) Add(name string) error {
return errors.Wrapf(err, "notify.Add(%q)", name) return errors.Wrapf(err, "notify.Add(%q)", name)
} }
} else { } else {
err = d.watcher.Add(name) err = d.watcher.Add(filepath.Dir(name))
if err != nil { if err != nil {
return errors.Wrapf(err, "notify.Add(%q)", name) return errors.Wrapf(err, "notify.Add(%q)", filepath.Dir(name))
} }
} }
d.mu.Lock()
defer d.mu.Unlock()
d.notifyList[name] = true d.notifyList[name] = true
return nil return nil
@ -64,6 +70,9 @@ func (d *naiveNotify) watchRecursively(dir string) error {
return err return err
} }
if !mode.IsDir() {
return nil
}
err = d.watcher.Add(path) err = d.watcher.Add(path)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@ -106,56 +115,63 @@ func (d *naiveNotify) Errors() chan error {
} }
func (d *naiveNotify) loop() { func (d *naiveNotify) loop() {
defer close(d.wrappedEvents)
for e := range d.events { for e := range d.events {
isCreateOp := e.Op&fsnotify.Create == fsnotify.Create shouldNotify := d.shouldNotify(e.Name)
shouldWalk := false
if isCreateOp { if e.Op&fsnotify.Create != fsnotify.Create {
isDir, err := isDir(e.Name) if shouldNotify {
if err != nil { d.wrappedEvents <- FileEvent{e.Name}
log.Printf("Error stat-ing file %s: %s", e.Name, err) }
continue continue
} }
shouldWalk = isDir
} // TODO(dbentley): if there's a delete should we call d.watcher.Remove to prevent leaking?
if shouldWalk { if err := filepath.Walk(e.Name, func(path string, mode os.FileInfo, err error) error {
err := filepath.Walk(e.Name, func(path string, mode os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
newE := fsnotify.Event{
Op: fsnotify.Create, if d.shouldNotify(path) {
Name: path, d.wrappedEvents <- FileEvent{path}
} }
if d.shouldNotify(newE) {
d.wrappedEvents <- FileEvent{newE.Name}
// TODO(dmiller): symlinks 😭 // TODO(dmiller): symlinks 😭
err = d.Add(path)
shouldWatch := false
if mode.IsDir() {
// watch all directories
shouldWatch = true
} else {
// watch files that are explicitly named, but don't watch others
_, ok := d.notifyList[path]
if ok {
shouldWatch = true
}
}
if shouldWatch {
err := d.watcher.Add(path)
if err != nil { if err != nil {
log.Printf("Error watching path %s: %s", e.Name, err) log.Printf("Error watching path %s: %s", e.Name, err)
} }
} }
return nil return nil
}) }); err != nil {
if err != nil {
log.Printf("Error walking directory %s: %s", e.Name, err) log.Printf("Error walking directory %s: %s", e.Name, err)
} }
} else if d.shouldNotify(e) {
d.wrappedEvents <- FileEvent{e.Name}
}
} }
} }
func (d *naiveNotify) shouldNotify(e fsnotify.Event) bool { func (d *naiveNotify) shouldNotify(path string) bool {
if _, ok := d.notifyList[e.Name]; ok { d.mu.Lock()
return true defer d.mu.Unlock()
} else { if _, ok := d.notifyList[path]; ok {
// TODO(dmiller): maybe use a prefix tree here?
for path := range d.notifyList {
if ospath.IsChild(path, e.Name) {
return true return true
} }
// TODO(dmiller): maybe use a prefix tree here?
for root := range d.notifyList {
if ospath.IsChild(root, path) {
return true
} }
} }
return false return false

View File

@ -0,0 +1,110 @@
// +build !darwin
package watch
import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"testing"
)
func TestDontWatchEachFile(t *testing.T) {
// fsnotify is not recursive, so we need to watch each directory
// you can watch individual files with fsnotify, but that is more prone to exhaust resources
// this test uses a Linux way to get the number of watches to make sure we're watching
// per-directory, not per-file
f := newNotifyFixture(t)
defer f.tearDown()
watched := f.TempDir("watched")
// there are a few different cases we want to test for because the code paths are slightly
// different:
// 1) initial: data there before we ever call watch
// 2) inplace: data we create while the watch is happening
// 3) staged: data we create in another directory and then atomically move into place
// initial
f.WriteFile(f.JoinPath(watched, "initial.txt"), "initial data")
initialDir := f.JoinPath(watched, "initial_dir")
if err := os.Mkdir(initialDir, 0777); err != nil {
t.Fatal(err)
}
for i := 0; i < 100; i++ {
f.WriteFile(f.JoinPath(initialDir, fmt.Sprintf("%d", i)), "initial data")
}
f.watch(watched)
f.fsync()
if len(f.events) != 0 {
t.Fatalf("expected 0 initial events; got %d events: %v", len(f.events), f.events)
}
f.events = nil
// inplace
inplace := f.JoinPath(watched, "inplace")
if err := os.Mkdir(inplace, 0777); err != nil {
t.Fatal(err)
}
f.WriteFile(f.JoinPath(inplace, "inplace.txt"), "inplace data")
inplaceDir := f.JoinPath(inplace, "inplace_dir")
if err := os.Mkdir(inplaceDir, 0777); err != nil {
t.Fatal(err)
}
for i := 0; i < 100; i++ {
f.WriteFile(f.JoinPath(inplaceDir, fmt.Sprintf("%d", i)), "inplace data")
}
f.fsync()
if len(f.events) < 100 {
t.Fatalf("expected >100 inplace events; got %d events: %v", len(f.events), f.events)
}
f.events = nil
// staged
staged := f.TempDir("staged")
f.WriteFile(f.JoinPath(staged, "staged.txt"), "staged data")
stagedDir := f.JoinPath(staged, "staged_dir")
if err := os.Mkdir(stagedDir, 0777); err != nil {
t.Fatal(err)
}
for i := 0; i < 100; i++ {
f.WriteFile(f.JoinPath(stagedDir, fmt.Sprintf("%d", i)), "staged data")
}
if err := os.Rename(staged, f.JoinPath(watched, "staged")); err != nil {
t.Fatal(err)
}
f.fsync()
if len(f.events) < 100 {
t.Fatalf("expected >100 staged events; got %d events: %v", len(f.events), f.events)
}
f.events = nil
pid := os.Getpid()
output, err := exec.Command("bash", "-c", fmt.Sprintf(
"find /proc/%d/fd -lname anon_inode:inotify -printf '%%hinfo/%%f\n' | xargs cat | grep -c '^inotify'", pid)).Output()
if err != nil {
t.Fatalf("error running command to determine number of watched files: %v", err)
}
n, err := strconv.Atoi(strings.TrimSpace(string(output)))
if err != nil {
t.Fatalf("couldn't parse number of watched files: %v", err)
}
if n > 10 {
t.Fatalf("watching more than 10 files: %d", n)
}
}