From c7d1e5d0693b78663f3160abc33d52a975173f72 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 19 Aug 2020 11:29:53 +0200 Subject: [PATCH] Allow to reduce the interval of some cronjobs (#1753) --- src/pretix/api/signals.py | 3 ++ src/pretix/base/services/orders.py | 2 + src/pretix/base/services/quotas.py | 2 + src/pretix/helpers/periodic.py | 61 ++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 src/pretix/helpers/periodic.py diff --git a/src/pretix/api/signals.py b/src/pretix/api/signals.py index 689cde048..b7c33909c 100644 --- a/src/pretix/api/signals.py +++ b/src/pretix/api/signals.py @@ -6,6 +6,7 @@ from django_scopes import scopes_disabled from pretix.api.models import ApiCall, WebHookCall from pretix.base.signals import periodic_task +from pretix.helpers.periodic import minimum_interval register_webhook_events = Signal( providing_args=[] @@ -19,11 +20,13 @@ instances. @receiver(periodic_task) @scopes_disabled() +@minimum_interval(minutes_after_success=12 * 60) def cleanup_webhook_logs(sender, **kwargs): WebHookCall.objects.filter(datetime__lte=now() - timedelta(days=30)).delete() @receiver(periodic_task) @scopes_disabled() +@minimum_interval(minutes_after_success=12 * 60) def cleanup_api_logs(sender, **kwargs): ApiCall.objects.filter(created__lte=now() - timedelta(hours=24)).delete() diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 91fd9a102..7763d914e 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -53,6 +53,7 @@ from pretix.base.signals import ( ) from pretix.celery_app import app from pretix.helpers.models import modelcopy +from pretix.helpers.periodic import minimum_interval error_messages = { 'unavailable': _('Some of the products you selected were no longer available. ' @@ -988,6 +989,7 @@ def expire_orders(sender, **kwargs): @receiver(signal=periodic_task) @scopes_disabled() +@minimum_interval(minutes_after_success=60) def send_expiry_warnings(sender, **kwargs): today = now().replace(hour=0, minute=0, second=0) days = None diff --git a/src/pretix/base/services/quotas.py b/src/pretix/base/services/quotas.py index 9dd78231d..110ddcdf9 100644 --- a/src/pretix/base/services/quotas.py +++ b/src/pretix/base/services/quotas.py @@ -18,6 +18,7 @@ from pretix.base.models import ( ) from pretix.celery_app import app +from ...helpers.periodic import minimum_interval from ..signals import periodic_task, quota_availability @@ -403,6 +404,7 @@ class QuotaAvailability: @receiver(signal=periodic_task) +@minimum_interval(minutes_after_success=60) def build_all_quota_caches(sender, **kwargs): refresh_quota_caches.apply_async() diff --git a/src/pretix/helpers/periodic.py b/src/pretix/helpers/periodic.py new file mode 100644 index 000000000..6263e086e --- /dev/null +++ b/src/pretix/helpers/periodic.py @@ -0,0 +1,61 @@ +import logging +import uuid +from functools import wraps + +from django.core.cache import cache + +logger = logging.getLogger(__name__) + + +def minimum_interval(minutes_after_success, minutes_after_error=0, minutes_running_timeout=30): + """ + This is intended to be used as a decorator on receivers of the ``periodic_task`` signal. + It stores the result in the task in the cache (usually redis) to ensure the receiver function + isn't executed less than ``minutes_after_success`` after the last successful run and no less + than ``minutes_after_error`` after the last failed run. There's also a simple locking mechanism + implemented making sure the function is not called a second time while it is running, unless + ``minutes_running_timeout`` have passed. This locking mechanism is naive and not safe of + race-conditions, it should not be relied upon. + """ + def deco(f): + @wraps(f) + def wrapper(*args, **kwargs): + key_running = f'pretix_periodic_{f.__module__}.{f.__name__}_running' + key_result = f'pretix_periodic_{f.__module__}.{f.__name__}_result' + + running_val = cache.get(key_running) + if running_val: + # Currently running + return + + result_val = cache.get(key_result) + if result_val: + # Has run recently + return + + uniqid = str(uuid.uuid4()) + cache.set(key_running, uniqid, timeout=minutes_running_timeout * 60) + try: + retval = f(*args, **kwargs) + except Exception as e: + try: + cache.set(key_result, 'error', timeout=minutes_after_error * 60) + except: + logger.exception('Could not store result') + raise e + else: + try: + cache.set(key_result, 'success', timeout=minutes_after_success * 60) + except: + logger.exception('Could not store result') + return retval + finally: + try: + if cache.get(key_running) == uniqid: + cache.delete(key_running) + except: + logger.exception('Could not release lock') + + return wrapper + + return deco