watch: use sinceWhen and HistoryDone to avoid spurious events (#557)

Here's our new watch strategy on Darwin in a nutshell:

1. Create an fsevents stream for events "since" the last event ID that
we saw globally.
2. Add a path that we want to watch
3. Add that path to a map of paths that we're watching _directly_.
4. Restart the event stream to pick up the new path.
5. Ignore all events for all watches until we've seen a `HistoryDone`
event.
6. Ignore the first `ItemCreated` event for paths we're watching that
are also directories
7. Otherwise, forward along all events.
This commit is contained in:
Dan Miller 2018-10-18 10:44:07 -04:00 committed by Nicolas De loof
parent c5bce8bd42
commit 38b3f3b678
2 changed files with 57 additions and 17 deletions

View File

@ -303,6 +303,40 @@ func TestWatchBrokenLink(t *testing.T) {
f.assertEvents(link) f.assertEvents(link)
} }
func TestMoveAndReplace(t *testing.T) {
f := newNotifyFixture(t)
defer f.tearDown()
root, err := f.root.NewDir("root")
if err != nil {
t.Fatal(err)
}
file := filepath.Join(root.Path(), "myfile")
err = ioutil.WriteFile(file, []byte("hello"), 0777)
if err != nil {
t.Fatal(err)
}
err = f.notify.Add(file)
if err != nil {
t.Fatal(err)
}
tmpFile := filepath.Join(root.Path(), ".myfile.swp")
err = ioutil.WriteFile(tmpFile, []byte("world"), 0777)
if err != nil {
t.Fatal(err)
}
err = os.Rename(tmpFile, file)
if err != nil {
t.Fatal(err)
}
f.assertEvents(file)
}
type notifyFixture struct { type notifyFixture struct {
t *testing.T t *testing.T
root *TempDir root *TempDir

View File

@ -21,9 +21,8 @@ type darwinNotify struct {
// change. // change.
sm *sync.Mutex sm *sync.Mutex
// When a watch is created for a directory, we've seen fsevents non-determistically pathsWereWatching map[string]interface{}
// fire 0-3 CREATE events for that directory. We want to ignore these. sawAnyHistoryDone bool
ignoreCreatedEvents map[string]bool
} }
func (d *darwinNotify) loop() { func (d *darwinNotify) loop() {
@ -39,20 +38,24 @@ func (d *darwinNotify) loop() {
for _, e := range events { for _, e := range events {
e.Path = filepath.Join("/", e.Path) e.Path = filepath.Join("/", e.Path)
if e.Flags&fsevents.ItemCreated == fsevents.ItemCreated { if e.Flags&fsevents.HistoryDone == fsevents.HistoryDone {
d.sm.Lock() d.sm.Lock()
shouldIgnore := d.ignoreCreatedEvents[e.Path] d.sawAnyHistoryDone = true
if !shouldIgnore {
// If we got a created event for something
// that's not on the ignore list, we assume
// we're done with the spurious events.
d.ignoreCreatedEvents = nil
}
d.sm.Unlock() d.sm.Unlock()
if shouldIgnore {
continue continue
} }
// We wait until we've seen the HistoryDone event for this watcher before processing any events
// so that we skip all of the "spurious" events that precede it.
if !d.sawAnyHistoryDone {
continue
}
_, isPathWereWatching := d.pathsWereWatching[e.Path]
if e.Flags&fsevents.ItemIsDir == fsevents.ItemIsDir && e.Flags&fsevents.ItemCreated == fsevents.ItemCreated && isPathWereWatching {
// This is the first create for the path that we're watching. We always get exactly one of these
// even after we get the HistoryDone event. Skip it.
continue
} }
d.events <- FileEvent{ d.events <- FileEvent{
@ -80,10 +83,10 @@ func (d *darwinNotify) Add(name string) error {
es.Paths = append(es.Paths, name) es.Paths = append(es.Paths, name)
if d.ignoreCreatedEvents == nil { if d.pathsWereWatching == nil {
d.ignoreCreatedEvents = make(map[string]bool, 1) d.pathsWereWatching = make(map[string]interface{})
} }
d.ignoreCreatedEvents[name] = true d.pathsWereWatching[name] = struct{}{}
if len(es.Paths) == 1 { if len(es.Paths) == 1 {
es.Start() es.Start()
@ -119,6 +122,9 @@ func NewWatcher() (Notify, error) {
stream: &fsevents.EventStream{ stream: &fsevents.EventStream{
Latency: 1 * time.Millisecond, Latency: 1 * time.Millisecond,
Flags: fsevents.FileEvents, Flags: fsevents.FileEvents,
// NOTE(dmiller): this corresponds to the `sinceWhen` parameter in FSEventStreamCreate
// https://developer.apple.com/documentation/coreservices/1443980-fseventstreamcreate
EventID: fsevents.LatestEventID(),
}, },
sm: &sync.Mutex{}, sm: &sync.Mutex{},
events: make(chan FileEvent), events: make(chan FileEvent),