diff --git a/heartbeat/heartbeat.py b/heartbeat/heartbeat.py index d5b3ef9..7d758ca 100644 --- a/heartbeat/heartbeat.py +++ b/heartbeat/heartbeat.py @@ -62,6 +62,11 @@ class HeartbeatRunner: if not self._config.enabled: return 0 + # Clear previous heartbeat jobs to avoid duplicates on restart + removed = self._scheduler.remove_by_channel_type("heartbeat") + if removed: + logger.info(f"Heartbeat: cleared {removed} stale job(s) from previous run") + self._ensure_heartbeat_file() tasks = self._parse_heartbeat_md() diff --git a/scheduler/scheduler.py b/scheduler/scheduler.py index ed2b7ea..5efb11b 100644 --- a/scheduler/scheduler.py +++ b/scheduler/scheduler.py @@ -220,6 +220,17 @@ class Scheduler: """List only recurring cron jobs.""" return self._store.list_recurring() + def remove_by_channel_type(self, channel_type: str) -> int: + """Remove all jobs with the given channel_type. Returns count removed.""" + ids = self._store.remove_by_channel_type(channel_type) + for job_id in ids: + for prefix in ("once-", "cron-"): + try: + self._scheduler.remove_job(f"{prefix}{job_id}") + except Exception: + pass + return len(ids) + # ------------------------------------------------------------------- # Internal # ------------------------------------------------------------------- diff --git a/scheduler/store.py b/scheduler/store.py index 7d35d14..e541fdb 100644 --- a/scheduler/store.py +++ b/scheduler/store.py @@ -147,6 +147,22 @@ class JobStore: conn.commit() return cursor.rowcount + def remove_by_channel_type(self, channel_type: str) -> list[str]: + """Remove all jobs with the given channel_type. Returns removed IDs.""" + with self._conn() as conn: + rows = conn.execute( + "SELECT id FROM jobs WHERE channel_type = ?", (channel_type,) + ).fetchall() + ids = [row["id"] for row in rows] + if ids: + conn.execute( + "DELETE FROM jobs WHERE channel_type = ?", (channel_type,) + ) + conn.commit() + if ids: + logger.info(f"Removed {len(ids)} jobs with channel_type={channel_type}") + return ids + @staticmethod def _row_to_job(row: sqlite3.Row) -> ScheduledJob: return ScheduledJob(