May 10, 2021·3 min read·engineeringkafkaconnectreliabilityoffsets

Kafka 2.7.1 repairs completion reporting for dead-letter records

Kafka 2.7.1 fixes 45 issues, including a prematurely completed errant-record future. The Connect API promises that completion represents a write to the error topic, with failures propagated to the caller.

Reporting a failed record is another write operation, with its own completion condition. Kafka 2.7.1, released May 10, fixes a Connect bug where the errant-record reporter always returned a completed future before the record had necessarily reached the dead-letter topic. The release notes identify it as KAFKA-10658 among 45 repairs since 2.7.0.

The distinction matters because a connector can use that future to decide when reporting has succeeded. An immediately completed handle says more than the fact that a reporting call was accepted. It can cause the application to proceed under a delivery assumption that has not yet become true.

I find Kafka's non-destructive consumption model useful because a consumer controls its position in retained history. That makes offset advancement an explicit statement about work already accounted for. An error path has to preserve that meaning when a record is sent somewhere other than the ordinary destination.

The API promises observable completion

The ErrantRecordReporter interface at 2.7.1 documents an asynchronous report method. It accepts the problematic sink record and the error describing the problem. Its result is a Future<Void> that the task can use to wait until reporting finishes.

Calling get must block until the record is written or throw the exception that occurred while sending it. That is a contract about an operation's outcome. A caller is not merely obtaining an acknowledgement that another object has been placed in a local queue.

The interface requires both the record and its associated error to be non-null inputs.

The interface also states that Connect writes reported records to the error topic before calling the sink task's preCommit method, and therefore before committing consumer offsets. Connector authors can use the returned future when they need stronger guarantees within their own task logic. Both uses depend on the future representing the real reporting work.

That makes the repair relevant even when the final consumer-offset commit already has a framework-level ordering guarantee. A connector may observe reporting completion before that point. Its own actions need the same distinction between pending work and finished work that the framework needs around offset advancement.

The worker retains the real reporting result

The worker implementation calls the retry-and-tolerance operator for the failed record and keeps the returned future. If the future is unfinished, the worker adds it to a list of outstanding work. The same future is returned to the connector.

This preserves one completion account for both observers. The task can wait on the result it received. The worker can later await its list of unfinished reports. It does not need to invent an already-successful placeholder for an operation whose producer requests are still pending.

The awaitAllFutures method removes futures from that list and calls get on each one. An interrupted or failed wait becomes a ConnectException. An unsuccessful error-topic write consequently remains a failure in the worker's processing path, rather than silently becoming a reason to discard the input record.

The wrapper for producer futures defines completion through all the contained futures. Its isDone checks every underlying result, and its get waits on them. Cancellation is unsupported. These methods give the returned handle behavior tied to the underlying sends, instead of reporting completion merely because the reporting method has returned.

The reported record also has an identity

The worker prefers the original consumer record when the supplied object is an internal sink record. That preserves the pre-transformation bytes for the dead-letter topic. A task may instead submit a modified or newly constructed sink record. In that case, the worker serializes its key and value through the configured converters and converts its headers.

It then constructs a consumer record using the sink record's topic, partition and offset information. This fallback is explicitly different from reporting the original input. The distinction is useful during investigation because the dead-letter contents may represent either original bytes or the record a connector supplied after modification.

I regard completion and record identity as related requirements. An error report is useful when it identifies the failed work and has actually reached the destination expected to retain it. A completed future with no completed write undermines the latter, even if the record description itself is correct.

Kafka 2.7.1 repairs that completion behavior within a broader maintenance release. The Connect interface still provides asynchronous reporting, and the worker still coordinates it with offset commit preparation. The repaired future lets connector code observe the same reporting operation that the framework is waiting to finish.