separated the scheduler from tasks, to allow us to schedule non add url tasks in the future
This commit is contained in:
parent
ea98562944
commit
d88ee15060
5 changed files with 173 additions and 51 deletions
|
|
@ -13,6 +13,9 @@ class Events:
|
||||||
The events that can be emitted.
|
The events that can be emitted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
STARTUP = "startup"
|
||||||
|
SHUTDOWN = "shutdown"
|
||||||
|
|
||||||
ADDED = "added"
|
ADDED = "added"
|
||||||
UPDATED = "updated"
|
UPDATED = "updated"
|
||||||
COMPLETED = "completed"
|
COMPLETED = "completed"
|
||||||
|
|
@ -42,6 +45,7 @@ class Events:
|
||||||
TASK_ERROR = "task_error"
|
TASK_ERROR = "task_error"
|
||||||
|
|
||||||
PRESETS_ADD = "preset_add"
|
PRESETS_ADD = "preset_add"
|
||||||
|
SCHEDULE_ADD = "schedule_add"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(kw_only=True)
|
||||||
|
|
|
||||||
|
|
@ -640,7 +640,7 @@ class HttpAPI(Common):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
data=Tasks.get_instance().get_tasks(), status=web.HTTPOk.status_code, dumps=self.encoder.encode
|
data=Tasks.get_instance().get_all(), status=web.HTTPOk.status_code, dumps=self.encoder.encode
|
||||||
)
|
)
|
||||||
|
|
||||||
@route("PUT", "api/tasks")
|
@route("PUT", "api/tasks")
|
||||||
|
|
@ -705,7 +705,7 @@ class HttpAPI(Common):
|
||||||
tasks.append(Task(**item))
|
tasks.append(Task(**item))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tasks = ins.save(tasks=tasks).load().get_tasks()
|
tasks = ins.save(tasks=tasks).load().get_all()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
|
|
|
||||||
136
app/library/Scheduler.py
Normal file
136
app/library/Scheduler.py
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aiocron import Cron
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from .EventsSubscriber import Event, Events, EventsSubscriber
|
||||||
|
from .Singleton import Singleton
|
||||||
|
|
||||||
|
LOG = logging.getLogger("scheduler")
|
||||||
|
|
||||||
|
|
||||||
|
class Scheduler(metaclass=Singleton):
|
||||||
|
"""
|
||||||
|
This class is used to manage the schedule.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_jobs: dict[str, Cron] = {}
|
||||||
|
"""The scheduled jobs."""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
"""The instance of the class."""
|
||||||
|
|
||||||
|
def __init__(self, loop: asyncio.AbstractEventLoop | None = None):
|
||||||
|
Scheduler._instance = self
|
||||||
|
|
||||||
|
self._loop = loop or asyncio.get_event_loop()
|
||||||
|
|
||||||
|
def handle_event(_, e: Event):
|
||||||
|
self.add(**e.data)
|
||||||
|
|
||||||
|
EventsSubscriber.get_instance().subscribe(Events.SCHEDULE_ADD, f"{__class__}.add", handle_event)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_instance() -> "Scheduler":
|
||||||
|
"""
|
||||||
|
Get the instance of the class.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Scheduler: The instance of the class
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not Scheduler._instance:
|
||||||
|
Scheduler._instance = Scheduler()
|
||||||
|
return Scheduler._instance
|
||||||
|
|
||||||
|
async def on_shutdown(self, _: web.Application):
|
||||||
|
"""
|
||||||
|
Do any jobs before shutdown.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
_: The aiohttp application.
|
||||||
|
|
||||||
|
"""
|
||||||
|
LOG.debug("Shutting down the scheduler.")
|
||||||
|
|
||||||
|
for job in self._jobs:
|
||||||
|
try:
|
||||||
|
job.stop()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
LOG.error(f"Failed to stop job '{job}'. Error message '{e!s}'.")
|
||||||
|
|
||||||
|
self._jobs = {}
|
||||||
|
|
||||||
|
LOG.debug("Scheduler has been shut down.")
|
||||||
|
|
||||||
|
def attach(self, app: web.Application):
|
||||||
|
app.on_shutdown.append(self.on_shutdown)
|
||||||
|
|
||||||
|
def get_all(self) -> dict[str, Cron]:
|
||||||
|
"""Return the jobs."""
|
||||||
|
return self._jobs
|
||||||
|
|
||||||
|
def get(self, id: str) -> Cron | None:
|
||||||
|
"""Return the job by id."""
|
||||||
|
return self._jobs.get(id)
|
||||||
|
|
||||||
|
def add(self, timer: str, func: callable, args: tuple = (),
|
||||||
|
kwargs: dict | None = None, id: str | None = None) -> str:
|
||||||
|
"""
|
||||||
|
Add a job to the schedule.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timer (str): The timer for the job.
|
||||||
|
func (callable): The function to call.
|
||||||
|
args (tuple): The arguments to pass to the function.
|
||||||
|
kwargs (dict): The keyword arguments to pass to the function.
|
||||||
|
id (str): The id of the job.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The id of the job
|
||||||
|
|
||||||
|
"""
|
||||||
|
if id and id in self._jobs:
|
||||||
|
self.remove(id)
|
||||||
|
|
||||||
|
job = Cron(spec=timer, func=func, args=args, kwargs=kwargs, uuid=id, start=True, loop=self._loop)
|
||||||
|
|
||||||
|
job_id = str(job.uuid)
|
||||||
|
|
||||||
|
self._jobs[job_id] = job
|
||||||
|
|
||||||
|
LOG.debug(f"Added job {job_id} to the schedule.")
|
||||||
|
|
||||||
|
return job_id
|
||||||
|
|
||||||
|
def remove(self, id: str | list[str]) -> bool:
|
||||||
|
"""
|
||||||
|
Remove a job from the schedule.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id (str|list[str]): The id of the job to remove.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the job was removed, False otherwise
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(id, list):
|
||||||
|
for i in id:
|
||||||
|
self.remove(i)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if id in self._jobs:
|
||||||
|
try:
|
||||||
|
self._jobs[id].stop()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
LOG.error(f"Failed to stop job '{id}'. Error message '{e!s}'.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
del self._jobs[id]
|
||||||
|
LOG.debug(f"Removed job {id} from the schedule.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
@ -8,13 +8,13 @@ from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from aiocron import Cron, crontab
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .Emitter import Emitter
|
from .Emitter import Emitter
|
||||||
from .encoder import Encoder
|
from .encoder import Encoder
|
||||||
from .EventsSubscriber import Event, Events, EventsSubscriber
|
from .EventsSubscriber import Event, Events, EventsSubscriber
|
||||||
|
from .Scheduler import Scheduler
|
||||||
from .Singleton import Singleton
|
from .Singleton import Singleton
|
||||||
|
|
||||||
LOG = logging.getLogger("tasks")
|
LOG = logging.getLogger("tasks")
|
||||||
|
|
@ -42,21 +42,13 @@ class Task:
|
||||||
return self.serialize().get(key, default)
|
return self.serialize().get(key, default)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
|
||||||
class Job:
|
|
||||||
id: str
|
|
||||||
name: str
|
|
||||||
task: Task
|
|
||||||
job: Cron
|
|
||||||
|
|
||||||
|
|
||||||
class Tasks(metaclass=Singleton):
|
class Tasks(metaclass=Singleton):
|
||||||
"""
|
"""
|
||||||
This class is used to manage the tasks.
|
This class is used to manage the tasks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_jobs: list[Job] = []
|
_tasks: list[Task] = []
|
||||||
"""The jobs for the tasks."""
|
"""The tasks."""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
"""The instance of the Tasks."""
|
"""The instance of the Tasks."""
|
||||||
|
|
@ -69,6 +61,7 @@ class Tasks(metaclass=Singleton):
|
||||||
config: Config | None = None,
|
config: Config | None = None,
|
||||||
encoder: Encoder | None = None,
|
encoder: Encoder | None = None,
|
||||||
client: httpx.AsyncClient | None = None,
|
client: httpx.AsyncClient | None = None,
|
||||||
|
scheduler: Scheduler | None = None,
|
||||||
):
|
):
|
||||||
Tasks._instance = self
|
Tasks._instance = self
|
||||||
|
|
||||||
|
|
@ -76,11 +69,12 @@ class Tasks(metaclass=Singleton):
|
||||||
|
|
||||||
self._debug = config.debug
|
self._debug = config.debug
|
||||||
self._default_preset = config.default_preset
|
self._default_preset = config.default_preset
|
||||||
self._file: str = file or os.path.join(config.config_path, "tasks.json")
|
self._file = file or os.path.join(config.config_path, "tasks.json")
|
||||||
self._client: httpx.AsyncClient = client or httpx.AsyncClient()
|
self._client = client or httpx.AsyncClient()
|
||||||
self._encoder: Encoder = encoder or Encoder()
|
self._encoder = encoder or Encoder()
|
||||||
self._loop: asyncio.AbstractEventLoop = loop or asyncio.get_event_loop()
|
self._loop = loop or asyncio.get_event_loop()
|
||||||
self._emitter: Emitter = emitter or Emitter.get_instance()
|
self._emitter = emitter or Emitter.get_instance()
|
||||||
|
self._scheduler = scheduler or Scheduler.get_instance()
|
||||||
|
|
||||||
if os.path.exists(self._file) and "600" != oct(os.stat(self._file).st_mode)[-3:]:
|
if os.path.exists(self._file) and "600" != oct(os.stat(self._file).st_mode)[-3:]:
|
||||||
try:
|
try:
|
||||||
|
|
@ -96,27 +90,19 @@ class Tasks(metaclass=Singleton):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_instance() -> "Tasks":
|
def get_instance() -> "Tasks":
|
||||||
"""
|
"""
|
||||||
Get the instance of the Tasks.
|
Get the instance of the class.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tasks: The instance of the Tasks
|
Tasks: The instance of the class.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not Tasks._instance:
|
if not Tasks._instance:
|
||||||
Tasks._instance = Tasks()
|
Tasks._instance = Tasks()
|
||||||
|
|
||||||
return Tasks._instance
|
return Tasks._instance
|
||||||
|
|
||||||
async def on_shutdown(self, _: web.Application):
|
async def on_shutdown(self, _: web.Application):
|
||||||
"""
|
pass
|
||||||
Shutdown the socket server.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
_: The aiohttp application.
|
|
||||||
|
|
||||||
"""
|
|
||||||
LOG.debug("Shutting down tasks runner.")
|
|
||||||
self.clear(shutdown=True)
|
|
||||||
LOG.debug("Tasks runner has been shut down.")
|
|
||||||
|
|
||||||
def attach(self, _: web.Application):
|
def attach(self, _: web.Application):
|
||||||
"""
|
"""
|
||||||
|
|
@ -125,15 +111,12 @@ class Tasks(metaclass=Singleton):
|
||||||
Args:
|
Args:
|
||||||
_ (web.Application): The aiohttp application.
|
_ (web.Application): The aiohttp application.
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
def get_tasks(self) -> list[Task]:
|
def get_all(self) -> list[Task]:
|
||||||
"""Return the tasks."""
|
"""Return the tasks."""
|
||||||
return [job.task for job in self._jobs]
|
return self._tasks
|
||||||
|
|
||||||
def load(self) -> "Tasks":
|
def load(self) -> "Tasks":
|
||||||
"""
|
"""
|
||||||
|
|
@ -168,18 +151,13 @@ class Tasks(metaclass=Singleton):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._jobs.append(
|
self._scheduler.add(timer=task.timer, func=self._runner, args=(task,), id=task.id)
|
||||||
Job(
|
self._tasks.append(task)
|
||||||
id=task.id,
|
|
||||||
name=task.name,
|
|
||||||
task=task,
|
|
||||||
job=crontab(spec=task.timer, func=self._runner, args=(task,), start=True, loop=self._loop),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
LOG.info(f"Task '{i}: {task.name}' queued to be executed every '{task.timer}'.")
|
LOG.info(f"Task '{i}: {task.name}' queued to be executed every '{task.timer}'.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
LOG.error(f"Failed to queue task '{i}: {task['name']}'. '{e!s}'.")
|
LOG.error(f"Failed to queue task '{i}: {task.name}'. '{e!s}'.")
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
@ -191,19 +169,19 @@ class Tasks(metaclass=Singleton):
|
||||||
Tasks: The current instance.
|
Tasks: The current instance.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if len(self._jobs) < 1:
|
if len(self._tasks) < 1:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
for task in self._jobs:
|
for task in self._tasks:
|
||||||
try:
|
try:
|
||||||
LOG.info(f"Stopping job '{task.id}: {task.name}'.")
|
LOG.info(f"Stopping task '{task.id}: {task.name}'.")
|
||||||
task.job.stop()
|
self._scheduler.remove(task.id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if not shutdown:
|
if not shutdown:
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
LOG.error(f"Failed to stop job '{task.id}: {task.name}'. '{e!s}'.")
|
LOG.error(f"Failed to stop task '{task.id}: {task.name}'. '{e!s}'.")
|
||||||
|
|
||||||
self._jobs.clear()
|
self._tasks.clear()
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,13 @@ from aiohttp import web
|
||||||
from library.config import Config
|
from library.config import Config
|
||||||
from library.DownloadQueue import DownloadQueue
|
from library.DownloadQueue import DownloadQueue
|
||||||
from library.Emitter import Emitter
|
from library.Emitter import Emitter
|
||||||
from library.EventsSubscriber import EventsSubscriber
|
from library.EventsSubscriber import Events, EventsSubscriber
|
||||||
from library.HttpAPI import HttpAPI
|
from library.HttpAPI import HttpAPI
|
||||||
from library.HttpSocket import HttpSocket
|
from library.HttpSocket import HttpSocket
|
||||||
from library.Notifications import Notification
|
from library.Notifications import Notification
|
||||||
from library.PackageInstaller import PackageInstaller
|
from library.PackageInstaller import PackageInstaller
|
||||||
from library.Presets import Presets
|
from library.Presets import Presets
|
||||||
|
from library.Scheduler import Scheduler
|
||||||
from library.Tasks import Tasks
|
from library.Tasks import Tasks
|
||||||
|
|
||||||
LOG = logging.getLogger("app")
|
LOG = logging.getLogger("app")
|
||||||
|
|
@ -93,9 +94,12 @@ class Main:
|
||||||
"""
|
"""
|
||||||
Start the application.
|
Start the application.
|
||||||
"""
|
"""
|
||||||
|
EventsSubscriber.get_instance().emit(Events.STARTUP, data={"app": self._app})
|
||||||
|
|
||||||
self._socket.attach(self._app)
|
self._socket.attach(self._app)
|
||||||
self._http.attach(self._app)
|
self._http.attach(self._app)
|
||||||
self._queue.attach(self._app)
|
self._queue.attach(self._app)
|
||||||
|
Scheduler.get_instance().attach(self._app)
|
||||||
Tasks.get_instance().attach(self._app)
|
Tasks.get_instance().attach(self._app)
|
||||||
Presets.get_instance().attach(self._app)
|
Presets.get_instance().attach(self._app)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue