CreaRack-SL

Modelo AsyncJob — estado de operaciones largas en background

Entidadactiveverificado Sun Sep 06#core#model#django#saas#rls#async#background-jobs

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

CampoTipoPropósito
job_idUUIDIdentificador opaco (público), indexado, único. Lo usa el cliente para polling.
organizationFK → OrganizationAislamiento tenant. NOT NULL.
kindCharField(choices)Tipo: autoplan, ai_analysis, backup_full, restore_full
statusCharField(choices, default=pending)Estado: pending → processing → done | error
progressPositiveSmallInt (default=0)0-100 (opcional; la task lo puede omitir)
resultJSONFieldResultado en éxito (p.ej. {"blueprint_id": 42}) — NUNCA lleva secretos
errorTextFieldMensaje de error apto para usuario, acotado a 2000 caracteres
created_byFK → User (nullable)Quién encoló la operación. Auditoría.
created_atDateTimeFieldTimestamp de creación (auto_now_add)
updated_atDateTimeFieldTimestamp 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)

  1. POST /api/blueprints/autoplan/import → crea AsyncJob + encola run_autoplan_import
  2. Devuelve 202 + job_id
  3. Cliente hace polling GET /api/jobs/{job_id} cada 2.5 seg
  4. status=done → cliente redirige a /blueprints/{blueprint_id} (extraído de result)

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]]