Skip to content

Commit c478bd1

Browse files
committed
fix: Ignore file open and read notifications in the watcher
The watchdog inotify mask includes open and close-without-write events, so reading a watched file produces notifications. A reload reads the files, so reacting to those notifications would make every reload trigger the next one. The watcher now reacts only to events that can change a file's content or presence: created, modified, moved, deleted, and closed after writing.
1 parent f55dc74 commit c478bd1

2 files changed

Lines changed: 34 additions & 0 deletions

File tree

‎ldclient/impl/integrations/files/filedata.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@
5353
# because it does not exist yet.
5454
_WATCH_RETRY_INTERVAL = 1.0
5555

56+
# The watchdog event types that can change a file's content or presence. Opening or reading a
57+
# file also produces events, and a reload reads the files, so those must not count as changes.
58+
_CHANGE_EVENT_TYPES = frozenset(["created", "modified", "moved", "deleted", "closed"])
59+
5660

5761
class DuplicateKeysHandling(str, Enum):
5862
"""
@@ -614,6 +618,8 @@ def _retry_pending(self) -> None:
614618
self._retry_task.stop()
615619

616620
def _handle_event(self, event) -> None:
621+
if getattr(event, "event_type", None) not in _CHANGE_EVENT_TYPES:
622+
return
617623
candidates = [getattr(event, "src_path", None), getattr(event, "dest_path", None)]
618624
for candidate in candidates:
619625
if isinstance(candidate, bytes):

‎ldclient/testing/integrations/test_filedata.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,34 @@ def test_watcher_matches_the_destination_of_a_move_event(tmp_path, make_watcher)
860860
assert temp not in w.watcher._watched_paths
861861

862862

863+
@watchdog_required
864+
def test_watcher_ignores_events_that_do_not_change_the_file(tmp_path, make_watcher):
865+
# Opening and reading a watched file produces notifications too. A reload reads the files,
866+
# so reacting to those would make every reload trigger the next one.
867+
import watchdog.events
868+
869+
path = os.path.join(str(tmp_path), 'data.json')
870+
real_path = os.path.join(os.path.realpath(str(tmp_path)), 'data.json')
871+
write_file(path, 'a')
872+
w = make_watcher([path])
873+
w.watcher._handle_event(watchdog.events.FileOpenedEvent(real_path))
874+
w.watcher._handle_event(watchdog.events.FileClosedNoWriteEvent(real_path))
875+
w.require_no_change()
876+
w.watcher._handle_event(watchdog.events.FileClosedEvent(real_path))
877+
w.require_change()
878+
879+
880+
@watchdog_required
881+
def test_watcher_does_not_signal_when_the_file_is_only_read(tmp_path, make_watcher):
882+
path = os.path.join(str(tmp_path), 'data.json')
883+
write_file(path, 'a')
884+
w = make_watcher([path])
885+
for _ in range(3):
886+
with open(path, 'rb') as f:
887+
f.read()
888+
w.require_no_change()
889+
890+
863891
@watchdog_required
864892
def test_watcher_detects_file_deletion(tmp_path, make_watcher):
865893
path = os.path.join(str(tmp_path), 'data.json')

0 commit comments

Comments
 (0)