Skip to main content

Eventos

Por qué existe esta historia​

Una transacción en Octopus tiene dos fases: primero la intención (siempre se persiste, incluso si se rechaza) y después la ejecución, que se dispara a partir de un evento. El camino canónico es InitiateTransaction → TransactionIntentApproved → ExecuteTransaction.

Lo que hace confiable ese salto no es el transporte de eventos sino el outbox: la fila del evento se escribe en la misma transacción de base de datos que la intención, así que o existen las dos o no existe ninguna. Kafka, el transporte en proceso o cualquier otro adaptador solo se ocupan de llevar esa fila hasta quien la ejecuta. Por eso el ADR-001 (estado accepted) pone el transporte detrás de puertos y deja el flujo de dos fases igual en todos los modos.

Quién participa​

Containers que participan en el flujo de eventos
Diagram: Containers que participan en el flujo de eventos

octopus-api recibe la operación y escribe en PostgreSQL. polling barre el outbox. effects procesa los eventos y ejecuta. Kafka es opcional: ver Modo monolito.

El outbox​

Los eventos pasan por dos steps de persistencia en el pipeline del ExecutionEngine: EmitPrimaryEventStep los prepara a partir del resultado del handler, y PersistOutboxStep los guarda como filas del outbox en una de dos fases. Los de fase BEFORE_COMMIT se guardan dentro de la transacción del handler; los de fase AFTER_COMMIT, en un paso posterior, ya confirmada esa transacción.

  • Atómico: lo que se guarda en BEFORE_COMMIT, que es el default de los eventos primarios: la intención y la fila de outbox se confirman juntas o no se confirma nada.
  • No es atómico: lo guardado en AFTER_COMMIT, la publicación al transporte y la ejecución posterior. Se completan cuando el poller y los efectos procesan la fila; mientras tanto la ejecución queda en QUEUED.

EventTraitsSpec.phase vale BEFORE_COMMIT por defecto, así que TransactionIntentApproved y el resto de los eventos primarios se escriben en la transacción del handler. AFTER_COMMIT solo es legal para eventos que nadie ejecuta (notificaciones, métricas): FederationEffectRegistry se niega a construirse si un evento primario AFTER_COMMIT tiene un efecto registrado. El poller solo reclama filas ya confirmadas, por lo que una fila BEFORE_COMMIT es invisible para los efectos hasta que commitea el handler. Resuelto en OCT-49 (decisión del ADR-001).

Si un evento primario declara serializationKeyRef o entityIdRef y la ref no resuelve a un valor sobre el resultado de la operación, EmitPrimaryEventStep falla con SERIAL_KEY_RESOLUTION_FAILED o ENTITY_ID_RESOLUTION_FAILED y la transacción del handler hace rollback, en lugar de encolar el evento con clave nula. Resuelto en OCT-50.

Del handler al outbox, dentro de octopus-api
Diagram: Del handler al outbox, dentro de octopus-api

La secuencia​

Initiate → outbox → Execute (modo distribuido)
Diagram: Initiate → outbox → Execute (modo distribuido)
  1. octopus-api persiste la intención y la fila de outbox en la misma transacción.
  2. polling toma las filas pendientes (SKIP LOCKED).
  3. polling publica en Kafka; la partition key es la serialization key.
  4. effects consume en orden por partición.
  5. effects invoca ExecuteTransaction en octopus-api.

Orden por clave​

La serialization key funciona como partition key: los eventos de una misma clave se procesan en orden. El orden por clave es una propiedad del puerto de transporte, no de un adaptador en particular; cualquier adaptador tiene que cumplirla (ver ADR-001, estado accepted).

Kafka solo es obligatorio cuando más de una instancia del kernel tiene que preservar el orden por clave, o cuando consumidores externos necesitan replay.

Modo monolito​

Modo monolito: sin Kafka
Diagram: Modo monolito: sin Kafka

Qué cambia, según el ADR-001 (accepted):

  • El adaptador inprocess usa el outbox de Postgres con FOR UPDATE SKIP LOCKED y lanes con clave para preservar el orden.
  • Kafka y el Schema Registry dejan de ser necesarios.
  • Los eventos son data classes de Kotlin, no esquemas del registry.

Qué no cambia: las dos fases (intención, después ejecución vía evento) y el outbox como garantía.

Lo que esta historia no garantiza​

  • Fan-out a consumidores externos y replay: requieren Kafka y no los cubre el modo monolito. Ver la Fase 2 del roadmap.
  • Que cada garantía de la matriz esté verificada: todas siguen en TODO.