Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Table of Contents

Status

Current state: AcceptedAdopted

Discussion thread: here

JIRA: here

...

A new configuration will be added: errors.dead.letter.deadletterqueuequeue.topic.name. When set, this configuration indicates the default exception handler implementation to build a Dead letter queue record during the error handling.

...

  1. Add a new getter and setter to configure dead letter queue records in the DeserializationExceptionHandler, ProductionExceptionHandler and ProcessExceptionHandler.
  2. Add a new attribute public static final String ERRORS_DEAD_DEADLETTERQUEUELETTER_QUEUE_TOPIC_NAME_CONFIG = "errors.dead.deadletterqueueletter.queue.topic.name".
  3. Change the existing exception handler to produce a DeadLetterQueue record if the parameter errors.dead.letter.deadletterqueuequeue.topic.name is set.
  4. If the DeadLetterQueue record can not be sent to Apache Kafka, the exception would be sent to the Kafka Streams uncaughtExceptionHandler.

...

The DLQ topic name is set through the configuration ERRORS_DEAD_LETTER_DEADLETTERQUEUEQUEUE_TOPIC_NAME_CONFIG = " errors.dead.letter.deadletterqueuequeue.topic.name". Users can override the default behavior by implementing custom exception handlers to implement a different DLQ topic strategy if required.

...

Changes:

  • Adding the errors.dead.letter.deadletterqueuequeue.topic.name configuration. This configuration is only modifying the behavior of the out of the box exceptions handlers and would have no effect if a custom exception handlers is implemented, for example:
    • if errors.dead.letter.deadletterqueuequeue.topic.name=null (default), then no records are sent to any dead letter queue
    • if errors.dead.letter.deadletterqueuequeue.topic.name is set, exceptions happening during processing, production or deserialization will result in the raw source messages that trigger the topology to be send to the DLQ topic. The processing might or might not continue depending of the configuration of the processing.exception.handler, default.production.exception.handler and default.deserialization.exception.handler configurations
Code Block
languagejava
public static final String ERRORS_DEAD_LETTER_DEADLETTERQUEUEQUEUE_TOPIC_NAME_CONFIG = "errors.dead.deadletterqueueletter.queue.topic.name";

.define(ERRORS_DEAD_DEADLETTERQUEUELETTER_QUEUE_TOPIC_NAME_CONFIG,
       Type.STRING,
       null, /* default */
       Importance.HIGHMEDIUM,
       ERRORS_DEAD_DEADLETTERQUEUELETTER_QUEUE_TOPIC_NAME_DOC)


If the user implements a custom exception handler, it is up to the custom handler to build DLQ records to send, in this case, the the errors.dead.letter.deadletterqueuequeue.topic.name configuration has no impact.

Code Block
languagejava
    @Override
    public ProcessingHandlerResponseResponse handleError(final ErrorHandlerContext context, final Record<?, ?> record, final Exception exception) {         
        List<ProducerRecord<byte[], byte[]>> records = Collections.singletonList(new ProducerRecord<>("app-dlq", "Hello".getBytes(StandardCharsets.UTF_8), "World".getBytes(StandardCharsets.UTF_8)));

        return ProcessingExceptionResponseProcessingExceptionHandler.Response.continueProcessingresume(records);  
    }

ProductionExceptionHandler.java

Changes:

  • Adding a ProductionExceptionResponse Response nested class,that contains a ProductionExceptionHandlerResponse Result indicating whether to continue processing, fail, or retry and the list of records to be sent to the dead letter queue topic
  • Deprecating the handler() and handlerSerializationException() methods and adding two new methods: handlerError() and handleSerializationException(). The return type is the ProductionExceptionResponseResponse
  • Deprecating the ProductionExceptionHandlerResponse enum and create a new Result enum. The result enum contains the 3 fields RESUME, FAIL and RETRY and a deprecated method to convert the old enum to the new one 
  • As the As the ErrorHandlerContext does not provide the sourceKey/Value in the handle method to limit the memory impact, the Dead Letter Queue record would only contains metadata. handleSerializationException is not impacted.
