Modelo AsyncJob — estado de operaciones largas en background
Definición
Localización: core/models_async_job.py
Tabla BD: core_asyncjob
RLS: sí (policy tenant_isolation, migration 0029)
Índices: (organization, -created_at), (status, -created_at)
Modelo que captura el estado de una operación larga encolada en Huey. Es la única tabla para todos los flujos async (Auto-Plan, análisis IA, backup/restore), clasificados por el enum kind.
Campos
| Campo | Tipo | Propósito |
|---|---|---|
job_id | UUID | Identificador opaco (público), indexado, único. Lo usa el cliente para polling. |
organization | FK → Organization | Aislamiento tenant. NOT NULL. |
kind | CharField(choices) | Tipo: autoplan, ai_analysis, backup_full, restore_full |
status | CharField(choices, default=pending) | Estado: pending → processing → done | error |
progress | PositiveSmallInt (default=0) | 0-100 (opcional; la task lo puede omitir) |
result | JSONField | Resultado en éxito (p.ej. {"blueprint_id": 42}) — NUNCA lleva secretos |
error | TextField | Mensaje de error apto para usuario, acotado a 2000 caracteres |
created_by | FK → User (nullable) | Quién encoló la operación. Auditoría. |
created_at | DateTimeField | Timestamp de creación (auto_now_add) |
updated_at | DateTimeField | Timestamp de última actualización (auto_now) |
Transiciones de estado (métodos)
job = AsyncJob.objects.create(
organization=org,
kind=AsyncJob.Kind.AUTOPLAN,
created_by=request.user
)
# Task Huey inicia el trabajo
job.mark_processing(progress=0)
# ... operación larga ...
# Éxito
job.mark_done(result={"blueprint_id": 12, "racks_count": 8})
# O error
job.mark_error("La imagen es demasiado grande")
Nota: cada método hace save(update_fields=[...]) para acotar la escritura y evitar pisar columnas concurrentes.
Expiración automática de jobs colgados (v1.124.0, task #296.3)
Un job que se queda en pending/processing para siempre — el worker de Huey muere a mitad de un deploy, un OOM — antes no lo veía nadie: el navegador se quedaba con la barra de progreso congelada sin mensaje, y el usuario no tenía forma de saber que la operación había muerto en silencio.
# core/tasks.py
STALE_ASYNC_JOB_HOURS = 2
@db_periodic_task(crontab(minute="*/15"))
@lock_task("expire-stale-async-jobs")
def expire_stale_async_jobs():
"""Marca error los AsyncJob pending/processing sin novedad en 2 h."""
Cada 15 minutos, expire_stale_async_jobs() (core/tasks.py) marca status=error (mensaje “The operation was interrupted before finishing. Please try again.”) a todo AsyncJob en pending/processing cuyo updated_at supere las 2 horas — bajo rls_bypass(), porque es una tarea de sistema sin organización propia. Las 2 horas son un margen generoso: ninguno de los 4 kind conocidos tarda ni de lejos eso (el restore tiene tope de 6 min en el cliente, el MIB de minutos en el Agente local). Es la misma transición que mark_error(), pero disparada por el reloj en vez de por el código de la tarea — así un worker que muere sin excepción también deja el job en un estado final visible.
Aislamiento por organización (RLS)
ALTER TABLE core_asyncjob ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_asyncjob
USING (
current_setting('app.current_org_id', true) IN ('0', '')
OR organization_id = NULLIF(current_setting('app.current_org_id', true), '')::int
)
WITH CHECK (...);
Comportamiento:
- Requests con GUC (
app.current_org_id) = ID de org → solo ve los jobs de esa org - Requests con GUC = ‘0’ (admin/Huey) → bypassean la política (FORCE + ‘0’ en la expresión)
- Polling de un usuario:
GET /api/jobs/{job_id}con org X → si el job está en otra org, devuelve 404 (no confirma existencia)
Casos de uso
Auto-Plan (v1.55.0, primer cliente)
- POST
/api/blueprints/autoplan/import→ crea AsyncJob + encolarun_autoplan_import - Devuelve
202 + job_id - Cliente hace polling
GET /api/jobs/{job_id}cada 2.5 seg status=done→ cliente redirige a/blueprints/{blueprint_id}(extraído deresult)
Análisis IA (pendiente)
Mismo patrón para análisis de perf/finops de configuraciones.
Backup/Restore full (pendiente)
Backup completo de BD + ficheros volúmenes → muy largo, apto para async.
Véase también
- [[concept—infra—async-job-pattern]]
- [[entity—core—endpoint—jobs-polling]]
- [[entity—blueprints—service—run-autoplan-import]]
- [[entity—core—model—organization]]
- [[feature—blueprints—autoplan-async]]