147 lines
4.4 KiB
Python
147 lines
4.4 KiB
Python
|
|
"""Background task scheduler - Phase 10.4"""
|
||
|
|
|
||
|
|
from collections.abc import Callable, Coroutine
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||
|
|
from apscheduler.triggers.base import BaseTrigger
|
||
|
|
|
||
|
|
from .manager import BackgroundTaskManager, get_background_task_manager
|
||
|
|
|
||
|
|
|
||
|
|
class BackgroundScheduler:
|
||
|
|
"""Background task scheduler using APScheduler.
|
||
|
|
|
||
|
|
Integrates with BackgroundTaskManager for task tracking and execution.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, task_manager: BackgroundTaskManager | None = None):
|
||
|
|
"""Initialize the scheduler.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
task_manager: Optional BackgroundTaskManager instance.
|
||
|
|
If not provided, uses the global singleton.
|
||
|
|
"""
|
||
|
|
self._scheduler = AsyncIOScheduler()
|
||
|
|
self._task_manager = task_manager or get_background_task_manager()
|
||
|
|
self._job_tasks: dict[str, str] = {} # Maps APScheduler job_id to task_id
|
||
|
|
|
||
|
|
def add_job(
|
||
|
|
self,
|
||
|
|
func: Callable[..., Coroutine[Any, Any, Any]],
|
||
|
|
trigger: BaseTrigger,
|
||
|
|
args: tuple[Any, ...] | None = None,
|
||
|
|
kwargs: dict[str, Any] | None = None,
|
||
|
|
id: str | None = None,
|
||
|
|
name: str | None = None,
|
||
|
|
**apscheduler_kwargs: Any,
|
||
|
|
) -> str:
|
||
|
|
"""Add a job to the scheduler.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
func: Async function to execute
|
||
|
|
trigger: APScheduler trigger (date, interval, cron, etc.)
|
||
|
|
args: Positional arguments for the function
|
||
|
|
kwargs: Keyword arguments for the function
|
||
|
|
id: Unique job ID (auto-generated if not provided)
|
||
|
|
name: Job name for display purposes
|
||
|
|
**apscheduler_kwargs: Additional APScheduler options
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
The job ID
|
||
|
|
"""
|
||
|
|
job_id = id or f"job_{len(self._job_tasks)}"
|
||
|
|
task_name = name or f"scheduled_task_{job_id}"
|
||
|
|
|
||
|
|
# Wrap the async function to integrate with BackgroundTaskManager
|
||
|
|
async def wrapped_func() -> None:
|
||
|
|
coro = func(*(args or ()), **(kwargs or {}))
|
||
|
|
task_id = self._task_manager.submit_task(task_name, coro)
|
||
|
|
self._job_tasks[job_id] = task_id
|
||
|
|
|
||
|
|
self._scheduler.add_job(
|
||
|
|
wrapped_func,
|
||
|
|
trigger=trigger,
|
||
|
|
id=job_id,
|
||
|
|
name=task_name,
|
||
|
|
**apscheduler_kwargs,
|
||
|
|
)
|
||
|
|
|
||
|
|
return job_id
|
||
|
|
|
||
|
|
def remove_job(self, job_id: str) -> bool:
|
||
|
|
"""Remove a job from the scheduler.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
job_id: The ID of the job to remove
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if job was removed, False if job didn't exist
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
self._scheduler.remove_job(job_id)
|
||
|
|
# Clean up task mapping if exists
|
||
|
|
if job_id in self._job_tasks:
|
||
|
|
task_id = self._job_tasks.pop(job_id)
|
||
|
|
# Cancel the background task if still running
|
||
|
|
self._task_manager.cancel_task(task_id)
|
||
|
|
return True
|
||
|
|
except Exception:
|
||
|
|
return False
|
||
|
|
|
||
|
|
def list_jobs(self) -> list[dict[str, Any]]:
|
||
|
|
"""List all scheduled jobs.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of job information dictionaries
|
||
|
|
"""
|
||
|
|
jobs = self._scheduler.get_jobs()
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
"id": job.id,
|
||
|
|
"name": job.name,
|
||
|
|
"next_run_time": job.next_run_time,
|
||
|
|
"trigger": str(job.trigger),
|
||
|
|
}
|
||
|
|
for job in jobs
|
||
|
|
]
|
||
|
|
|
||
|
|
def start(self) -> None:
|
||
|
|
"""Start the scheduler."""
|
||
|
|
if not self._scheduler.running:
|
||
|
|
self._scheduler.start()
|
||
|
|
|
||
|
|
def shutdown(self, wait: bool = True) -> None:
|
||
|
|
"""Shutdown the scheduler.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
wait: Whether to wait for running jobs to complete
|
||
|
|
"""
|
||
|
|
if self._scheduler.running:
|
||
|
|
self._scheduler.shutdown(wait=wait)
|
||
|
|
|
||
|
|
def pause(self) -> None:
|
||
|
|
"""Pause the scheduler."""
|
||
|
|
self._scheduler.pause()
|
||
|
|
|
||
|
|
def resume(self) -> None:
|
||
|
|
"""Resume the scheduler."""
|
||
|
|
self._scheduler.resume()
|
||
|
|
|
||
|
|
@property
|
||
|
|
def task_manager(self) -> BackgroundTaskManager:
|
||
|
|
"""Get the underlying task manager."""
|
||
|
|
return self._task_manager
|
||
|
|
|
||
|
|
|
||
|
|
# Global scheduler instance
|
||
|
|
_scheduler: BackgroundScheduler | None = None
|
||
|
|
|
||
|
|
|
||
|
|
def get_background_scheduler() -> BackgroundScheduler:
|
||
|
|
"""Get the global BackgroundScheduler instance."""
|
||
|
|
global _scheduler
|
||
|
|
if _scheduler is None:
|
||
|
|
_scheduler = BackgroundScheduler()
|
||
|
|
return _scheduler
|