config reload: fix feedback loop — react to modified/created/moved only, not open/close (v3.12.1 spun cpu on read-opens)
This commit is contained in:
@@ -88,10 +88,22 @@ class ConfigFileHandler(FileSystemEventHandler):
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_any_event(self, event):
|
||||
def _dispatch(self, event):
|
||||
if not event.is_directory and self._hits_config(event):
|
||||
self._on_change()
|
||||
|
||||
# Only write/rename events — NOT on_opened/on_closed, whose read-opens
|
||||
# (our own load_config re-reads the file) would otherwise feed back into
|
||||
# a reload loop (CFG-05).
|
||||
def on_modified(self, event):
|
||||
self._dispatch(event)
|
||||
|
||||
def on_created(self, event):
|
||||
self._dispatch(event)
|
||||
|
||||
def on_moved(self, event):
|
||||
self._dispatch(event)
|
||||
|
||||
|
||||
class FjerkroaBot(commands.Bot):
|
||||
def __init__(self, config_file: str):
|
||||
|
||||
@@ -35,9 +35,12 @@ applies directly — there are no concurrent readers yet.
|
||||
### CFG-05 — Hot-reload is rename-safe (coverage: test)
|
||||
|
||||
The watcher observes the config file's **directory**, not the file, and
|
||||
reacts to any event (modified, created, moved) whose source or
|
||||
reacts to a **modified, created, or moved** event whose source or
|
||||
destination path is the config file. This catches atomic saves — write
|
||||
a temp file, then rename it over the target — which replace the inode
|
||||
and fire a move/create rather than a modify; watching the file directly
|
||||
would go deaf after the first such save. Events for other files in the
|
||||
directory, and directory events themselves, are ignored.
|
||||
would go deaf after the first such save. Open/close events are
|
||||
deliberately not handled: reloading re-opens the file to read it, so
|
||||
reacting to opens would feed back into an endless reload loop. Events
|
||||
for other files in the directory, and directory events themselves, are
|
||||
ignored.
|
||||
|
||||
+11
-6
@@ -157,10 +157,15 @@ class TestConfigReloadRenameSafe(unittest.TestCase):
|
||||
event.dest_path = dest if dest is not None else ""
|
||||
return event
|
||||
|
||||
handler.on_any_event(evt(src=str(config))) # in-place modify
|
||||
handler.on_any_event(evt(src=str(Path(tmp) / "kroa.toml.tmp"), dest=str(config))) # atomic rename over
|
||||
self.assertEqual(len(hits), 2)
|
||||
handler.on_modified(evt(src=str(config))) # in-place modify
|
||||
handler.on_moved(evt(src=str(Path(tmp) / "kroa.toml.tmp"), dest=str(config))) # atomic rename over
|
||||
handler.on_created(evt(src=str(config))) # write-new
|
||||
self.assertEqual(len(hits), 3)
|
||||
|
||||
handler.on_any_event(evt(src=str(Path(tmp) / "other.txt"))) # unrelated file
|
||||
handler.on_any_event(evt(is_dir=True, src=str(config))) # directory event
|
||||
self.assertEqual(len(hits), 2) # neither fired
|
||||
handler.on_modified(evt(src=str(Path(tmp) / "other.txt"))) # unrelated file
|
||||
handler.on_modified(evt(is_dir=True, src=str(config))) # directory event
|
||||
self.assertEqual(len(hits), 3) # neither fired
|
||||
|
||||
# open/close of the config (our own load_config re-reads) must NOT be handled — else a reload loop.
|
||||
self.assertNotIn("on_opened", vars(ConfigFileHandler))
|
||||
self.assertNotIn("on_closed", vars(ConfigFileHandler))
|
||||
|
||||
Reference in New Issue
Block a user