Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions ldclient/impl/integrations/files/filedata.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,15 @@

# The watchdog event types that can change a file's content or presence. Opening or reading a
# file also produces events, and a reload reads the files, so those must not count as changes.
_CHANGE_EVENT_TYPES = frozenset(["created", "modified", "moved", "deleted", "closed"])
# watchdog 7 reports a change of a file's metadata, such as a new modification time from a
# touch, as "attrib", where earlier versions reported "modified". A read does not produce it.
_CHANGE_EVENT_TYPES = frozenset(["created", "modified", "moved", "deleted", "closed", "attrib"])

# How long, in seconds, the watchdog observer waits for further notifications before it delivers
# the ones it has. watchdog 7 on Windows keeps waiting while notifications arrive, so a change is
# reported this late, and not at all while another file in the same directory changes more often
# than this. The watchdog default is one second.
_OBSERVER_TIMEOUT = 0.1


class DuplicateKeysHandling(str, Enum):
Expand Down Expand Up @@ -984,7 +992,7 @@ def on_any_event(self, event):
watcher._handle_event(event)

self._handler = _Handler()
self._observer = watchdog.observers.Observer()
self._observer = watchdog.observers.Observer(timeout=_OBSERVER_TIMEOUT)

# Guards the state below. It is never held while the observer is called, because the
# observer holds its own lock while it dispatches an event to the handler.
Expand Down Expand Up @@ -1068,6 +1076,11 @@ def _schedule(self, directory: str) -> Optional[Any]:
except Exception as e:
self._log_unwatchable(directory, 'Cannot watch directory "%s" for changes yet: %s' % (directory, e))
return None
# watchdog 6 on Windows starts a watch's reader thread and returns before the thread has
# asked for its first notification, and a change made before then is not reported.
# Yielding lets the thread make that request before the setup goes on, for example to
# the initial load.
time.sleep(0)
with self._lock:
self._unwatchable_messages.pop(directory, None)
return watch
Expand Down
54 changes: 54 additions & 0 deletions ldclient/testing/integrations/test_filedata.py
Original file line number Diff line number Diff line change
Expand Up @@ -2456,6 +2456,60 @@ def test_watcher_detects_file_deletion(tmp_path, make_watcher):
w.require_change()


@watchdog_required
def test_watcher_reports_a_change_within_a_fraction_of_a_second(tmp_path, make_watcher):
# watchdog 7 on Windows holds each notification for the observer's timeout, one second by
# default, before it delivers it. The watcher sets a shorter timeout.
path = os.path.join(str(tmp_path), 'data.json')
write_file(path, 'a')
w = make_watcher([path])
w.require_no_change(0.15)
write_file(path, 'bb')
take(w.changes, timeout=0.5)


@watchdog_required
def test_watcher_reports_a_change_while_another_file_in_the_directory_keeps_changing(tmp_path, make_watcher):
# watchdog 7 on Windows delivers nothing while notifications for the directory keep arriving
# within the observer's timeout, so a file appended to five times a second, such as a log,
# would hold back the change to the watched file for as long as it is written.
path = os.path.join(str(tmp_path), 'data.json')
other = os.path.join(str(tmp_path), 'app.log')
write_file(path, 'a')
write_file(other, '')
w = make_watcher([path])
stop = threading.Event()

def append_to_other():
while not stop.is_set():
with open(other, 'a') as f:
f.write('line\n')
stop.wait(0.2)

writer = threading.Thread(target=append_to_other, daemon=True)
writer.start()
try:
w.require_no_change(0.5)
write_file(path, 'bb')
take(w.changes, timeout=2.0)
finally:
stop.set()
writer.join()


@watchdog_required
def test_watcher_reports_a_new_modification_time(tmp_path, make_watcher):
# A touch changes only the modification time. watchdog 7 reports it as an "attrib" event,
# where earlier versions report a modification.
path = os.path.join(str(tmp_path), 'data.json')
write_file(path, 'a')
w = make_watcher([path])
w.require_no_change(0.15)
info = os.stat(path)
os.utime(path, ns=(info.st_atime_ns, info.st_mtime_ns + 1_000_000_000))
w.require_change()


def make_symlink(target: str, link: str, directory: bool = False) -> None:
# Windows needs to know whether a link leads to a directory when the link is created.
try:
Expand Down
Loading