scheduler now runs

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
Marc 'risson' Schmitt
2025-06-19 19:08:50 +02:00
parent e6614a0705
commit d18a54e9e6
9 changed files with 95 additions and 19 deletions

View File

@ -159,6 +159,7 @@ web:
worker: worker:
processes: 2 processes: 2
threads: 1 threads: 1
consumer_listen_timeout: 30
storage: storage:
media: media:

View File

@ -357,7 +357,7 @@ TEST_RUNNER = "authentik.root.test_runner.PytestTestRunner"
DRAMATIQ = { DRAMATIQ = {
"broker_class": "authentik.tasks.broker.Broker", "broker_class": "authentik.tasks.broker.Broker",
"channel_prefix": "authentik", "channel_prefix": "authentik",
"task_class": "authentik.tasks.models.Task", "task_model": "authentik.tasks.models.Task",
"autodiscovery": { "autodiscovery": {
"enabled": True, "enabled": True,
"setup_module": "authentik.tasks.setup", "setup_module": "authentik.tasks.setup",
@ -366,8 +366,12 @@ DRAMATIQ = {
"worker": { "worker": {
"processes": CONFIG.get_int("worker.processes", 2), "processes": CONFIG.get_int("worker.processes", 2),
"threads": CONFIG.get_int("worker.threads", 1), "threads": CONFIG.get_int("worker.threads", 1),
"consumer_listen_timeout": CONFIG.get_int("worker.consumer_listen_timeout", 30),
}, },
"scheduler_class": "authentik.tasks.schedules.scheduler.Scheduler",
"schedule_model": "authentik.tasks.schedules.models.Schedule",
"middlewares": ( "middlewares": (
("django_dramatiq_postgres.middleware.SchedulerMiddleware", {}),
("django_dramatiq_postgres.middleware.FullyQualifiedActorName", {}), ("django_dramatiq_postgres.middleware.FullyQualifiedActorName", {}),
# TODO: fixme # TODO: fixme
# ("dramatiq.middleware.prometheus.Prometheus", {}), # ("dramatiq.middleware.prometheus.Prometheus", {}),

View File

@ -1,3 +1,5 @@
from time import sleep
from django_dramatiq_postgres.conf import Conf
import pglock import pglock
from django_dramatiq_postgres.scheduler import Scheduler as SchedulerBase from django_dramatiq_postgres.scheduler import Scheduler as SchedulerBase
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -20,5 +22,8 @@ class Scheduler(SchedulerBase):
with tenant: with tenant:
with self._lock(tenant) as lock_acquired: with self._lock(tenant) as lock_acquired:
if not lock_acquired: if not lock_acquired:
self.logger.debug("Could not acquire lock, skipping scheduling")
return return
self._run() count = self._run()
self.logger.info(f"Sent {count} scheduled tasks")
sleep(Conf().scheduler_interval)

View File

@ -1,4 +1,6 @@
import authentik.lib.setup # noqa from authentik.root.setup import setup
setup()
import django # noqa: E402 import django # noqa: E402

View File

@ -83,7 +83,7 @@ class PostgresBroker(Broker):
@cached_property @cached_property
def model(self) -> type[TaskBase]: def model(self) -> type[TaskBase]:
return import_string(Conf().task_class) return import_string(Conf().task_model)
@property @property
def query_set(self) -> QuerySet: def query_set(self) -> QuerySet:
@ -231,7 +231,7 @@ class _PostgresConsumer(Consumer):
# Override because dramatiq doesn't allow us setting this manually # Override because dramatiq doesn't allow us setting this manually
# TODO: turn it into a setting # TODO: turn it into a setting
self.timeout = 30000 // 1000 self.timeout = Conf().worker["consumer_listen_timeout"]
@property @property
def connection(self) -> DatabaseWrapper: def connection(self) -> DatabaseWrapper:

View File

@ -10,8 +10,8 @@ class Conf:
_ = settings.DRAMATIQ _ = settings.DRAMATIQ
except AttributeError as exc: except AttributeError as exc:
raise ImproperlyConfigured("Setting DRAMATIQ not set.") from exc raise ImproperlyConfigured("Setting DRAMATIQ not set.") from exc
if "task_class" not in self.conf: if "task_model" not in self.conf:
raise ImproperlyConfigured("DRAMATIQ.task_class not defined") raise ImproperlyConfigured("DRAMATIQ.task_model not defined")
@property @property
def conf(self) -> dict[str, Any]: def conf(self) -> dict[str, Any]:
@ -53,8 +53,8 @@ class Conf:
return self.conf.get("channel_prefix", "dramatiq") return self.conf.get("channel_prefix", "dramatiq")
@property @property
def task_class(self) -> str: def task_model(self) -> str:
return self.conf["task_class"] return self.conf["task_model"]
@property @property
def autodiscovery(self) -> dict[str, Any]: def autodiscovery(self) -> dict[str, Any]:
@ -81,9 +81,22 @@ class Conf:
"watch_use_polling": False, "watch_use_polling": False,
"processes": None, "processes": None,
"threads": None, "threads": None,
"consumer_listen_timeout": 30,
**self.conf.get("worker", {}), **self.conf.get("worker", {}),
} }
@property
def scheduler_class(self) -> str:
return self.conf.get("scheduler_class", "django_dramatiq_postgres.scheduler.Scheduler")
@property
def schedule_model(self) -> str | None:
return self.conf.get("schedule_model")
@property
def scheduler_interval(self) -> int:
return self.conf.get("scheduler_interval", 60)
@property @property
def test(self) -> bool: def test(self) -> bool:
return self.conf.get("test", False) return self.conf.get("test", False)

View File

@ -1,10 +1,13 @@
import contextvars import contextvars
from threading import Event
from typing import Any from typing import Any
from django.core.exceptions import ImproperlyConfigured
from django.db import ( from django.db import (
close_old_connections, close_old_connections,
connections, connections,
) )
from django.utils.module_loading import import_string
from dramatiq.actor import Actor from dramatiq.actor import Actor
from dramatiq.broker import Broker from dramatiq.broker import Broker
from dramatiq.logging import get_logger from dramatiq.logging import get_logger
@ -13,6 +16,7 @@ from dramatiq.middleware.middleware import Middleware
from django_dramatiq_postgres.conf import Conf from django_dramatiq_postgres.conf import Conf
from django_dramatiq_postgres.models import TaskBase from django_dramatiq_postgres.models import TaskBase
from django_dramatiq_postgres.scheduler import Scheduler
class DbConnectionMiddleware(Middleware): class DbConnectionMiddleware(Middleware):
@ -74,5 +78,39 @@ class CurrentTask(Middleware):
self.logger.warning("Task was None, not saving. This should not happen.") self.logger.warning("Task was None, not saving. This should not happen.")
return return
else: else:
tasks[-1].save() task = tasks[-1]
fields_to_exclude = {
"message_id",
"queue_name",
"actor_name",
"message",
"state",
"mtime",
"result",
"result_expiry",
}
fields_to_update = [
f.name
for f in task._meta.get_fields()
if f.name not in fields_to_exclude and not f.auto_created and f.column
]
if fields_to_update:
tasks[-1].save(update_fields=fields_to_update)
self._TASKS.set(tasks[:-1]) self._TASKS.set(tasks[:-1])
class SchedulerMiddleware(Middleware):
def __init__(self):
self.logger = get_logger(__name__, type(self))
if not Conf().schedule_model:
raise ImproperlyConfigured(
"When using the scheduler, DRAMATIQ.schedule_class must be set."
)
self.scheduler_stop_event = Event()
self.scheduler: Scheduler = import_string(Conf().scheduler_class)(self.scheduler_stop_event)
def after_process_boot(self, broker: Broker):
self.scheduler.broker = broker
self.scheduler.start()

View File

@ -1,3 +1,5 @@
from threading import Event, Thread
from time import sleep
import pglock import pglock
from django.db import router, transaction from django.db import router, transaction
from django.db.models import QuerySet from django.db.models import QuerySet
@ -11,14 +13,17 @@ from django_dramatiq_postgres.conf import Conf
from django_dramatiq_postgres.models import ScheduleBase from django_dramatiq_postgres.models import ScheduleBase
class Scheduler: class Scheduler(Thread):
def __init__(self, broker: Broker): broker: Broker
def __init__(self, stop_event: Event, *args, **kwargs):
super().__init__(*args, **kwargs)
self.stop_event = stop_event
self.logger = get_logger(__name__, type(self)) self.logger = get_logger(__name__, type(self))
self.broker = broker
@cached_property @cached_property
def model(self) -> type[ScheduleBase]: def model(self) -> type[ScheduleBase]:
return import_string(Conf().task_class) return import_string(Conf().schedule_model)
@property @property
def query_set(self) -> QuerySet: def query_set(self) -> QuerySet:
@ -36,15 +41,22 @@ class Scheduler:
timeout=0, timeout=0,
) )
def _run(self): def _run(self) -> int:
count = 0
with transaction.atomic(using=router.db_for_write(self.model)): with transaction.atomic(using=router.db_for_write(self.model)):
for schedule in self.query_set.select_for_update().filter( for schedule in self.query_set.select_for_update().filter(
next_run__lt=now(), next_run__lt=now(),
): ):
self.process_schedule(schedule) self.process_schedule(schedule)
count += 1
return count
def run(self): def run(self):
with self._lock() as lock_acquired: while not self.stop_event.is_set():
if not lock_acquired: with self._lock() as lock_acquired:
return if not lock_acquired:
self._run() self.logger.debug("Could not acquire lock, skipping scheduling")
return
count = self._run()
self.logger.info(f"Sent {count} scheduled tasks")
sleep(Conf().scheduler_interval)

View File

@ -49,6 +49,7 @@ def generate_local_config():
"worker": { "worker": {
"processes": 1, "processes": 1,
"threads": 1, "threads": 1,
"consumer_listen_timeout": 10,
}, },
} }