minor changes to the event bus dispatcher

This commit is contained in:
arabcoders 2025-06-23 01:00:36 +03:00
parent b37d614e27
commit f6e3c94432
5 changed files with 92 additions and 28 deletions

View file

@ -108,15 +108,17 @@ class DownloadQueue(metaclass=Singleton):
return DownloadQueue._instance return DownloadQueue._instance
def attach(self, app: web.Application): def attach(self, _: web.Application):
""" """
Attach the download queue to the application. Attach the download queue to the application.
Args: Args:
app (web.Application): The application to attach the download queue to. _ (web.Application): The application to attach the download queue to.
""" """
app.on_startup.append(lambda _: self.initialize()) self._notify.subscribe(
Events.STARTED, lambda _, __: self.initialize(), f"{__class__.__name__}.{__class__.__name__}.initialize"
)
Scheduler.get_instance().add( Scheduler.get_instance().add(
timer="* * * * *", timer="* * * * *",

View file

@ -1,6 +1,7 @@
import asyncio import asyncio
import datetime import datetime
import logging import logging
from typing import Any
import uuid import uuid
from collections.abc import Awaitable from collections.abc import Awaitable
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -93,6 +94,7 @@ class Events:
STARTUP = "startup" STARTUP = "startup"
LOADED = "loaded" LOADED = "loaded"
STARTED = "started"
SHUTDOWN = "shutdown" SHUTDOWN = "shutdown"
ADDED = "added" ADDED = "added"
@ -346,44 +348,71 @@ class EventBus(metaclass=Singleton):
return self return self
def sync_emit(self, event: str, data: any, **kwargs) -> list: def sync_emit(self, event: str, data: Any, loop=None, wait: bool = True, **kwargs):
""" """
Emit an event synchronously. Emit event and (optionally) wait for results.
Args: Args:
event (str): The event to emit. event (str): The event to emit.
data (any): The data to pass to the event. data (Any): The data to pass to the event.
**kwargs: The keyword arguments to pass to the event. loop (asyncio.AbstractEventLoop | None): The event loop to use. If None, the current running loop is used.
wait (bool): Whether to wait for the results of the event handlers. Defaults to True.
**kwargs: Additional keyword arguments to pass to the event
Returns: Returns:
list: The results are the return values of the coroutines. If the coroutine raises an exception, list: The results are the return values of the coroutines. If the coroutine raises an exception,
the exception is caught and logged. If event does not exist, an empty list is returned. the exception is caught and logged. If event does not exist, an empty list is returned.
If wait is False, a list of asyncio.Task objects is returned instead if we are in a running event loop,
or the result of the coroutine if we are not in a running event loop.
""" """
if event not in self._listeners: if event not in self._listeners:
return [] return []
ev = Event(event=event, data=data) async def emit_all():
LOG.debug(f"Emitting event '{ev.id}: {ev.event}'.", extra={"data": data}) ev = Event(event=event, data=data)
LOG.debug(f"Emitting event '{ev.id}: {ev.event}'.", extra={"data": data})
results = [] res: list = []
for handler in self._listeners[event].values(): for h in self._listeners[event].values():
try: try:
results.append(asyncio.get_event_loop().run_until_complete(handler.handle(ev, **kwargs))) res.append(await h.handle(ev, **kwargs))
except Exception as e: except Exception as e:
LOG.exception(e) LOG.exception(e)
LOG.error(f"Failed to emit event '{event}' to '{handler.name}'. Error message '{e!s}'.") LOG.error(f"Failed to emit event '{event}' to '{h.name}'. Error message '{e!s}'.")
return results return res
async def emit(self, event: str, data: any, **kwargs) -> Awaitable: try:
loop = loop or asyncio.get_running_loop()
in_same_loop: bool = asyncio.get_running_loop() is loop
except RuntimeError:
loop = None
in_same_loop = False
if loop is None or not loop.is_running():
return asyncio.run(emit_all())
if in_same_loop:
if wait:
msg = (
"Calling EventsBus.sync_emit(...,wait=True) from within the running event loop would cause dead-lock. "
"Use `await EventsBus.emit(...)` or `EventsBus.sync_emit(..., wait=False)`."
)
raise RuntimeError(msg)
return loop.create_task(emit_all())
fut = asyncio.run_coroutine_threadsafe(emit_all(), loop)
return fut.result() if wait else fut
async def emit(self, event: str, data: Any, **kwargs) -> Awaitable:
""" """
Emit an event. Emit an event.
Args: Args:
event (str): The event to emit. event (str): The event to emit.
data (any): The data to pass to the event. data (Any): The data to pass to the event.
**kwargs: The keyword arguments to pass to the event. **kwargs: The keyword arguments to pass to the event.
Returns: Returns:

View file

@ -74,9 +74,31 @@ class Scheduler(metaclass=Singleton):
return self._jobs return self._jobs
def get(self, id: str) -> Cron | None: def get(self, id: str) -> Cron | None:
"""Return the job by id.""" """
Get a job by its id.
Args:
id (str): The id of the job.
Returns:
Cron | None: The job if it exists, None otherwise
"""
return self._jobs.get(id) return self._jobs.get(id)
def has(self, id: str) -> bool:
"""
Check if a job with the given id exists.
Args:
id (str): The id of the job.
Returns:
bool: True if the job exists, False otherwise
"""
return id in self._jobs
def add( def add(
self, timer: str, func: callable, args: tuple = (), kwargs: dict | None = None, id: str | None = None self, timer: str, func: callable, args: tuple = (), kwargs: dict | None = None, id: str | None = None
) -> str: ) -> str:

View file

@ -162,11 +162,12 @@ class Tasks(metaclass=Singleton):
from cronsim import CronSim from cronsim import CronSim
cs = CronSim(task.timer, datetime.now(UTC)) cs = CronSim(task.timer, datetime.now(UTC))
schedule_time = cs.explain() schedule_time: str = cs.explain()
except Exception: except Exception:
schedule_time = task.timer schedule_time = task.timer
LOG.info(f"Task '{task.name}' queued to be executed '{schedule_time}'.") if not has_tasks:
LOG.info(f"Task '{task.name}' queued to be executed '{schedule_time}'.")
except Exception as e: except Exception as e:
LOG.exception(e) LOG.exception(e)
LOG.error(f"Failed to queue '{i}: {task.name}'. '{e!s}'.") LOG.error(f"Failed to queue '{i}: {task.name}'. '{e!s}'.")
@ -185,8 +186,11 @@ class Tasks(metaclass=Singleton):
return self return self
for task in self._tasks: for task in self._tasks:
if not self._scheduler.has(task.id):
continue
try: try:
LOG.info(f"Stopping '{task.name}'.") LOG.debug(f"Stopping '{task.name}'.")
self._scheduler.remove(task.id) self._scheduler.remove(task.id)
except Exception as e: except Exception as e:
if not shutdown: if not shutdown:

View file

@ -38,13 +38,14 @@ class Main:
def __init__(self, is_native: bool = False): def __init__(self, is_native: bool = False):
self._config = Config.get_instance(is_native=is_native) self._config = Config.get_instance(is_native=is_native)
self._app = web.Application() self._app = web.Application()
self.loop = asyncio.get_event_loop()
self._app.on_shutdown.append(self.on_shutdown)
Services.get_instance().add("app", self._app) Services.get_instance().add("app", self._app)
if self._config.debug: if self._config.debug:
loop = asyncio.get_event_loop() self.loop.set_debug(True)
loop.set_debug(True) self.loop.slow_callback_duration = 0.05
loop.slow_callback_duration = 0.05
self._check_folders() self._check_folders()
@ -97,6 +98,9 @@ class Main:
LOG.error(f"Could not create database file at '{self._config.db_file}'. {e!s}") LOG.error(f"Could not create database file at '{self._config.db_file}'. {e!s}")
raise raise
async def on_shutdown(self, _: web.Application):
await EventBus.get_instance().emit(Events.SHUTDOWN, data={"app": self._app})
def start(self, host: str | None = None, port: int | None = None, cb=None): def start(self, host: str | None = None, port: int | None = None, cb=None):
""" """
Start the application. Start the application.
@ -122,6 +126,9 @@ class Main:
LOG.info("=" * 40) LOG.info("=" * 40)
LOG.info(f"YTPTube {self._config.version} - started on http://{host}:{port}{self._config.base_path}") LOG.info(f"YTPTube {self._config.version} - started on http://{host}:{port}{self._config.base_path}")
LOG.info("=" * 40) LOG.info("=" * 40)
EventBus.get_instance().sync_emit(Events.STARTED, data={"app": self._app}, loop=self.loop, wait=False)
if cb: if cb:
cb() cb()
@ -138,7 +145,7 @@ class Main:
host=host, host=host,
port=port, port=port,
reuse_port="win32" != sys.platform, reuse_port="win32" != sys.platform,
loop=asyncio.get_event_loop(), loop=self.loop,
access_log=HTTP_LOGGER, access_log=HTTP_LOGGER,
print=started, print=started,
handle_signals=cb is None, handle_signals=cb is None,