import fcntl import logging import os import threading from collectors import InventoryCollectorOrchestrator from config import AppConfig LOGGER = logging.getLogger(__name__) class CollectionScheduler: def __init__(self, config: AppConfig, orchestrator: InventoryCollectorOrchestrator): self.config = config self.orchestrator = orchestrator self._stop_event = threading.Event() self._thread: threading.Thread | None = None self._leader_file = None def start(self) -> bool: if not self.config.scheduler_enabled: LOGGER.info("Scheduler disabled via config") return False leader_file = open("/tmp/inventory-scheduler.lock", "w", encoding="utf-8") try: fcntl.flock(leader_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError: leader_file.close() LOGGER.info("Another worker owns scheduler lock") return False self._leader_file = leader_file self._thread = threading.Thread(target=self._run_loop, name="inventory-scheduler", daemon=True) self._thread.start() LOGGER.info("Scheduler started with %s second interval", self.config.poll_interval_seconds) return True def _run_loop(self) -> None: while not self._stop_event.is_set(): self.orchestrator.collect_once() self._stop_event.wait(self.config.poll_interval_seconds) def shutdown(self) -> None: self._stop_event.set() if self._thread and self._thread.is_alive(): self._thread.join(timeout=1.0) if self._leader_file: fcntl.flock(self._leader_file.fileno(), fcntl.LOCK_UN) self._leader_file.close() self._leader_file = None def should_autostart_scheduler() -> bool: # Do not auto-start on Flask's reloader child process. return os.getenv("WERKZEUG_RUN_MAIN", "true") != "false"