Code Block
languagejava
public interface ProductionExceptionHandler extends Configurable {
   ...    /**
     

    @Deprecated
    default ProductionExceptionHandlerResponse handle(final ErrorHandlerContext context,
                                                      final ProducerRecord<byte[], byte[]> record,
                                                      final Exception exception) {
        throw new UnsupportedOperationException(); ...
    }

    /**
     * Inspect a record that we attempted to produce, and the exception that resulted
     * from attempting to produce it and determine to continue or stop processing.
     *
     * @param context
     *     The error handler context metadata.
     * @param record
     *     The record that failed to produce.
     * @param exception
     *     The exception that occurred during production.
     *
     * @return a {@link ProductionExceptionResponseResponse} object
     */
    default ProductionExceptionResponseResponse handleError(final ErrorHandlerContext context,
                                                    final ProducerRecord<byte[], byte[]> record,
                                                    final Exception exception) {                          
          return new ProductionExceptionResponseResponse(Result.from(handle(context, record, exception)), Collections.emptyList());
     }

    @Deprecated
    default ProductionExceptionHandlerResponse handleSerializationException(final ProducerRecord record,
                                                                            final Exception exception) {
        return ProductionExceptionHandler..ProductionExceptionHandlerResponse.FAIL;
    }

    /**
     * Handles serialization exception and determine if the process should continue. The default implementation is to
     * fail the process.
     *
     * @param context
     *     The error handler context metadata.
     * @param record
     *     The record that failed to serialize.
     * @param exception
     *     The exception that occurred during serialization.
     * @param origin
     *     The origin of the serialization exception.
     *
     * @return a {@link ProductionExceptionResponseResponse} object
     */     
    default ProductionExceptionResponseResponse handleSerializationError(final ErrorHandlerContext context,
                                                                 final ProducerRecord record,
                                                                 final Exception exception,
                                                                 final SerializationExceptionOrigin origin) {                  
        return new ProductionExceptionResponseResponse(Result.from(handleSerializationException(context, record, exception, origin)), Collections.emptyList());
    }   
      }
 
    ...

    /**@Deprecated
    enum ProductionExceptionHandlerResponse {
  * Represents the result of...
 handling a production exception.}

        /** <p>
     * Enumeration Thethat {@codedescribes Response}the classresponse encapsulatesfrom athe {@link ProductionExceptionHandlerResponse},exception handler.
     */
 indicating whether processing shouldenum continueResult or{
 fail, along with an optional list of
 /** Resume processing.
  * {@link ProducerRecord} instances to be sent to*
 a dead letter queue.
     * </p>
<p> For this case, output */
records which could not classbe ProductionExceptionResponsewritten {

successfully are lost.
      private ProductionExceptionHandlerResponse productionExceptionHandlerResponse;

 * Use this option only if you private List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords;

can tolerate data loss.
         */**
        RESUME(0, "RESUME"),
    * Constructs a new {@code ProductionExceptionResponse} object/** Fail processing.
         *
         * @param<p> productionExceptionHandlerResponseKafka theStreams responsewill indicatingraise whetheran processingexception shouldand continuethe or fail;{@code StreamsThread} will fail.
         * No offsets (for {@link org.apache.kafka.streams.StreamsConfig#AT_LEAST_ONCE at-least-once}) or transactions
         * (for {@link org.apache.kafka.streams.StreamsConfig#EXACTLY_ONCE_V2 exactly-once}) will be committed.
         */
     must not be {@code null}. FAIL(1, "FAIL"),
         /** @paramRetry deadLetterQueueRecordsthe failed operation.
  the list of records to be sent to*
 the dead letter queue; may be {@code null}.
 * <p> Retrying might imply that a  */
        private ProductionExceptionResponse(final ProductionExceptionHandlerResponse productionExceptionHandlerResponse,{@link TaskCorruptedException} exception is thrown, and that the retry
         * is started from the last committed offset.
         *
         * <p> <b>NOTE:</b> {@code RETRY} is only a valid return final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords) {value for
         *   this.productionExceptionHandlerResponse = productionExceptionHandlerResponse;{@link org.apache.kafka.common.errors.RetriableException retriable exceptions}.
         * If  this.deadLetterQueueRecords = deadLetterQueueRecords;
        }

        /**{@code RETRY} is returned for a non-retriable exception it will be interpreted as {@link #FAIL}.
         */
 Creates a {@code ProductionExceptionResponse} indicating that processing should fail.RETRY(2, "RETRY");

         /**
         * @paramAn deadLetterQueueRecordsenglish thedescription listfor ofthe recordsused tooption. beThis sentis tofor thedebugging dead letter queue;only and may be {@code null}change.
         */
 @return a {@code ProductionExceptionResponse}   with apublic {@link DeserializationExceptionHandler.DeserializationHandlerResponse#FAIL} status.final String name;

         /**/
        public * staticThe ProductionExceptionResponsepermanent failProcessing(final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords) {
      and immutable id for the used option. This can't change ever.
      return new ProductionExceptionResponse(ProductionExceptionHandlerResponse.FAIL, deadLetterQueueRecords);
 */
        public final int }id;

        /**
      Result(final int id, final String name) {
   * Creates a {@code ProductionExceptionResponse} indicating that processing should failthis.
id = id;
       *
     this.name =  name;
 * @return a {@code ProductionExceptionResponse} with a {@link DeserializationExceptionHandler.DeserializationHandlerResponse#FAIL} status.}

         /**/
         * Converts publicthe staticdeprecated ProductionExceptionResponseenum failProcessing() {
    ProductionExceptionHandlerResponse into the new Result enum.
        return failProcessing(Collections.emptyList()); *
        }

 * @param value the old ProductionExceptionHandlerResponse enum /**value
         * Creates@return a {@code@link ProductionExceptionResponseProductionExceptionHandler.Result} indicating that processing should continue.enum value
         *
 @throws IllegalArgumentException       * @param deadLetterQueueRecords if the listprovided ofvalue recordsdoes tonot be sent to the dead letter queue; may be {@code null}.
         * @return a {@code ProductionExceptionResponse} with amap to a valid {@link DeserializationExceptionHandlerProductionExceptionHandler.DeserializationHandlerResponse#CONTINUEResult} status.
         */
        publicprivate static ProductionExceptionResponseProductionExceptionHandler.Result continueProcessingfrom(final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecordsProductionExceptionHandlerResponse value) {
            returnswitch new ProductionExceptionResponse(ProductionExceptionHandlerResponse.CONTINUE, deadLetterQueueRecords);(value) {
        }

        /**
case FAIL:
            * Creates a {@code ProductionExceptionResponse} indicating that processing shouldreturn continueResult.FAIL;
               * case CONTINUE:
          *   @return a {@code ProductionExceptionResponse} with a {@link DeserializationExceptionHandler.DeserializationHandlerResponse#CONTINUE} status.
 return Result.RESUME;
            */
    case RETRY:
    public static ProductionExceptionResponse continueProcessing() {
            return continueProcessing(Collections.emptyList())Result.RETRY;
            }

        /**default:
         * Creates a {@code ProductionExceptionResponse} indicating that processing should retry.
  throw new IllegalArgumentException("No Result enum found for *
old value: " + value);
     * @return a {@code ProductionExceptionResponse} with a {@link DeserializationExceptionHandler.DeserializationHandlerResponse#CONTINUE} status.
         */}
    }

    public/**
 static ProductionExceptionResponse retryProcessing() {
 * Represents the result of handling a production exception.
   return new ProductionExceptionResponse(ProductionExceptionHandlerResponse.RETRY, Collections.emptyList()); * <p>
     * The {@code Response}

 class encapsulates a {@link ProductionExceptionHandlerResponse},
   /**
  * indicating whether processing should continue or *fail, Retrievesalong thewith productionan exceptionoptional handler response.
         *
    list of
     * @return the {@link ProductionExceptionHandlerResponseProducerRecord} instances indicatingto be whethersent processingto shoulda continuedead orletter failqueue.
     *    *</p>
     */
   public ProductionExceptionHandlerResponseclass response()Response {

        private Result result;

  return productionExceptionHandlerResponse;
     private List<ProducerRecord<byte[],  }byte[]>> deadLetterQueueRecords;

        /**
         * RetrievesConstructs ana unmodifiablenew list{@code of records to be sent to the dead letter queue.Response} object.
         *
         * <p>
         * If the list is {@code null}, an empty list is returned.
 @param result the result indicating whether processing should continue or fail;
         *            * </p>
         *
         * @return an unmodifiablemust listnot ofbe {@link@code ProducerRecordnull} instances.
         * @param deadLetterQueueRecords    the list of records to be sent forto the dead letter queue,; ormay anbe empty list if no records are available{@code null}.
         */
        publicprivate Response(final Result result,
                         final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords() {
            this.result = result;
 if (deadLetterQueueRecords == null) {
       this.deadLetterQueueRecords = deadLetterQueueRecords;
       return Collections.emptyList(); }

        /**
     }
    * Creates a {@code Response} indicating that processing returnshould Collectionsfail.unmodifiableList(deadLetterQueueRecords);
         }*
    }
      ...  
}

DeserializationExceptionHandler.java

Changes:

  • Adding a DeserializationExceptionResponse nested class,that contains a DeserializationHandlerResponse indicating whether to continue processing or fail, and the list of records to be sent to the dead letter queue topic
  • Deprecating the handler() method and adding a new method: handlerError(). The return type is the DeserializationExceptionResponse
Code Block
languagejava
public interface DeserializationExceptionHandler extends Configurable {

   ...      
  
    @Deprecated
    default DeserializationHandlerResponse handle(final ErrorHandlerContext context,
* @param deadLetterQueueRecords the list of records to be sent to the dead letter queue; may be {@code null}.
         * @return a {@code Response} with a {@link ProductionExceptionHandler.Result#FAIL} status.
         */
        public static Response fail(final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords) {
            return new Response(Result.FAIL, deadLetterQueueRecords);
        }

      final ConsumerRecord<byte[], byte[]> record, /**
         * Creates a {@code Response} indicating that processing should fail.
         *
         * @return a {@code Response} with a {@link ProductionExceptionHandler.Result#FAIL} status.
      final Exception exception) {*/
        throw new UnsupportedOperationException();
  public static }

    /**
Response fail() {
       * Inspects a record and the exception received during deserialization.
return fail(Collections.emptyList());
        *}

      * @param context/**
     *     Error* handlerCreates context.
a {@code Response} indicating that *processing @paramshould recordcontinue.
     *    *
 Record that failed deserialization.
     * @param exception
deadLetterQueueRecords the list of records *to be sent to the Thedead actual exception.
     *
letter queue; may be {@code null}.
         * @return a {@code Response} with a {@link DeserializationExceptionResponseProductionExceptionHandler.Result#RESUME} object
status.
         */
    default   DeserializationExceptionResponse handleError(finalpublic ErrorHandlerContextstatic context,Response resume(final ConsumerRecord<byteList<ProducerRecord<byte[], byte[]> record, final Exception exception) {        >> deadLetterQueueRecords) {
            return new DeserializationExceptionResponse(handle(context, record, exception), Collections.emptyList());
    }     Response(Result.RESUME, deadLetterQueueRecords);
        }

    ...

    /**
     * Represents the result of* handlingCreates a deserialization exception.
{@code Response} indicating that processing should continue.
         *
    <p>
     * The@return a {@code Response} class encapsulateswith a {@link ProcessingExceptionHandlerProductionExceptionHandler.ProcessingHandlerResponseResult#RESUME}, status.
     * indicating whether processing should*/
 continue or fail, along with an optional listpublic of
static Response resume() {
  * {@link ProducerRecord} instances to be sent to a dead letterreturn queue.resume(Collections.emptyList());
     * </p>
  }

   */
    class DeserializationExceptionResponse {

 /**
        private DeserializationHandlerResponse deserializationHandlerResponse;

        private List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords;

* Creates a {@code Response} indicating that processing should retry.
         /**
         * Constructs@return a new {@code Response} with a {@code@link DeserializationExceptionResponseProductionExceptionHandler.Result#RETRY} objectstatus.
         */
        public *static @paramResponse deserializationHandlerResponse the response indicating whether processing should continue or fail;
retry() {
            return new Response(Result.RETRY, Collections.emptyList());
  *      }

        /**
         * Retrieves the production exception handler result.
     must not be {@code null}.*
         * @param@return deadLetterQueueRecordsthe {@link Result} indicating thewhether listprocessing ofshould recordscontinue, tofail beor sentretry.
 to the dead letter queue; may be {@code null}. */
        public */
  Result result() {
      private DeserializationExceptionResponse(final DeserializationHandlerResponse deserializationHandlerResponse,
   return result;
        }

        /**
         * Retrieves an unmodifiable list of records to be sent to the dead letter queue.
      final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords) { * <p>
         * If the this.deserializationHandlerResponselist = deserializationHandlerResponse;
            this.deadLetterQueueRecords = deadLetterQueueRecords;
is {@code null}, an empty list is returned.
         * }</p>

         /**
         * Creates a @return an unmodifiable list of {@code@link DeserializationExceptionResponseProducerRecord} indicatinginstances
 that processing should fail.
     *    *
     for the dead letter *queue, @paramor deadLetterQueueRecordsan theempty list if ofno records to be sent to the dead letter queue; may be {@code null}.are available.
         */
        public * @return a {@code DeserializationExceptionResponse} with a {@link DeserializationHandlerResponse#FAIL} status.
  List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords() {
       */
     if (deadLetterQueueRecords == public static DeserializationExceptionResponse failProcessing(final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords) {
null) {
                return new DeserializationExceptionResponse(DeserializationHandlerResponse.FAIL, deadLetterQueueRecordsCollections.emptyList();
        }

        /**
 }
        * Creates a {@code DeserializationExceptionResponse} indicating that processing should fail.return Collections.unmodifiableList(deadLetterQueueRecords);
        }
    }
}

DeserializationExceptionHandler.java

Changes:

  • Adding a Response nested class,that contains a Result indicating whether to continue processing or fail, and the list of records to be sent to the dead letter queue topic
  • Deprecating the handler() method and adding a new method: handlerError(). The return type is the Response
  • Deprecating the DeserializationHandlerResponse enum and create a new Result enum. The result enum contains the 2 fields RESUME and Fail and a deprecated method to convert the old enum to the new one 
Code Block
languagejava
public interface DeserializationExceptionHandler extends Configurable {

   ...      
  
    @Deprecated
    default DeserializationHandlerResponse handle(final ErrorHandlerContext context,
                                                  final ConsumerRecord<byte[], byte[]> record,
                                                  final Exception exception) {
        ...
    }

    /**
     * Inspects a record and the exception received during deserialization.
     *
     * @param context
     *     Error handler context.
     * @param record
     *     Record that failed deserialization.
     * @param exception
     *     The actual exception.
     *
     * @return a {@link Response} object
     */
    default Response handleError(final ErrorHandlerContext context, final ConsumerRecord<byte[], byte[]> record, final Exception exception) {           
        return new Response(Result.from(handle(context, record, exception)), Collections.emptyList());
    }     

    @Deprecated
    enum DeserializationHandlerResponse {
        ...
    }

    /**
     * Enumeration that describes the response from the exception handler.
     */
    enum Result {
        /** Continue processing. */
        RESUME(0, "RESUME"),
        /** Fail processing. */
        FAIL(1, "FAIL");

        /**
         * An english description for the used option. This is for debugging only and may change.
         */
        public final String name;

        /**
         * The permanent and immutable id for the used option. This can't change ever.
         */
        public final int id;

        Result(final int id, final String name) {
            this.id = id;
            this.name = name;
        }

        /**
         * Converts the deprecated enum DeserializationHandlerResponse into the new Result enum.
         *
         * @param value the old DeserializationHandlerResponse enum value
         * @return a {@link Result} enum value
         * @throws IllegalArgumentException if the provided value does not map to a valid {@link Result}
         */
        private static DeserializationExceptionHandler.Result from(final DeserializationHandlerResponse value) {
            switch (value) {
                case FAIL:
                    return Result.FAIL;
                case CONTINUE:
                    return Result.RESUME;
                default:
                    throw new IllegalArgumentException("No Result enum found for old value: " + value);
            }
        }
    }

    /**
     * Represents the result of handling a deserialization exception.
     * <p>
     * The {@code Response} class encapsulates a {@link ProcessingExceptionHandler.Result},
     * indicating whether processing should continue or fail, along with an optional list of
     * {@link ProducerRecord} instances to be sent to a dead letter queue.
     * </p>
     */
    class Response {

        private Result result;

        private List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords;

        /**
         * Constructs a new {@code DeserializationExceptionResponse} object.
         *
         * @param result the result indicating whether processing should continue or fail;
         *                                  must not be {@code null}.
         * @param deadLetterQueueRecords    the list of records to be sent to the dead letter queue; may be {@code null}.
         */
        private Response(final Result result,
                         final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords) {
            this.result = result;
            this.deadLetterQueueRecords = deadLetterQueueRecords;
        }

        /**
         * Creates a {@code Response} indicating that processing should fail.
         *
         * @param deadLetterQueueRecords the list of records to be sent to the dead letter queue; may be {@code null}.
         * @return a {@code Response} with a {@link DeserializationExceptionHandler.Result#FAIL} status.
         */
        public static Response fail(final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords) {
            return new Response(Result.FAIL, deadLetterQueueRecords);
        }

        /**
         * Creates a {@code Response} indicating that processing should fail.
         *
         * @return a {@code Response} with a {@link DeserializationExceptionHandler.Result#FAIL} status.
         */
        public static Response fail() {
            return fail(Collections.emptyList());
        }

        /**
         * Creates a {@code Response} indicating that processing should continue.
         *
         * @param deadLetterQueueRecords the list of records to be sent to the dead letter queue; may be {@code null}.
         * @return a {@code Response} with a {@link DeserializationExceptionHandler.Result#RESUME} status.
         */
        public static Response resume(final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords) {
            return new Response(Result.RESUME, deadLetterQueueRecords);
        }

        /**
         * Creates a {@code Response} indicating that processing should continue.
         *
         * @return a {@code Response} with a {@link DeserializationHandlerResponse#CONTINUE} status.
         */
        public static Response resume() {
            return resume(Collections.emptyList());
        }

        /**
         * Retrieves the deserialization handler result.
         *
         * @return the {@link Result} indicating whether processing should continue or fail.
         */
        public Result result() {
            return result;
        }

        /**
         * Retrieves an unmodifiable list of records to be sent to the dead letter queue.
         * <p>
         * If the list is {@code null}, an empty list is returned.
         * </p>
         *
         * @return an unmodifiable list of {@link ProducerRecord} instances
         *         for the dead letter queue, or an empty list if no records are available.
         */
        public List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords() {
            if (deadLetterQueueRecords == null) {
                return Collections.emptyList();
            }
            return Collections.unmodifiableList(deadLetterQueueRecords); *
         * @return a {@code DeserializationExceptionResponse} with a {@link DeserializationHandlerResponse#FAIL} status.
         */}
        public static DeserializationExceptionResponse failProcessing() {
        }
}

ProcessingExceptionHandler.java

Changes:

  • Adding a Response nested class,that contains a Result indicating whether to continue processing or fail, and the list of records to be sent to the dead letter queue topic
  • Deprecating the handler() method and adding a new method: handlerError(). The return type is the Response
  • Deprecating the ProcessingHandlerResponse enum and create a new Result enum. The result enum contains the 2 fields RESUME and Fail and a deprecated method to convert the old enum to the new one 
Code Block
languagejava
public interface ProcessingExceptionHandler extends Configurable {

   ...   
    
    @Deprecated
    default ProcessingHandlerResponse handle(final ErrorHandlerContext context, final Record<?, ?> record, final Exception exception){
       ...
    };

    /**
    return failProcessing(Collections.emptyList());
        }

        /**
         * Creates a {@code DeserializationExceptionResponse} indicating that processing should continue.
         *
         * @param deadLetterQueueRecords the list of records to be sent to the dead letter queue; may be {@code null}.
         * @returnInspects a {@coderecord DeserializationExceptionResponse}and withthe aexception {@linkreceived DeserializationHandlerResponse#CONTINUE}during statusprocessing.
         */
     * @param context
 public static DeserializationExceptionResponse continueProcessing(final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords) {
 *     Processing context   metadata.
   return new DeserializationExceptionResponse(DeserializationHandlerResponse.CONTINUE, deadLetterQueueRecords);
   * @param record
     * }

    Record where the exception /**occurred.
     * @param exception
   * Creates a* {@code DeserializationExceptionResponse} indicating that processingThe shouldactual continueexception.
     *
     *
 @return a {@link Response} object
     */
    @returndefault a {@code DeserializationExceptionResponse} with a {@link DeserializationHandlerResponse#CONTINUE} status.
         */
        public static DeserializationExceptionResponse continueProcessing() {Response handleError(final ErrorHandlerContext context, final Record<?, ?> record, final Exception exception) {                          
        return new Response(ProcessingExceptionHandler.Result.from(handle(context,  return continueProcessing(record, exception)), Collections.emptyList());
    }   

    }@Deprecated

    enum ProcessingHandlerResponse   /**{
       ...
  * Retrieves the deserialization handler response.} 

    /**
     *
 Enumeration that describes the response from the exception *handler.
 @return the {@link DeserializationHandlerResponse} indicating*/
 whether processing should continueenum orResult fail.{
        /** Resume processing. */
        public DeserializationHandlerResponse response() {RESUME(1, "RESUME"),
        /** Fail   return deserializationHandlerResponse;
processing. */
         }FAIL(2, "FAIL");

        /**
         * RetrievesAn anenglish unmodifiable list of records to be sent to the dead letter queue.
         * <p>description for the used option. This is for debugging only and may change.
         */
 If the list is {@code null}, an emptypublic listfinal is returned.String name;

         * </p>/**
         *
 The permanent and immutable id    * @return an unmodifiable list of {@link ProducerRecord} instancesfor the used option. This can't change ever.
         */
        public final int id;

 for the dead letter queue, or an emptyResult(final listint ifid, nofinal recordsString arename) available.{
         */
   this.id = id;
   public List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords() {
     this.name = name;
     if (deadLetterQueueRecords == null) {
 }

        /**
         return Collections.emptyList();
        * Converts the deprecated enum ProcessingHandlerResponse into the new Result enum.
    }
     *
       return Collections.unmodifiableList(deadLetterQueueRecords);
        } * @param value the old DeserializationHandlerResponse enum value
    }   

}

ProcessingExceptionHandler.java

Changes:

  • Adding a ProcessingExceptionResponse nested class,that contains a ProcessingHandlerResponse indicating whether to continue processing or fail, and the list of records to be sent to the dead letter queue topic
  • Deprecating the handler() method and adding a new method: handlerError(). The return type is the ProcessingExceptionResponse
Code Block
languagejava
public interface ProcessingExceptionHandler extends Configurable {

   ...   
    
    @Deprecated
    default ProcessingHandlerResponse handle(final ErrorHandlerContext context, final Record<?, ?> record, final Exception exception){
    * @return a {@link ProcessingExceptionHandler.Result} enum value
         * @throws IllegalArgumentException if the provided value does not map to a valid {@link ProcessingExceptionHandler.Result}
         */
        private static ProcessingExceptionHandler.Result from(final ProcessingHandlerResponse value) {
           throw newswitch UnsupportedOperationException(value); {
      };

    /**
     * Inspectscase aFAIL:
 record and the exception received during processing.
     *
     * @param context
 return Result.FAIL;
   *     Processing context metadata.
     * @paramcase recordCONTINUE:
     *     Record where the exception occurred.
     * @param exceptionreturn Result.RESUME;
     *     The actual exception.
    default:
 *
     * @return a {@link ProcessingExceptionResponse} object
     */
    defaultthrow ProcessingExceptionResponsenew handleErrorIllegalArgumentException(final"No ErrorHandlerContextResult context,enum finalfound Record<?, ?> record, final Exception exception) {        for old value: " + value);
             }
       return new ProcessingExceptionResponse(handle(context, record, exception), Collections.emptyList());}
    }
    }   

...

   /**
     * Represents the result of handling a processing exception.
     * <p>
     * The {@code Response} class encapsulates a {@link ProcessingHandlerResponseResult},
     * indicating whether processing should continue or fail, along with an optional list of
     * {@link org.apache.kafka.clients.producer.ProducerRecord} instances to be sent to a dead letter queue.
     * </p>
     */
    class ProcessingExceptionResponseResponse {

        private ProcessingHandlerResponseResult processingHandlerResponseresult;

        private List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords;

        /**
         * Constructs a new {@code ProcessingExceptionResponse} object.
         *
         * @param processingHandlerResponseresult the responseresult indicating whether processing should continue or fail;
         *                                  must not be {@code null}.
         * @param deadLetterQueueRecords    the list of records to be sent to the dead letter queue; may be {@code null}.
         */
        private ProcessingExceptionResponseResponse(final ProcessingHandlerResponseResult processingHandlerResponseresult,
                                            final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords) {
            this.processingHandlerResponseresult = processingHandlerResponseresult;
            this.deadLetterQueueRecords = deadLetterQueueRecords;
        }

        /**
         * Creates a {@code ProcessingExceptionResponseResponse} indicating that processing should fail.
         *
         * @param deadLetterQueueRecords the list of records to be sent to the dead letter queue; may be {@code null}.
         * @return a {@code ProcessingExceptionResponseResponse} with a {@link ProcessingHandlerResponse#FAILProcessingExceptionHandler.Result#FAIL} status.
         */
        public static ProcessingExceptionResponseResponse failProcessingfail(final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords) {
            return new ProcessingExceptionResponseResponse(ProcessingHandlerResponseResult.FAIL, deadLetterQueueRecords);
        }

        /**
         * Creates a {@code ProcessingExceptionResponseResponse} indicating that processing should fail.
         *
         * @return a {@code ProcessingExceptionResponseResponse} with a {@link ProcessingHandlerResponse#FAILProcessingExceptionHandler.Result#FAIL} status.
         */
        public static ProcessingExceptionResponseResponse failProcessingfail() {
            return failProcessingfail(Collections.emptyList());
        }

        /**
         * Creates a {@code ProcessingExceptionResponseResponse} indicating that processing should continue.
         *
         * @param deadLetterQueueRecords the list of records to be sent to the dead letter queue; may be {@code null}.
         * @return a {@code Response} with a {@link ProcessingHandlerResponse#CONTINUEProcessingExceptionHandler.Result#RESUME} status.
         */
        public static ProcessingExceptionResponseResponse continueProcessingresume(final List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords) {
            return new ProcessingExceptionResponseResponse(ProcessingHandlerResponseResult.CONTINUERESUME, deadLetterQueueRecords);
        }

        /**
         * Creates a {@code ProcessingExceptionResponseResponse} indicating that processing should continue.
         *
         * @return a {@code ProcessingExceptionResponseResponse} with a {@link ProcessingHandlerResponse#CONTINUEProcessingExceptionHandler.Result#RESUME} status.
         */
        public static ProcessingExceptionResponseResponse continueProcessingresume() {
            return continueProcessingresume(Collections.emptyList());
        }

        /**
         * Retrieves the processing handler responseresult.
         *
         * @return the {@link ProcessingHandlerResponseResult} indicating whether processing should continue or fail.
         */
        public ProcessingHandlerResponseResult responseresult() {
            return processingHandlerResponseresult;
        }

        /**
         * Retrieves an unmodifiable list of records to be sent to the dead letter queue.
         * <p>
         * If the list is {@code null}, an empty list is returned.
         * </p>
         *
         * @return an unmodifiable list of {@link ProducerRecord} instances
         *         for the dead letter queue, or an empty list if no records are available.
         */
        public List<ProducerRecord<byte[], byte[]>> deadLetterQueueRecords() {
            if (deadLetterQueueRecords == null) {
                return Collections.emptyList();
            }
            return Collections.unmodifiableList(deadLetterQueueRecords);
        }
    }
  

}

RecordContext.java

Changes:

...

Code Block
languagejava
titleErrorHandlerContext.java
/**
 * RecordContext interface
 */
public interface RecordContext {
    . . .          
    /**
     * 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 {@code 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.
     *
     * <p> Always returns null if this method is invoked within a
     * ProductionExceptionHandler.handle(ErrorHandlerContext, ProducerRecord, Exception) the repartition topic.
     *
     * @return the raw byte of the key of the source message
     */
    byte[] sourceRawKey();


    /**
     * 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 {@code 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.
     *
     * <p> Always returns null if this method is invoked within a
     * ProductionExceptionHandler.handle(ErrorHandlerContext, ProducerRecord, Exception)
     *
     * @return the raw byte of the value of the source message
     */
    byte[] sourceRawValue();
       . . .
}

...

Code Block
languagejava
titleErrorHandlerContext.java
/**
 * ErrorHandlerContext interface
 */
public interface ErrorHandlerContext {
    . . .
     
    /**
    * 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 {@code 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.
     *
     * <p> Always returns null if this method is invoked within a
     * {@link ProductionExceptionHandler.handle(ErrorHandlerContext, ProducerRecord, Exception)}
     *
     * @return the raw byte of the key of the source message
     */
     byte[] sourceRawKey();
 
     /**
     * 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 {@code 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.
     *
     * <p> Always returns null if this method is invoked within a
     * {@link ProductionExceptionHandler.handle(ErrorHandlerContext, ProducerRecord, Exception)}
     *
     * @return the raw byte of the value of the source message
     */
     byte[] sourceRawValue();
 
    . . .
}

...

  • Managing DeadLetterQueue directly in the DSL by extending the KStreams interface.
  • Providing no default implementation to build the Dead letter queue record and delegating this task to the user.
  • Only providing exception and metadata information in the default DLQ implementation.
  • Adding a new interface, that could be overloaded by the user, to build the DLQ records.
  • Adding dead letter queue records to the enum: problematic because the collection would be shared by all stream threads, leading to unnecessary concurrency and potential transaction issues.