Repository: kafka Updated Branches: refs/heads/trunk 135488352 -> 5b4e299a4
KAFKA-4630; Implement RecordTooLargeException when communicating with pre-KIP-74 brokers Author: Colin P. Mccabe <[email protected]> Reviewers: Jason Gustafson <[email protected]>, Ismael Juma <[email protected]> Closes #2390 from cmccabe/KAFKA-4630 Project: http://git-wip-us.apache.org/repos/asf/kafka/repo Commit: http://git-wip-us.apache.org/repos/asf/kafka/commit/5b4e299a Tree: http://git-wip-us.apache.org/repos/asf/kafka/tree/5b4e299a Diff: http://git-wip-us.apache.org/repos/asf/kafka/diff/5b4e299a Branch: refs/heads/trunk Commit: 5b4e299a46a8ee92e450c54880408713606e4f52 Parents: 1354883 Author: Colin P. Mccabe <[email protected]> Authored: Thu Jan 26 09:24:17 2017 +0000 Committer: Ismael Juma <[email protected]> Committed: Thu Jan 26 11:19:32 2017 +0000 ---------------------------------------------------------------------- .../apache/kafka/clients/NodeApiVersions.java | 37 +++++++++ .../clients/consumer/internals/Fetcher.java | 82 +++++++++++--------- .../kafka/common/requests/FetchRequest.java | 1 + .../kafka/common/requests/FetchResponse.java | 6 ++ .../org/apache/kafka/clients/MockClient.java | 7 +- .../kafka/clients/NodeApiVersionsTest.java | 3 +- .../clients/consumer/internals/FetcherTest.java | 58 ++++++++++++++ .../admin/BrokerApiVersionsCommandTest.scala | 2 +- .../client_compatibility_features_test.py | 4 + .../kafka/tools/ClientCompatibilityTest.java | 62 +++++++++------ 10 files changed, 198 insertions(+), 64 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/kafka/blob/5b4e299a/clients/src/main/java/org/apache/kafka/clients/NodeApiVersions.java ---------------------------------------------------------------------- diff --git a/clients/src/main/java/org/apache/kafka/clients/NodeApiVersions.java b/clients/src/main/java/org/apache/kafka/clients/NodeApiVersions.java index b90009b..1c5d8fb 100644 --- a/clients/src/main/java/org/apache/kafka/clients/NodeApiVersions.java +++ b/clients/src/main/java/org/apache/kafka/clients/NodeApiVersions.java @@ -19,7 +19,10 @@ import org.apache.kafka.common.requests.ApiVersionsResponse.ApiVersion; import org.apache.kafka.common.utils.Utils; import java.util.Collection; +import java.util.Collections; import java.util.EnumMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.TreeMap; @@ -30,6 +33,40 @@ public class NodeApiVersions { // An array of the usable versions of each API, indexed by ApiKeys ID. private final Map<ApiKeys, Short> usableVersions = new EnumMap<>(ApiKeys.class); + /** + * Create a NodeApiVersions object with the current ApiVersions. + * + * @return A new NodeApiVersions object. + */ + public static NodeApiVersions create() { + return create(Collections.EMPTY_LIST); + } + + /** + * Create a NodeApiVersions object. + * + * @param overrides API versions to override. Any ApiVersion not specified here will be set to the current client + * value. + * @return A new NodeApiVersions object. + */ + public static NodeApiVersions create(Collection<ApiVersion> overrides) { + List<ApiVersion> apiVersions = new LinkedList<>(overrides); + for (ApiKeys apiKey : ApiKeys.values()) { + boolean exists = false; + for (ApiVersion apiVersion : apiVersions) { + if (apiVersion.apiKey == apiKey.id) { + exists = true; + break; + } + } + if (!exists) { + apiVersions.add(new ApiVersion(apiKey.id, ProtoUtils.oldestVersion(apiKey.id), + ProtoUtils.latestVersion(apiKey.id))); + } + } + return new NodeApiVersions(apiVersions); + } + public NodeApiVersions(Collection<ApiVersion> apiVersions) { this.apiVersions = apiVersions; for (ApiVersion apiVersion : apiVersions) { http://git-wip-us.apache.org/repos/asf/kafka/blob/5b4e299a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/Fetcher.java ---------------------------------------------------------------------- diff --git a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/Fetcher.java b/clients/src/main/java/org/apache/kafka/clients/consumer/internals/Fetcher.java index 6bd454e..af82570 100644 --- a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/Fetcher.java +++ b/clients/src/main/java/org/apache/kafka/clients/consumer/internals/Fetcher.java @@ -15,6 +15,7 @@ package org.apache.kafka.clients.consumer.internals; import org.apache.kafka.clients.ClientResponse; import org.apache.kafka.clients.Metadata; +import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.NoOffsetForPartitionException; import org.apache.kafka.clients.consumer.OffsetAndTimestamp; @@ -27,6 +28,7 @@ import org.apache.kafka.common.PartitionInfo; import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.errors.InvalidMetadataException; import org.apache.kafka.common.errors.InvalidTopicException; +import org.apache.kafka.common.errors.RecordTooLargeException; import org.apache.kafka.common.errors.RetriableException; import org.apache.kafka.common.errors.SerializationException; import org.apache.kafka.common.errors.TimeoutException; @@ -174,7 +176,8 @@ public class Fetcher<K, V> implements SubscriptionState.Listener { TopicPartition partition = entry.getKey(); long fetchOffset = request.fetchData().get(partition).offset; FetchResponse.PartitionData fetchData = entry.getValue(); - completedFetches.add(new CompletedFetch(partition, fetchOffset, fetchData, metricAggregator)); + completedFetches.add(new CompletedFetch(partition, fetchOffset, fetchData, metricAggregator, + request.version())); } sensors.fetchLatency.record(resp.requestLatencyMs()); @@ -334,44 +337,24 @@ public class Fetcher<K, V> implements SubscriptionState.Listener { private void resetOffset(TopicPartition partition) { OffsetResetStrategy strategy = subscriptions.resetStrategy(partition); log.debug("Resetting offset for partition {} to {} offset.", partition, strategy.name().toLowerCase(Locale.ROOT)); - long offset = listOffset(partition, strategy); + final long timestamp; + if (strategy == OffsetResetStrategy.EARLIEST) + timestamp = ListOffsetRequest.EARLIEST_TIMESTAMP; + else if (strategy == OffsetResetStrategy.LATEST) + timestamp = ListOffsetRequest.LATEST_TIMESTAMP; + else + throw new NoOffsetForPartitionException(partition); + Map<TopicPartition, OffsetAndTimestamp> offsetsByTimes = retrieveOffsetsByTimes( + Collections.singletonMap(partition, timestamp), Long.MAX_VALUE, false); + OffsetAndTimestamp offsetAndTimestamp = offsetsByTimes.get(partition); + if (offsetAndTimestamp == null) + throw new NoOffsetForPartitionException(partition); + long offset = offsetAndTimestamp.offset(); // we might lose the assignment while fetching the offset, so check it is still active if (subscriptions.isAssigned(partition)) this.subscriptions.seek(partition, offset); } - private long listOffset(TopicPartition partition, OffsetResetStrategy strategy) { - final long timestamp; - switch (strategy) { - case EARLIEST: - timestamp = ListOffsetRequest.EARLIEST_TIMESTAMP; - break; - case LATEST: - timestamp = ListOffsetRequest.LATEST_TIMESTAMP; - break; - default: - throw new NoOffsetForPartitionException(partition); - } - while (true) { - RequestFuture<Map<TopicPartition, OffsetAndTimestamp>> future = - sendListOffsetRequests(false, Collections.singletonMap(partition, timestamp)); - client.poll(future); - if (future.succeeded()) { - OffsetAndTimestamp offsetAndTimestamp = future.value().get(partition); - if (offsetAndTimestamp == null) - throw new NoOffsetForPartitionException(partition); - return offsetAndTimestamp.offset(); - } - if (!future.isRetriable()) - throw future.exception(); - if (future.exception() instanceof InvalidMetadataException) - client.awaitMetadataUpdate(); - else - time.sleep(retryBackoffMs); - } - } - - public Map<TopicPartition, OffsetAndTimestamp> getOffsetsByTimes(Map<TopicPartition, Long> timestampsToSearch, long timeout) { return retrieveOffsetsByTimes(timestampsToSearch, timeout, true); @@ -636,7 +619,7 @@ public class Fetcher<K, V> implements SubscriptionState.Listener { } else { offset = partitionData.offsets.get(0); } - log.debug("Handling v0 ListOffsetResponse response for {}. Fetched offset {}", + log.debug("Handling v0 ListOffsetResponse response for {}. Fetched offset {}", topicPartition, offset); if (offset != ListOffsetResponse.UNKNOWN_OFFSET) { OffsetAndTimestamp offsetAndTimestamp = new OffsetAndTimestamp(offset, -1); @@ -644,7 +627,7 @@ public class Fetcher<K, V> implements SubscriptionState.Listener { } } else { // Handle v1 and later response - log.debug("Handling ListOffsetResponse response for {}. Fetched offset {}, timestamp {}", + log.debug("Handling ListOffsetResponse response for {}. Fetched offset {}, timestamp {}", topicPartition, partitionData.offset, partitionData.timestamp); if (partitionData.offset != ListOffsetResponse.UNKNOWN_OFFSET) { OffsetAndTimestamp offsetAndTimestamp = @@ -754,12 +737,14 @@ public class Fetcher<K, V> implements SubscriptionState.Listener { } List<ConsumerRecord<K, V>> parsed = new ArrayList<>(); + boolean skippedRecords = false; for (LogEntry logEntry : partition.records.deepEntries()) { // Skip the messages earlier than current position. if (logEntry.offset() >= position) { parsed.add(parseRecord(tp, logEntry)); bytes += logEntry.sizeInBytes(); - } + } else + skippedRecords = true; } recordsCount = parsed.size(); @@ -767,6 +752,24 @@ public class Fetcher<K, V> implements SubscriptionState.Listener { log.trace("Adding fetched record for partition {} with offset {} to buffered record list", tp, position); parsedRecords = new PartitionRecords<>(fetchOffset, tp, parsed); + if (parsed.isEmpty() && !skippedRecords && (partition.records.sizeInBytes() > 0)) { + if (completedFetch.responseVersion < 3) { + // Implement the pre KIP-74 behavior of throwing a RecordTooLargeException. + Map<TopicPartition, Long> recordTooLargePartitions = Collections.singletonMap(tp, fetchOffset); + throw new RecordTooLargeException("There are some messages at [Partition=Offset]: " + + recordTooLargePartitions + " whose size is larger than the fetch size " + this.fetchSize + + " and hence cannot be returned. Please considering upgrading your broker to 0.10.1.0 or " + + "newer to avoid this issue. Alternately, increase the fetch size on the client (using " + + ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG + ")", + recordTooLargePartitions); + } else { + // This should not happen with brokers that support FetchRequest/Response V3 or higher (i.e. KIP-74) + throw new KafkaException("Failed to make progress reading messages at " + tp + "=" + + fetchOffset + ". Received a non-empty fetch response from the server, but no " + + "complete records were found."); + } + } + if (partition.highWatermark >= 0) { log.trace("Received {} records in fetch response for partition {} with offset {}", parsed.size(), tp, position); subscriptions.updateHighWatermark(tp, partition.highWatermark); @@ -894,15 +897,18 @@ public class Fetcher<K, V> implements SubscriptionState.Listener { private final long fetchedOffset; private final FetchResponse.PartitionData partitionData; private final FetchResponseMetricAggregator metricAggregator; + private final short responseVersion; private CompletedFetch(TopicPartition partition, long fetchedOffset, FetchResponse.PartitionData partitionData, - FetchResponseMetricAggregator metricAggregator) { + FetchResponseMetricAggregator metricAggregator, + short responseVersion) { this.partition = partition; this.fetchedOffset = fetchedOffset; this.partitionData = partitionData; this.metricAggregator = metricAggregator; + this.responseVersion = responseVersion; } } http://git-wip-us.apache.org/repos/asf/kafka/blob/5b4e299a/clients/src/main/java/org/apache/kafka/common/requests/FetchRequest.java ---------------------------------------------------------------------- diff --git a/clients/src/main/java/org/apache/kafka/common/requests/FetchRequest.java b/clients/src/main/java/org/apache/kafka/common/requests/FetchRequest.java index 4768a20..5700e9e 100644 --- a/clients/src/main/java/org/apache/kafka/common/requests/FetchRequest.java +++ b/clients/src/main/java/org/apache/kafka/common/requests/FetchRequest.java @@ -142,6 +142,7 @@ public class FetchRequest extends AbstractRequest { append(", replicaId=").append(replicaId). append(", maxWait=").append(maxWait). append(", minBytes=").append(minBytes). + append(", maxBytes=").append(maxBytes). append(", fetchData=").append(Utils.mkString(fetchData)). append(")"); return bld.toString(); http://git-wip-us.apache.org/repos/asf/kafka/blob/5b4e299a/clients/src/main/java/org/apache/kafka/common/requests/FetchResponse.java ---------------------------------------------------------------------- diff --git a/clients/src/main/java/org/apache/kafka/common/requests/FetchResponse.java b/clients/src/main/java/org/apache/kafka/common/requests/FetchResponse.java index 66b01a0..965b207 100644 --- a/clients/src/main/java/org/apache/kafka/common/requests/FetchResponse.java +++ b/clients/src/main/java/org/apache/kafka/common/requests/FetchResponse.java @@ -82,6 +82,12 @@ public class FetchResponse extends AbstractResponse { this.highWatermark = highWatermark; this.records = records; } + + @Override + public String toString() { + return "(errorCode=" + errorCode + ", highWaterMark=" + highWatermark + + ", records=" + records + ")"; + } } /** http://git-wip-us.apache.org/repos/asf/kafka/blob/5b4e299a/clients/src/test/java/org/apache/kafka/clients/MockClient.java ---------------------------------------------------------------------- diff --git a/clients/src/test/java/org/apache/kafka/clients/MockClient.java b/clients/src/test/java/org/apache/kafka/clients/MockClient.java index bfde9f9..50ed131 100644 --- a/clients/src/test/java/org/apache/kafka/clients/MockClient.java +++ b/clients/src/test/java/org/apache/kafka/clients/MockClient.java @@ -73,6 +73,7 @@ public class MockClient implements KafkaClient { private final Queue<ClientResponse> responses = new ConcurrentLinkedDeque<>(); private final Queue<FutureResponse> futureResponses = new ArrayDeque<>(); private final Queue<Cluster> metadataUpdates = new ArrayDeque<>(); + private volatile NodeApiVersions nodeApiVersions = NodeApiVersions.create(); public MockClient(Time time) { this.time = time; @@ -145,7 +146,8 @@ public class MockClient implements KafkaClient { FutureResponse futureResp = iterator.next(); if (futureResp.node != null && !request.destination().equals(futureResp.node.idString())) continue; - + request.requestBuilder().setVersion(nodeApiVersions.usableVersion( + request.requestBuilder().apiKey())); AbstractRequest abstractRequest = request.requestBuilder().build(); if (!futureResp.requestMatcher.matches(abstractRequest)) throw new IllegalStateException("Next in line response did not match expected request"); @@ -334,4 +336,7 @@ public class MockClient implements KafkaClient { boolean matches(AbstractRequest body); } + public void setNodeApiVersions(NodeApiVersions nodeApiVersions) { + this.nodeApiVersions = nodeApiVersions; + } } http://git-wip-us.apache.org/repos/asf/kafka/blob/5b4e299a/clients/src/test/java/org/apache/kafka/clients/NodeApiVersionsTest.java ---------------------------------------------------------------------- diff --git a/clients/src/test/java/org/apache/kafka/clients/NodeApiVersionsTest.java b/clients/src/test/java/org/apache/kafka/clients/NodeApiVersionsTest.java index 861a28f..f72137b 100644 --- a/clients/src/test/java/org/apache/kafka/clients/NodeApiVersionsTest.java +++ b/clients/src/test/java/org/apache/kafka/clients/NodeApiVersionsTest.java @@ -33,8 +33,7 @@ public class NodeApiVersionsTest { @Test public void testUnsupportedVersionsToString() { - NodeApiVersions versions = new NodeApiVersions( - Collections.<ApiVersion>emptyList()); + NodeApiVersions versions = new NodeApiVersions(Collections.<ApiVersion>emptyList()); StringBuilder bld = new StringBuilder(); String prefix = "("; for (ApiKeys apiKey : ApiKeys.values()) { http://git-wip-us.apache.org/repos/asf/kafka/blob/5b4e299a/clients/src/test/java/org/apache/kafka/clients/consumer/internals/FetcherTest.java ---------------------------------------------------------------------- diff --git a/clients/src/test/java/org/apache/kafka/clients/consumer/internals/FetcherTest.java b/clients/src/test/java/org/apache/kafka/clients/consumer/internals/FetcherTest.java index 210af6d..ecaade2 100644 --- a/clients/src/test/java/org/apache/kafka/clients/consumer/internals/FetcherTest.java +++ b/clients/src/test/java/org/apache/kafka/clients/consumer/internals/FetcherTest.java @@ -18,6 +18,7 @@ package org.apache.kafka.clients.consumer.internals; import org.apache.kafka.clients.Metadata; import org.apache.kafka.clients.MockClient; +import org.apache.kafka.clients.NodeApiVersions; import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.OffsetAndMetadata; @@ -31,11 +32,13 @@ import org.apache.kafka.common.Node; import org.apache.kafka.common.PartitionInfo; import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.errors.InvalidTopicException; +import org.apache.kafka.common.errors.RecordTooLargeException; import org.apache.kafka.common.errors.SerializationException; import org.apache.kafka.common.errors.TimeoutException; import org.apache.kafka.common.errors.TopicAuthorizationException; import org.apache.kafka.common.metrics.KafkaMetric; import org.apache.kafka.common.metrics.Metrics; +import org.apache.kafka.common.protocol.ApiKeys; import org.apache.kafka.common.protocol.Errors; import org.apache.kafka.common.record.ByteBufferOutputStream; import org.apache.kafka.common.record.CompressionType; @@ -44,6 +47,7 @@ import org.apache.kafka.common.record.MemoryRecordsBuilder; import org.apache.kafka.common.record.Record; import org.apache.kafka.common.record.TimestampType; import org.apache.kafka.common.requests.AbstractRequest; +import org.apache.kafka.common.requests.ApiVersionsResponse; import org.apache.kafka.common.requests.FetchRequest; import org.apache.kafka.common.requests.FetchResponse; import org.apache.kafka.common.requests.ListOffsetRequest; @@ -316,6 +320,60 @@ public class FetcherTest { assertEquals(30L, consumerRecords.get(2).offset()); } + /** + * Test the case where the client makes a pre-v3 FetchRequest, but the server replies with only a partial + * request. This happens when a single message is larger than the per-partition limit. + */ + @Test + public void testFetchRequestWhenRecordTooLarge() { + try { + client.setNodeApiVersions(NodeApiVersions.create(Collections.singletonList( + new ApiVersionsResponse.ApiVersion(ApiKeys.FETCH.id, (short) 2, (short) 2)))); + makeFetchRequestWithIncompleteRecord(); + try { + fetcher.fetchedRecords(); + fail("RecordTooLargeException should have been raised"); + } catch (RecordTooLargeException e) { + assertTrue(e.getMessage().startsWith("There are some messages at [Partition=Offset]: ")); + // the position should not advance since no data has been returned + assertEquals(0, subscriptions.position(tp).longValue()); + } + } finally { + client.setNodeApiVersions(NodeApiVersions.create()); + } + } + + /** + * Test the case where the client makes a post KIP-74 FetchRequest, but the server replies with only a + * partial request. For v3 and later FetchRequests, the implementation of KIP-74 changed the behavior + * so that at least one message is always returned. Therefore, this case should not happen, and it indicates + * that an internal error has taken place. + */ + @Test + public void testFetchRequestInternalError() { + makeFetchRequestWithIncompleteRecord(); + try { + fetcher.fetchedRecords(); + fail("RecordTooLargeException should have been raised"); + } catch (KafkaException e) { + assertTrue(e.getMessage().startsWith("Failed to make progress reading messages")); + // the position should not advance since no data has been returned + assertEquals(0, subscriptions.position(tp).longValue()); + } + } + + private void makeFetchRequestWithIncompleteRecord() { + subscriptions.assignFromUser(singleton(tp)); + subscriptions.seek(tp, 0); + assertEquals(1, fetcher.sendFetches()); + assertFalse(fetcher.hasCompletedFetches()); + MemoryRecords partialRecord = MemoryRecords.readableRecords( + ByteBuffer.wrap(new byte[]{0, 0, 0, 0, 0, 0, 0, 0})); + client.prepareResponse(fetchResponse(partialRecord, Errors.NONE.code(), 100L, 0)); + consumerClient.poll(0); + assertTrue(fetcher.hasCompletedFetches()); + } + @Test public void testUnauthorizedTopic() { subscriptions.assignFromUser(singleton(tp)); http://git-wip-us.apache.org/repos/asf/kafka/blob/5b4e299a/core/src/test/scala/integration/kafka/admin/BrokerApiVersionsCommandTest.scala ---------------------------------------------------------------------- diff --git a/core/src/test/scala/integration/kafka/admin/BrokerApiVersionsCommandTest.scala b/core/src/test/scala/integration/kafka/admin/BrokerApiVersionsCommandTest.scala index ff93f22..8fa489d 100644 --- a/core/src/test/scala/integration/kafka/admin/BrokerApiVersionsCommandTest.scala +++ b/core/src/test/scala/integration/kafka/admin/BrokerApiVersionsCommandTest.scala @@ -42,7 +42,7 @@ class BrokerApiVersionsCommandTest extends KafkaServerTestHarness { val lineIter = content.split("\n").iterator assertTrue(lineIter.hasNext) assertEquals(s"$brokerList (id: 0 rack: null) -> (", lineIter.next) - val nodeApiVersions = new NodeApiVersions(ApiVersionsResponse.API_VERSIONS_RESPONSE.apiVersions) + val nodeApiVersions = NodeApiVersions.create for (apiKey <- ApiKeys.values) { val apiVersion = nodeApiVersions.apiVersion(apiKey) val versionRangeStr = http://git-wip-us.apache.org/repos/asf/kafka/blob/5b4e299a/tests/kafkatest/tests/client/client_compatibility_features_test.py ---------------------------------------------------------------------- diff --git a/tests/kafkatest/tests/client/client_compatibility_features_test.py b/tests/kafkatest/tests/client/client_compatibility_features_test.py index 3c96b84..36d76b4 100644 --- a/tests/kafkatest/tests/client/client_compatibility_features_test.py +++ b/tests/kafkatest/tests/client/client_compatibility_features_test.py @@ -30,9 +30,11 @@ def get_broker_features(broker_version): if (broker_version < V_0_10_1_0): features["offsets-for-times-supported"] = False features["cluster-id-supported"] = False + features["expect-record-too-large-exception"] = True else: features["offsets-for-times-supported"] = True features["cluster-id-supported"] = True + features["expect-record-too-large-exception"] = False return features def run_command(node, cmd, ssh_log_file): @@ -74,10 +76,12 @@ class ClientCompatibilityFeaturesTest(Test): "--bootstrap-server %s " "--offsets-for-times-supported %s " "--cluster-id-supported %s " + "--expect-record-too-large-exception %s " "--topic %s " % (self.zk.path.script("kafka-run-class.sh", node), self.kafka.bootstrap_servers(), features["offsets-for-times-supported"], features["cluster-id-supported"], + features["expect-record-too-large-exception"], self.topics.keys()[0])) results_dir = TestContext.results_dir(self.test_context, 0) os.makedirs(results_dir) http://git-wip-us.apache.org/repos/asf/kafka/blob/5b4e299a/tools/src/main/java/org/apache/kafka/tools/ClientCompatibilityTest.java ---------------------------------------------------------------------- diff --git a/tools/src/main/java/org/apache/kafka/tools/ClientCompatibilityTest.java b/tools/src/main/java/org/apache/kafka/tools/ClientCompatibilityTest.java index 69202a0..d7eb717 100644 --- a/tools/src/main/java/org/apache/kafka/tools/ClientCompatibilityTest.java +++ b/tools/src/main/java/org/apache/kafka/tools/ClientCompatibilityTest.java @@ -22,6 +22,7 @@ import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.inf.ArgumentParser; import net.sourceforge.argparse4j.inf.ArgumentParserException; import net.sourceforge.argparse4j.inf.Namespace; +import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; @@ -35,6 +36,7 @@ import org.apache.kafka.common.ClusterResourceListener; import org.apache.kafka.common.PartitionInfo; import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.errors.ObsoleteBrokerException; +import org.apache.kafka.common.errors.RecordTooLargeException; import org.apache.kafka.common.serialization.ByteArraySerializer; import org.apache.kafka.common.serialization.Deserializer; import org.apache.kafka.common.utils.Time; @@ -66,12 +68,14 @@ public class ClientCompatibilityTest { final String topic; final boolean offsetsForTimesSupported; final boolean expectClusterId; + final boolean expectRecordTooLargeException; TestConfig(Namespace res) { this.bootstrapServer = res.getString("bootstrapServer"); this.topic = res.getString("topic"); this.offsetsForTimesSupported = res.getBoolean("offsetsForTimesSupported"); this.expectClusterId = res.getBoolean("clusterIdSupported"); + this.expectRecordTooLargeException = res.getBoolean("expectRecordTooLargeException"); } } @@ -108,6 +112,15 @@ public class ClientCompatibilityTest { .dest("clusterIdSupported") .metavar("CLUSTER_ID_SUPPORTED") .help("True if cluster IDs are supported. False if cluster ID always appears as null."); + parser.addArgument("--expect-record-too-large-exception") + .action(store()) + .required(true) + .type(Boolean.class) + .dest("expectRecordTooLargeException") + .metavar("EXPECT_RECORD_TOO_LARGE_EXCEPTION") + .help("True if we should expect a RecordTooLargeException when trying to read from a topic " + + "that contains a message that is bigger than " + ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG + + ". This is pre-KIP-74 behavior."); Namespace res = null; try { res = parser.parseArgs(args); @@ -133,19 +146,6 @@ public class ClientCompatibilityTest { System.exit(0); } - private static byte[] asByteArray(long a) { - ByteBuffer buf = ByteBuffer.allocate(8); - buf.putLong(a); - return buf.array(); - } - - private static byte[] asByteArray(long a, long b) { - ByteBuffer buf = ByteBuffer.allocate(16); - buf.putLong(a); - buf.putLong(b); - return buf.array(); - } - private static String toHexString(byte[] buf) { StringBuilder bld = new StringBuilder(); for (byte b : buf) { @@ -169,8 +169,16 @@ public class ClientCompatibilityTest { ClientCompatibilityTest(TestConfig testConfig) { this.testConfig = testConfig; long curTime = Time.SYSTEM.milliseconds(); - this.message1 = asByteArray(curTime); - this.message2 = asByteArray(curTime, curTime); + + ByteBuffer buf = ByteBuffer.allocate(8); + buf.putLong(curTime); + this.message1 = buf.array(); + + ByteBuffer buf2 = ByteBuffer.allocate(4096); + for (long i = 0; i < buf2.capacity(); i += 8) { + buf2.putLong(curTime + i); + } + this.message2 = buf2.array(); } void run() throws Exception { @@ -242,6 +250,7 @@ public class ClientCompatibilityTest { public void testConsume(final long prodTimeMs) throws Exception { Properties consumerProps = new Properties(); consumerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, testConfig.bootstrapServer); + consumerProps.put(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG, 512); ClientCompatibilityTestDeserializer deserializer = new ClientCompatibilityTestDeserializer(testConfig.expectClusterId); final KafkaConsumer<byte[], byte[]> consumer = new KafkaConsumer<>(consumerProps, deserializer, deserializer); @@ -318,7 +327,6 @@ public class ClientCompatibilityTest { throw new UnsupportedOperationException(); } }; - byte[] next = iter.next(); try { compareArrays(message1, next); @@ -327,13 +335,23 @@ public class ClientCompatibilityTest { throw new RuntimeException("The first message in this topic was not ours. Please use a new topic when " + "running this program."); } - next = iter.next(); try { - compareArrays(message2, next); - log.debug("Found second message..."); - } catch (RuntimeException e) { - throw new RuntimeException("The second message in this topic was not ours. Please use a new topic when " + - "running this program."); + next = iter.next(); + if (testConfig.expectRecordTooLargeException) + throw new RuntimeException("Expected to get a RecordTooLargeException when reading a record " + + "bigger than " + ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG); + try { + compareArrays(message2, next); + } catch (RuntimeException e) { + System.out.println("The second message in this topic was not ours. Please use a new " + + "topic when running this program."); + System.exit(1); + } + } catch (RecordTooLargeException e) { + log.debug("Got RecordTooLargeException", e); + if (!testConfig.expectRecordTooLargeException) + throw new RuntimeException("Got an unexpected RecordTooLargeException when reading a record " + + "bigger than " + ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG); } log.debug("Closing consumer."); consumer.close();
