From ee2856c5fcdb9a720ce62bc58b5e4d3c4d5cb693 Mon Sep 17 00:00:00 2001 From: Kevin Alberts Date: Wed, 11 Nov 2020 17:18:21 +0100 Subject: [PATCH] Add lock to make sure only one ical_sync can run simultaneously --- .gitignore | 1 + davinci/icalendar/decorators.py | 61 +++++++++++++++++++ .../management/commands/ical_sync.py | 2 + requirements.txt | 1 + 4 files changed, 65 insertions(+) create mode 100644 davinci/icalendar/decorators.py diff --git a/.gitignore b/.gitignore index 75efcf1..0bb3718 100644 --- a/.gitignore +++ b/.gitignore @@ -215,6 +215,7 @@ fabric.properties .idea/ venv/ +static/ davinci/local.py diff --git a/davinci/icalendar/decorators.py b/davinci/icalendar/decorators.py new file mode 100644 index 0000000..b9d8c02 --- /dev/null +++ b/davinci/icalendar/decorators.py @@ -0,0 +1,61 @@ +import time +import logging + +from lockfile import FileLock, AlreadyLocked, LockTimeout +from django.conf import settings + +# Lock timeout value - how long to wait for the lock to become available. +# Default behavior is to never wait for the lock to be available (fail fast) +LOCK_WAIT_TIMEOUT = getattr(settings, "DEFAULT_LOCK_WAIT_TIMEOUT", -1) + + +def handle_lock(handle): + """ + Decorate the handle method with a file lock to ensure there is only ever + one process running at any one time. + """ + + def wrapper(self, *args, **options): + + start_time = time.time() + verbosity = options.get('verbosity', 0) + if verbosity == 0: + level = logging.WARNING + elif verbosity == 1: + level = logging.INFO + else: + level = logging.DEBUG + + logging.basicConfig(level=level, format="%(message)s") + logging.debug("-" * 72) + + lock_name = self.__module__.split('.').pop() + lock = FileLock(lock_name) + + logging.debug("%s - acquiring lock..." % lock_name) + try: + lock.acquire(LOCK_WAIT_TIMEOUT) + except AlreadyLocked: + logging.debug("lock already in place. quitting.") + return + except LockTimeout: + logging.debug("waiting for the lock timed out. quitting.") + return + logging.debug("acquired.") + + try: + handle(self, *args, **options) + except: + import traceback + logging.warning("Command Failed") + logging.warning('==' * 72) + logging.warning(traceback.format_exc()) + logging.warning('==' * 72) + + logging.debug("releasing lock...") + lock.release() + logging.debug("released.") + + logging.info("done in %.2f seconds" % (time.time() - start_time)) + + return wrapper diff --git a/davinci/icalendar/management/commands/ical_sync.py b/davinci/icalendar/management/commands/ical_sync.py index 527bdd1..3f65370 100644 --- a/davinci/icalendar/management/commands/ical_sync.py +++ b/davinci/icalendar/management/commands/ical_sync.py @@ -3,12 +3,14 @@ import logging from django.core.management.base import BaseCommand from django.utils import timezone +from davinci.icalendar.decorators import handle_lock from davinci.icalendar.models import ICalSync class Command(BaseCommand): help = 'Sychronize iCal syncs that need to be synchronized.' + @handle_lock def handle(self, *args, **options): logger = logging.getLogger("davinci.icalendar.ical_sync") logger.debug(f"Command `ical_sync` invoked") diff --git a/requirements.txt b/requirements.txt index e0e4906..7037f3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ django-durationwidget>=1.0.5,<1.1 humanize>=3.1.0,<3.2 ics>=0.7,<0.8 caldav>=0.7.1,<0.8 +lockfile>=0.12.2,<0.13