import uuid from django.db import models from django.db.models import Q class Job(models.Model): class Status(models.TextChoices): QUEUED = "queued", "Queued" RUNNING = "running", "Running" SUCCEEDED = "succeeded", "Succeeded" FAILED = "failed", "Failed" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) type = models.CharField(max_length=120) payload = models.JSONField(default=dict, blank=True) status = models.CharField(max_length=20, choices=Status.choices, default=Status.QUEUED, db_index=True) priority = models.SmallIntegerField(default=50) available_at = models.DateTimeField(db_index=True) attempts = models.PositiveIntegerField(default=0) max_attempts = models.PositiveIntegerField(default=3) idempotency_key = models.CharField(max_length=255, null=True, blank=True) locked_by = models.CharField(max_length=255, null=True, blank=True) locked_until = models.DateTimeField(null=True, blank=True) last_error = models.TextField(blank=True, default="") result = models.JSONField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True, db_index=True) updated_at = models.DateTimeField(auto_now=True) finished_at = models.DateTimeField(null=True, blank=True) class Meta: ordering = ("-created_at",) constraints = [ models.CheckConstraint( name="job_status_valid_shape", condition=( Q( status="queued", locked_by__isnull=True, locked_until__isnull=True, finished_at__isnull=True, ) | Q( status="running", locked_by__isnull=False, locked_until__isnull=False, finished_at__isnull=True, ) | Q( status__in=["succeeded", "failed"], locked_by__isnull=True, locked_until__isnull=True, finished_at__isnull=False, ) ), ), models.CheckConstraint( name="job_attempts_valid", condition=Q(attempts__gte=0, max_attempts__gte=1, attempts__lte=models.F("max_attempts")), ), models.UniqueConstraint( fields=["idempotency_key"], condition=Q(idempotency_key__isnull=False), name="job_idempotency_key_unique", ), ] indexes = [ models.Index( fields=["available_at", "-priority", "created_at", "id"], name="job_claim_idx", condition=Q(status="queued"), ), models.Index(fields=["locked_until"], name="job_timeout_idx", condition=Q(status="running")), ] def __str__(self): return f"{self.type}:{self.id}" class JobEvent(models.Model): class Type(models.TextChoices): CREATED = "created", "Created" CLAIMED = "claimed", "Claimed" PROGRESS = "progress", "Progress" LEASE_RENEWED = "lease_renewed", "Lease renewed" SUCCEEDED = "succeeded", "Succeeded" RETRY_SCHEDULED = "retry_scheduled", "Retry scheduled" FAILED = "failed", "Failed" TIMEOUT_REQUEUED = "timeout_requeued", "Timeout requeued" MANUAL_RETRY = "manual_retry", "Manual retry" job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name="events") type = models.CharField(max_length=40, choices=Type.choices, db_index=True) attempt = models.PositiveIntegerField(default=0) worker_id = models.CharField(max_length=255, null=True, blank=True) message = models.TextField(blank=True, default="") data = models.JSONField(default=dict, blank=True) created_at = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: ordering = ("id",) indexes = [ models.Index(fields=["job", "id"], name="job_event_job_id_idx"), models.Index(fields=["id"], name="job_event_id_idx"), ] def __str__(self): return f"{self.type}:{self.job_id}:{self.id}"