-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscheduler.py
More file actions
80 lines (66 loc) · 3.62 KB
/
Copy pathscheduler.py
File metadata and controls
80 lines (66 loc) · 3.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
"""Durable local scheduler used by the OpenKyrozen Gateway."""
from __future__ import annotations
import threading
import time
import uuid
from datetime import datetime, timedelta, timezone
from typing import Any, Callable
from event_store import EventStore, utc_now
class JobScheduler:
"""Polls SQLite and claims due jobs exactly once per process tick."""
def __init__(self, store: EventStore, *, workspace_id: str = "default", poll_seconds: float = 2.0):
self.store = store
self.workspace_id = workspace_id
self.poll_seconds = max(0.25, poll_seconds)
self._callbacks: dict[str, Callable[[dict[str, Any]], Any]] = {}
self._stop = threading.Event()
self._thread: threading.Thread | None = None
def register_callback(self, job_type: str, callback: Callable[[dict[str, Any]], Any]) -> None:
self._callbacks[job_type] = callback
def schedule_every(self, name: str, interval_seconds: float, *, payload: dict[str, Any] | None = None,
job_id: str | None = None, delay_seconds: float | None = None) -> str:
interval_seconds = max(1.0, float(interval_seconds))
job_id = job_id or f"job_{uuid.uuid4().hex}"
next_run = datetime.now(timezone.utc) + timedelta(seconds=delay_seconds if delay_seconds is not None else interval_seconds)
return self.store.upsert_schedule(job_id, name, interval_seconds=interval_seconds,
next_run_at=next_run.isoformat(), payload=payload,
workspace_id=self.workspace_id)
def schedule_once(self, name: str, run_at: str, *, payload: dict[str, Any] | None = None,
job_id: str | None = None) -> str:
datetime.fromisoformat(run_at)
job_id = job_id or f"job_{uuid.uuid4().hex}"
return self.store.upsert_schedule(job_id, name, run_at=run_at, next_run_at=run_at,
payload=payload, workspace_id=self.workspace_id)
def list_jobs(self, *, enabled: bool | None = None) -> list[dict[str, Any]]:
return self.store.list_schedules(workspace_id=self.workspace_id, enabled=enabled)
def start(self) -> None:
if self._thread and self._thread.is_alive():
return
self._stop.clear()
self._thread = threading.Thread(target=self._run, name="openkyrozen-scheduler", daemon=True)
self._thread.start()
def stop(self) -> None:
self._stop.set()
if self._thread:
self._thread.join(timeout=3)
def tick(self) -> int:
now = utc_now()
jobs = self.store.claim_due_schedules(now=now, workspace_id=self.workspace_id)
for job in jobs:
job_type = str(job.get("payload", {}).get("type", "default"))
callback = self._callbacks.get(job_type)
if callback is None:
self.store.append_event("scheduler.unhandled", {"job_id": job["id"], "type": job_type},
workspace_id=self.workspace_id)
continue
try:
callback(job)
self.store.append_event("scheduler.completed", {"job_id": job["id"], "type": job_type},
workspace_id=self.workspace_id)
except Exception as exc:
self.store.append_event("scheduler.failed", {"job_id": job["id"], "type": job_type, "error": str(exc)[:1000]},
workspace_id=self.workspace_id)
return len(jobs)
def _run(self) -> None:
while not self._stop.wait(self.poll_seconds):
self.tick()