February 24, 2021·4 min read·engineeringkafkatransactionsclientsreliability

librdkafka 1.6.1 stops transactions after fatal idempotent errors

librdkafka 1.6.1 changes how transactional producers handle fatal errors from idempotent production. Applications need to distinguish retiring a producer from retrying an operation or aborting one transaction.

Aborting a transaction cannot be assumed to repair every failure in the producer beneath it. The February 24 librdkafka 1.6.1 release makes fatal idempotent-producer errors fatal to the transactional producer as well. Its upgrade guidance calls this necessary for data integrity before the library supports KIP-360. Applications must inspect transactional API errors for the fatal flag and retire the affected producer when it is set.

This is a change in the permitted recovery action. A transaction-level failure can allow an application to abort and begin another transaction. A fatal producer failure means the same instance cannot safely continue. Treating both as a reason to retry the next transaction would erase the distinction the maintenance release is introducing.

I find Kafka's retained history useful only when its delivery guarantees are stated precisely. A client reporting that it can no longer maintain a guarantee needs to affect application control flow. Continuing to send because the API still has a familiar method name is not a meaningful recovery policy.

The error object describes the recovery scope

The public header at v1.6.1 exposes separate queries for fatal, retriable and abortable errors. rd_kafka_error_is_fatal identifies a fatal error. rd_kafka_error_txn_requires_abort identifies a failed transaction that must be aborted before another can begin. rd_kafka_error_is_retriable identifies an operation that can be attempted again.

These are different scopes of recovery. Retrying preserves the current producer and operation. Aborting discards the current transaction's work while leaving open the possibility of another transaction. A fatal error invalidates continuation with that producer instance. An application needs the returned classification because the method that failed does not by itself identify the appropriate scope.

The header's general transaction documentation still includes an older statement about treating some fatal idempotent errors as abortable. The release's explicit upgrade instruction changes that behavior for 1.6.1. Reading both sources matters here, since copying the older explanatory comment without the release note would invert the required response to the new fatal classification.

The error-query functions also define behavior for a null error pointer, returning false for the relevant predicate. Transaction APIs document a null result as success. That makes success separate from deciding what kind of failure occurred. A caller should not need to invent a synthetic failure category when the operation completed normally.

The header provides rd_kafka_error_destroy to release returned error objects. Recovery code also needs to preserve that ownership requirement when it takes a retry path.

A timeout does not necessarily end the operation

The commit API documentation says a timeout can be retriable because the commit continues in the background. That explains why a timed-out call and an aborted transaction are not interchangeable states. The application has stopped waiting for a result, while the underlying operation may still be active.

Retrying the appropriate API lets the caller continue according to the library's state machine. Starting unrelated transactional work merely because the wait ended would assume a conclusion the timeout does not provide. The distinction is particularly relevant when coordinating input positions with produced output.

The header documents transaction completion through commit or abort, followed by beginning another transaction. Its abortable-error guidance requires aborting the current transaction before continuing. These rules make recovery an explicit sequence, rather than an exception handler that always returns to the start of the processing loop.

Fatal errors have a longer lifetime. The documentation says subsequent transactional calls return a fatal condition, and the originating error can be obtained through rd_kafka_fatal_error. Retiring the instance is consequently a response to persistent producer state, not an attempt to clear a single failed request.

The maintenance work also handles failed coordination

The tagged changelog includes repairs around send_offsets_to_transaction. It records crashes and leaked coordinator requests associated with a transaction failing during that operation. These fixes concern the implementation's handling of overlapping failure and coordination work.

Another repair prevents partitions from remaining pending when production gradually adds several partitions to a transaction. Previously, queued messages for such partitions could fail to reach the broker and eventually time out the transaction. The release also allows committing an empty transaction, which had incorrectly produced a fatal state error.

Those changes complement the stricter fatal classification. Correctly accepting a no-op transaction and correctly refusing an unsafe producer continuation are different decisions. Reliability requires both to follow the intended state machine, instead of treating every unusual case as either harmless or irrecoverable.

I regard the upgrade guidance as the main application-facing requirement. The library repairs internal coordination, but application code still owns its response to an error object. It must preserve the difference between waiting again, aborting current work and retiring a producer that can no longer maintain its guarantees.