CreaRack-SL

Endpoint GET /api/jobs/{job_id} — consulta estado de operaciones async

Definición

Ruta: GET /api/jobs/{job_id}
Locación: core/api/jobs.py (jobs_router)
Autenticación: sí (requiere sesión válida)
RLS: sí, aislamiento por organization (doble anclaje: filtro ORM + policy BD)

Endpoint de polling que el cliente usa para consultar el estado de una operación larga encolada en Huey. Parte del patrón job + polling (ADR T2 / R5).

Parámetros

ParámetroUbicaciónTipoDescripción
job_idPathUUID (str)Identificador opaco de la operación (recibido del endpoint que la encoló)

Respuesta (200 OK)

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "kind": "autoplan",
  "status": "processing",
  "progress": 45,
  "result": null,
  "error": ""
}

Campos:

  • job_id (str): eco del request
  • kind (str): tipo de operación (autoplan, ai_analysis, backup_full, restore_full)
  • status (str): pending | processing | done | error
  • progress (int): 0-100 (si la task no lo reporta, queda en 0 hasta que sea done)
  • result (dict | null): en done, contiene datos (p.ej. {"blueprint_id": 12}); en otros estados, null. NUNCA incluye secretos.
  • error (str): en error, mensaje apto para usuario; en otros estados, vacío

Respuesta (404 Not Found)

{
  "message": "Job not found"
}

Causas:

  • El job_id no existe
  • El job_id existe pero pertenece a una organización distinta (aislamiento tenant)

Nota: ambos casos devuelven 404, no 403, para no revelar si el job existe en otra org.

Lógica de aislamiento

# core/api/jobs.py
org = get_current_org(request)  # GUC de la sesión
job = AsyncJob.objects.filter(job_id=job_id, organization=org).first()
# Doble anclaje:
# 1. Filtro ORM: solo jobs de esta org
# 2. RLS de BD: la tabla tiene política tenant_isolation (policy 0029)
#    Si por error se bypaseara el ORM, la BD igualmente rechazaría el acceso.
if not job:
    return 404, {"message": "Job not found"}

Flujo cliente típico

// 1. Encola operación
const enqueued = await fetch('/api/blueprints/autoplan/import', { ... });
const { job_id } = await enqueued.json();  // 202 Accepted

// 2. Polling
const poll = async () => {
    const job = await fetch(`/api/jobs/${job_id}`);
    const { status, progress, result, error } = await job.json();
    
    if (status === 'done') {
        // Éxito
        window.location.href = `/blueprints/${result.blueprint_id}`;
    } else if (status === 'error') {
        // Error
        alert(`Failed: ${error}`);
    } else {
        // pending o processing → vuelve a preguntar en 2.5 seg
        setTimeout(poll, 2500);
    }
};
setTimeout(poll, 2500);  // Espera inicial

Tope de seguridad recomendado

El cliente debe implementar un timeout máximo para no preguntar infinitamente. Sugerencia:

const MAX_MS = 6 * 60 * 1000;  // 6 minutos (Auto-Plan tardío)
if (Date.now() - startedAt > MAX_MS) {
    alert("Timed out. Please try again.");
}

Véase también

  • [[entity—core—model—asyncjob]]
  • [[concept—infra—async-job-pattern]]
  • [[entity—blueprints—service—run-autoplan-import]]
  • [[feature—blueprints—autoplan-async]]