Commit a288598e authored by Raphaël Hertzog's avatar Raphaël Hertzog

tasks: implement BaseTask.extend_lock() and BaseTask.lock_expires_soon()

parent d3ab23b6
...@@ -13,6 +13,7 @@ import os ...@@ -13,6 +13,7 @@ import os
import random import random
import re import re
import string import string
import warnings
from datetime import timedelta from datetime import timedelta
from email.iterators import typed_subpart_iterator from email.iterators import typed_subpart_iterator
from email.utils import getaddresses, parseaddr from email.utils import getaddresses, parseaddr
...@@ -22,7 +23,7 @@ from debian.debian_support import AptPkgVersion ...@@ -22,7 +23,7 @@ from debian.debian_support import AptPkgVersion
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError, ObjectDoesNotExist from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import models from django.db import models, connection
from django.db.models import Q from django.db.models import Q
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
...@@ -2641,7 +2642,16 @@ class TaskData(models.Model): ...@@ -2641,7 +2642,16 @@ class TaskData(models.Model):
Extend the duration of the lock for the given delay. Calling this Extend the duration of the lock for the given delay. Calling this
method when the lock is not yet acquired will raise an exception. method when the lock is not yet acquired will raise an exception.
Note that you should always run this outside of any transaction so
that the new expiration time is immediately visible, otherwise
it might only be committed much later when the transaction ends.
The
:param int delay: the number of seconds to add to lock expiration date :param int delay: the number of seconds to add to lock expiration date
""" """
if connection.in_atomic_block:
m = 'extend_run_lock() should be called outside of any transaction'
warnings.warn(RuntimeWarning(m))
self.run_lock += timedelta(seconds=delay) self.run_lock += timedelta(seconds=delay)
self.save(update_fields=['run_lock']) self.save(update_fields=['run_lock'])
...@@ -15,6 +15,7 @@ to happen regularly to update distro-tracker's data. ...@@ -15,6 +15,7 @@ to happen regularly to update distro-tracker's data.
""" """
import logging import logging
import importlib import importlib
from datetime import timedelta
from django.conf import settings from django.conf import settings
...@@ -299,6 +300,33 @@ class BaseTask(metaclass=PluginRegistry): ...@@ -299,6 +300,33 @@ class BaseTask(metaclass=PluginRegistry):
for function in self.event_handlers.get(event, []): for function in self.event_handlers.get(event, []):
function(*args, **kwargs) function(*args, **kwargs)
def lock_expires_soon(self, delay=600):
"""
:param int delay: The number of seconds allowed before the lock is
considered to expire soon.
:return: True if the lock is about to expire in the given delay. Returns
False otherwise.
:rtype: bool
"""
if self.task_data.run_lock is None:
return False
return self.task_data.run_lock <= now() + timedelta(seconds=delay)
def extend_lock(self, delay=1800, expire_delay=600):
"""
Extends the duration of the lock with the given `delay` if it's
about to expire soon (as defined by the `expire_delay` parameter).
:param int expire_delay: The number of seconds allowed before the lock
is considered to expire soon.
:param int delay: The number of seconds to add the expiration time of
the lock.
"""
if self.lock_expires_soon(delay=expire_delay):
self.task_data.extend_run_lock(delay=delay)
return True
return False
def import_all_tasks(): def import_all_tasks():
""" """
......
...@@ -15,11 +15,12 @@ Tests for the Distro Tracker core module's models. ...@@ -15,11 +15,12 @@ Tests for the Distro Tracker core module's models.
""" """
import email import email
import itertools import itertools
import warnings
from datetime import timedelta from datetime import timedelta
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import IntegrityError from django.db import IntegrityError, transaction
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.test.utils import override_settings from django.test.utils import override_settings
from django.urls import reverse from django.urls import reverse
...@@ -1960,6 +1961,10 @@ class TaskDataTests(TestCase): ...@@ -1960,6 +1961,10 @@ class TaskDataTests(TestCase):
self.taskdata.save() self.taskdata.save()
self.sample_data = {'foo': 'bar'} self.sample_data = {'foo': 'bar'}
self.sample_data_checksum = get_data_checksum(self.sample_data) self.sample_data_checksum = get_data_checksum(self.sample_data)
self.warning_re = \
r'extend_run_lock\(\) should be called outside of any transaction'
warnings.filterwarnings('ignore', message=self.warning_re,
category=RuntimeWarning)
def test_default_values(self): def test_default_values(self):
self.assertFalse(self.taskdata.task_is_pending) self.assertFalse(self.taskdata.task_is_pending)
...@@ -2062,3 +2067,10 @@ class TaskDataTests(TestCase): ...@@ -2062,3 +2067,10 @@ class TaskDataTests(TestCase):
# data has not been saved, after reload it's again the default value # data has not been saved, after reload it's again the default value
self.assertDictEqual(self.taskdata.data, {}) self.assertDictEqual(self.taskdata.data, {})
def test_extend_run_lock_warns_in_transaction(self):
self.taskdata.get_run_lock()
with self.assertWarnsRegex(RuntimeWarning, self.warning_re):
with transaction.atomic():
self.taskdata.extend_run_lock()
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
Tests for the Distro Tracker core's tasks framework. Tests for the Distro Tracker core's tasks framework.
""" """
import logging import logging
import warnings
from datetime import timedelta from datetime import timedelta
from unittest import mock from unittest import mock
...@@ -492,6 +493,54 @@ class BaseTaskTests(TestCase): ...@@ -492,6 +493,54 @@ class BaseTaskTests(TestCase):
[mock.call('execute-started'), mock.call('execute-failed')] [mock.call('execute-started'), mock.call('execute-failed')]
) )
def test_lock_expires_soon_when_it_is_true(self):
self.init_task_data(run_lock=now() + timedelta(seconds=120))
result = self.task.lock_expires_soon(delay=300)
self.assertEqual(result, True)
def test_lock_expires_soon_when_it_is_false(self):
self.init_task_data(run_lock=now() + timedelta(seconds=1800))
result = self.task.lock_expires_soon(delay=300)
self.assertEqual(result, False)
def test_lock_expires_soon_when_no_lock_acquired(self):
result = self.task.lock_expires_soon(delay=300)
self.assertEqual(result, False)
def test_lock_extend_when_lock_expires_soon(self):
initial_lock = now()
self.init_task_data(run_lock=initial_lock)
with warnings.catch_warnings():
warnings.simplefilter('ignore')
with mock.patch.object(self.task, 'lock_expires_soon') as mocked:
mocked.return_value = True
result = self.task.extend_lock(delay=1800)
self.assertGreater(self.task.task_data.run_lock, initial_lock)
self.assertEqual(result, True)
def test_lock_extend_when_lock_does_not_expire_soon(self):
initial_lock = now()
self.init_task_data(run_lock=initial_lock)
with mock.patch.object(self.task, 'lock_expires_soon') as mocked:
mocked.return_value = False
result = self.task.extend_lock(delay=1800)
self.assertEqual(self.task.task_data.run_lock, initial_lock)
self.assertEqual(result, False)
def test_lock_extend_when_no_lock_acquired(self):
result = self.task.extend_lock(delay=1800)
self.assertIsNone(self.task.task_data.run_lock)
self.assertEqual(result, False)
class SchedulerTests(TestCase): class SchedulerTests(TestCase):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment