/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.kafka.clients.producer.internals; import org.apache.kafka.clients.producer.ProducerInterceptor; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.clients.producer.RecordMetadata; import org.apache.kafka.common.KafkaException; import org.apache.kafka.common.TopicPartition; import org.junit.Test; import java.util.ArrayList; import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; public class ProducerInterceptorsTest { private final TopicPartition tp = new TopicPartition("test", 0); private final ProducerRecord<Integer, String> producerRecord = new ProducerRecord<>("test", 0, 1, "value"); private int onAckCount = 0; private int onErrorAckCount = 0; private int onErrorAckWithTopicSetCount = 0; private int onErrorAckWithTopicPartitionSetCount = 0; private int onSendCount = 0; private class AppendProducerInterceptor implements ProducerInterceptor<Integer, String> { private String appendStr = ""; private boolean throwExceptionOnSend = false; private boolean throwExceptionOnAck = false; public AppendProducerInterceptor(String appendStr) { this.appendStr = appendStr; } @Override public void configure(Map<String, ?> configs) { } @Override public ProducerRecord<Integer, String> onSend(ProducerRecord<Integer, String> record) { onSendCount++; if (throwExceptionOnSend) throw new KafkaException("Injected exception in AppendProducerInterceptor.onSend"); return new ProducerRecord<>( record.topic(), record.partition(), record.key(), record.value().concat(appendStr)); } @Override public void onAcknowledgement(RecordMetadata metadata, Exception exception) { onAckCount++; if (exception != null) { onErrorAckCount++; // the length check is just to call topic() method and let it throw an exception // if RecordMetadata.TopicPartition is null if (metadata != null && metadata.topic().length() >= 0) { onErrorAckWithTopicSetCount++; if (metadata.partition() >= 0) onErrorAckWithTopicPartitionSetCount++; } } if (throwExceptionOnAck) throw new KafkaException("Injected exception in AppendProducerInterceptor.onAcknowledgement"); } @Override public void close() { } // if 'on' is true, onSend will always throw an exception public void injectOnSendError(boolean on) { throwExceptionOnSend = on; } // if 'on' is true, onAcknowledgement will always throw an exception public void injectOnAcknowledgementError(boolean on) { throwExceptionOnAck = on; } } @Test public void testOnSendChain() { List<ProducerInterceptor<Integer, String>> interceptorList = new ArrayList<>(); // we are testing two different interceptors by configuring the same interceptor differently, which is not // how it would be done in KafkaProducer, but ok for testing interceptor callbacks AppendProducerInterceptor interceptor1 = new AppendProducerInterceptor("One"); AppendProducerInterceptor interceptor2 = new AppendProducerInterceptor("Two"); interceptorList.add(interceptor1); interceptorList.add(interceptor2); ProducerInterceptors<Integer, String> interceptors = new ProducerInterceptors<>(interceptorList); // verify that onSend() mutates the record as expected ProducerRecord<Integer, String> interceptedRecord = interceptors.onSend(producerRecord); assertEquals(2, onSendCount); assertEquals(producerRecord.topic(), interceptedRecord.topic()); assertEquals(producerRecord.partition(), interceptedRecord.partition()); assertEquals(producerRecord.key(), interceptedRecord.key()); assertEquals(interceptedRecord.value(), producerRecord.value().concat("One").concat("Two")); // onSend() mutates the same record the same way ProducerRecord<Integer, String> anotherRecord = interceptors.onSend(producerRecord); assertEquals(4, onSendCount); assertEquals(interceptedRecord, anotherRecord); // verify that if one of the interceptors throws an exception, other interceptors' callbacks are still called interceptor1.injectOnSendError(true); ProducerRecord<Integer, String> partInterceptRecord = interceptors.onSend(producerRecord); assertEquals(6, onSendCount); assertEquals(partInterceptRecord.value(), producerRecord.value().concat("Two")); // verify the record remains valid if all onSend throws an exception interceptor2.injectOnSendError(true); ProducerRecord<Integer, String> noInterceptRecord = interceptors.onSend(producerRecord); assertEquals(producerRecord, noInterceptRecord); interceptors.close(); } @Test public void testOnAcknowledgementChain() { List<ProducerInterceptor<Integer, String>> interceptorList = new ArrayList<>(); // we are testing two different interceptors by configuring the same interceptor differently, which is not // how it would be done in KafkaProducer, but ok for testing interceptor callbacks AppendProducerInterceptor interceptor1 = new AppendProducerInterceptor("One"); AppendProducerInterceptor interceptor2 = new AppendProducerInterceptor("Two"); interceptorList.add(interceptor1); interceptorList.add(interceptor2); ProducerInterceptors<Integer, String> interceptors = new ProducerInterceptors<>(interceptorList); // verify onAck is called on all interceptors RecordMetadata meta = new RecordMetadata(tp, 0, 0, 0, 0, 0, 0); interceptors.onAcknowledgement(meta, null); assertEquals(2, onAckCount); // verify that onAcknowledgement exceptions do not propagate interceptor1.injectOnAcknowledgementError(true); interceptors.onAcknowledgement(meta, null); assertEquals(4, onAckCount); interceptor2.injectOnAcknowledgementError(true); interceptors.onAcknowledgement(meta, null); assertEquals(6, onAckCount); interceptors.close(); } @Test public void testOnAcknowledgementWithErrorChain() { List<ProducerInterceptor<Integer, String>> interceptorList = new ArrayList<>(); AppendProducerInterceptor interceptor1 = new AppendProducerInterceptor("One"); interceptorList.add(interceptor1); ProducerInterceptors<Integer, String> interceptors = new ProducerInterceptors<>(interceptorList); // verify that metadata contains both topic and partition interceptors.onSendError(producerRecord, new TopicPartition(producerRecord.topic(), producerRecord.partition()), new KafkaException("Test")); assertEquals(1, onErrorAckCount); assertEquals(1, onErrorAckWithTopicPartitionSetCount); // verify that metadata contains both topic and partition (because record already contains partition) interceptors.onSendError(producerRecord, null, new KafkaException("Test")); assertEquals(2, onErrorAckCount); assertEquals(2, onErrorAckWithTopicPartitionSetCount); // if producer record does not contain partition, interceptor should get partition == -1 ProducerRecord<Integer, String> record2 = new ProducerRecord<>("test2", null, 1, "value"); interceptors.onSendError(record2, null, new KafkaException("Test")); assertEquals(3, onErrorAckCount); assertEquals(3, onErrorAckWithTopicSetCount); assertEquals(2, onErrorAckWithTopicPartitionSetCount); // if producer record does not contain partition, but topic/partition is passed to // onSendError, then interceptor should get valid partition int reassignedPartition = producerRecord.partition() + 1; interceptors.onSendError(record2, new TopicPartition(record2.topic(), reassignedPartition), new KafkaException("Test")); assertEquals(4, onErrorAckCount); assertEquals(4, onErrorAckWithTopicSetCount); assertEquals(3, onErrorAckWithTopicPartitionSetCount); // if both record and topic/partition are null, interceptor should not receive metadata interceptors.onSendError(null, null, new KafkaException("Test")); assertEquals(5, onErrorAckCount); assertEquals(4, onErrorAckWithTopicSetCount); assertEquals(3, onErrorAckWithTopicPartitionSetCount); interceptors.close(); } }