- infrasynth.configs: typed multi-tenant config store (registry, service, secrets, cache) + public config_changed/config_reset signals and API - emit the declared-but-dead signals (features flags/overrides, scheduler task_completed/task_failed, tenancy tenant_updated, audit model_changed) and per-model audit field exclusions - security: permission catalog (security_permission), Django-style model-derived AutoPermission, PermissionRegistry, RoleAssignment, global-or-tenant Grant/Revoke, catalog API - consolidate the permission surface: PermissionRegistry only (drop the settings dict), IsAuthenticatedAndPermitted aliases HybridPermission, require_permission replaced by required_permissions + require_all - packaging: add [build-system]; add Forgejo publish workflow (.forgejo)
65 lines
2.3 KiB
Python
65 lines
2.3 KiB
Python
"""Receivers emitting terminal ``TaskExecution`` signals.
|
|
|
|
``task_completed``/``task_failed`` fire exactly once, on the transition into a
|
|
terminal status, so the sync/plain-callable path and any task that updates its
|
|
own ``TaskExecution`` row are both covered without duplicate emissions.
|
|
|
|
Note: for the ``apply_async`` branch the execution row currently stays
|
|
``RUNNING`` (no Celery callback), so completion is only observable when the task
|
|
updates its own ``TaskExecution`` or runs synchronously. Tracking async
|
|
completion is a separate enhancement (``PLAN.md`` §2.8).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from django.db.models.signals import post_save, pre_save
|
|
from django.dispatch import receiver
|
|
|
|
from .models import TaskExecution
|
|
from .signals import task_completed, task_failed
|
|
|
|
|
|
@receiver(pre_save, sender=TaskExecution)
|
|
def _capture_previous_status(sender, instance, raw=False, **kwargs):
|
|
if raw or instance.pk is None:
|
|
return
|
|
previous = TaskExecution.all_objects.filter(pk=instance.pk).values_list("status", flat=True).first()
|
|
if previous is not None:
|
|
instance._previous_status = previous
|
|
|
|
|
|
@receiver(post_save, sender=TaskExecution)
|
|
def _emit_terminal_status(sender, instance, created, raw=False, **kwargs):
|
|
if raw or created:
|
|
return
|
|
previous = getattr(instance, "_previous_status", None)
|
|
if previous is None or previous == instance.status:
|
|
return
|
|
|
|
tenant_id = str(instance.tenant_id) if instance.tenant_id else None
|
|
task_name = getattr(instance.task, "name", "")
|
|
|
|
if instance.status == TaskExecution.Status.SUCCESS:
|
|
task_completed.send(
|
|
sender=TaskExecution,
|
|
tenant_id=tenant_id,
|
|
task_name=task_name,
|
|
task_id=instance.pk,
|
|
duration_ms=_duration_ms(instance),
|
|
result=instance.result,
|
|
)
|
|
elif instance.status == TaskExecution.Status.FAILURE:
|
|
task_failed.send(
|
|
sender=TaskExecution,
|
|
tenant_id=tenant_id,
|
|
task_name=task_name,
|
|
task_id=instance.pk,
|
|
error=instance.error_traceback,
|
|
traceback="",
|
|
)
|
|
|
|
|
|
def _duration_ms(execution: TaskExecution) -> int | None:
|
|
if execution.started_at is None or execution.completed_at is None:
|
|
return None
|
|
return int((execution.completed_at - execution.started_at).total_seconds() * 1000)
|