mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-23 19:22:27 +02:00
Compare commits
2 Commits
7022cb7050
...
a66aa6de80
Author | SHA1 | Date | |
---|---|---|---|
|
a66aa6de80 | ||
|
d12d6e7cdb |
@ -8,7 +8,7 @@ class AllianceAuthConfig(AppConfig):
|
|||||||
def ready(self) -> None:
|
def ready(self) -> None:
|
||||||
import allianceauth.checks # noqa
|
import allianceauth.checks # noqa
|
||||||
from django_celery_beat.models import CrontabSchedule, PeriodicTask
|
from django_celery_beat.models import CrontabSchedule, PeriodicTask
|
||||||
from allianceauth.framework.cron import offset_cron
|
from allianceauth.crontab.cron import offset_cron
|
||||||
|
|
||||||
PeriodicTask.objects.update_or_create(
|
PeriodicTask.objects.update_or_create(
|
||||||
name='esi_cleanup_callbackredirect',
|
name='esi_cleanup_callbackredirect',
|
||||||
|
3
allianceauth/crontab/__init__.py
Normal file
3
allianceauth/crontab/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Alliance Auth Crontab Utilities
|
||||||
|
"""
|
14
allianceauth/crontab/apps.py
Normal file
14
allianceauth/crontab/apps.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""
|
||||||
|
Crontab App Config
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CrontabConfig(AppConfig):
|
||||||
|
"""
|
||||||
|
Crontab App Config
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "allianceauth.crontab"
|
||||||
|
label = "crontab"
|
@ -1,10 +1,11 @@
|
|||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
import logging
|
import logging
|
||||||
from allianceauth.framework.models import CronOffset
|
from allianceauth.crontab.models import CronOffset
|
||||||
from django.db import ProgrammingError
|
from django.db import ProgrammingError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def offset_cron(schedule: crontab) -> crontab:
|
def offset_cron(schedule: crontab) -> crontab:
|
||||||
"""Take a crontab and apply a series of precalculated offsets to spread out tasks execution on remote resources
|
"""Take a crontab and apply a series of precalculated offsets to spread out tasks execution on remote resources
|
||||||
|
|
@ -13,7 +13,7 @@ class CronOffset(SingletonModel):
|
|||||||
minute = models.FloatField(_("Minute Offset"), default=random_default)
|
minute = models.FloatField(_("Minute Offset"), default=random_default)
|
||||||
hour = models.FloatField(_("Hour Offset"), default=random_default)
|
hour = models.FloatField(_("Hour Offset"), default=random_default)
|
||||||
day_of_month = models.FloatField(_("Day of Month Offset"), default=random_default)
|
day_of_month = models.FloatField(_("Day of Month Offset"), default=random_default)
|
||||||
month_of_year = models.FloatField(_("Month Of Year Offset"), default=random_default)
|
month_of_year = models.FloatField(_("Month of Year Offset"), default=random_default)
|
||||||
day_of_week = models.FloatField(_("Day of Week Offset"), default=random_default)
|
day_of_week = models.FloatField(_("Day of Week Offset"), default=random_default)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
0
allianceauth/crontab/tests/__init__.py
Normal file
0
allianceauth/crontab/tests/__init__.py
Normal file
79
allianceauth/crontab/tests/test_cron.py
Normal file
79
allianceauth/crontab/tests/test_cron.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# myapp/tests/test_tasks.py
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from unittest.mock import patch
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.db import ProgrammingError
|
||||||
|
from celery.schedules import crontab
|
||||||
|
|
||||||
|
from allianceauth.crontab.cron import offset_cron
|
||||||
|
from allianceauth.crontab.models import CronOffset
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class TestOffsetCron(TestCase):
|
||||||
|
|
||||||
|
def test_offset_cron_normal(self):
|
||||||
|
"""
|
||||||
|
Test that offset_cron modifies the minute/hour fields
|
||||||
|
based on the CronOffset values when everything is normal.
|
||||||
|
"""
|
||||||
|
# We'll create a mock CronOffset instance
|
||||||
|
mock_offset = CronOffset(minute=0.5, hour=0.5)
|
||||||
|
|
||||||
|
# Our initial crontab schedule
|
||||||
|
original_schedule = crontab(
|
||||||
|
minute=[0, 5, 55],
|
||||||
|
hour=[0, 3, 23],
|
||||||
|
day_of_month='*',
|
||||||
|
month_of_year='*',
|
||||||
|
day_of_week='*'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Patch CronOffset.get_solo to return our mock offset
|
||||||
|
with patch('allianceauth.crontab.models.CronOffset.get_solo', return_value=mock_offset):
|
||||||
|
new_schedule = offset_cron(original_schedule)
|
||||||
|
|
||||||
|
# Check the new minute/hour
|
||||||
|
# minute 0 -> 0 + round(60 * 0.5) = 30 % 60 = 30
|
||||||
|
# minute 5 -> 5 + 30 = 35 % 60 = 35
|
||||||
|
# minute 55 -> 55 + 30 = 85 % 60 = 25 --> sorted => 25,30,35
|
||||||
|
self.assertEqual(new_schedule._orig_minute, '25,30,35')
|
||||||
|
|
||||||
|
# hour 0 -> 0 + round(24 * 0.5) = 12 % 24 = 12
|
||||||
|
# hour 3 -> 3 + 12 = 15 % 24 = 15
|
||||||
|
# hour 23 -> 23 + 12 = 35 % 24 = 11 --> sorted => 11,12,15
|
||||||
|
self.assertEqual(new_schedule._orig_hour, '11,12,15')
|
||||||
|
|
||||||
|
# Check that other fields are unchanged
|
||||||
|
self.assertEqual(new_schedule._orig_day_of_month, '*')
|
||||||
|
self.assertEqual(new_schedule._orig_month_of_year, '*')
|
||||||
|
self.assertEqual(new_schedule._orig_day_of_week, '*')
|
||||||
|
|
||||||
|
def test_offset_cron_programming_error(self):
|
||||||
|
"""
|
||||||
|
Test that if a ProgrammingError is raised (e.g. before migrations),
|
||||||
|
offset_cron just returns the original schedule.
|
||||||
|
"""
|
||||||
|
original_schedule = crontab(minute=[0, 15, 30], hour=[1, 2, 3])
|
||||||
|
|
||||||
|
# Force get_solo to raise ProgrammingError
|
||||||
|
with patch('allianceauth.crontab.models.CronOffset.get_solo', side_effect=ProgrammingError()):
|
||||||
|
new_schedule = offset_cron(original_schedule)
|
||||||
|
|
||||||
|
# Should return the original schedule unchanged
|
||||||
|
self.assertEqual(new_schedule, original_schedule)
|
||||||
|
|
||||||
|
def test_offset_cron_unexpected_exception(self):
|
||||||
|
"""
|
||||||
|
Test that if any other exception is raised, offset_cron
|
||||||
|
also returns the original schedule, and logs the error.
|
||||||
|
"""
|
||||||
|
original_schedule = crontab(minute='0', hour='0')
|
||||||
|
|
||||||
|
# Force get_solo to raise a generic Exception
|
||||||
|
with patch('allianceauth.crontab.models.CronOffset.get_solo', side_effect=Exception("Something bad")):
|
||||||
|
new_schedule = offset_cron(original_schedule)
|
||||||
|
|
||||||
|
# Should return the original schedule unchanged
|
||||||
|
self.assertEqual(new_schedule, original_schedule)
|
64
allianceauth/crontab/tests/test_models.py
Normal file
64
allianceauth/crontab/tests/test_models.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
from unittest.mock import patch
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from allianceauth.crontab.models import CronOffset
|
||||||
|
|
||||||
|
|
||||||
|
class CronOffsetModelTest(TestCase):
|
||||||
|
def test_cron_offset_is_singleton(self):
|
||||||
|
"""
|
||||||
|
Test that CronOffset is indeed a singleton and that
|
||||||
|
multiple calls to get_solo() return the same instance.
|
||||||
|
"""
|
||||||
|
offset1 = CronOffset.get_solo()
|
||||||
|
offset2 = CronOffset.get_solo()
|
||||||
|
|
||||||
|
# They should be the exact same object in memory
|
||||||
|
self.assertEqual(offset1.pk, offset2.pk)
|
||||||
|
self.assertIs(offset1, offset2)
|
||||||
|
|
||||||
|
def test_default_values_random(self):
|
||||||
|
"""
|
||||||
|
Test that the default values are set via random_default() when
|
||||||
|
no explicit value is provided. We'll patch 'random.random' to
|
||||||
|
produce predictable output.
|
||||||
|
"""
|
||||||
|
with patch('allianceauth.crontab.models.random', return_value=0.1234):
|
||||||
|
# Force creation of a new CronOffset by clearing the existing one
|
||||||
|
CronOffset.objects.all().delete()
|
||||||
|
|
||||||
|
offset = CronOffset.get_solo() # This triggers creation
|
||||||
|
|
||||||
|
# All fields should be 0.1234, because we patched random()
|
||||||
|
self.assertAlmostEqual(offset.minute, 0.1234)
|
||||||
|
self.assertAlmostEqual(offset.hour, 0.1234)
|
||||||
|
self.assertAlmostEqual(offset.day_of_month, 0.1234)
|
||||||
|
self.assertAlmostEqual(offset.month_of_year, 0.1234)
|
||||||
|
self.assertAlmostEqual(offset.day_of_week, 0.1234)
|
||||||
|
|
||||||
|
def test_update_offset_values(self):
|
||||||
|
"""
|
||||||
|
Test that we can update the offsets and retrieve them.
|
||||||
|
"""
|
||||||
|
offset = CronOffset.get_solo()
|
||||||
|
offset.minute = 0.5
|
||||||
|
offset.hour = 0.25
|
||||||
|
offset.day_of_month = 0.75
|
||||||
|
offset.month_of_year = 0.99
|
||||||
|
offset.day_of_week = 0.33
|
||||||
|
offset.save()
|
||||||
|
|
||||||
|
# Retrieve again to ensure changes persist
|
||||||
|
saved_offset = CronOffset.get_solo()
|
||||||
|
self.assertEqual(saved_offset.minute, 0.5)
|
||||||
|
self.assertEqual(saved_offset.hour, 0.25)
|
||||||
|
self.assertEqual(saved_offset.day_of_month, 0.75)
|
||||||
|
self.assertEqual(saved_offset.month_of_year, 0.99)
|
||||||
|
self.assertEqual(saved_offset.day_of_week, 0.33)
|
||||||
|
|
||||||
|
def test_str_representation(self):
|
||||||
|
"""
|
||||||
|
Verify the __str__ method returns 'Cron Offsets'.
|
||||||
|
"""
|
||||||
|
offset = CronOffset.get_solo()
|
||||||
|
self.assertEqual(str(offset), "Cron Offsets")
|
@ -1,29 +0,0 @@
|
|||||||
# Generated by Django 4.2.16 on 2024-12-29 05:02
|
|
||||||
|
|
||||||
import allianceauth.framework.models
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CronOffset',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('minute', models.FloatField(default=allianceauth.framework.models.random_default, verbose_name='Minute Offset')),
|
|
||||||
('hour', models.FloatField(default=allianceauth.framework.models.random_default, verbose_name='Hour Offset')),
|
|
||||||
('day_of_month', models.FloatField(default=allianceauth.framework.models.random_default, verbose_name='Day of Month Offset')),
|
|
||||||
('month_of_year', models.FloatField(default=allianceauth.framework.models.random_default, verbose_name='Month Of Year Offset')),
|
|
||||||
('day_of_week', models.FloatField(default=allianceauth.framework.models.random_default, verbose_name='Day of Week Offset')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Cron Offsets',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -43,6 +43,7 @@ INSTALLED_APPS = [
|
|||||||
'allianceauth.theme.flatly',
|
'allianceauth.theme.flatly',
|
||||||
'allianceauth.theme.materia',
|
'allianceauth.theme.materia',
|
||||||
"allianceauth.custom_css",
|
"allianceauth.custom_css",
|
||||||
|
'allianceauth.crontab',
|
||||||
]
|
]
|
||||||
|
|
||||||
SECRET_KEY = "wow I'm a really bad default secret key"
|
SECRET_KEY = "wow I'm a really bad default secret key"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user