Skip to content

Commit c6640f9

Browse files
committed
monitors loader: load only updated monitors
1 parent 2535c66 commit c6640f9

File tree

3 files changed

+46
-51
lines changed

3 files changed

+46
-51
lines changed

src/components/monitors_loader/monitors_loader.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -243,43 +243,33 @@ async def _get_monitors_to_load(
243243
return monitors, code_modules
244244

245245

246-
async def _load_monitors() -> None:
246+
async def _load_monitors(last_load_time: datetime | None) -> None:
247247
"""Load all enabled monitors from the database and add them to the registry. If any of the
248248
monitor's modules fails to load, the monitor will not be added to the registry"""
249249
registry.monitors_ready.clear()
250250

251-
loaded_monitors = await Monitor.get_all(Monitor.enabled.is_(True))
252-
monitors_ids = [monitor.id for monitor in loaded_monitors]
253-
254-
code_modules = await CodeModule.get_all(CodeModule.monitor_id.in_(monitors_ids))
255-
code_modules_map = {code_module.monitor_id: code_module for code_module in code_modules}
256-
257-
_logger.info(f"Monitors found: {len(loaded_monitors)}")
251+
monitors, code_modules = await _get_monitors_to_load(last_load_time)
252+
_logger.info(f"Monitors to load: {len(code_modules)}")
258253

259254
# To load the monitors safely, first create all the files and then import them
260255
# Loading right after writing the files can result in an error where the Monitor module is not
261256
# found
262257
monitors_paths = {}
263-
for monitor in loaded_monitors:
258+
for code_module in code_modules:
264259
with catch_exceptions(_logger):
265-
code_module = code_modules_map.get(monitor.id)
266-
if code_module is None:
267-
await monitor.set_enabled(False)
268-
_logger.warning(f"Monitor '{monitor.name}' has no code module, it will be disabled")
269-
continue
260+
monitor = monitors[code_module.monitor_id]
270261

271262
monitors_paths[monitor.id] = module_loader.create_module_files(
272263
module_name=monitor.name,
273264
module_code=code_module.code,
274265
additional_files=code_module.additional_files,
275266
)
276267

277-
for monitor in loaded_monitors:
268+
for code_module in code_modules:
278269
with catch_exceptions(_logger):
279-
monitor_path = monitors_paths.get(monitor.id)
280-
if monitor_path is None:
281-
continue
270+
monitor = monitors[code_module.monitor_id]
282271

272+
monitor_path = monitors_paths[monitor.id]
283273
monitor_module = cast(MonitorModule, module_loader.load_module_from_file(monitor_path))
284274
_configure_monitor(monitor_module)
285275

@@ -291,11 +281,12 @@ async def _load_monitors() -> None:
291281

292282
async def _run() -> None:
293283
"""Monitors loading loop, loading them recurrently. Stops automatically when the app stops"""
294-
last_load_time: datetime
284+
last_load_time: datetime | None = None
295285

296286
while app.running():
297287
with catch_exceptions(_logger):
298-
await _load_monitors()
288+
await _disable_monitors_without_code_modules()
289+
await _load_monitors(last_load_time)
299290
last_load_time = now()
300291

301292
# The sleep task will start seconds earlier to try to load all monitors before the

tests/components/controller/test_controller.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ async def test_run(monkeypatch, clear_queue, clear_database):
338338
assert len(queue_items) == 0
339339

340340
# Load the monitors and wait for a while
341-
await monitors_loader._load_monitors()
341+
await monitors_loader._load_monitors(None)
342342
await asyncio.sleep(0.2)
343343

344344
# Stop the app and wait for the controller task
@@ -399,7 +399,7 @@ async def test_run_monitors_not_ready(caplog, monkeypatch, mocker):
399399

400400
# Run the controller for a while then stop it
401401
await monitors_loader._register_monitors()
402-
await monitors_loader._load_monitors()
402+
await monitors_loader._load_monitors(None)
403403
registry.monitors_ready.clear()
404404

405405
controller_task = asyncio.create_task(controller.run())
@@ -419,7 +419,7 @@ async def test_run_monitors_not_registered(caplog, monkeypatch, mocker):
419419

420420
# Run the controller for a while then stop it
421421
await monitors_loader._register_monitors()
422-
await monitors_loader._load_monitors()
422+
await monitors_loader._load_monitors(None)
423423

424424
controller_task = asyncio.create_task(controller.run())
425425
await asyncio.sleep(0.2)
@@ -442,7 +442,7 @@ def error(*args):
442442
# Run the controller for a while then stop it
443443
await monitors_loader._register_monitors()
444444
controller_task = asyncio.create_task(controller.run())
445-
await monitors_loader._load_monitors()
445+
await monitors_loader._load_monitors(None)
446446
await asyncio.sleep(0.3)
447447

448448
assert_message_in_log(caplog, "ValueError: Not able to get the monitors")

tests/components/monitors_loader/test_monitors_loader.py

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -680,13 +680,41 @@ async def test_load_monitors(clear_database):
680680
"(9999457, 'def get_value(): return 12');"
681681
)
682682

