Skip to content

Commit a8fcc43

Browse files
hc-sousacursoragent
andcommitted
feat(traffic): Celery lifecycle task — per-minute activation + expiry
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent edd9d23 commit a8fcc43

3 files changed

Lines changed: 104 additions & 0 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Register the per-minute traffic lifecycle task (activation + expiry)."""
2+
3+
from django.db import migrations
4+
5+
6+
def _every_minute_schedule(apps):
7+
CrontabSchedule = apps.get_model('django_celery_beat', 'CrontabSchedule')
8+
schedule, _ = CrontabSchedule.objects.get_or_create(
9+
minute='*',
10+
hour='*',
11+
day_of_week='*',
12+
day_of_month='*',
13+
month_of_year='*',
14+
defaults={'timezone': 'Atlantic/Azores'},
15+
)
16+
if schedule.timezone != 'Atlantic/Azores':
17+
schedule.timezone = 'Atlantic/Azores'
18+
schedule.save(update_fields=['timezone'])
19+
return schedule
20+
21+
22+
def register_lifecycle_task(apps, schema_editor):
23+
PeriodicTask = apps.get_model('django_celery_beat', 'PeriodicTask')
24+
every_minute = _every_minute_schedule(apps)
25+
PeriodicTask.objects.update_or_create(
26+
name='traffic.run_lifecycle',
27+
defaults={
28+
'task': 'traffic.run_lifecycle',
29+
'crontab': every_minute,
30+
'enabled': True,
31+
},
32+
)
33+
34+
35+
def unregister_lifecycle_task(apps, schema_editor):
36+
PeriodicTask = apps.get_model('django_celery_beat', 'PeriodicTask')
37+
PeriodicTask.objects.filter(name='traffic.run_lifecycle').delete()
38+
39+
40+
class Migration(migrations.Migration):
41+
dependencies = [
42+
('traffic', '0002_seed_default_categories'),
43+
('django_celery_beat', '__latest__'),
44+
]
45+
46+
operations = [
47+
migrations.RunPython(register_lifecycle_task, unregister_lifecycle_task),
48+
]

src/traffic/tasks.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Traffic Celery tasks."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
7+
from celery import shared_task
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
@shared_task(name='traffic.run_lifecycle')
13+
def run_lifecycle_task() -> dict:
14+
from traffic.services import run_lifecycle
15+
16+
counts = run_lifecycle()
17+
logger.info('traffic.run_lifecycle counts=%s', counts)
18+
return {'status': 'ok', **counts}

src/traffic/tests/test_tasks.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""U4 task test: the Celery task drives the lifecycle transitions."""
2+
3+
from __future__ import annotations
4+
5+
from datetime import timedelta
6+
7+
import pytest
8+
from django.utils import timezone
9+
10+
from tenancy.services import for_island, get_or_create_default_island
11+
from traffic.models import TrafficCategory, TrafficReport
12+
from traffic.tasks import run_lifecycle_task
13+
14+
pytestmark = pytest.mark.django_db
15+
16+
17+
def test_run_lifecycle_task_transitions():
18+
island = get_or_create_default_island()
19+
now = timezone.now()
20+
with for_island(island):
21+
cat = TrafficCategory.objects.create(
22+
island=island, name='Radar', slug='radar', is_schedulable=True,
23+
)
24+
due = TrafficReport.objects.create(
25+
island=island, category=cat, latitude=37.78, longitude=-25.50,
26+
status=TrafficReport.SCHEDULED, active_from=now - timedelta(minutes=1),
27+
expires_at=now + timedelta(hours=1),
28+
)
29+
stale = TrafficReport.objects.create(
30+
island=island, category=cat, latitude=37.78, longitude=-25.50,
31+
status=TrafficReport.ACTIVE, expires_at=now - timedelta(minutes=1),
32+
)
33+
34+
result = run_lifecycle_task()
35+
36+
assert result['status'] == 'ok'
37+
assert TrafficReport.objects.unscoped().get(id=due.id).status == TrafficReport.ACTIVE
38+
assert TrafficReport.objects.unscoped().get(id=stale.id).status == TrafficReport.EXPIRED

0 commit comments

Comments
 (0)