migrated to redis

This commit is contained in:
nvrl committed 2026-04-24 18:42:53 +02:00
1 parent 33e4b0376e
commit 9a3f6b5db8
9 files changed
+112 -45

No files matched your search

@@ -0,0 +1,18 @@
# Generated by Django 6.0.4 on 2026-04-24 16:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0030_flavor_image_urls'),
]
operations = [
migrations.AlterField(
model_name='job',
name='name',
field=models.CharField(choices=[('sync_flavors', 'Sync Holy Flavors'), ('cleanup_duplicates', 'Cleanup Duplicate Flavors'), ('backup_db', 'Database Backup'), ('seed_legacy', 'Seed Legacy Data'), ('seed_banners', 'Seed Banner Configurations')], max_length=100),
),
]
+1
View File
@@ -56,6 +56,7 @@ class Job(models.Model):
("cleanup_duplicates", "Cleanup Duplicate Flavors"),
("backup_db", "Database Backup"),
("seed_legacy", "Seed Legacy Data"),
("seed_banners", "Seed Banner Configurations"),
]
STATUS_CHOICES = [
("pending", "Pending"),
+24
View File
@@ -1,7 +1,16 @@
from datetime import timedelta
from rest_framework import serializers
from api.models import Banner, Job, SystemConfig
_INTERVAL_PERIOD_TO_TIMEDELTA = {
"days": lambda every: timedelta(days=every),
"hours": lambda every: timedelta(hours=every),
"minutes": lambda every: timedelta(minutes=every),
"seconds": lambda every: timedelta(seconds=every),
}
class SystemConfigSerializer(serializers.ModelSerializer):
class Meta:
@@ -17,6 +26,21 @@ class SystemConfigSerializer(serializers.ModelSerializer):
class JobSerializer(serializers.ModelSerializer):
name_display = serializers.CharField(source="get_name_display", read_only=True)
next_run = serializers.SerializerMethodField()
def get_next_run(self, obj):
from django_celery_beat.models import PeriodicTask
task = PeriodicTask.objects.filter(name=f"job:{obj.name}", enabled=True).first()
if not task or not task.interval:
return None
base = task.last_run_at or obj.last_run
if not base:
return None
delta_fn = _INTERVAL_PERIOD_TO_TIMEDELTA.get(task.interval.period)
if not delta_fn:
return None
return base + delta_fn(task.interval.every)
class Meta:
model = Job
+19 -8
View File
@@ -8,7 +8,6 @@ status/output/timestamps so the admin UI keeps working.
from __future__ import annotations
from datetime import timedelta
from io import StringIO
from typing import Any
@@ -22,7 +21,12 @@ logger = get_task_logger(__name__)
def _run_command_job(job_name: str, command: str, *args: str) -> str:
"""Run a management command and mirror status/output into the Job row."""
"""Run a management command and mirror status/output into the Job row.
Scheduling (next_run) is owned by django_celery_beat.PeriodicTask; the Job
serializer derives next_run from it. This function only writes execution
state: status, last_run, last_output, error_message.
"""
from api.models import Job
job, _ = Job.objects.get_or_create(name=job_name)
@@ -43,16 +47,18 @@ def _run_command_job(job_name: str, command: str, *args: str) -> str:
raise
finally:
job.last_output = out.getvalue()
if job.interval_hours > 0:
job.next_run = timezone.now() + timedelta(hours=job.interval_hours)
else:
job.next_run = None
job.save()
job.save(update_fields=["status", "last_output", "error_message"])
return job.last_output
@shared_task(name="api.sync_flavors")
@shared_task(
name="api.sync_flavors",
autoretry_for=(Exception,),
retry_backoff=60,
retry_backoff_max=600,
retry_kwargs={"max_retries": 2},
)
def sync_flavors_task() -> str:
return _run_command_job("sync_flavors", "sync_flavors")
@@ -72,6 +78,11 @@ def seed_legacy_task() -> str:
return _run_command_job("seed_legacy", "seed_legacy_flavors")
@shared_task(name="api.seed_banners")
def seed_banners_task() -> str:
return _run_command_job("seed_banners", "seed_banners")
@shared_task(
name="api.send_email",
autoretry_for=(Exception,),
+9 -26
View File
@@ -20,6 +20,7 @@ JOB_TASK_MAP = {
"cleanup_duplicates": "api.cleanup_duplicates",
"backup_db": "api.backup_db",
"seed_legacy": "api.seed_legacy",
"seed_banners": "api.seed_banners",
}
@@ -130,35 +131,17 @@ class AdminViewSet(viewsets.ViewSet):
def update_job_schedule(self, request: Request, pk=None) -> Response:
try:
job = Job.objects.get(pk=pk)
interval = request.data.get("interval_hours")
next_run = request.data.get("next_run")
if interval is not None:
job.interval_hours = int(interval)
if next_run is not None:
from django.utils import timezone
from django.utils.dateparse import parse_datetime
parsed_dt = parse_datetime(next_run)
if parsed_dt:
if timezone.is_naive(parsed_dt):
job.next_run = timezone.make_aware(
parsed_dt, timezone.get_current_timezone()
)
else:
job.next_run = parsed_dt
elif interval is not None and job.interval_hours > 0 and not job.next_run:
from django.utils import timezone
job.next_run = timezone.now()
job.save()
_sync_periodic_task(job)
return Response(JobSerializer(job).data)
except Job.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
interval = request.data.get("interval_hours")
if interval is not None:
job.interval_hours = int(interval)
job.save(update_fields=["interval_hours"])
_sync_periodic_task(job)
return Response(JobSerializer(job).data)
@action(detail=False, methods=["post"])
def send_test_email(self, request: Request) -> Response:
try:
-7
View File
@@ -18,15 +18,10 @@ fi
echo "Applying database migrations..."
python manage.py migrate --noinput
echo "Creating cache table..."
python manage.py createcachetable
echo "Collecting static files..."
python manage.py collectstatic --no-input
mkdir -p media/flavors
mkdir -p django_cache
chmod -R 777 django_cache
# One-shot seed jobs. These should run on image first boot, not on every
# restart. A marker file on the bind-mounted volume gates them.
@@ -40,8 +35,6 @@ if [ ! -f "$SEED_MARKER" ]; then
touch "$SEED_MARKER"
else
echo "Seed marker present — skipping seed commands. Trigger them from /admin if needed."
# Banners are cheap + config-driven; always re-sync them.
python manage.py seed_banners || echo "seed_banners failed (non-fatal)"
fi
if [ "$#" -gt 0 ]; then
+8 -2
View File
@@ -87,10 +87,12 @@ DATABASES = {
}
}
DJANGO_CACHE_URL = os.environ.get("DJANGO_CACHE_URL", "redis://redis:6379/1")
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "django_ratelimit_cache",
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": DJANGO_CACHE_URL,
}
}
@@ -189,3 +191,7 @@ CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
CELERY_TASK_ALWAYS_EAGER = os.environ.get("CELERY_TASK_ALWAYS_EAGER", "false").lower() == "true"
CELERY_TASK_ACKS_LATE = True
CELERY_TASK_REJECT_ON_WORKER_LOST = True
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
CELERY_RESULT_EXPIRES = 3600