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
import random
import re
import string
import warnings
from datetime import timedelta
from email.iterators import typed_subpart_iterator
from email.utils import getaddresses, parseaddr
......@@ -22,7 +23,7 @@ from debian.debian_support import AptPkgVersion
from django.conf import settings
from django.core.exceptions import ValidationError, ObjectDoesNotExist
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.utils import IntegrityError
from django.template.defaultfilters import slugify
......@@ -2641,7 +2642,16 @@ class TaskData(models.Model):
Extend the duration of the lock for the given delay. Calling this
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
"""
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.save(update_fields=['run_lock'])
......@@ -15,6 +15,7 @@ to happen regularly to update distro-tracker's data.
"""
import logging
import importlib
from datetime import timedelta
from django.conf import settings
......@@ -299,6 +300,33 @@ class BaseTask(metaclass=PluginRegistry):
for function in self.event_handlers.get(event, []):
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():
"""
......
......@@ -15,11 +15,12 @@ Tests for the Distro Tracker core module's models.
"""
import email
import itertools
import warnings
from datetime import timedelta
from django.core.exceptions import ObjectDoesNotExist, ValidationError
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.test.utils import override_settings
from django.urls import reverse
......@@ -1960,6 +1961,10 @@ class TaskDataTests(TestCase):
self.taskdata.save()
self.sample_data = {'foo': 'bar'}
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):
self.assertFalse(self.taskdata.task_is_pending)
......@@ -2062,3 +2067,10 @@ class TaskDataTests(TestCase):
# data has not been saved, after reload it's again the default value
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 @@
Tests for the Distro Tracker core's tasks framework.
"""
import logging
import warnings
from datetime import timedelta
from unittest import mock
......@@ -492,6 +493,54 @@ class BaseTaskTests(TestCase):
[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):
......
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