683-
await monitors_loader._load_monitors()
683+
await monitors_loader._load_monitors(None)
684684

685685
assert len(registry._monitors) == 2
686686
assert isinstance(registry._monitors[9999123]["module"], ModuleType)
687687
assert isinstance(registry._monitors[9999456]["module"], ModuleType)
688688

689689

690+
async def test_load_monitors_only_updated(clear_database):
691+
"""'_load_monitors' should load all enabled monitors that were updated after the provided
692+
timestamp"""
693+
await databases.execute_application(
694+
'insert into "Monitors"(id, name, enabled) values'
695+
"(9999123, 'monitor_1', true),"
696+
"(9999456, 'internal.monitor_2', true),"
697+
"(9999457, 'disabled_monitor', false);"
698+
)
699+
await databases.execute_application(
700+
'insert into "CodeModules"(monitor_id, code, registered_at) values'
701+
"(9999123, 'def get_value(): return 10', '2025-01-10 00:00'),"
702+
"(9999456, 'def get_value(): return 11', '2025-01-20 00:00'),"
703+
"(9999457, 'def get_value(): return 12', '2025-01-30 00:00');"
704+
)
705+
706+
registry.add_monitor(9999123, "monitor_1", ModuleType(name="MockMonitorModule"))
707+
await monitors_loader._load_monitors(datetime(2025, 1, 15, tzinfo=timezone.utc))
708+
709+
assert len(registry._monitors) == 2
710+
assert isinstance(registry._monitors[9999123]["module"], ModuleType)
711+
assert isinstance(registry._monitors[9999456]["module"], ModuleType)
712+
# Only the monitor 9999456 was updated after the timestamp, so only it should be reloaded
713+
with pytest.raises(AttributeError):
714+
registry._monitors[9999123]["module"].get_value()
715+
assert registry._monitors[9999456]["module"].get_value() == 11
716+
717+
690718
async def test_load_monitors_monitors_ready_flag(monkeypatch, clear_database):
691719
"""'_load_monitors' should clear and set the registry's 'monitors_ready' while loading the
692720
monitors"""
@@ -711,7 +739,7 @@ async def slow_get_all(self, *args, **kwargs):
711739
assert registry.monitors_ready.is_set()
712740
assert registry.monitors_pending.is_set()
713741

714-
load_monitors_task = asyncio.create_task(monitors_loader._load_monitors())
742+
load_monitors_task = asyncio.create_task(monitors_loader._load_monitors(None))
715743

716744
await asyncio.sleep(0.1)
717745
assert not registry.monitors_ready.is_set()
@@ -722,30 +750,6 @@ async def slow_get_all(self, *args, **kwargs):
722750
assert not registry.monitors_pending.is_set()
723751

724752

725-
async def test_load_monitors_monitor_without_code_module(caplog, monkeypatch, clear_database):
726-
"""'_load_monitors' should disable the monitor if it doesn't have a code module"""
727-
monitor_get_all = Monitor.get_all
728-
729-
async def slow_get_all(self, *args, **kwargs):
730-
await asyncio.sleep(0.2)
731-
return await monitor_get_all(self, *args, **kwargs)
732-
733-
monkeypatch.setattr(Monitor, "get_all", slow_get_all)
734-
735-
await databases.execute_application(
736-
"insert into \"Monitors\"(id, name, enabled) values (9999123, 'monitor_1', true);"
737-
)
738-
739-
await monitors_loader._load_monitors()
740-
741-
monitor = await Monitor.get_by_id(9999123)
742-
assert monitor is not None
743-
assert not monitor.enabled
744-
745-
assert len(registry._monitors) == 0
746-
assert_message_in_log(caplog, "Monitor 'monitor_1' has no code module, it will be disabled")
747-
748-
749753
async def test_load_monitors_error(caplog, clear_database):
750754
"""'_load_monitors' should load all the monitors from the database and add them to the
751755
registry, even if an error occurs while loading any of them. Monitors with errors will not be
@@ -761,7 +765,7 @@ async def test_load_monitors_error(caplog, clear_database):
761765
"(9999456, 'def get_value(): return 10');"
762766
)
763767

764-
await monitors_loader._load_monitors()
768+
await monitors_loader._load_monitors(None)
765769

766770
assert len(registry._monitors) == 1
767771
assert isinstance(registry._monitors[9999456]["module"], ModuleType)

0 commit comments

Comments
 (0)