A mentira de marketing — e a verdade
"Exactly-once" vende cursos e gera tweets, mas o que existe na prática é "effectively-once end-to-end": EOS dentro do broker + idempotência no receiver. Quem promete mais está vendendo abstração que vaza.
O teorema FLP de Fischer-Lynch-Paterson (1985) já estabeleceu o limite: em sistemas assíncronos com falhas, não há protocolo determinístico que resolva consenso. Exactly-once para side effects externos cai no mesmo balde.
Os 3 níveis reais de garantia
at-most-once: fire-and-forget; perde em falha. Throughput máximo. Logs não críticos.
at-least-once: default prático; nunca perde, duplica em retry. Exige idempotência no receiver.
exactly-once: dentro do Kafka/Flink via 2PC. Fora deles, vira at-least-once + dedupe.Two-Phase Commit no Kafka 0.11+
O producer idempotente garante que retries de rede não dupliquem dentro de uma partition. Transactions vão além: agrupam writes a múltiplas partitions + commit de offsets de consumo em UMA transação atômica.
prod.initTransactions(); // 1x no startup, faz fencing por epoch
while (running) {
ConsumerRecords<String,String> recs = consumer.poll(Duration.ofMillis(200));
prod.beginTransaction();
try {
for (ConsumerRecord<String,String> r : recs)
prod.send(new ProducerRecord<>("out", r.key(), transform(r.value())));
prod.sendOffsetsToTransaction(offsetsFrom(recs), consumer.groupMetadata());
prod.commitTransaction(); // commit atômico: writes + offsets
} catch (Exception e) { prod.abortTransaction(); }
}Flink 2PC sinks: o padrão mais elegante
Flink integra 2PC com checkpointing Chandy-Lamport. O sink faz pre-commit ao receber o barrier de checkpoint, e só commita após o coordenador confirmar o checkpoint global. Em falha, restaura do último checkpoint e descarta pre-commits órfãos.
KafkaSink, FileSink e JdbcSink do Flink já implementam TwoPhaseCommitSinkFunction. Para sinks customizados, você herda essa classe e implementa pre-commit/commit/abort.
Onde o castelo cai: side effects externos
Chamou Stripe API, enviou push notification, gravou em DynamoDB fora da transação Kafka? Não há 2PC. O receiver precisa ser idempotente. Padrão Stripe: idempotency-key no header, dedupe table do lado server.
A regra é: EOS resolve dentro do broker; do broker para fora, o receiver decide. Tratar isso como problema do receiver (não do producer) é o mindset correto.