Saltar al contenido principal

20 · Orquestación en producción

Ejecución durable de agentes

Runners que hacen pull, leases, fencing tokens y resultados ambiguos: ejecutar agentes que pueden caerse sin ejecutarse dos veces.

13 minCaso de estudio: Tack (se abre en una ventana nueva)

Un puede trabajar durante veinte minutos: clona un repositorio, edita archivos, corre tests, hace commits. En ese tiempo pueden fallar muchas cosas: la laptop se suspende, la red se corta, el proceso muere. La pregunta difícil no es "¿cómo reintento?", sino "¿cómo sé si ya se ejecutó?". Un reintento ciego puede duplicar un push o una migración; no reintentar puede perder trabajo en silencio.

Esta lección trata de la aplicada a : cómo registrar cada ejecución de forma que sobreviva a caídas, cómo garantizar que un solo trabajador sea dueño de ella a la vez y qué hacer cuando el resultado es genuinamente ambiguo. El caso de estudio es la flota de de Tack.

Por qué un agente necesita ejecución durable

Una llamada HTTP corta se reintenta sin pensarlo mucho. Una corrida de agente, no:

Propiedad Consecuencia
Duración de minutos u horas la probabilidad de una falla parcial deja de ser despreciable
Efectos fuera del sistema un commit o un deploy no se "deshace" al reiniciar
Estado repartido ningún componente, por sí solo, sabe qué pasó

Las ideas de reintentos, timeouts y cancelación se tratan en Observabilidad, auditoría y confiabilidad; aquí nos concentramos en la propiedad del trabajo.

Runners que hacen pull

La primera decisión de arquitectura es quién inicia la conexión. En un modelo push, el servidor central llama a las máquinas que ejecutan. En un modelo pull, cada trabajador pregunta "¿hay algo para mí?" y reporta lo que hizo.

  Tablero (uno)                      Runners (muchos)
 +--------------+   pull de trabajo  +-----------------+
 | plan, policy |<-------------------| laptop, CI, GPU |
 | leases       |   eventos,         | credenciales    |
 | historial    |<-------------------| harness local   |
 +--------------+   resultados       +-----------------+
   nunca llama hacia afuera

El modelo pull tiene ventajas concretas: los runners pueden estar detrás de NAT o de un firewall, el servidor no necesita credenciales para entrar en ninguna máquina y un runner caído simplemente deja de preguntar. Es el esquema de los runners de GitHub Actions o GitLab CI.

Del pedido al intento

El flujo de Tack separa conceptos que muchos sistemas mezclan. Un ítem del tablero no es una ejecución; una ejecución no es un intento.

ítem del tablero
   | crear execution request (durable, idempotente)
   v
scheduler -- policy + capacidades --> runners elegibles
   | lease con fencing token
   v
intento durable <-- eventos / decisiones / artefactos -- runner
  1. : registro durable de "ejecuta este ítem con este , bajo esta política". Lleva una clave de (UNIQUE(idempotency_scope, idempotency_key) en crates/tack-db/src/migrations.rs), así que crearlo dos veces no produce dos pedidos.
  2. Matching: el scheduler compara lo que el pedido exige con lo que cada runner declara (harness instalado, modelos, capacidad libre). La compatibilidad se decide por capacidades, no por nombres.
  3. : el runner elegido recibe un préstamo con vencimiento y un .
  4. Attempt: cada intento es historia inmutable; un pedido puede tener varios.
"SELECT COALESCE(MAX(fencing_token),0)+1 FROM execution_attempts WHERE request_id=?",

Leases, heartbeats y fencing tokens

Un lease es un lock con fecha de vencimiento: si el dueño desaparece, el lock no queda tomado para siempre. El dueño lo renueva con heartbeats periódicos. Pero el vencimiento crea un problema clásico: el dueño anterior puede no saber que perdió el lease.

Runner A          Almacén de leases          Runner B
   |--claim--------->| token 33
   |  (pausa larga, red cortada)
   |                 | lease 33 vence
   |                 |<---------claim-----------|
   |                 | token 34                 |
   |                 |<----write(token 34)------|  aceptado
   |--write(33)----->| 33 ya no vale: rechazado

El fencing token resuelve el caso: cada escritura lleva el token, y el almacén rechaza cualquiera que no corresponda al lease vigente. El proceso "zombi" puede seguir vivo, pero ya no puede modificar el registro.

Resultados ambiguos: detenerse en vez de adivinar

Cuando un runner se cae después de lanzar el proceso del harness, nadie en el servidor puede probar si ese proceso sigue vivo ni qué efectos produjo. La respuesta tentadora es "el lease venció, relanzo". Tack elige lo contrario.

| (Leased, Lost | NeedsOperator, RecoveryService)
| (Preparing, Running | Failed | Cancelled, LeaseOwner)
| (Preparing, Lost | NeedsOperator, RecoveryService)

La idempotencia aparece en cada borde: los lotes de eventos repetidos y los reportes finales duplicados son no-ops, y reutilizar una clave con un cuerpo distinto devuelve idempotency_conflict en lugar de sobrescribir.

La prueba de fuego: matar el runner a mitad de camino

Un diseño así solo vale si se prueba. El test más honesto es brutal: lanzar un intento real, matar el runner mientras trabaja y verificar tres cosas: que el intento no se pierde, que no se lanza un duplicado y que tras la decisión del operador el reintento termina bien.

El ecosistema

Estas herramientas resuelven la reanudación, pero el efecto ambiguo sigue dependiendo de que tus activities sean idempotentes. Que Tack se detenga para revisión humana encaja con agentes de código cuyos efectos cuesta revertir; no es la única respuesta válida.

Para CTOs

  • ¿Qué efectos de tus agentes no se pueden repetir sin daño? Esa lista define dónde necesitas idempotencia o revisión humana.
  • ¿Construir o adoptar? Un motor como Temporal da garantías fuertes a cambio de infraestructura y un modelo de programación restringido. Un diseño propio más estrecho, como el de Tack, cuesta menos de operar pero exige pruebas de caída serias.
  • ¿Dónde viven las credenciales? Un modelo pull permite que nunca salgan de la máquina que ejecuta.

Para llevar

  • No existe exactly-once para efectos externos; apunta a "a lo sumo un dueño válido" más idempotencia.
  • Un lease sin fencing token permite que un proceso zombi escriba sobre el trabajo de otro.
  • El fencing protege tu registro, no el mundo exterior: los resultados ambiguos necesitan una regla explícita.
  • Detenerse para revisión humana es más seguro que reintentar a ciegas cuando el efecto no es reversible.
  • Un diseño durable solo es creíble si una prueba mata al trabajador a mitad de camino.

Comprueba lo aprendido

¿Qué problema resuelve un fencing token que un lease con vencimiento no resuelve?

El lease evita que un lock quede tomado para siempre, pero el dueño anterior puede seguir escribiendo sin saber que lo perdió. El fencing token hace que el almacén rechace esas escrituras obsoletas.

En Tack, ¿cuándo se reencola automáticamente un intento tras una caída?

Solo cuando el runner observa que el proceso está detenido y el journal muestra que el harness nunca llegó a iniciarse. Cualquier otro caso pasa a needs_operator.

¿Por qué el modelo pull favorece la seguridad de las credenciales?

Porque el servidor nunca necesita conectarse a las máquinas que ejecutan ni guardar sus claves: cada runner inicia la conexión y conserva localmente las credenciales del proveedor.