CreaRack-SL

Patrón de trabajos asíncronos: job + polling

Conceptoactivecreado Thu Jul 16#infra#async#huey#polling#architecture#core

Introducción

El patrón async job + polling es el modelo recomendado en CreaRack Pro para operaciones que tardan más de ~1 segundo (ADR T2, Regla de Arquitectura R5). La idea es sencilla:

  1. El cliente encola una operación sin esperar a su resultado → el servidor responde 202 Accepted + job_id
  2. El cliente hace polling periódico a GET /api/jobs/{job_id} para consultar el estado
  3. Cuando el estado es done o error, se redirige al recurso creado o se muestra el error

Ventaja principal: el worker ASGI no se bloquea durante la operación larga (ej. análisis IA de ~4 min); otros usuarios siguen siendo atendidos.

Componentes

1. Modelo AsyncJob (core/models_async_job.py)

class AsyncJob(models.Model):
    job_id = UUIDField(unique=True)           # Identificador opaco del cliente
    organization = ForeignKey(Organization)   # Aislamiento tenant
    kind = CharField(choices=['autoplan', 'ai_analysis', 'backup_full', 'restore_full'])
    status = CharField(choices=['pending', 'processing', 'done', 'error'])
    progress = PositiveSmallIntegerField()    # 0-100 (opcional)
    result = JSONField()                      # Resultado en éxito (sin secretos)
    error = TextField()                       # Mensaje apto para usuario
    created_by = ForeignKey(User)             # Quién encoló
    created_at, updated_at = DateTimeField()

Propiedades:

  • Tabla ÚNICA reusable por todos los flujos async (no una tabla por tipo de job)
  • Tiene RLS (aislamiento por organization en BD), como todas las tablas tenant
  • Los índices (organization, -created_at y status, -created_at) permiten listar jobs rápidamente
  • Los métodos mark_processing(), mark_done(), mark_error() encapsulan las transiciones de estado

2. Endpoint de polling (core/api/jobs.py)

GET /api/jobs/{job_id}

Respuesta:

{
  "job_id": "abc-123",
  "kind": "autoplan",
  "status": "processing",
  "progress": 45,
  "result": null,
  "error": ""
}

Aislamiento: doble anclaje (filtro ORM + RLS en BD). Un job_id de otra org devuelve 404 (no confirma existencia).

3. Tareas Huey

Cada flujo que se quiere async encola una tarea en blueprints/tasks.py, racks/tasks.py, etc. con firma:

@db_task()
def run_<operation>(job_id, ...args, org_id):
    job = AsyncJob.objects.get(job_id=job_id)
    try:
        job.mark_processing(progress=0)
        # ... lógica larga ...
        job.mark_done(result={...})
    except Exception as e:
        job.mark_error(str(e))

Seguridad: credenciales (ej. API key) se resuelven dentro de la task desde settings, NO se serializan en el payload de Redis.

Ciclo de vida

1. Cliente POST /api/<operation>
   ↓
2. Servidor crea AsyncJob (status=pending) + encola task → responde 202 + job_id
   ↓
3. Cliente hace polling GET /api/jobs/{job_id} cada N ms
   ↓
4. Task Huey corre en background:
   - mark_processing() → status=processing
   - ... trabajo ...
   - mark_done(result) → status=done, progress=100
   ↓
5. Cliente detecta status=done, redirige o muestra resultado

Flujos migrados (ADR T2)

  • ✅ Auto-Plan: análisis IA de planos (~4 min) → run_autoplan_import
  • 🔄 Análisis IA de config: perf/finops (pendiente)
  • 🔄 Backup/Restore full: (pendiente)

Restricciones y consideraciones

  • Timeout de cliente: sugerencia 6 min (tope de seguridad en Auto-Plan)
  • Poll interval: 2.5 seg (equilibrio entre feedback y carga)
  • Progreso opcional: si la task no reporta progress, el cliente solo ve 0 → done
  • Result sin secretos: AsyncJob.result NUNCA lleva credenciales, tokens o datos sensibles
  • Estado READ-ONLY para el cliente: no puede cambiar status vía API, solo leer

Véase también

  • [[entity—core—model—asyncjob]]
  • [[entity—core—endpoint—jobs-polling]]
  • [[entity—blueprints—service—run-autoplan-import]]
  • [[feature—blueprints—autoplan-async]]