This page is meant as a template for writing a KIP. To create a KIP choose Tools->Copy on this page and modify with your content and replace the heading with the next KIP number and a description of your issue. Replace anything in italics with your own description.
Current state: Under Discussion
Discussion thread: here [Change the link from the KIP proposal email archive to your own email thread]
JIRA: here [Change the link from KAFKA-1 to your own ticket]
Please keep the discussion on the mailing list rather than commenting on the wiki (wiki discussions get unwieldy fast).
KIP inspired by Michelin kstreamplify and coauthored by Damien Gasparina, Loic Greffier and Sebastien Viale.
Kafka Streams does have multiple exception handlers to handle issues while processing messages. Each handler proposes two options: either to log the faulty message and continue processing, or to fail and stop KafkaStreams, the default value.
Both out-of-the-box implementations are not suitable for most use-cases as stopping Kafka Streams due to a single faulty message might be problematic and logging and skipping is at high risk of being missed if the user does not actually check the logs.
Most applications tend to rely on the Dead Letter Queue (DLQ) pattern: in case of an issue, the faulty message that can not be processed is stored in a separate topic. This approach has many advantages:
DLQ pattern is becoming a standard, it is already available out of the box in Kafka Connect. Many applications I worked with already implemented this pattern in Kafka Streams. Including a DLQ feature directly in Kafka Streams would allow users to configure production-ready error handlers without having to write custom code.
To allow users to send a record in the Deal letter queue, a new attribute "deadLetterQueueRecord'' will be added in each exception handler's responses. If this attribute is set, KafkaStreams will send the provided record to Kafka.
A new configuration will be added: errors.deadletterqueue.topic.name. When set, this configuration indicates the default exception handler implementation to build a Dead letter queue record during the error handling.
In order to build a valid Dead letter queue payload, some additional information needs to be captured and forwarded in the processor context: the source message raw key and value.
Storing the raw key and the raw value allows us to send those raw information in the DLQ topic without having to infer the right serializer. All metadata, e.g. Exceptions, StackTrace, topic, partitions and offset would be provided in the record headers by default.
Additionally, the ProcessorContext would need to be available in each ExceptionHandler. It is currently not available in the ProductionExceptionHandler, thus the handle method will need to be overloaded to provide the context and a default implementation needs to be provided to ensure backward compatibility.
If the default values are not suitable for an application, developers could still reimplement the required exception handlers to build custom DLQ records.
This proposal is to:
Key | Key of the input message, null if triggered by punctuate |
Value |
|
Header: exception | Name of the thrown exception |
Header: stacktrace | Stacktrace of the thrown exception |
Header: message | Thrown exception message |
Header: topic | Source input topic, null if triggered by punctuate |
Header: partition | Source input partition, null if triggered by punctuate |
Header: offset | Source input offset, null if triggered by punctuate |
public static final String ERRORS_DEADLETTERQUEUE_TOPIC_NAME_CONFIG = "errors.deadletterqueue.topic.name"; .define(ERRORS_DEADLETTERQUEUE_TOPIC_NAME_CONFIG, // required with no default value Type.STRING, null, /* default */ Importance.HIGH, ERRORS_DEADLETTERQUEUE_TOPIC_NAME_DOC) |
public interface ProcessorContext {
. . .
/**
* Return the non-deserialized byte[] of the input message key if the context has been triggered by a message.
*
* <p> If this method is invoked within a {@link Punctuator#punctuate(long)
* punctuation callback}, or while processing a record that was forwarded by a punctuation
* callback, it will return null.
*
* <p> If this method is invoked in a sub-topology due to a repartition, the returned key would be one sent
* to the repartition topic.
*
* @return the raw byte of the key of the source message
*/
byte[] source_raw_key();
/**
* Return the non-deserialized byte[] of the input message value if the context has been triggered by a message.
*
* <p> If this method is invoked within a {@link Punctuator#punctuate(long)
* punctuation callback}, or while processing a record that was forwarded by a punctuation
* callback, it will return null.
*
* <p> If this method is invoked in a sub-topology due to a repartition, the returned value would be one sent
* to the repartition topic.
*
* @return the raw byte of the value of the source message
*/
byte[] source_raw_value();
. . .
} |
Changes:
/**
* Interface that specifies how an exception when attempting to produce a result to
* Kafka should be handled.
*/
public interface ProductionExceptionHandler extends Configurable {
/**
* Inspect a record that we attempted to produce, and the exception that resulted
* from attempting to produce it and determine whether or not to continue processing.
*
* @param record The record that failed to produce
* @param exception The exception that occurred during production
* @deprecated Please use the ProductionExceptionHandlerResponse.handle(record, exception, context)
*/
@Deprecated
ProductionExceptionHandlerResponse handle(final ProducerRecord<byte[], byte[]> record,
final Exception exception);
/**
* Inspect a record that we attempted to produce, and the exception that resulted
* from attempting to produce it and determine whether or not to continue processing.
*
* @param record The record that failed to produce
* @param exception The exception that occurred during production
* @param context Processor context
*/
@SuppressWarnings("deprecation")
default ProductionExceptionHandlerResponse handle(final ProducerRecord<byte[], byte[]> record,
final Exception exception,
final ProcessorContext context) {
return handle(record, exception);
}
/**
* Handles serialization exception and determine if the process should continue. The default implementation is to
* fail the process.
*
* @param record the record that failed to serialize
* @param exception the exception that occurred during serialization
* @deprecated Please use the handleSerializationException(record, exception, context)
*/
@Deprecated
default ProductionExceptionHandlerResponse handleSerializationException(final ProducerRecord record,
final Exception exception) {
return ProductionExceptionHandlerResponse.FAIL;
}
/**
* Handles serialization exception and determine if the process should continue. The default implementation is to
* fail the process.
*
* @param record the record that failed to serialize
* @param exception the exception that occurred during serialization
* @param context Processor context
*/
@SuppressWarnings("deprecation")
default ProductionExceptionHandlerResponse handleSerializationException(final ProducerRecord record,
final Exception exception,
final ProcessorContext context
) {
return handleSerializationException(record, exception);
}
enum ProductionExceptionHandlerResponse {
/* continue processing */
CONTINUE(0, "CONTINUE"),
/* fail processing */
FAIL(1, "FAIL");
/**
* an english description of the api--this is for debugging and can change
*/
public final String name;
/**
* the permanent and immutable id of an API--this can't change ever
*/
public final int id;
public ProducerRecord<byte[], byte[]> deadLetterQueueRecord;
ProductionExceptionHandlerResponse(final int id,
final String name) {
this.id = id;
this.name = name;
}
public ProductionExceptionHandlerResponse withDeadLetterQueueRecord(ProducerRecord<byte[], byte[]> deadLetterQueueRecord) {
this.deadLetterQueueRecord = deadLetterQueueRecord;
return this;
}
}
}
|
Changes:
public interface DeserializationExceptionHandler extends Configurable {
/**
* Inspect a record and the exception received.
* <p>
* Note, that the passed in {@link ProcessorContext} only allows to access metadata like the task ID.
* However, it cannot be used to emit records via {@link ProcessorContext#forward(Object, Object)};
* calling {@code forward()} (and some other methods) would result in a runtime exception.
*
* @param context processor context
* @param record record that failed deserialization
* @param exception the actual exception
*/
@SuppressWarnings("deprecation") // Old PAPI. Needs to be migrated.
DeserializationHandlerResponse handle(final ProcessorContext context,
final ConsumerRecord<byte[], byte[]> record,
final Exception exception);
/**
* Enumeration that describes the response from the exception handler.
*/
enum DeserializationHandlerResponse {
/* continue with processing */
CONTINUE(0, "CONTINUE"),
/* fail the processing and stop */
FAIL(1, "FAIL");
/** an english description of the api--this is for debugging and can change */
public final String name;
/** the permanent and immutable id of an API--this can't change ever */
public final int id;
public ProducerRecord<byte[], byte[]> deadLetterQueueRecord;
DeserializationHandlerResponse(final int id, final String name) {
this.id = id;
this.name = name;
}
public DeserializationHandlerResponse withDeadLetterQueueRecord(ProducerRecord<byte[], byte[]> deadLetterQueueRecord) {
this.deadLetterQueueRecord = deadLetterQueueRecord;
return this;
}
}
}
|
With KIP-1033, a similar behavior would be added to the potential new ProcessExceptionHandler: adding adding a public ProducerRecord<byte[], byte[]> deadLetterQueueRecord; attribute in the ProcessExceptionHandlerResponse
To build a valid record for the DeadLetterQueue, the ProductionExceptionHandler.handle method needs to have access to the ProcessorContext. To ensure backward compatibility, the previous interface would be deprecated and the default implementation of the new interface would invoke the previous one.
All other changes are backward compatible and should not impact existing applications.