From 348adefb7c5375f403501cae60ac2005d9629a86 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Mon, 6 Dec 2021 23:48:05 +0100 Subject: [PATCH] feat(scheduler): Scheduler is now a plugin Scheduler allows to schedule tasks, like cron does, but requires special configuration. --- flaschengeist/config.py | 1 + flaschengeist/flaschengeist.toml | 6 ++- flaschengeist/plugins/scheduler.py | 83 ++++++++++++++++++++++++++++++ flaschengeist/utils/scheduler.py | 17 ------ readme.md | 13 +++++ setup.py | 1 + 6 files changed, 103 insertions(+), 18 deletions(-) create mode 100644 flaschengeist/plugins/scheduler.py delete mode 100644 flaschengeist/utils/scheduler.py diff --git a/flaschengeist/config.py b/flaschengeist/config.py index 524ed6e..8b0b6ed 100644 --- a/flaschengeist/config.py +++ b/flaschengeist/config.py @@ -77,6 +77,7 @@ def configure_app(app, test_config=None): "auth": {"enabled": True}, "roles": {"enabled": True}, "users": {"enabled": True}, + "scheduler": {"enabled": True}, }, ) diff --git a/flaschengeist/flaschengeist.toml b/flaschengeist/flaschengeist.toml index 2ef5c84..d8a32f0 100644 --- a/flaschengeist/flaschengeist.toml +++ b/flaschengeist/flaschengeist.toml @@ -11,7 +11,11 @@ root = "/api" # Set secret key secret_key = "V3ryS3cr3t" # Domain used by frontend -#domain = "flaschengeist.local" + +[scheduler] +# Possible values are: "passive_web" (default), "active_web" and "system" +# See documentation +# cron = "passive_web" [LOGGING] # You can override all settings from the logging.toml here diff --git a/flaschengeist/plugins/scheduler.py b/flaschengeist/plugins/scheduler.py new file mode 100644 index 0000000..2a4af6a --- /dev/null +++ b/flaschengeist/plugins/scheduler.py @@ -0,0 +1,83 @@ +import pkg_resources +from datetime import datetime, timedelta +from flask import Blueprint + +from flaschengeist import logger +from flaschengeist.utils.HTTP import no_content + +from . import Plugin + + +class __Task: + def __init__(self, function, **kwags): + self.function = function + self.interval = timedelta(**kwags) + + +_scheduled_tasks: dict[__Task] = dict() + + +def scheduled(id: str, replace=False, **kwargs): + """ + kwargs: days, hours, minutes + """ + + def real_decorator(function): + if id not in _scheduled_tasks or replace: + logger.info(f"Registered task: {id}") + _scheduled_tasks[id] = __Task(function, **kwargs) + else: + logger.debug(f"Skipping already registered task: {id}") + return function + + return real_decorator + + +class SchedulerPlugin(Plugin): + id = "dev.flaschengeist.scheduler" + name = "scheduler" + blueprint = Blueprint(name, __name__) + + def __init__(self, config=None): + """Constructor called by create_app + Args: + config: Dict configuration containing the plugin section + """ + + def __view_func(): + self.run_tasks() + return no_content() + + def __passiv_func(v): + try: + self.run_tasks() + except: + logger.error("Error while executing scheduled tasks!", exc_info=True) + + self.version = pkg_resources.get_distribution(self.__module__.split(".")[0]).version + cron = None if config is None else config.get("cron", "passive_web").lower() + + if cron is None or cron == "passive_web": + self.blueprint.teardown_app_request(__passiv_func) + elif cron == "active_web": + self.blueprint.add_url_rule("/cron", view_func=__view_func) + + def run_tasks(self): + changed = False + now = datetime.now() + status = self.get_setting("status", default=dict()) + + for id, task in _scheduled_tasks.items(): + last_run = status.setdefault(id, now) + if last_run + task.interval <= now: + logger.debug( + f"Run task {id}, was scheduled for {last_run + task.interval}, next iteration: {now + task.interval}" + ) + task.function() + changed = True + if changed: + # Remove not registered tasks + for id in status.keys(): + if id not in _scheduled_tasks.keys(): + del status[id] + self.set_setting("status", status) diff --git a/flaschengeist/utils/scheduler.py b/flaschengeist/utils/scheduler.py deleted file mode 100644 index aefbddd..0000000 --- a/flaschengeist/utils/scheduler.py +++ /dev/null @@ -1,17 +0,0 @@ -from flask import current_app - -from flaschengeist.utils.HTTP import no_content - -_scheduled = set() - - -def scheduled(func): - _scheduled.add(func) - return func - - -@current_app.route("/cron") -def __run_scheduled(): - for function in _scheduled: - function() - return no_content() diff --git a/readme.md b/readme.md index 7b64951..9e32c8e 100644 --- a/readme.md +++ b/readme.md @@ -39,6 +39,19 @@ Configuration is done within the a `flaschengeist.toml`file, you can copy the on Uncomment and change at least all the database parameters! +#### CRON +Some functionality used by some plugins rely on regular updates, +but as flaschengeists works as an WSGI app it can not controll when it gets called. + +So you have to configure one of the following options to call flaschengeists CRON tasks: + +1. Passive Web-CRON: Every time an users calls flaschengeist a task is scheduled (**NOT RECOMMENDED**) + - Pros: No external configuration needed + - Cons: Slower user experience, no guaranteed execution time of tasks +2. Active Web-CRON: You configure a webworker to call `/cron` + - Pros: Guaranteed execution interval, no impact on user experience (at least if you do not limit wsgi worker threads) + - Cons: Uses one of the webserver threads while executing + ### Database installation The user needs to have full permissions to the database. If not you need to create user and database manually do (or similar on Windows): diff --git a/setup.py b/setup.py index 4728772..f57a18b 100644 --- a/setup.py +++ b/setup.py @@ -68,6 +68,7 @@ setup( "balance = flaschengeist.plugins.balance:BalancePlugin", "mail = flaschengeist.plugins.message_mail:MailMessagePlugin", "pricelist = flaschengeist.plugins.pricelist:PriceListPlugin", + "scheduler = flaschengeist.plugins.scheduler:SchedulerPlugin", ], }, cmdclass={