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
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 enQUEUED.
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.
La secuencia
octopus-apipersiste la intención y la fila de outbox en la misma transacción.pollingtoma las filas pendientes (SKIP LOCKED).pollingpublica en Kafka; la partition key es la serialization key.effectsconsume en orden por partición.effectsinvocaExecuteTransactionenoctopus-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
Qué cambia, según el ADR-001 (accepted):
- El adaptador
inprocessusa el outbox de Postgres conFOR UPDATE SKIP LOCKEDy 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.