diff --git a/checkstyle/suppressions.xml b/checkstyle/suppressions.xml index 21cd5f4cc5ebc..a93581bf9f355 100644 --- a/checkstyle/suppressions.xml +++ b/checkstyle/suppressions.xml @@ -152,7 +152,7 @@ + files="(KafkaStreams|KStreamImpl|KTableImpl|InternalTopologyBuilder|StreamsPartitionAssignor).java"/> @@ -195,7 +195,7 @@ + files="(StreamsPartitionAssignorTest|StreamThreadTest|StreamTaskTest|TaskManagerTest|TopologyTestDriverTest).java"/> diff --git a/clients/src/main/java/org/apache/kafka/clients/producer/ProducerConfig.java b/clients/src/main/java/org/apache/kafka/clients/producer/ProducerConfig.java index cc013704313b5..300f5a3bd5f64 100644 --- a/clients/src/main/java/org/apache/kafka/clients/producer/ProducerConfig.java +++ b/clients/src/main/java/org/apache/kafka/clients/producer/ProducerConfig.java @@ -200,8 +200,8 @@ public class ProducerConfig extends AbstractConfig { /** max.in.flight.requests.per.connection */ public static final String MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION = "max.in.flight.requests.per.connection"; private static final String MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION_DOC = "The maximum number of unacknowledged requests the client will send on a single connection before blocking." - + " Note that if this setting is set to be greater than 1 and there are failed sends, there is a risk of" - + " message re-ordering due to retries (i.e., if retries are enabled)."; + + " Note that if this config is set to be greater than 1 and enable.idempotence is set to false, there is a risk of" + + " message re-ordering after a failed send due to retries (i.e., if retries are enabled)."; /** retries */ public static final String RETRIES_CONFIG = CommonClientConfigs.RETRIES_CONFIG; @@ -246,10 +246,10 @@ public class ProducerConfig extends AbstractConfig { public static final String ENABLE_IDEMPOTENCE_CONFIG = "enable.idempotence"; public static final String ENABLE_IDEMPOTENCE_DOC = "When set to 'true', the producer will ensure that exactly one copy of each message is written in the stream. If 'false', producer " + "retries due to broker failures, etc., may write duplicates of the retried message in the stream. " - + "Note that enabling idempotence requires " + MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION + " to be less than or equal to 5, " - + "" + RETRIES_CONFIG + " to be greater than 0 and " + ACKS_CONFIG + " must be 'all'. If these values " - + "are not explicitly set by the user, suitable values will be chosen. If incompatible values are set, " - + "a ConfigException will be thrown."; + + "Note that enabling idempotence requires " + MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION + " to be less than or equal to 5 " + + "(with message ordering preserved for any allowable value), " + RETRIES_CONFIG + " to be greater than 0, and " + + ACKS_CONFIG + " must be 'all'. If these values are not explicitly set by the user, suitable values will be chosen. If incompatible " + + "values are set, a ConfigException will be thrown."; /** transaction.timeout.ms */ public static final String TRANSACTION_TIMEOUT_CONFIG = "transaction.timeout.ms"; diff --git a/clients/src/main/java/org/apache/kafka/common/protocol/ApiKeys.java b/clients/src/main/java/org/apache/kafka/common/protocol/ApiKeys.java index 8ec2d02ea3de6..bd28aa26aa977 100644 --- a/clients/src/main/java/org/apache/kafka/common/protocol/ApiKeys.java +++ b/clients/src/main/java/org/apache/kafka/common/protocol/ApiKeys.java @@ -107,7 +107,8 @@ public enum ApiKeys { BROKER_HEARTBEAT(ApiMessageType.BROKER_HEARTBEAT, true, RecordBatch.MAGIC_VALUE_V0, false), UNREGISTER_BROKER(ApiMessageType.UNREGISTER_BROKER, false, RecordBatch.MAGIC_VALUE_V0, true), DESCRIBE_TRANSACTIONS(ApiMessageType.DESCRIBE_TRANSACTIONS), - LIST_TRANSACTIONS(ApiMessageType.LIST_TRANSACTIONS); + LIST_TRANSACTIONS(ApiMessageType.LIST_TRANSACTIONS), + ALLOCATE_PRODUCER_IDS(ApiMessageType.ALLOCATE_PRODUCER_IDS, true, false); private static final Map> APIS_BY_LISTENER = new EnumMap<>(ApiMessageType.ListenerType.class); diff --git a/clients/src/main/java/org/apache/kafka/common/requests/AbstractRequest.java b/clients/src/main/java/org/apache/kafka/common/requests/AbstractRequest.java index 3137e3e500aef..0c38e998fe247 100644 --- a/clients/src/main/java/org/apache/kafka/common/requests/AbstractRequest.java +++ b/clients/src/main/java/org/apache/kafka/common/requests/AbstractRequest.java @@ -301,6 +301,8 @@ private static AbstractRequest doParseRequest(ApiKeys apiKey, short apiVersion, return DescribeTransactionsRequest.parse(buffer, apiVersion); case LIST_TRANSACTIONS: return ListTransactionsRequest.parse(buffer, apiVersion); + case ALLOCATE_PRODUCER_IDS: + return AllocateProducerIdsRequest.parse(buffer, apiVersion); default: throw new AssertionError(String.format("ApiKey %s is not currently handled in `parseRequest`, the " + "code should be updated to do so.", apiKey)); diff --git a/clients/src/main/java/org/apache/kafka/common/requests/AbstractResponse.java b/clients/src/main/java/org/apache/kafka/common/requests/AbstractResponse.java index 5f7b88f269405..47f2b3c7f3099 100644 --- a/clients/src/main/java/org/apache/kafka/common/requests/AbstractResponse.java +++ b/clients/src/main/java/org/apache/kafka/common/requests/AbstractResponse.java @@ -245,6 +245,8 @@ public static AbstractResponse parseResponse(ApiKeys apiKey, ByteBuffer response return DescribeTransactionsResponse.parse(responseBuffer, version); case LIST_TRANSACTIONS: return ListTransactionsResponse.parse(responseBuffer, version); + case ALLOCATE_PRODUCER_IDS: + return AllocateProducerIdsResponse.parse(responseBuffer, version); default: throw new AssertionError(String.format("ApiKey %s is not currently handled in `parseResponse`, the " + "code should be updated to do so.", apiKey)); diff --git a/clients/src/main/java/org/apache/kafka/common/requests/AllocateProducerIdsRequest.java b/clients/src/main/java/org/apache/kafka/common/requests/AllocateProducerIdsRequest.java new file mode 100644 index 0000000000000..7938f92df56d9 --- /dev/null +++ b/clients/src/main/java/org/apache/kafka/common/requests/AllocateProducerIdsRequest.java @@ -0,0 +1,72 @@ +/* + * 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.common.requests; + +import org.apache.kafka.common.message.AllocateProducerIdsRequestData; +import org.apache.kafka.common.message.AllocateProducerIdsResponseData; +import org.apache.kafka.common.protocol.ApiKeys; +import org.apache.kafka.common.protocol.ByteBufferAccessor; +import org.apache.kafka.common.protocol.Errors; + +import java.nio.ByteBuffer; + +public class AllocateProducerIdsRequest extends AbstractRequest { + private final AllocateProducerIdsRequestData data; + + public AllocateProducerIdsRequest(AllocateProducerIdsRequestData data, short version) { + super(ApiKeys.ALLOCATE_PRODUCER_IDS, version); + this.data = data; + } + + @Override + public AbstractResponse getErrorResponse(int throttleTimeMs, Throwable e) { + return new AllocateProducerIdsResponse(new AllocateProducerIdsResponseData() + .setThrottleTimeMs(throttleTimeMs) + .setErrorCode(Errors.forException(e).code())); + } + + @Override + public AllocateProducerIdsRequestData data() { + return data; + } + + public static class Builder extends AbstractRequest.Builder { + + private final AllocateProducerIdsRequestData data; + + public Builder(AllocateProducerIdsRequestData data) { + super(ApiKeys.ALLOCATE_PRODUCER_IDS); + this.data = data; + } + + @Override + public AllocateProducerIdsRequest build(short version) { + return new AllocateProducerIdsRequest(data, version); + } + + @Override + public String toString() { + return data.toString(); + } + } + + public static AllocateProducerIdsRequest parse(ByteBuffer buffer, short version) { + return new AllocateProducerIdsRequest(new AllocateProducerIdsRequestData( + new ByteBufferAccessor(buffer), version), version); + } +} diff --git a/clients/src/main/java/org/apache/kafka/common/requests/AllocateProducerIdsResponse.java b/clients/src/main/java/org/apache/kafka/common/requests/AllocateProducerIdsResponse.java new file mode 100644 index 0000000000000..5d48c39e8019d --- /dev/null +++ b/clients/src/main/java/org/apache/kafka/common/requests/AllocateProducerIdsResponse.java @@ -0,0 +1,63 @@ +/* + * 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.common.requests; + +import org.apache.kafka.common.message.AllocateProducerIdsResponseData; +import org.apache.kafka.common.protocol.ApiKeys; +import org.apache.kafka.common.protocol.ByteBufferAccessor; +import org.apache.kafka.common.protocol.Errors; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Map; + +public class AllocateProducerIdsResponse extends AbstractResponse { + + private final AllocateProducerIdsResponseData data; + + public AllocateProducerIdsResponse(AllocateProducerIdsResponseData data) { + super(ApiKeys.ALLOCATE_PRODUCER_IDS); + this.data = data; + } + + @Override + public AllocateProducerIdsResponseData data() { + return data; + } + + /** + * The number of each type of error in the response, including {@link Errors#NONE} and top-level errors as well as + * more specifically scoped errors (such as topic or partition-level errors). + * + * @return A count of errors. + */ + @Override + public Map errorCounts() { + return Collections.singletonMap(Errors.forCode(data.errorCode()), 1); + } + + @Override + public int throttleTimeMs() { + return data.throttleTimeMs(); + } + + public static AllocateProducerIdsResponse parse(ByteBuffer buffer, short version) { + return new AllocateProducerIdsResponse(new AllocateProducerIdsResponseData( + new ByteBufferAccessor(buffer), version)); + } +} diff --git a/clients/src/main/resources/common/message/AllocateProducerIdsRequest.json b/clients/src/main/resources/common/message/AllocateProducerIdsRequest.json new file mode 100644 index 0000000000000..0cfa494291a36 --- /dev/null +++ b/clients/src/main/resources/common/message/AllocateProducerIdsRequest.json @@ -0,0 +1,29 @@ +// 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 implie +// See the License for the specific language governing permissions and +// limitations under the License. + +{ + "apiKey": 67, + "type": "request", + "listeners": ["controller", "zkBroker"], + "name": "AllocateProducerIdsRequest", + "validVersions": "0", + "flexibleVersions": "0+", + "fields": [ + { "name": "BrokerId", "type": "int32", "versions": "0+", "entityType": "brokerId", + "about": "The ID of the requesting broker" }, + { "name": "BrokerEpoch", "type": "int64", "versions": "0+", "default": "-1", + "about": "The epoch of the requesting broker" } + ] +} diff --git a/clients/src/main/resources/common/message/AllocateProducerIdsResponse.json b/clients/src/main/resources/common/message/AllocateProducerIdsResponse.json new file mode 100644 index 0000000000000..0d849c098568b --- /dev/null +++ b/clients/src/main/resources/common/message/AllocateProducerIdsResponse.json @@ -0,0 +1,32 @@ +// 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. + +{ + "apiKey": 67, + "type": "response", + "name": "AllocateProducerIdsResponse", + "validVersions": "0", + "flexibleVersions": "0+", + "fields": [ + { "name": "ThrottleTimeMs", "type": "int32", "versions": "0+", + "about": "The duration in milliseconds for which the request was throttled due to a quota violation, or zero if the request did not violate any quota." }, + { "name": "ErrorCode", "type": "int16", "versions": "0+", + "about": "The top level response error code" }, + { "name": "ProducerIdStart", "type": "int64", "versions": "0+", "entityType": "producerId", + "about": "The first producer ID in this range, inclusive"}, + { "name": "ProducerIdLen", "type": "int32", "versions": "0+", + "about": "The number of producer IDs in this range"} + ] +} diff --git a/clients/src/test/java/org/apache/kafka/common/protocol/ApiKeysTest.java b/clients/src/test/java/org/apache/kafka/common/protocol/ApiKeysTest.java index 17d2e1ce26e43..3c66b211bec4f 100644 --- a/clients/src/test/java/org/apache/kafka/common/protocol/ApiKeysTest.java +++ b/clients/src/test/java/org/apache/kafka/common/protocol/ApiKeysTest.java @@ -62,7 +62,7 @@ public void testAlterIsrIsClusterAction() { public void testResponseThrottleTime() { Set authenticationKeys = EnumSet.of(ApiKeys.SASL_HANDSHAKE, ApiKeys.SASL_AUTHENTICATE); // Newer protocol apis include throttle time ms even for cluster actions - Set clusterActionsWithThrottleTimeMs = EnumSet.of(ApiKeys.ALTER_ISR); + Set clusterActionsWithThrottleTimeMs = EnumSet.of(ApiKeys.ALTER_ISR, ApiKeys.ALLOCATE_PRODUCER_IDS); for (ApiKeys apiKey: ApiKeys.zkBrokerApis()) { Schema responseSchema = apiKey.messageType.responseSchemas()[apiKey.latestVersion()]; BoundField throttleTimeField = responseSchema.get("throttle_time_ms"); diff --git a/connect/transforms/src/main/java/org/apache/kafka/connect/transforms/Cast.java b/connect/transforms/src/main/java/org/apache/kafka/connect/transforms/Cast.java index 0a763cc24f85b..cf31a0082ed1f 100644 --- a/connect/transforms/src/main/java/org/apache/kafka/connect/transforms/Cast.java +++ b/connect/transforms/src/main/java/org/apache/kafka/connect/transforms/Cast.java @@ -120,6 +120,10 @@ public void configure(Map props) { @Override public R apply(R record) { + if (operatingValue(record) == null) { + return record; + } + if (operatingSchema(record) == null) { return applySchemaless(record); } else { diff --git a/connect/transforms/src/test/java/org/apache/kafka/connect/transforms/CastTest.java b/connect/transforms/src/test/java/org/apache/kafka/connect/transforms/CastTest.java index 764b904ea3e24..60744b275e42f 100644 --- a/connect/transforms/src/test/java/org/apache/kafka/connect/transforms/CastTest.java +++ b/connect/transforms/src/test/java/org/apache/kafka/connect/transforms/CastTest.java @@ -89,6 +89,42 @@ public void testConfigMixWholeAndFieldTransformation() { assertThrows(ConfigException.class, () -> xformKey.configure(Collections.singletonMap(Cast.SPEC_CONFIG, "foo:int8,int32"))); } + @Test + public void castNullValueRecordWithSchema() { + xformValue.configure(Collections.singletonMap(Cast.SPEC_CONFIG, "foo:int64")); + SourceRecord original = new SourceRecord(null, null, "topic", 0, + Schema.STRING_SCHEMA, "key", Schema.STRING_SCHEMA, null); + SourceRecord transformed = xformValue.apply(original); + assertEquals(original, transformed); + } + + @Test + public void castNullValueRecordSchemaless() { + xformValue.configure(Collections.singletonMap(Cast.SPEC_CONFIG, "foo:int64")); + SourceRecord original = new SourceRecord(null, null, "topic", 0, + Schema.STRING_SCHEMA, "key", null, null); + SourceRecord transformed = xformValue.apply(original); + assertEquals(original, transformed); + } + + @Test + public void castNullKeyRecordWithSchema() { + xformKey.configure(Collections.singletonMap(Cast.SPEC_CONFIG, "foo:int64")); + SourceRecord original = new SourceRecord(null, null, "topic", 0, + Schema.STRING_SCHEMA, null, Schema.STRING_SCHEMA, "value"); + SourceRecord transformed = xformKey.apply(original); + assertEquals(original, transformed); + } + + @Test + public void castNullKeyRecordSchemaless() { + xformKey.configure(Collections.singletonMap(Cast.SPEC_CONFIG, "foo:int64")); + SourceRecord original = new SourceRecord(null, null, "topic", 0, + null, null, Schema.STRING_SCHEMA, "value"); + SourceRecord transformed = xformKey.apply(original); + assertEquals(original, transformed); + } + @Test public void castWholeRecordKeyWithSchema() { xformKey.configure(Collections.singletonMap(Cast.SPEC_CONFIG, "int8")); diff --git a/core/src/main/scala/kafka/api/ApiVersion.scala b/core/src/main/scala/kafka/api/ApiVersion.scala index 879373787365f..724bc9894045a 100644 --- a/core/src/main/scala/kafka/api/ApiVersion.scala +++ b/core/src/main/scala/kafka/api/ApiVersion.scala @@ -112,7 +112,9 @@ object ApiVersion { // Flexible versioning on ListOffsets, WriteTxnMarkers and OffsetsForLeaderEpoch. Also adds topic IDs (KIP-516) KAFKA_2_8_IV0, // Introduced topic IDs to LeaderAndIsr and UpdateMetadata requests/responses (KIP-516) - KAFKA_2_8_IV1 + KAFKA_2_8_IV1, + // Introduce AllocateProducerIds (KIP-730) + KAFKA_3_0_IV0 ) // Map keys are the union of the short and full versions @@ -197,6 +199,8 @@ sealed trait ApiVersion extends Ordered[ApiVersion] { def isAlterIsrSupported: Boolean = this >= KAFKA_2_7_IV2 + def isAllocateProducerIdsSupported: Boolean = this >= KAFKA_3_0_IV0 + override def compare(that: ApiVersion): Int = ApiVersion.orderingByVersion.compare(this, that) @@ -447,6 +451,13 @@ case object KAFKA_2_8_IV1 extends DefaultApiVersion { val id: Int = 32 } +case object KAFKA_3_0_IV0 extends DefaultApiVersion { + val shortVersion: String = "3.0" + val subVersion = "IV0" + val recordVersion = RecordVersion.V2 + val id: Int = 33 +} + object ApiVersionValidator extends Validator { override def ensureValid(name: String, value: Any): Unit = { diff --git a/core/src/main/scala/kafka/controller/KafkaController.scala b/core/src/main/scala/kafka/controller/KafkaController.scala index b17a6e8e6f7da..654649b4f48c2 100644 --- a/core/src/main/scala/kafka/controller/KafkaController.scala +++ b/core/src/main/scala/kafka/controller/KafkaController.scala @@ -18,13 +18,13 @@ package kafka.controller import java.util import java.util.concurrent.TimeUnit - import kafka.admin.AdminOperationException import kafka.api._ import kafka.common._ import kafka.controller.KafkaController.AlterIsrCallback import kafka.cluster.Broker import kafka.controller.KafkaController.{AlterReassignmentsCallback, ElectLeadersCallback, ListReassignmentsCallback, UpdateFeaturesCallback} +import kafka.coordinator.transaction.ZkProducerIdManager import kafka.metrics.{KafkaMetricsGroup, KafkaTimer} import kafka.server._ import kafka.utils._ @@ -37,20 +37,20 @@ import org.apache.kafka.common.ElectionType import org.apache.kafka.common.KafkaException import org.apache.kafka.common.TopicPartition import org.apache.kafka.common.errors.{BrokerNotAvailableException, ControllerMovedException, StaleBrokerEpochException} -import org.apache.kafka.common.message.{AlterIsrRequestData, AlterIsrResponseData} +import org.apache.kafka.common.message.{AllocateProducerIdsRequestData, AllocateProducerIdsResponseData, AlterIsrRequestData, AlterIsrResponseData, UpdateFeaturesRequestData} import org.apache.kafka.common.feature.{Features, FinalizedVersionRange} -import org.apache.kafka.common.message.UpdateFeaturesRequestData import org.apache.kafka.common.metrics.Metrics import org.apache.kafka.common.protocol.Errors import org.apache.kafka.common.requests.{AbstractControlRequest, ApiError, LeaderAndIsrResponse, UpdateFeaturesRequest, UpdateMetadataResponse} import org.apache.kafka.common.utils.{Time, Utils} +import org.apache.kafka.server.common.ProducerIdsBlock import org.apache.zookeeper.KeeperException import org.apache.zookeeper.KeeperException.Code import scala.collection.{Map, Seq, Set, immutable, mutable} import scala.collection.mutable.ArrayBuffer import scala.jdk.CollectionConverters._ -import scala.util.{Failure, Try} +import scala.util.{Failure, Success, Try} sealed trait ElectionTrigger final case object AutoTriggered extends ElectionTrigger @@ -2376,6 +2376,54 @@ class KafkaController(val config: KafkaConfig, } } + def allocateProducerIds(allocateProducerIdsRequest: AllocateProducerIdsRequestData, + callback: AllocateProducerIdsResponseData => Unit): Unit = { + + def eventManagerCallback(results: Either[Errors, ProducerIdsBlock]): Unit = { + results match { + case Left(error) => callback.apply(new AllocateProducerIdsResponseData().setErrorCode(error.code)) + case Right(pidBlock) => callback.apply( + new AllocateProducerIdsResponseData() + .setProducerIdStart(pidBlock.producerIdStart()) + .setProducerIdLen(pidBlock.producerIdLen())) + } + } + eventManager.put(AllocateProducerIds(allocateProducerIdsRequest.brokerId, + allocateProducerIdsRequest.brokerEpoch, eventManagerCallback)) + } + + def processAllocateProducerIds(brokerId: Int, brokerEpoch: Long, callback: Either[Errors, ProducerIdsBlock] => Unit): Unit = { + // Handle a few short-circuits + if (!isActive) { + callback.apply(Left(Errors.NOT_CONTROLLER)) + return + } + + val brokerEpochOpt = controllerContext.liveBrokerIdAndEpochs.get(brokerId) + if (brokerEpochOpt.isEmpty) { + warn(s"Ignoring AllocateProducerIds due to unknown broker $brokerId") + callback.apply(Left(Errors.BROKER_ID_NOT_REGISTERED)) + return + } + + if (!brokerEpochOpt.contains(brokerEpoch)) { + warn(s"Ignoring AllocateProducerIds due to stale broker epoch $brokerEpoch for broker $brokerId") + callback.apply(Left(Errors.STALE_BROKER_EPOCH)) + return + } + + val maybeNewProducerIdsBlock = try { + Try(ZkProducerIdManager.getNewProducerIdBlock(brokerId, zkClient, this)) + } catch { + case ke: KafkaException => Failure(ke) + } + + maybeNewProducerIdsBlock match { + case Failure(exception) => callback.apply(Left(Errors.forException(exception))) + case Success(newProducerIdBlock) => callback.apply(Right(newProducerIdBlock)) + } + } + private def processControllerChange(): Unit = { maybeResign() } @@ -2454,6 +2502,8 @@ class KafkaController(val config: KafkaConfig, processIsrChangeNotification() case AlterIsrReceived(brokerId, brokerEpoch, isrsToAlter, callback) => processAlterIsr(brokerId, brokerEpoch, isrsToAlter, callback) + case AllocateProducerIds(brokerId, brokerEpoch, callback) => + processAllocateProducerIds(brokerId, brokerEpoch, callback) case Startup => processStartup() } @@ -2747,6 +2797,12 @@ case class UpdateFeatures(request: UpdateFeaturesRequest, override def preempt(): Unit = {} } +case class AllocateProducerIds(brokerId: Int, brokerEpoch: Long, callback: Either[Errors, ProducerIdsBlock] => Unit) + extends ControllerEvent { + override def state: ControllerState = ControllerState.Idle + override def preempt(): Unit = {} +} + // Used only in test cases abstract class MockEvent(val state: ControllerState) extends ControllerEvent { diff --git a/core/src/main/scala/kafka/coordinator/transaction/ProducerIdManager.scala b/core/src/main/scala/kafka/coordinator/transaction/ProducerIdManager.scala index 8d3f542c2b725..b5d419ddf4acb 100644 --- a/core/src/main/scala/kafka/coordinator/transaction/ProducerIdManager.scala +++ b/core/src/main/scala/kafka/coordinator/transaction/ProducerIdManager.scala @@ -16,146 +16,227 @@ */ package kafka.coordinator.transaction -import java.nio.charset.StandardCharsets - -import kafka.utils.{Json, Logging} +import kafka.server.{BrokerToControllerChannelManager, ControllerRequestCompletionHandler} +import kafka.utils.Logging import kafka.zk.{KafkaZkClient, ProducerIdBlockZNode} +import org.apache.kafka.clients.ClientResponse import org.apache.kafka.common.KafkaException +import org.apache.kafka.common.message.AllocateProducerIdsRequestData +import org.apache.kafka.common.protocol.Errors +import org.apache.kafka.common.requests.{AllocateProducerIdsRequest, AllocateProducerIdsResponse} +import org.apache.kafka.server.common.ProducerIdsBlock -import scala.jdk.CollectionConverters._ +import java.util.concurrent.{ArrayBlockingQueue, TimeUnit} +import java.util.concurrent.atomic.AtomicBoolean +import scala.util.{Failure, Success, Try} /** * ProducerIdManager is the part of the transaction coordinator that provides ProducerIds in a unique way * such that the same producerId will not be assigned twice across multiple transaction coordinators. * - * ProducerIds are managed via ZooKeeper, where the latest producerId block is written on the corresponding ZK - * path by the manager who claims the block, where the written block_start and block_end are both inclusive. + * ProducerIds are managed by the controller. When requesting a new range of IDs, we are guaranteed to receive + * a unique block. */ -object ProducerIdManager extends Logging { - val CurrentVersion: Long = 1L - val PidBlockSize: Long = 1000L - - def generateProducerIdBlockJson(producerIdBlock: ProducerIdBlock): Array[Byte] = { - Json.encodeAsBytes(Map("version" -> CurrentVersion, - "broker" -> producerIdBlock.brokerId, - "block_start" -> producerIdBlock.blockStartId.toString, - "block_end" -> producerIdBlock.blockEndId.toString).asJava - ) - } - def parseProducerIdBlockData(jsonData: Array[Byte]): ProducerIdBlock = { - try { - Json.parseBytes(jsonData).map(_.asJsonObject).flatMap { js => - val brokerId = js("broker").to[Int] - val blockStart = js("block_start").to[String].toLong - val blockEnd = js("block_end").to[String].toLong - Some(ProducerIdBlock(brokerId, blockStart, blockEnd)) - }.getOrElse(throw new KafkaException(s"Failed to parse the producerId block json $jsonData")) - } catch { - case e: java.lang.NumberFormatException => - // this should never happen: the written data has exceeded long type limit - fatal(s"Read jason data $jsonData contains producerIds that have exceeded long type limit") - throw e - } +object ProducerIdManager { + // Once we reach this percentage of PIDs consumed from the current block, trigger a fetch of the next block + val PidPrefetchThreshold = 0.90 + + // Creates a ProducerIdGenerate that directly interfaces with ZooKeeper, IBP < 3.0-IV0 + def zk(brokerId: Int, zkClient: KafkaZkClient): ZkProducerIdManager = { + new ZkProducerIdManager(brokerId, zkClient) } -} -case class ProducerIdBlock(brokerId: Int, blockStartId: Long, blockEndId: Long) { - override def toString: String = { - val producerIdBlockInfo = new StringBuilder - producerIdBlockInfo.append("(brokerId:" + brokerId) - producerIdBlockInfo.append(",blockStartProducerId:" + blockStartId) - producerIdBlockInfo.append(",blockEndProducerId:" + blockEndId + ")") - producerIdBlockInfo.toString() + // Creates a ProducerIdGenerate that uses AllocateProducerIds RPC, IBP >= 3.0-IV0 + def rpc(brokerId: Int, + brokerEpochSupplier: () => Long, + controllerChannel: BrokerToControllerChannelManager, + maxWaitMs: Int): RPCProducerIdManager = { + new RPCProducerIdManager(brokerId, brokerEpochSupplier, controllerChannel, maxWaitMs) } } -trait ProducerIdGenerator { +trait ProducerIdManager { def generateProducerId(): Long def shutdown() : Unit = {} } -class ProducerIdManager(val brokerId: Int, val zkClient: KafkaZkClient) extends ProducerIdGenerator with Logging { - - this.logIdent = "[ProducerId Manager " + brokerId + "]: " - - private var currentProducerIdBlock: ProducerIdBlock = null - private var nextProducerId: Long = -1L - - // grab the first block of producerIds - this synchronized { - getNewProducerIdBlock() - nextProducerId = currentProducerIdBlock.blockStartId - } - - private def getNewProducerIdBlock(): Unit = { +object ZkProducerIdManager { + def getNewProducerIdBlock(brokerId: Int, zkClient: KafkaZkClient, logger: Logging): ProducerIdsBlock = { + // Get or create the existing PID block from ZK and attempt to update it. We retry in a loop here since other + // brokers may be generating PID blocks during a rolling upgrade var zkWriteComplete = false while (!zkWriteComplete) { // refresh current producerId block from zookeeper again val (dataOpt, zkVersion) = zkClient.getDataAndVersion(ProducerIdBlockZNode.path) // generate the new producerId block - currentProducerIdBlock = dataOpt match { + val newProducerIdBlock = dataOpt match { case Some(data) => - val currProducerIdBlock = ProducerIdManager.parseProducerIdBlockData(data) - debug(s"Read current producerId block $currProducerIdBlock, Zk path version $zkVersion") + val currProducerIdBlock = ProducerIdBlockZNode.parseProducerIdBlockData(data) + logger.debug(s"Read current producerId block $currProducerIdBlock, Zk path version $zkVersion") - if (currProducerIdBlock.blockEndId > Long.MaxValue - ProducerIdManager.PidBlockSize) { + if (currProducerIdBlock.producerIdEnd > Long.MaxValue - ProducerIdsBlock.PRODUCER_ID_BLOCK_SIZE) { // we have exhausted all producerIds (wow!), treat it as a fatal error - fatal(s"Exhausted all producerIds as the next block's end producerId is will has exceeded long type limit (current block end producerId is ${currProducerIdBlock.blockEndId})") + logger.fatal(s"Exhausted all producerIds as the next block's end producerId is will has exceeded long type limit (current block end producerId is ${currProducerIdBlock.producerIdEnd})") throw new KafkaException("Have exhausted all producerIds.") } - ProducerIdBlock(brokerId, currProducerIdBlock.blockEndId + 1L, currProducerIdBlock.blockEndId + ProducerIdManager.PidBlockSize) + new ProducerIdsBlock(brokerId, currProducerIdBlock.producerIdEnd + 1L, ProducerIdsBlock.PRODUCER_ID_BLOCK_SIZE) case None => - debug(s"There is no producerId block yet (Zk path version $zkVersion), creating the first block") - ProducerIdBlock(brokerId, 0L, ProducerIdManager.PidBlockSize - 1) + logger.debug(s"There is no producerId block yet (Zk path version $zkVersion), creating the first block") + new ProducerIdsBlock(brokerId, 0L, ProducerIdsBlock.PRODUCER_ID_BLOCK_SIZE) } - val newProducerIdBlockData = ProducerIdManager.generateProducerIdBlockJson(currentProducerIdBlock) + val newProducerIdBlockData = ProducerIdBlockZNode.generateProducerIdBlockJson(newProducerIdBlock) // try to write the new producerId block into zookeeper - val (succeeded, version) = zkClient.conditionalUpdatePath(ProducerIdBlockZNode.path, - newProducerIdBlockData, zkVersion, Some(checkProducerIdBlockZkData)) + val (succeeded, version) = zkClient.conditionalUpdatePath(ProducerIdBlockZNode.path, newProducerIdBlockData, zkVersion, None) zkWriteComplete = succeeded - if (zkWriteComplete) - info(s"Acquired new producerId block $currentProducerIdBlock by writing to Zk with path version $version") + if (zkWriteComplete) { + logger.info(s"Acquired new producerId block $newProducerIdBlock by writing to Zk with path version $version") + return newProducerIdBlock + } } + throw new IllegalStateException() } +} - private def checkProducerIdBlockZkData(zkClient: KafkaZkClient, path: String, expectedData: Array[Byte]): (Boolean, Int) = { - try { - val expectedPidBlock = ProducerIdManager.parseProducerIdBlockData(expectedData) - zkClient.getDataAndVersion(ProducerIdBlockZNode.path) match { - case (Some(data), zkVersion) => - val currProducerIdBLock = ProducerIdManager.parseProducerIdBlockData(data) - (currProducerIdBLock == expectedPidBlock, zkVersion) - case (None, _) => (false, -1) - } - } catch { - case e: Exception => - warn(s"Error while checking for producerId block Zk data on path $path: expected data " + - s"${new String(expectedData, StandardCharsets.UTF_8)}", e) - (false, -1) +class ZkProducerIdManager(brokerId: Int, + zkClient: KafkaZkClient) extends ProducerIdManager with Logging { + + this.logIdent = "[ZK ProducerId Manager " + brokerId + "]: " + + private var currentProducerIdBlock: ProducerIdsBlock = ProducerIdsBlock.EMPTY + private var nextProducerId: Long = _ + + // grab the first block of producerIds + this synchronized { + allocateNewProducerIdBlock() + nextProducerId = currentProducerIdBlock.producerIdStart + } + + private def allocateNewProducerIdBlock(): Unit = { + this synchronized { + currentProducerIdBlock = ZkProducerIdManager.getNewProducerIdBlock(brokerId, zkClient, this) } } def generateProducerId(): Long = { this synchronized { // grab a new block of producerIds if this block has been exhausted - if (nextProducerId > currentProducerIdBlock.blockEndId) { - getNewProducerIdBlock() - nextProducerId = currentProducerIdBlock.blockStartId + 1 + if (nextProducerId > currentProducerIdBlock.producerIdEnd) { + allocateNewProducerIdBlock() + nextProducerId = currentProducerIdBlock.producerIdStart + } + nextProducerId += 1 + nextProducerId - 1 + } + } +} + +class RPCProducerIdManager(brokerId: Int, + brokerEpochSupplier: () => Long, + controllerChannel: BrokerToControllerChannelManager, + maxWaitMs: Int) extends ProducerIdManager with Logging { + + this.logIdent = "[RPC ProducerId Manager " + brokerId + "]: " + + private val nextProducerIdBlock = new ArrayBlockingQueue[Try[ProducerIdsBlock]](1) + private val requestInFlight = new AtomicBoolean(false) + private var currentProducerIdBlock: ProducerIdsBlock = ProducerIdsBlock.EMPTY + private var nextProducerId: Long = -1L + + override def generateProducerId(): Long = { + this synchronized { + if (nextProducerId == -1L) { + // Send an initial request to get the first block + maybeRequestNextBlock() + nextProducerId = 0L } else { nextProducerId += 1 + + // Check if we need to fetch the next block + if (nextProducerId >= (currentProducerIdBlock.producerIdStart + currentProducerIdBlock.producerIdLen * ProducerIdManager.PidPrefetchThreshold)) { + maybeRequestNextBlock() + } } - nextProducerId - 1 + // If we've exhausted the current block, grab the next block (waiting if necessary) + if (nextProducerId > currentProducerIdBlock.producerIdEnd) { + val block = nextProducerIdBlock.poll(maxWaitMs, TimeUnit.MILLISECONDS) + if (block == null) { + throw Errors.REQUEST_TIMED_OUT.exception("Timed out waiting for next producer ID block") + } else { + block match { + case Success(nextBlock) => + currentProducerIdBlock = nextBlock + nextProducerId = currentProducerIdBlock.producerIdStart + case Failure(t) => throw t + } + } + } + nextProducerId + } + } + + + private def maybeRequestNextBlock(): Unit = { + if (nextProducerIdBlock.isEmpty && requestInFlight.compareAndSet(false, true)) { + sendRequest() + } + } + + private[transaction] def sendRequest(): Unit = { + val message = new AllocateProducerIdsRequestData() + .setBrokerEpoch(brokerEpochSupplier.apply()) + .setBrokerId(brokerId) + + val request = new AllocateProducerIdsRequest.Builder(message) + debug("Requesting next Producer ID block") + controllerChannel.sendRequest(request, new ControllerRequestCompletionHandler() { + override def onComplete(response: ClientResponse): Unit = { + val message = response.responseBody().asInstanceOf[AllocateProducerIdsResponse] + handleAllocateProducerIdsResponse(message) + } + + override def onTimeout(): Unit = handleTimeout() + }) + } + + private[transaction] def handleAllocateProducerIdsResponse(response: AllocateProducerIdsResponse): Unit = { + requestInFlight.set(false) + val data = response.data + Errors.forCode(data.errorCode()) match { + case Errors.NONE => + debug(s"Got next producer ID block from controller $data") + // Do some sanity checks on the response + if (data.producerIdStart() < currentProducerIdBlock.producerIdEnd) { + nextProducerIdBlock.put(Failure(new KafkaException( + s"Producer ID block is not monotonic with current block: current=$currentProducerIdBlock response=$data"))) + } else if (data.producerIdStart() < 0 || data.producerIdLen() < 0 || data.producerIdStart() > Long.MaxValue - data.producerIdLen()) { + nextProducerIdBlock.put(Failure(new KafkaException(s"Producer ID block includes invalid ID range: $data"))) + } else { + nextProducerIdBlock.put( + Success(new ProducerIdsBlock(brokerId, data.producerIdStart(), data.producerIdLen()))) + } + case Errors.STALE_BROKER_EPOCH => + warn("Our broker epoch was stale, trying again.") + maybeRequestNextBlock() + case Errors.BROKER_ID_NOT_REGISTERED => + warn("Our broker ID is not yet known by the controller, trying again.") + maybeRequestNextBlock() + case e: Errors => + warn("Had an unknown error from the controller, giving up.") + nextProducerIdBlock.put(Failure(e.exception())) } } - override def shutdown(): Unit = { - info(s"Shutdown complete: last producerId assigned $nextProducerId") + private[transaction] def handleTimeout(): Unit = { + warn("Timed out when requesting AllocateProducerIds from the controller.") + requestInFlight.set(false) + nextProducerIdBlock.put(Failure(Errors.REQUEST_TIMED_OUT.exception)) + maybeRequestNextBlock() } } diff --git a/core/src/main/scala/kafka/coordinator/transaction/TransactionCoordinator.scala b/core/src/main/scala/kafka/coordinator/transaction/TransactionCoordinator.scala index 0e3fa28098118..543e9c85c36d5 100644 --- a/core/src/main/scala/kafka/coordinator/transaction/TransactionCoordinator.scala +++ b/core/src/main/scala/kafka/coordinator/transaction/TransactionCoordinator.scala @@ -35,7 +35,7 @@ object TransactionCoordinator { def apply(config: KafkaConfig, replicaManager: ReplicaManager, scheduler: Scheduler, - createProducerIdGenerator: () => ProducerIdGenerator, + createProducerIdGenerator: () => ProducerIdManager, metrics: Metrics, metadataCache: MetadataCache, time: Time): TransactionCoordinator = { @@ -82,7 +82,7 @@ object TransactionCoordinator { class TransactionCoordinator(brokerId: Int, txnConfig: TransactionConfig, scheduler: Scheduler, - createProducerIdGenerator: () => ProducerIdGenerator, + createProducerIdManager: () => ProducerIdManager, txnManager: TransactionStateManager, txnMarkerChannelManager: TransactionMarkerChannelManager, time: Time, @@ -99,7 +99,7 @@ class TransactionCoordinator(brokerId: Int, /* Active flag of the coordinator */ private val isActive = new AtomicBoolean(false) - val producerIdGenerator = createProducerIdGenerator() + val producerIdManager = createProducerIdManager() def handleInitProducerId(transactionalId: String, transactionTimeoutMs: Int, @@ -109,7 +109,7 @@ class TransactionCoordinator(brokerId: Int, if (transactionalId == null) { // if the transactional id is null, then always blindly accept the request // and return a new producerId from the producerId manager - val producerId = producerIdGenerator.generateProducerId() + val producerId = producerIdManager.generateProducerId() responseCallback(InitProducerIdResult(producerId, producerEpoch = 0, Errors.NONE)) } else if (transactionalId.isEmpty) { // if transactional id is empty then return error as invalid request. This is @@ -121,7 +121,7 @@ class TransactionCoordinator(brokerId: Int, } else { val coordinatorEpochAndMetadata = txnManager.getTransactionState(transactionalId).flatMap { case None => - val producerId = producerIdGenerator.generateProducerId() + val producerId = producerIdManager.generateProducerId() val createdMetadata = new TransactionMetadata(transactionalId = transactionalId, producerId = producerId, lastProducerId = RecordBatch.NO_PRODUCER_ID, @@ -225,7 +225,7 @@ class TransactionCoordinator(brokerId: Int, // If the epoch is exhausted and the expected epoch (if provided) matches it, generate a new producer ID if (txnMetadata.isProducerEpochExhausted && expectedProducerIdAndEpoch.forall(_.epoch == txnMetadata.producerEpoch)) { - val newProducerId = producerIdGenerator.generateProducerId() + val newProducerId = producerIdManager.generateProducerId() Right(txnMetadata.prepareProducerIdRotation(newProducerId, transactionTimeoutMs, time.milliseconds(), expectedProducerIdAndEpoch.isDefined)) } else { @@ -675,7 +675,7 @@ class TransactionCoordinator(brokerId: Int, info("Shutting down.") isActive.set(false) scheduler.shutdown() - producerIdGenerator.shutdown() + producerIdManager.shutdown() txnManager.shutdown() txnMarkerChannelManager.shutdown() info("Shutdown complete.") diff --git a/core/src/main/scala/kafka/log/Log.scala b/core/src/main/scala/kafka/log/Log.scala index ef0d6aed5f0d8..0a4851ba6842a 100644 --- a/core/src/main/scala/kafka/log/Log.scala +++ b/core/src/main/scala/kafka/log/Log.scala @@ -557,7 +557,7 @@ class Log(@volatile private var _dir: File, } private def initializeLeaderEpochCache(): Unit = lock synchronized { - leaderEpochCache = Log.maybeCreateLeaderEpochCache(dir, topicPartition, logDirFailureChannel, recordVersion) + leaderEpochCache = Log.maybeCreateLeaderEpochCache(dir, topicPartition, logDirFailureChannel, recordVersion, logIdent) } private def updateLogEndOffset(offset: Long): Unit = { @@ -592,7 +592,7 @@ class Log(@volatile private var _dir: File, producerStateManager: ProducerStateManager): Unit = lock synchronized { checkIfMemoryMappedBufferClosed() Log.rebuildProducerState(producerStateManager, segments, logStartOffset, lastOffset, recordVersion, time, - reloadFromCleanShutdown = false) + reloadFromCleanShutdown = false, logIdent) } def activeProducers: Seq[DescribeProducersResponseData.ProducerState] = { @@ -1888,14 +1888,14 @@ class Log(@volatile private var _dir: File, private def deleteSegmentFiles(segments: Iterable[LogSegment], asyncDelete: Boolean, deleteProducerStateSnapshots: Boolean = true): Unit = { Log.deleteSegmentFiles(segments, asyncDelete, deleteProducerStateSnapshots, dir, topicPartition, - config, scheduler, logDirFailureChannel, producerStateManager) + config, scheduler, logDirFailureChannel, producerStateManager, this.logIdent) } private[log] def replaceSegments(newSegments: Seq[LogSegment], oldSegments: Seq[LogSegment], isRecoveredSwapFile: Boolean = false): Unit = { lock synchronized { checkIfMemoryMappedBufferClosed() Log.replaceSegments(segments, newSegments, oldSegments, isRecoveredSwapFile, dir, topicPartition, - config, scheduler, logDirFailureChannel, producerStateManager) + config, scheduler, logDirFailureChannel, producerStateManager, this.logIdent) } } @@ -1937,7 +1937,7 @@ class Log(@volatile private var _dir: File, } private[log] def splitOverflowedSegment(segment: LogSegment): List[LogSegment] = lock synchronized { - Log.splitOverflowedSegment(segment, segments, dir, topicPartition, config, scheduler, logDirFailureChannel, producerStateManager) + Log.splitOverflowedSegment(segment, segments, dir, topicPartition, config, scheduler, logDirFailureChannel, producerStateManager, this.logIdent) } } @@ -2005,7 +2005,12 @@ object Log extends Logging { Files.createDirectories(dir.toPath) val topicPartition = Log.parseTopicPartitionName(dir) val segments = new LogSegments(topicPartition) - val leaderEpochCache = Log.maybeCreateLeaderEpochCache(dir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion) + val leaderEpochCache = Log.maybeCreateLeaderEpochCache( + dir, + topicPartition, + logDirFailureChannel, + config.messageFormatVersion.recordVersion, + s"[Log partition=$topicPartition, dir=${dir.getParent}] ") val producerStateManager = new ProducerStateManager(topicPartition, dir, maxProducerIdExpirationMs) val offsets = LogLoader.load(LoadLogParams( dir, @@ -2226,12 +2231,14 @@ object Log extends Logging { * @param topicPartition The topic partition * @param logDirFailureChannel The LogDirFailureChannel to asynchronously handle log dir failure * @param recordVersion The record version + * @param logPrefix The logging prefix * @return The new LeaderEpochFileCache instance (if created), none otherwise */ def maybeCreateLeaderEpochCache(dir: File, topicPartition: TopicPartition, logDirFailureChannel: LogDirFailureChannel, - recordVersion: RecordVersion): Option[LeaderEpochFileCache] = { + recordVersion: RecordVersion, + logPrefix: String): Option[LeaderEpochFileCache] = { val leaderEpochFile = LeaderEpochCheckpointFile.newFile(dir) def newLeaderEpochFileCache(): LeaderEpochFileCache = { @@ -2246,7 +2253,7 @@ object Log extends Logging { None if (currentCache.exists(_.nonEmpty)) - warn(s"Deleting non-empty leader epoch cache due to incompatible message format $recordVersion") + warn(s"${logPrefix}Deleting non-empty leader epoch cache due to incompatible message format $recordVersion") Files.deleteIfExists(leaderEpochFile.toPath) None @@ -2293,6 +2300,7 @@ object Log extends Logging { * @param logDirFailureChannel The LogDirFailureChannel to asynchronously handle log dir failure * @param producerStateManager The ProducerStateManager instance (if any) containing state associated * with the existingSegments + * @param logPrefix The logging prefix */ private[log] def replaceSegments(existingSegments: LogSegments, newSegments: Seq[LogSegment], @@ -2303,7 +2311,8 @@ object Log extends Logging { config: LogConfig, scheduler: Scheduler, logDirFailureChannel: LogDirFailureChannel, - producerStateManager: ProducerStateManager): Unit = { + producerStateManager: ProducerStateManager, + logPrefix: String): Unit = { val sortedNewSegments = newSegments.sortBy(_.baseOffset) // Some old segments may have been removed from index and scheduled for async deletion after the caller reads segments // but before this method is executed. We want to filter out those segments to avoid calling asyncDeleteSegment() @@ -2332,7 +2341,8 @@ object Log extends Logging { config, scheduler, logDirFailureChannel, - producerStateManager) + producerStateManager, + logPrefix) } // okay we are safe now, remove the swap suffix sortedNewSegments.foreach(_.changeFileSuffixes(Log.SwapFileSuffix, "")) @@ -2359,7 +2369,7 @@ object Log extends Logging { * @param logDirFailureChannel The LogDirFailureChannel to asynchronously handle log dir failure * @param producerStateManager The ProducerStateManager instance (if any) containing state associated * with the existingSegments - * + * @param logPrefix The logging prefix * @throws IOException if the file can't be renamed and still exists */ private[log] def deleteSegmentFiles(segmentsToDelete: Iterable[LogSegment], @@ -2370,11 +2380,12 @@ object Log extends Logging { config: LogConfig, scheduler: Scheduler, logDirFailureChannel: LogDirFailureChannel, - producerStateManager: ProducerStateManager): Unit = { + producerStateManager: ProducerStateManager, + logPrefix: String): Unit = { segmentsToDelete.foreach(_.changeFileSuffixes("", Log.DeletedFileSuffix)) def deleteSegments(): Unit = { - info(s"Deleting segment files ${segmentsToDelete.mkString(",")}") + info(s"${logPrefix}Deleting segment files ${segmentsToDelete.mkString(",")}") val parentDir = dir.getParent maybeHandleIOException(logDirFailureChannel, parentDir, s"Error while deleting segments for $topicPartition in dir $parentDir") { segmentsToDelete.foreach { segment => @@ -2429,6 +2440,7 @@ object Log extends Logging { * @param time The time instance used for checking the clock * @param reloadFromCleanShutdown True if the producer state is being built after a clean shutdown, * false otherwise. + * @param logPrefix The logging prefix */ private[log] def rebuildProducerState(producerStateManager: ProducerStateManager, segments: LogSegments, @@ -2436,7 +2448,8 @@ object Log extends Logging { lastOffset: Long, recordVersion: RecordVersion, time: Time, - reloadFromCleanShutdown: Boolean): Unit = { + reloadFromCleanShutdown: Boolean, + logPrefix: String): Unit = { val allSegments = segments.values val offsetsToSnapshot = if (allSegments.nonEmpty) { @@ -2445,7 +2458,7 @@ object Log extends Logging { } else { Seq(Some(lastOffset)) } - info(s"Loading producer state till offset $lastOffset with message format version ${recordVersion.value}") + info(s"${logPrefix}Loading producer state till offset $lastOffset with message format version ${recordVersion.value}") // We want to avoid unnecessary scanning of the log to build the producer state when the broker is being // upgraded. The basic idea is to use the absence of producer snapshot files to detect the upgrade case, @@ -2469,7 +2482,7 @@ object Log extends Logging { producerStateManager.takeSnapshot() } } else { - info(s"Reloading from producer snapshot and rebuilding producer state from offset $lastOffset") + info(s"${logPrefix}Reloading from producer snapshot and rebuilding producer state from offset $lastOffset") val isEmptyBeforeTruncation = producerStateManager.isEmpty && producerStateManager.mapEndOffset >= lastOffset val producerStateLoadStart = time.milliseconds() producerStateManager.truncateAndReload(logStartOffset, lastOffset, time.milliseconds()) @@ -2508,7 +2521,7 @@ object Log extends Logging { } producerStateManager.updateMapEndOffset(lastOffset) producerStateManager.takeSnapshot() - info(s"Producer state recovery took ${segmentRecoveryStart - producerStateLoadStart}ms for snapshot load " + + info(s"${logPrefix}Producer state recovery took ${segmentRecoveryStart - producerStateLoadStart}ms for snapshot load " + s"and ${time.milliseconds() - segmentRecoveryStart}ms for segment recovery from offset $lastOffset") } } @@ -2535,6 +2548,7 @@ object Log extends Logging { * @param logDirFailureChannel The LogDirFailureChannel to asynchronously handle log dir failure * @param producerStateManager The ProducerStateManager instance (if any) containing state associated * with the existingSegments + * @param logPrefix The logging prefix * @return List of new segments that replace the input segment */ private[log] def splitOverflowedSegment(segment: LogSegment, @@ -2544,11 +2558,12 @@ object Log extends Logging { config: LogConfig, scheduler: Scheduler, logDirFailureChannel: LogDirFailureChannel, - producerStateManager: ProducerStateManager): List[LogSegment] = { + producerStateManager: ProducerStateManager, + logPrefix: String): List[LogSegment] = { require(Log.isLogFile(segment.log.file), s"Cannot split file ${segment.log.file.getAbsoluteFile}") require(segment.hasOverflow, "Split operation is only permitted for segments with overflow") - info(s"Splitting overflowed segment $segment") + info(s"${logPrefix}Splitting overflowed segment $segment") val newSegments = ListBuffer[LogSegment]() try { @@ -2581,9 +2596,9 @@ object Log extends Logging { s" before: ${segment.log.sizeInBytes} after: $totalSizeOfNewSegments") // replace old segment with new ones - info(s"Replacing overflowed segment $segment with split segments $newSegments") + info(s"${logPrefix}Replacing overflowed segment $segment with split segments $newSegments") replaceSegments(existingSegments, newSegments.toList, List(segment), isRecoveredSwapFile = false, - dir, topicPartition, config, scheduler, logDirFailureChannel, producerStateManager) + dir, topicPartition, config, scheduler, logDirFailureChannel, producerStateManager, logPrefix) newSegments.toList } catch { case e: Exception => diff --git a/core/src/main/scala/kafka/log/LogLoader.scala b/core/src/main/scala/kafka/log/LogLoader.scala index 6b28ec51bb91d..bfadb78f7ab5a 100644 --- a/core/src/main/scala/kafka/log/LogLoader.scala +++ b/core/src/main/scala/kafka/log/LogLoader.scala @@ -67,7 +67,7 @@ case class LoadLogParams(dir: File, maxProducerIdExpirationMs: Int, leaderEpochCache: Option[LeaderEpochFileCache], producerStateManager: ProducerStateManager) { - val logIdentifier: String = s"[LogLoader partition=$topicPartition, dir=${dir.getParent}]" + val logIdentifier: String = s"[LogLoader partition=$topicPartition, dir=${dir.getParent}] " } /** @@ -152,7 +152,8 @@ object LogLoader extends Logging { nextOffset, params.config.messageFormatVersion.recordVersion, params.time, - reloadFromCleanShutdown = params.hadCleanShutdown) + reloadFromCleanShutdown = params.hadCleanShutdown, + params.logIdentifier) val activeSegment = params.segments.lastSegment.get LoadedLogOffsets( @@ -172,7 +173,7 @@ object LogLoader extends Logging { private def removeTempFilesAndCollectSwapFiles(params: LoadLogParams): Set[File] = { def deleteIndicesIfExist(baseFile: File, suffix: String = ""): Unit = { - info(s"${params.logIdentifier} Deleting index files with suffix $suffix for baseFile $baseFile") + info(s"${params.logIdentifier}Deleting index files with suffix $suffix for baseFile $baseFile") val offset = offsetFromFile(baseFile) Files.deleteIfExists(Log.offsetIndexFile(params.dir, offset, suffix).toPath) Files.deleteIfExists(Log.timeIndexFile(params.dir, offset, suffix).toPath) @@ -188,7 +189,7 @@ object LogLoader extends Logging { throw new IOException(s"Could not read file $file") val filename = file.getName if (filename.endsWith(DeletedFileSuffix)) { - debug(s"${params.logIdentifier} Deleting stray temporary file ${file.getAbsolutePath}") + debug(s"${params.logIdentifier}Deleting stray temporary file ${file.getAbsolutePath}") Files.deleteIfExists(file.toPath) } else if (filename.endsWith(CleanedFileSuffix)) { minCleanedFileOffset = Math.min(offsetFromFileName(filename), minCleanedFileOffset) @@ -198,7 +199,7 @@ object LogLoader extends Logging { // if a log, delete the index files, complete the swap operation later // if an index just delete the index files, they will be rebuilt val baseFile = new File(CoreUtils.replaceSuffix(file.getPath, SwapFileSuffix, "")) - info(s"${params.logIdentifier} Found file ${file.getAbsolutePath} from interrupted swap operation.") + info(s"${params.logIdentifier}Found file ${file.getAbsolutePath} from interrupted swap operation.") if (Log.isIndexFile(baseFile)) { deleteIndicesIfExist(baseFile) } else if (Log.isLogFile(baseFile)) { @@ -213,7 +214,7 @@ object LogLoader extends Logging { // for more details about the split operation. val (invalidSwapFiles, validSwapFiles) = swapFiles.partition(file => offsetFromFile(file) >= minCleanedFileOffset) invalidSwapFiles.foreach { file => - debug(s"${params.logIdentifier} Deleting invalid swap file ${file.getAbsoluteFile} minCleanedFileOffset: $minCleanedFileOffset") + debug(s"${params.logIdentifier}Deleting invalid swap file ${file.getAbsoluteFile} minCleanedFileOffset: $minCleanedFileOffset") val baseFile = new File(CoreUtils.replaceSuffix(file.getPath, SwapFileSuffix, "")) deleteIndicesIfExist(baseFile, SwapFileSuffix) Files.deleteIfExists(file.toPath) @@ -221,7 +222,7 @@ object LogLoader extends Logging { // Now that we have deleted all .swap files that constitute an incomplete split operation, let's delete all .clean files cleanFiles.foreach { file => - debug(s"${params.logIdentifier} Deleting stray .clean file ${file.getAbsolutePath}") + debug(s"${params.logIdentifier}Deleting stray .clean file ${file.getAbsolutePath}") Files.deleteIfExists(file.toPath) } @@ -245,7 +246,7 @@ object LogLoader extends Logging { return fn } catch { case e: LogSegmentOffsetOverflowException => - info(s"${params.logIdentifier} Caught segment overflow error: ${e.getMessage}. Split segment and retry.") + info(s"${params.logIdentifier}Caught segment overflow error: ${e.getMessage}. Split segment and retry.") Log.splitOverflowedSegment( e.segment, params.segments, @@ -254,7 +255,8 @@ object LogLoader extends Logging { params.config, params.scheduler, params.logDirFailureChannel, - params.producerStateManager) + params.producerStateManager, + params.logIdentifier) } } throw new IllegalStateException() @@ -280,7 +282,7 @@ object LogLoader extends Logging { val offset = offsetFromFile(file) val logFile = Log.logFile(params.dir, offset) if (!logFile.exists) { - warn(s"${params.logIdentifier} Found an orphaned index file ${file.getAbsolutePath}, with no corresponding log file.") + warn(s"${params.logIdentifier}Found an orphaned index file ${file.getAbsolutePath}, with no corresponding log file.") Files.deleteIfExists(file.toPath) } } else if (isLogFile(file)) { @@ -297,11 +299,11 @@ object LogLoader extends Logging { try segment.sanityCheck(timeIndexFileNewlyCreated) catch { case _: NoSuchFileException => - error(s"${params.logIdentifier} Could not find offset index file corresponding to log file" + + error(s"${params.logIdentifier}Could not find offset index file corresponding to log file" + s" ${segment.log.file.getAbsolutePath}, recovering segment and rebuilding index files...") recoverSegment(segment, params) case e: CorruptIndexException => - warn(s"${params.logIdentifier} Found a corrupted index file corresponding to log file" + + warn(s"${params.logIdentifier}Found a corrupted index file corresponding to log file" + s" ${segment.log.file.getAbsolutePath} due to ${e.getMessage}}, recovering segment and" + " rebuilding index files...") recoverSegment(segment, params) @@ -330,7 +332,8 @@ object LogLoader extends Logging { segment.baseOffset, params.config.messageFormatVersion.recordVersion, params.time, - reloadFromCleanShutdown = false) + reloadFromCleanShutdown = false, + params.logIdentifier) val bytesTruncated = segment.recover(producerStateManager, params.leaderEpochCache) // once we have recovered the segment's data, take a snapshot to ensure that we won't // need to reload the same segment again while recovering another segment. @@ -390,7 +393,8 @@ object LogLoader extends Logging { params.config, params.scheduler, params.logDirFailureChannel, - params.producerStateManager) + params.producerStateManager, + params.logIdentifier) } } @@ -434,20 +438,20 @@ object LogLoader extends Logging { while (unflushed.hasNext && !truncated) { val segment = unflushed.next() - info(s"${params.logIdentifier} Recovering unflushed segment ${segment.baseOffset}") + info(s"${params.logIdentifier}Recovering unflushed segment ${segment.baseOffset}") val truncatedBytes = try { recoverSegment(segment, params) } catch { case _: InvalidOffsetException => val startOffset = segment.baseOffset - warn(s"${params.logIdentifier} Found invalid offset during recovery. Deleting the" + + warn(s"${params.logIdentifier}Found invalid offset during recovery. Deleting the" + s" corrupt segment and creating an empty one with starting offset $startOffset") segment.truncateTo(startOffset) } if (truncatedBytes > 0) { // we had an invalid message, delete all remaining log - warn(s"${params.logIdentifier} Corruption found in segment ${segment.baseOffset}," + + warn(s"${params.logIdentifier}Corruption found in segment ${segment.baseOffset}," + s" truncating to offset ${segment.readNextOffset}") removeAndDeleteSegmentsAsync(unflushed.toList, params) truncated = true @@ -519,7 +523,8 @@ object LogLoader extends Logging { params.config, params.scheduler, params.logDirFailureChannel, - params.producerStateManager) + params.producerStateManager, + params.logIdentifier) } } } diff --git a/core/src/main/scala/kafka/network/RequestConvertToJson.scala b/core/src/main/scala/kafka/network/RequestConvertToJson.scala index c0fbd68e75bf2..bb8e327b1890a 100644 --- a/core/src/main/scala/kafka/network/RequestConvertToJson.scala +++ b/core/src/main/scala/kafka/network/RequestConvertToJson.scala @@ -29,6 +29,7 @@ object RequestConvertToJson { request match { case req: AddOffsetsToTxnRequest => AddOffsetsToTxnRequestDataJsonConverter.write(req.data, request.version) case req: AddPartitionsToTxnRequest => AddPartitionsToTxnRequestDataJsonConverter.write(req.data, request.version) + case req: AllocateProducerIdsRequest => AllocateProducerIdsRequestDataJsonConverter.write(req.data, request.version) case req: AlterClientQuotasRequest => AlterClientQuotasRequestDataJsonConverter.write(req.data, request.version) case req: AlterConfigsRequest => AlterConfigsRequestDataJsonConverter.write(req.data, request.version) case req: AlterIsrRequest => AlterIsrRequestDataJsonConverter.write(req.data, request.version) @@ -103,6 +104,7 @@ object RequestConvertToJson { response match { case res: AddOffsetsToTxnResponse => AddOffsetsToTxnResponseDataJsonConverter.write(res.data, version) case res: AddPartitionsToTxnResponse => AddPartitionsToTxnResponseDataJsonConverter.write(res.data, version) + case res: AllocateProducerIdsResponse => AllocateProducerIdsResponseDataJsonConverter.write(res.data, version) case res: AlterClientQuotasResponse => AlterClientQuotasResponseDataJsonConverter.write(res.data, version) case res: AlterConfigsResponse => AlterConfigsResponseDataJsonConverter.write(res.data, version) case res: AlterIsrResponse => AlterIsrResponseDataJsonConverter.write(res.data, version) diff --git a/core/src/main/scala/kafka/server/BrokerServer.scala b/core/src/main/scala/kafka/server/BrokerServer.scala index 08b4653e1684f..bea0c53c1d78f 100644 --- a/core/src/main/scala/kafka/server/BrokerServer.scala +++ b/core/src/main/scala/kafka/server/BrokerServer.scala @@ -25,7 +25,7 @@ import java.net.InetAddress import kafka.cluster.Broker.ServerInfo import kafka.coordinator.group.GroupCoordinator -import kafka.coordinator.transaction.{ProducerIdGenerator, TransactionCoordinator} +import kafka.coordinator.transaction.{ProducerIdManager, TransactionCoordinator} import kafka.log.LogManager import kafka.metrics.KafkaYammerMetrics import kafka.network.SocketServer @@ -376,7 +376,7 @@ class BrokerServer( } } - class TemporaryProducerIdManager() extends ProducerIdGenerator { + class TemporaryProducerIdManager() extends ProducerIdManager { val maxProducerIdsPerBrokerEpoch = 1000000 var currentOffset = -1 override def generateProducerId(): Long = { @@ -390,7 +390,7 @@ class BrokerServer( } } - def createTemporaryProducerIdManager(): ProducerIdGenerator = { + def createTemporaryProducerIdManager(): ProducerIdManager = { new TemporaryProducerIdManager() } diff --git a/core/src/main/scala/kafka/server/KafkaApis.scala b/core/src/main/scala/kafka/server/KafkaApis.scala index 3f83eb3af3b4e..d75e4ae3651d7 100644 --- a/core/src/main/scala/kafka/server/KafkaApis.scala +++ b/core/src/main/scala/kafka/server/KafkaApis.scala @@ -216,6 +216,7 @@ class KafkaApis(val requestChannel: RequestChannel, case ApiKeys.UNREGISTER_BROKER => maybeForwardToController(request, handleUnregisterBrokerRequest) case ApiKeys.DESCRIBE_TRANSACTIONS => handleDescribeTransactionsRequest(request) case ApiKeys.LIST_TRANSACTIONS => handleListTransactionsRequest(request) + case ApiKeys.ALLOCATE_PRODUCER_IDS => handleAllocateProducerIdsRequest(request) case _ => throw new IllegalStateException(s"No handler for request api key ${request.header.apiKey}") } } catch { @@ -3267,6 +3268,22 @@ class KafkaApis(val requestChannel: RequestChannel, new ListTransactionsResponse(response.setThrottleTimeMs(requestThrottleMs))) } + def handleAllocateProducerIdsRequest(request: RequestChannel.Request): Unit = { + val zkSupport = metadataSupport.requireZkOrThrow(KafkaApis.shouldNeverReceive(request)) + authHelper.authorizeClusterOperation(request, CLUSTER_ACTION) + + val allocateProducerIdsRequest = request.body[AllocateProducerIdsRequest] + + if (!zkSupport.controller.isActive) + requestHelper.sendResponseMaybeThrottle(request, throttleTimeMs => + allocateProducerIdsRequest.getErrorResponse(throttleTimeMs, Errors.NOT_CONTROLLER.exception)) + else + zkSupport.controller.allocateProducerIds(allocateProducerIdsRequest.data, producerIdsResponse => + requestHelper.sendResponseMaybeThrottle(request, throttleTimeMs => + new AllocateProducerIdsResponse(producerIdsResponse.setThrottleTimeMs(throttleTimeMs))) + ) + } + private def updateRecordConversionStats(request: RequestChannel.Request, tp: TopicPartition, conversionStats: RecordConversionStats): Unit = { diff --git a/core/src/main/scala/kafka/server/KafkaServer.scala b/core/src/main/scala/kafka/server/KafkaServer.scala index 7014574eacb0b..fa182c0609daa 100755 --- a/core/src/main/scala/kafka/server/KafkaServer.scala +++ b/core/src/main/scala/kafka/server/KafkaServer.scala @@ -134,7 +134,7 @@ class KafkaServer( var autoTopicCreationManager: AutoTopicCreationManager = null - var clientToControllerChannelManager: Option[BrokerToControllerChannelManager] = None + var clientToControllerChannelManager: BrokerToControllerChannelManager = null var alterIsrManager: AlterIsrManager = null @@ -257,19 +257,21 @@ class KafkaServer( tokenCache = new DelegationTokenCache(ScramMechanism.mechanismNames) credentialProvider = new CredentialProvider(ScramMechanism.mechanismNames, tokenCache) + clientToControllerChannelManager = BrokerToControllerChannelManager( + controllerNodeProvider = MetadataCacheControllerNodeProvider(config, metadataCache), + time = time, + metrics = metrics, + config = config, + channelName = "forwarding", + threadNamePrefix = threadNamePrefix, + retryTimeoutMs = config.requestTimeoutMs.longValue) + clientToControllerChannelManager.start() + /* start forwarding manager */ + var autoTopicCreationChannel = Option.empty[BrokerToControllerChannelManager] if (enableForwarding) { - val brokerToControllerManager = BrokerToControllerChannelManager( - controllerNodeProvider = MetadataCacheControllerNodeProvider(config, metadataCache), - time = time, - metrics = metrics, - config = config, - channelName = "forwarding", - threadNamePrefix = threadNamePrefix, - retryTimeoutMs = config.requestTimeoutMs.longValue) - brokerToControllerManager.start() - this.forwardingManager = Some(ForwardingManager(brokerToControllerManager)) - clientToControllerChannelManager = Some(brokerToControllerManager) + this.forwardingManager = Some(ForwardingManager(clientToControllerChannelManager)) + autoTopicCreationChannel = Some(clientToControllerChannelManager) } val apiVersionManager = ApiVersionManager( @@ -330,10 +332,21 @@ class KafkaServer( groupCoordinator = GroupCoordinator(config, replicaManager, Time.SYSTEM, metrics) groupCoordinator.startup(() => zkClient.getTopicPartitionCount(Topic.GROUP_METADATA_TOPIC_NAME).getOrElse(config.offsetsTopicPartitions)) + /* create producer ids manager */ + val producerIdManager = if (config.interBrokerProtocolVersion.isAllocateProducerIdsSupported) { + ProducerIdManager.rpc( + config.brokerId, + brokerEpochSupplier = () => kafkaController.brokerEpoch, + clientToControllerChannelManager, + config.requestTimeoutMs + ) + } else { + ProducerIdManager.zk(config.brokerId, zkClient) + } /* start transaction coordinator, with a separate background thread scheduler for transaction expiration and log loading */ // Hardcode Time.SYSTEM for now as some Streams tests fail otherwise, it would be good to fix the underlying issue transactionCoordinator = TransactionCoordinator(config, replicaManager, new KafkaScheduler(threads = 1, threadNamePrefix = "transaction-log-manager-"), - () => new ProducerIdManager(config.brokerId, zkClient), metrics, metadataCache, Time.SYSTEM) + () => producerIdManager, metrics, metadataCache, Time.SYSTEM) transactionCoordinator.startup( () => zkClient.getTopicPartitionCount(Topic.TRANSACTION_STATE_TOPIC_NAME).getOrElse(config.transactionTopicPartitions)) @@ -342,7 +355,7 @@ class KafkaServer( config, metadataCache, threadNamePrefix, - clientToControllerChannelManager, + autoTopicCreationChannel, Some(adminManager), Some(kafkaController), groupCoordinator, @@ -680,7 +693,8 @@ class KafkaServer( if (alterIsrManager != null) CoreUtils.swallow(alterIsrManager.shutdown(), this) - CoreUtils.swallow(clientToControllerChannelManager.foreach(_.shutdown()), this) + if (clientToControllerChannelManager != null) + CoreUtils.swallow(clientToControllerChannelManager.shutdown(), this) if (logManager != null) CoreUtils.swallow(logManager.shutdown(), this) diff --git a/core/src/main/scala/kafka/zk/ZkData.scala b/core/src/main/scala/kafka/zk/ZkData.scala index 4941ece5163cc..0f6db4a5d3dc1 100644 --- a/core/src/main/scala/kafka/zk/ZkData.scala +++ b/core/src/main/scala/kafka/zk/ZkData.scala @@ -19,7 +19,6 @@ package kafka.zk import java.nio.charset.StandardCharsets.UTF_8 import java.util import java.util.Properties - import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.core.JsonProcessingException import kafka.api.{ApiVersion, KAFKA_0_10_0_IV1, KAFKA_2_7_IV0, LeaderAndIsr} @@ -40,6 +39,7 @@ import org.apache.kafka.common.resource.{PatternType, ResourcePattern, ResourceT import org.apache.kafka.common.security.auth.SecurityProtocol import org.apache.kafka.common.security.token.delegation.{DelegationToken, TokenInformation} import org.apache.kafka.common.utils.{SecurityUtils, Time} +import org.apache.kafka.server.common.ProducerIdsBlock import org.apache.zookeeper.ZooDefs import org.apache.zookeeper.data.{ACL, Stat} @@ -766,7 +766,33 @@ object BrokerSequenceIdZNode { } object ProducerIdBlockZNode { + val CurrentVersion: Long = 1L + def path = "/latest_producer_id_block" + + def generateProducerIdBlockJson(producerIdBlock: ProducerIdsBlock): Array[Byte] = { + Json.encodeAsBytes(Map("version" -> CurrentVersion, + "broker" -> producerIdBlock.brokerId, + "block_start" -> producerIdBlock.producerIdStart.toString, + "block_end" -> producerIdBlock.producerIdEnd.toString).asJava + ) + } + + def parseProducerIdBlockData(jsonData: Array[Byte]): ProducerIdsBlock = { + val jsonDataAsString = jsonData.map(_.toChar).mkString + try { + Json.parseBytes(jsonData).map(_.asJsonObject).flatMap { js => + val brokerId = js("broker").to[Int] + val blockStart = js("block_start").to[String].toLong + val blockEnd = js("block_end").to[String].toLong + Some(new ProducerIdsBlock(brokerId, blockStart, Math.toIntExact(blockEnd - blockStart + 1))) + }.getOrElse(throw new KafkaException(s"Failed to parse the producerId block json $jsonDataAsString")) + } catch { + case e: java.lang.NumberFormatException => + // this should never happen: the written data has exceeded long type limit + throw new KafkaException(s"Read jason data $jsonDataAsString contains producerIds that have exceeded long type limit", e) + } + } } object DelegationTokenAuthZNode { diff --git a/core/src/test/java/kafka/test/ClusterConfig.java b/core/src/test/java/kafka/test/ClusterConfig.java index e7e8bf5a0234a..20b74cf43244b 100644 --- a/core/src/test/java/kafka/test/ClusterConfig.java +++ b/core/src/test/java/kafka/test/ClusterConfig.java @@ -21,6 +21,7 @@ import org.apache.kafka.common.security.auth.SecurityProtocol; import java.io.File; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; @@ -40,6 +41,7 @@ public class ClusterConfig { private final SecurityProtocol securityProtocol; private final String listenerName; private final File trustStoreFile; + private final String ibp; private final Properties serverProperties = new Properties(); private final Properties producerProperties = new Properties(); @@ -47,9 +49,11 @@ public class ClusterConfig { private final Properties adminClientProperties = new Properties(); private final Properties saslServerProperties = new Properties(); private final Properties saslClientProperties = new Properties(); + private final Map perBrokerOverrideProperties = new HashMap<>(); ClusterConfig(Type type, int brokers, int controllers, String name, boolean autoStart, - SecurityProtocol securityProtocol, String listenerName, File trustStoreFile) { + SecurityProtocol securityProtocol, String listenerName, File trustStoreFile, + String ibp) { this.type = type; this.brokers = brokers; this.controllers = controllers; @@ -58,6 +62,7 @@ public class ClusterConfig { this.securityProtocol = securityProtocol; this.listenerName = listenerName; this.trustStoreFile = trustStoreFile; + this.ibp = ibp; } public Type clusterType() { @@ -116,16 +121,25 @@ public Optional trustStoreFile() { return Optional.ofNullable(trustStoreFile); } + public Optional ibp() { + return Optional.ofNullable(ibp); + } + + public Properties brokerServerProperties(int brokerId) { + return perBrokerOverrideProperties.computeIfAbsent(brokerId, __ -> new Properties()); + } + public Map nameTags() { Map tags = new LinkedHashMap<>(3); name().ifPresent(name -> tags.put("Name", name)); + ibp().ifPresent(ibp -> tags.put("IBP", ibp)); tags.put("Security", securityProtocol.name()); listenerName().ifPresent(listener -> tags.put("Listener", listener)); return tags; } public ClusterConfig copyOf() { - ClusterConfig copy = new ClusterConfig(type, brokers, controllers, name, autoStart, securityProtocol, listenerName, trustStoreFile); + ClusterConfig copy = new ClusterConfig(type, brokers, controllers, name, autoStart, securityProtocol, listenerName, trustStoreFile, ibp); copy.serverProperties.putAll(serverProperties); copy.producerProperties.putAll(producerProperties); copy.consumerProperties.putAll(consumerProperties); @@ -151,6 +165,7 @@ public static class Builder { private SecurityProtocol securityProtocol; private String listenerName; private File trustStoreFile; + private String ibp; Builder(Type type, int brokers, int controllers, boolean autoStart, SecurityProtocol securityProtocol) { this.type = type; @@ -200,8 +215,13 @@ public Builder trustStoreFile(File trustStoreFile) { return this; } + public Builder ibp(String ibp) { + this.ibp = ibp; + return this; + } + public ClusterConfig build() { - return new ClusterConfig(type, brokers, controllers, name, autoStart, securityProtocol, listenerName, trustStoreFile); + return new ClusterConfig(type, brokers, controllers, name, autoStart, securityProtocol, listenerName, trustStoreFile, ibp); } } } diff --git a/core/src/test/java/kafka/test/ClusterInstance.java b/core/src/test/java/kafka/test/ClusterInstance.java index cac986fdc317c..021db5a34381e 100644 --- a/core/src/test/java/kafka/test/ClusterInstance.java +++ b/core/src/test/java/kafka/test/ClusterInstance.java @@ -94,4 +94,6 @@ default Admin createAdminClient() { void start(); void stop(); + + void rollingBrokerRestart(); } diff --git a/core/src/test/java/kafka/test/annotation/ClusterTest.java b/core/src/test/java/kafka/test/annotation/ClusterTest.java index 687255c3c4705..11336ab87a15f 100644 --- a/core/src/test/java/kafka/test/annotation/ClusterTest.java +++ b/core/src/test/java/kafka/test/annotation/ClusterTest.java @@ -40,5 +40,6 @@ String name() default ""; SecurityProtocol securityProtocol() default SecurityProtocol.PLAINTEXT; String listener() default ""; + String ibp() default ""; ClusterConfigProperty[] serverProperties() default {}; } diff --git a/core/src/test/java/kafka/test/junit/ClusterTestExtensions.java b/core/src/test/java/kafka/test/junit/ClusterTestExtensions.java index 94eefbd91cb81..293f00b035ca5 100644 --- a/core/src/test/java/kafka/test/junit/ClusterTestExtensions.java +++ b/core/src/test/java/kafka/test/junit/ClusterTestExtensions.java @@ -183,7 +183,7 @@ private void processClusterTest(ExtensionContext context, ClusterTest annot, Clu if (!annot.name().isEmpty()) { builder.name(annot.name()); } else { - builder.name(context.getDisplayName()); + builder.name(context.getRequiredTestMethod().getName()); } if (!annot.listener().isEmpty()) { builder.listenerName(annot.listener()); @@ -194,6 +194,10 @@ private void processClusterTest(ExtensionContext context, ClusterTest annot, Clu properties.put(property.key(), property.value()); } + if (!annot.ibp().isEmpty()) { + builder.ibp(annot.ibp()); + } + ClusterConfig config = builder.build(); config.serverProperties().putAll(properties); type.invocationContexts(config, testInvocations); diff --git a/core/src/test/java/kafka/test/junit/RaftClusterInvocationContext.java b/core/src/test/java/kafka/test/junit/RaftClusterInvocationContext.java index b4e50c59bfe98..fc6b55736c3fc 100644 --- a/core/src/test/java/kafka/test/junit/RaftClusterInvocationContext.java +++ b/core/src/test/java/kafka/test/junit/RaftClusterInvocationContext.java @@ -66,8 +66,8 @@ public RaftClusterInvocationContext(ClusterConfig clusterConfig) { @Override public String getDisplayName(int invocationIndex) { String clusterDesc = clusterConfig.nameTags().entrySet().stream() - .map(Object::toString) - .collect(Collectors.joining(", ")); + .map(Object::toString) + .collect(Collectors.joining(", ")); return String.format("[%d] Type=Raft, %s", invocationIndex, clusterDesc); } @@ -75,10 +75,14 @@ public String getDisplayName(int invocationIndex) { public List getAdditionalExtensions() { return Arrays.asList( (BeforeTestExecutionCallback) context -> { - KafkaClusterTestKit.Builder builder = new KafkaClusterTestKit.Builder( - new TestKitNodes.Builder(). + TestKitNodes nodes = new TestKitNodes.Builder(). setNumBrokerNodes(clusterConfig.numBrokers()). - setNumControllerNodes(clusterConfig.numControllers()).build()); + setNumControllerNodes(clusterConfig.numControllers()).build(); + nodes.brokerNodes().forEach((brokerId, brokerNode) -> { + clusterConfig.brokerServerProperties(brokerId).forEach( + (key, value) -> brokerNode.propertyOverrides().put(key.toString(), value.toString())); + }); + KafkaClusterTestKit.Builder builder = new KafkaClusterTestKit.Builder(nodes); // Copy properties into the TestKit builder clusterConfig.serverProperties().forEach((key, value) -> builder.setConfigProp(key.toString(), value.toString())); @@ -192,5 +196,10 @@ public void stop() { } } } + + @Override + public void rollingBrokerRestart() { + throw new UnsupportedOperationException("Restarting Raft servers is not yet supported."); + } } -} \ No newline at end of file +} diff --git a/core/src/test/java/kafka/test/junit/ZkClusterInvocationContext.java b/core/src/test/java/kafka/test/junit/ZkClusterInvocationContext.java index 8d4660b77382e..cd8cdc11a800d 100644 --- a/core/src/test/java/kafka/test/junit/ZkClusterInvocationContext.java +++ b/core/src/test/java/kafka/test/junit/ZkClusterInvocationContext.java @@ -19,6 +19,7 @@ import kafka.api.IntegrationTestHarness; import kafka.network.SocketServer; +import kafka.server.KafkaConfig; import kafka.server.KafkaServer; import kafka.utils.TestUtils; import org.apache.kafka.clients.admin.Admin; @@ -32,6 +33,7 @@ import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import scala.Option; import scala.collection.JavaConverters; +import scala.collection.Seq; import scala.compat.java8.OptionConverters; import java.io.File; @@ -89,9 +91,20 @@ public List getAdditionalExtensions() { // This is what tests normally extend from to start a cluster, here we create it anonymously and // configure the cluster using values from ClusterConfig IntegrationTestHarness cluster = new IntegrationTestHarness() { + + @Override + public void modifyConfigs(Seq props) { + super.modifyConfigs(props); + for (int i = 0; i < props.length(); i++) { + props.apply(i).putAll(clusterConfig.brokerServerProperties(i)); + } + } + @Override public Properties serverConfig() { - return clusterConfig.serverProperties(); + Properties props = clusterConfig.serverProperties(); + clusterConfig.ibp().ifPresent(ibp -> props.put(KafkaConfig.InterBrokerProtocolVersionProp(), ibp)); + return props; } @Override @@ -248,5 +261,16 @@ public void stop() { clusterReference.get().tearDown(); } } + + @Override + public void rollingBrokerRestart() { + if (!started.get()) { + throw new IllegalStateException("Tried to restart brokers but the cluster has not been started!"); + } + for (int i = 0; i < clusterReference.get().brokerCount(); i++) { + clusterReference.get().killBroker(i); + } + clusterReference.get().restartDeadBrokers(true); + } } } diff --git a/core/src/test/java/kafka/testkit/BrokerNode.java b/core/src/test/java/kafka/testkit/BrokerNode.java index 0b5859404b346..32bd51b4b56dc 100644 --- a/core/src/test/java/kafka/testkit/BrokerNode.java +++ b/core/src/test/java/kafka/testkit/BrokerNode.java @@ -21,7 +21,9 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class BrokerNode implements TestKitNode { public static class Builder { @@ -68,6 +70,7 @@ public BrokerNode build() { private final Uuid incarnationId; private final String metadataDirectory; private final List logDataDirectories; + private final Map propertyOverrides; BrokerNode(int id, Uuid incarnationId, @@ -77,6 +80,7 @@ public BrokerNode build() { this.incarnationId = incarnationId; this.metadataDirectory = metadataDirectory; this.logDataDirectories = new ArrayList<>(logDataDirectories); + this.propertyOverrides = new HashMap<>(); } @Override @@ -96,4 +100,8 @@ public String metadataDirectory() { public List logDataDirectories() { return logDataDirectories; } + + public Map propertyOverrides() { + return propertyOverrides; + } } diff --git a/core/src/test/java/kafka/testkit/KafkaClusterTestKit.java b/core/src/test/java/kafka/testkit/KafkaClusterTestKit.java index 9c7e4e0b7e089..79d7bda1accbd 100644 --- a/core/src/test/java/kafka/testkit/KafkaClusterTestKit.java +++ b/core/src/test/java/kafka/testkit/KafkaClusterTestKit.java @@ -216,6 +216,7 @@ metaProperties, config, new MetadataRecordSerde(), metadataPartition, KafkaRaftS // Just like above, we set a placeholder voter list here until we // find out what ports the controllers picked. props.put(RaftConfig.QUORUM_VOTERS_CONFIG, uninitializedQuorumVotersString); + props.putAll(node.propertyOverrides()); KafkaConfig config = new KafkaConfig(props, false, Option.empty()); String threadNamePrefix = String.format("broker%d_", node.id()); diff --git a/core/src/test/scala/integration/kafka/coordinator/transaction/ProducerIdsIntegrationTest.scala b/core/src/test/scala/integration/kafka/coordinator/transaction/ProducerIdsIntegrationTest.scala new file mode 100644 index 0000000000000..6c7c248e63188 --- /dev/null +++ b/core/src/test/scala/integration/kafka/coordinator/transaction/ProducerIdsIntegrationTest.scala @@ -0,0 +1,85 @@ +/** + * 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 kafka.coordinator.transaction + +import kafka.network.SocketServer +import kafka.server.{IntegrationTestUtils, KafkaConfig} +import kafka.test.annotation.{AutoStart, ClusterTest, ClusterTests, Type} +import kafka.test.junit.ClusterTestExtensions +import kafka.test.{ClusterConfig, ClusterInstance} +import org.apache.kafka.common.message.InitProducerIdRequestData +import org.apache.kafka.common.network.ListenerName +import org.apache.kafka.common.record.RecordBatch +import org.apache.kafka.common.requests.{InitProducerIdRequest, InitProducerIdResponse} +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.extension.ExtendWith + +import java.util.stream.{Collectors, IntStream} +import scala.jdk.CollectionConverters._ + +@ExtendWith(value = Array(classOf[ClusterTestExtensions])) +class ProducerIdsIntegrationTest { + + @BeforeEach + def setup(clusterConfig: ClusterConfig): Unit = { + clusterConfig.serverProperties().put(KafkaConfig.TransactionsTopicPartitionsProp, "1") + clusterConfig.serverProperties().put(KafkaConfig.TransactionsTopicReplicationFactorProp, "3") + } + + @ClusterTests(Array( + new ClusterTest(clusterType = Type.ZK, brokers = 3, ibp = "2.8"), + new ClusterTest(clusterType = Type.ZK, brokers = 3, ibp = "3.0-IV0") + )) + def testUniqueProducerIds(clusterInstance: ClusterInstance): Unit = { + verifyUniqueIds(clusterInstance) + } + + @ClusterTest(clusterType = Type.ZK, brokers = 3, autoStart = AutoStart.NO) + def testUniqueProducerIdsBumpIBP(clusterInstance: ClusterInstance): Unit = { + clusterInstance.config().serverProperties().put(KafkaConfig.InterBrokerProtocolVersionProp, "2.8") + clusterInstance.config().brokerServerProperties(0).put(KafkaConfig.InterBrokerProtocolVersionProp, "3.0-IV0") + clusterInstance.start() + verifyUniqueIds(clusterInstance) + clusterInstance.stop() + } + + private def verifyUniqueIds(clusterInstance: ClusterInstance): Unit = { + // Request enough PIDs from each broker to ensure each broker generates two PID blocks + val ids = clusterInstance.brokerSocketServers().stream().flatMap( broker => { + IntStream.range(0, 1001).parallel().mapToObj( _ => nextProducerId(broker, clusterInstance.clientListener())) + }).collect(Collectors.toList[Long]).asScala.toSeq + + assertEquals(3003, ids.size, "Expected exactly 3003 IDs") + assertEquals(ids.size, ids.distinct.size, "Found duplicate producer IDs") + } + + private def nextProducerId(broker: SocketServer, listener: ListenerName): Long = { + val data = new InitProducerIdRequestData() + .setProducerEpoch(RecordBatch.NO_PRODUCER_EPOCH) + .setProducerId(RecordBatch.NO_PRODUCER_ID) + .setTransactionalId(null) + .setTransactionTimeoutMs(10) + val request = new InitProducerIdRequest.Builder(data).build() + + val response = IntegrationTestUtils.connectAndReceive[InitProducerIdResponse](request, + destination = broker, + listenerName = listener) + response.data().producerId() + } +} diff --git a/core/src/test/scala/unit/kafka/cluster/PartitionLockTest.scala b/core/src/test/scala/unit/kafka/cluster/PartitionLockTest.scala index 8bb54c4ce070c..0cbeec9798c13 100644 --- a/core/src/test/scala/unit/kafka/cluster/PartitionLockTest.scala +++ b/core/src/test/scala/unit/kafka/cluster/PartitionLockTest.scala @@ -283,7 +283,7 @@ class PartitionLockTest extends Logging { val log = super.createLog(isNew, isFutureReplica, offsetCheckpoints, None) val logDirFailureChannel = new LogDirFailureChannel(1) val segments = new LogSegments(log.topicPartition) - val leaderEpochCache = Log.maybeCreateLeaderEpochCache(log.dir, log.topicPartition, logDirFailureChannel, log.config.messageFormatVersion.recordVersion) + val leaderEpochCache = Log.maybeCreateLeaderEpochCache(log.dir, log.topicPartition, logDirFailureChannel, log.config.messageFormatVersion.recordVersion, "") val maxProducerIdExpirationMs = 60 * 60 * 1000 val producerStateManager = new ProducerStateManager(log.topicPartition, log.dir, maxProducerIdExpirationMs) val offsets = LogLoader.load(LoadLogParams( diff --git a/core/src/test/scala/unit/kafka/cluster/PartitionTest.scala b/core/src/test/scala/unit/kafka/cluster/PartitionTest.scala index 8e492acb7438a..703071568a0c0 100644 --- a/core/src/test/scala/unit/kafka/cluster/PartitionTest.scala +++ b/core/src/test/scala/unit/kafka/cluster/PartitionTest.scala @@ -236,7 +236,7 @@ class PartitionTest extends AbstractPartitionTest { val log = super.createLog(isNew, isFutureReplica, offsetCheckpoints, None) val logDirFailureChannel = new LogDirFailureChannel(1) val segments = new LogSegments(log.topicPartition) - val leaderEpochCache = Log.maybeCreateLeaderEpochCache(log.dir, log.topicPartition, logDirFailureChannel, log.config.messageFormatVersion.recordVersion) + val leaderEpochCache = Log.maybeCreateLeaderEpochCache(log.dir, log.topicPartition, logDirFailureChannel, log.config.messageFormatVersion.recordVersion, "") val maxProducerIdExpirationMs = 60 * 60 * 1000 val producerStateManager = new ProducerStateManager(log.topicPartition, log.dir, maxProducerIdExpirationMs) val offsets = LogLoader.load(LoadLogParams( diff --git a/core/src/test/scala/unit/kafka/coordinator/transaction/ProducerIdManagerTest.scala b/core/src/test/scala/unit/kafka/coordinator/transaction/ProducerIdManagerTest.scala index be3e8144f6859..9232bf03c0e0e 100644 --- a/core/src/test/scala/unit/kafka/coordinator/transaction/ProducerIdManagerTest.scala +++ b/core/src/test/scala/unit/kafka/coordinator/transaction/ProducerIdManagerTest.scala @@ -16,23 +16,44 @@ */ package kafka.coordinator.transaction -import kafka.zk.KafkaZkClient +import kafka.server.BrokerToControllerChannelManager +import kafka.zk.{KafkaZkClient, ProducerIdBlockZNode} import org.apache.kafka.common.KafkaException +import org.apache.kafka.common.message.AllocateProducerIdsResponseData +import org.apache.kafka.common.protocol.Errors +import org.apache.kafka.common.requests.AllocateProducerIdsResponse +import org.apache.kafka.server.common.ProducerIdsBlock import org.easymock.{Capture, EasyMock} -import org.junit.jupiter.api.{AfterEach, Test} import org.junit.jupiter.api.Assertions._ +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.{EnumSource, ValueSource} + +import java.util.stream.IntStream class ProducerIdManagerTest { - private val zkClient: KafkaZkClient = EasyMock.createNiceMock(classOf[KafkaZkClient]) + var brokerToController: BrokerToControllerChannelManager = EasyMock.niceMock(classOf[BrokerToControllerChannelManager]) + val zkClient: KafkaZkClient = EasyMock.createNiceMock(classOf[KafkaZkClient]) - @AfterEach - def tearDown(): Unit = { - EasyMock.reset(zkClient) + // Mutable test implementation that lets us easily set the idStart and error + class MockProducerIdManager(val brokerId: Int, var idStart: Long, val idLen: Int, var error: Errors = Errors.NONE) + extends RPCProducerIdManager(brokerId, () => 1, brokerToController, 100) { + + override private[transaction] def sendRequest(): Unit = { + if (error == Errors.NONE) { + handleAllocateProducerIdsResponse(new AllocateProducerIdsResponse( + new AllocateProducerIdsResponseData().setProducerIdStart(idStart).setProducerIdLen(idLen))) + idStart += idLen + } else { + handleAllocateProducerIdsResponse(new AllocateProducerIdsResponse( + new AllocateProducerIdsResponseData().setErrorCode(error.code))) + } + } } @Test - def testGetProducerId(): Unit = { + def testGetProducerIdZk(): Unit = { var zkVersion: Option[Int] = None var data: Array[Byte] = null EasyMock.expect(zkClient.getDataAndVersion(EasyMock.anyString)).andAnswer(() => @@ -53,34 +74,71 @@ class ProducerIdManagerTest { EasyMock.replay(zkClient) - val manager1 = new ProducerIdManager(0, zkClient) - val manager2 = new ProducerIdManager(1, zkClient) + val manager1 = new ZkProducerIdManager(0, zkClient) + val manager2 = new ZkProducerIdManager(1, zkClient) val pid1 = manager1.generateProducerId() val pid2 = manager2.generateProducerId() assertEquals(0, pid1) - assertEquals(ProducerIdManager.PidBlockSize, pid2) + assertEquals(ProducerIdsBlock.PRODUCER_ID_BLOCK_SIZE, pid2) - for (i <- 1L until ProducerIdManager.PidBlockSize) + for (i <- 1L until ProducerIdsBlock.PRODUCER_ID_BLOCK_SIZE) assertEquals(pid1 + i, manager1.generateProducerId()) - for (i <- 1L until ProducerIdManager.PidBlockSize) + for (i <- 1L until ProducerIdsBlock.PRODUCER_ID_BLOCK_SIZE) assertEquals(pid2 + i, manager2.generateProducerId()) - assertEquals(pid2 + ProducerIdManager.PidBlockSize, manager1.generateProducerId()) - assertEquals(pid2 + ProducerIdManager.PidBlockSize * 2, manager2.generateProducerId()) + assertEquals(pid2 + ProducerIdsBlock.PRODUCER_ID_BLOCK_SIZE, manager1.generateProducerId()) + assertEquals(pid2 + ProducerIdsBlock.PRODUCER_ID_BLOCK_SIZE * 2, manager2.generateProducerId()) + + EasyMock.reset(zkClient) } @Test - def testExceedProducerIdLimit(): Unit = { + def testExceedProducerIdLimitZk(): Unit = { EasyMock.expect(zkClient.getDataAndVersion(EasyMock.anyString)).andAnswer(() => { - val json = ProducerIdManager.generateProducerIdBlockJson( - ProducerIdBlock(0, Long.MaxValue - ProducerIdManager.PidBlockSize, Long.MaxValue)) + val json = ProducerIdBlockZNode.generateProducerIdBlockJson( + new ProducerIdsBlock(0, Long.MaxValue - ProducerIdsBlock.PRODUCER_ID_BLOCK_SIZE, ProducerIdsBlock.PRODUCER_ID_BLOCK_SIZE)) (Some(json), 0) }).anyTimes() EasyMock.replay(zkClient) - assertThrows(classOf[KafkaException], () => new ProducerIdManager(0, zkClient)) + assertThrows(classOf[KafkaException], () => new ZkProducerIdManager(0, zkClient)) + } + + @ParameterizedTest + @ValueSource(ints = Array(1, 2, 10)) + def testContiguousIds(idBlockLen: Int): Unit = { + val manager = new MockProducerIdManager(0, 0, idBlockLen) + + IntStream.range(0, idBlockLen * 3).forEach { i => + assertEquals(i, manager.generateProducerId()) + } + } + + @ParameterizedTest + @EnumSource(value = classOf[Errors], names = Array("UNKNOWN_SERVER_ERROR", "INVALID_REQUEST")) + def testUnrecoverableErrors(error: Errors): Unit = { + val manager = new MockProducerIdManager(0, 0, 1) + assertEquals(0, manager.generateProducerId()) + + manager.error = error + assertThrows(classOf[Throwable], () => manager.generateProducerId()) + + manager.error = Errors.NONE + assertEquals(1, manager.generateProducerId()) + } + + @Test + def testInvalidRanges(): Unit = { + var manager = new MockProducerIdManager(0, -1, 10) + assertThrows(classOf[KafkaException], () => manager.generateProducerId()) + + manager = new MockProducerIdManager(0, 0, -1) + assertThrows(classOf[KafkaException], () => manager.generateProducerId()) + + manager = new MockProducerIdManager(0, Long.MaxValue-1, 10) + assertThrows(classOf[KafkaException], () => manager.generateProducerId()) } } diff --git a/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorConcurrencyTest.scala b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorConcurrencyTest.scala index ad337038761b4..e1786d0ee21ff 100644 --- a/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorConcurrencyTest.scala +++ b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorConcurrencyTest.scala @@ -77,7 +77,7 @@ class TransactionCoordinatorConcurrencyTest extends AbstractCoordinatorConcurren for (i <- 0 until numPartitions) txnStateManager.addLoadedTransactionsToCache(i, coordinatorEpoch, new Pool[String, TransactionMetadata]()) - val pidGenerator: ProducerIdGenerator = EasyMock.createNiceMock(classOf[ProducerIdGenerator]) + val pidGenerator: ProducerIdManager = EasyMock.createNiceMock(classOf[ProducerIdManager]) EasyMock.expect(pidGenerator.generateProducerId()) .andAnswer(() => if (bumpProducerId) producerId + 1 else producerId) .anyTimes() diff --git a/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorTest.scala b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorTest.scala index 72ccc8de6bf4d..f6b5e54dfe19a 100644 --- a/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorTest.scala +++ b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorTest.scala @@ -34,7 +34,7 @@ class TransactionCoordinatorTest { val time = new MockTime() var nextPid: Long = 0L - val pidGenerator: ProducerIdGenerator = EasyMock.createNiceMock(classOf[ProducerIdGenerator]) + val pidGenerator: ProducerIdManager = EasyMock.createNiceMock(classOf[ProducerIdManager]) val transactionManager: TransactionStateManager = EasyMock.createNiceMock(classOf[TransactionStateManager]) val transactionMarkerChannelManager: TransactionMarkerChannelManager = EasyMock.createNiceMock(classOf[TransactionMarkerChannelManager]) val capturedTxn: Capture[TransactionMetadata] = EasyMock.newCapture() diff --git a/core/src/test/scala/unit/kafka/integration/KafkaServerTestHarness.scala b/core/src/test/scala/unit/kafka/integration/KafkaServerTestHarness.scala index 5369fdd66fae7..6fc18f0d10c68 100755 --- a/core/src/test/scala/unit/kafka/integration/KafkaServerTestHarness.scala +++ b/core/src/test/scala/unit/kafka/integration/KafkaServerTestHarness.scala @@ -161,8 +161,19 @@ abstract class KafkaServerTestHarness extends ZooKeeperTestHarness { /** * Restart any dead brokers */ - def restartDeadBrokers(): Unit = { + def restartDeadBrokers(reconfigure: Boolean = false): Unit = { + if (reconfigure) { + instanceConfigs = null + } for(i <- servers.indices if !alive(i)) { + if (reconfigure) { + servers(i) = TestUtils.createServer( + configs(i), + time = brokerTime(configs(i).brokerId), + threadNamePrefix = None, + enableForwarding + ) + } servers(i).startup() alive(i) = true } diff --git a/core/src/test/scala/unit/kafka/log/LogCleanerManagerTest.scala b/core/src/test/scala/unit/kafka/log/LogCleanerManagerTest.scala index 3cb32c0724e09..fbdbe207a1898 100644 --- a/core/src/test/scala/unit/kafka/log/LogCleanerManagerTest.scala +++ b/core/src/test/scala/unit/kafka/log/LogCleanerManagerTest.scala @@ -100,7 +100,7 @@ class LogCleanerManagerTest extends Logging { val config = createLowRetentionLogConfig(logSegmentSize, LogConfig.Compact) val maxProducerIdExpirationMs = 60 * 60 * 1000 val segments = new LogSegments(tp) - val leaderEpochCache = Log.maybeCreateLeaderEpochCache(tpDir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion) + val leaderEpochCache = Log.maybeCreateLeaderEpochCache(tpDir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion, "") val producerStateManager = new ProducerStateManager(topicPartition, tpDir, maxProducerIdExpirationMs) val offsets = LogLoader.load(LoadLogParams( tpDir, diff --git a/core/src/test/scala/unit/kafka/log/LogCleanerTest.scala b/core/src/test/scala/unit/kafka/log/LogCleanerTest.scala index 99ff1aac0a77b..c137eab0a4194 100755 --- a/core/src/test/scala/unit/kafka/log/LogCleanerTest.scala +++ b/core/src/test/scala/unit/kafka/log/LogCleanerTest.scala @@ -105,7 +105,7 @@ class LogCleanerTest { val logDirFailureChannel = new LogDirFailureChannel(10) val maxProducerIdExpirationMs = 60 * 60 * 1000 val logSegments = new LogSegments(topicPartition) - val leaderEpochCache = Log.maybeCreateLeaderEpochCache(dir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion) + val leaderEpochCache = Log.maybeCreateLeaderEpochCache(dir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion, "") val producerStateManager = new ProducerStateManager(topicPartition, dir, maxProducerIdExpirationMs) val offsets = LogLoader.load(LoadLogParams( dir, diff --git a/core/src/test/scala/unit/kafka/log/LogLoaderTest.scala b/core/src/test/scala/unit/kafka/log/LogLoaderTest.scala index 0546db4db7ea6..d59ed1d0c4c63 100644 --- a/core/src/test/scala/unit/kafka/log/LogLoaderTest.scala +++ b/core/src/test/scala/unit/kafka/log/LogLoaderTest.scala @@ -94,7 +94,7 @@ class LogLoaderTest { val logDirFailureChannel: LogDirFailureChannel = new LogDirFailureChannel(1) val maxProducerIdExpirationMs = 60 * 60 * 1000 val segments = new LogSegments(topicPartition) - val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion) + val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion, "") val producerStateManager = new ProducerStateManager(topicPartition, logDir, maxProducerIdExpirationMs) val loadLogParams = LoadLogParams(logDir, topicPartition, config, time.scheduler, time, logDirFailureChannel, hadCleanShutdown, segments, logStartOffset, logRecoveryPoint, @@ -264,7 +264,7 @@ class LogLoaderTest { super.add(wrapper) } } - val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, logConfig.messageFormatVersion.recordVersion) + val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, logConfig.messageFormatVersion.recordVersion, "") val producerStateManager = new ProducerStateManager(topicPartition, logDir, maxProducerIdExpirationMs) val loadLogParams = LoadLogParams( logDir, @@ -337,7 +337,7 @@ class LogLoaderTest { val config = LogConfig(new Properties()) val maxProducerIdExpirationMs = 300000 val segments = new LogSegments(topicPartition) - val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion) + val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion, "") val offsets = LogLoader.load(LoadLogParams( logDir, topicPartition, @@ -471,7 +471,7 @@ class LogLoaderTest { val maxProducerIdExpirationMs = 300000 val logDirFailureChannel = null val segments = new LogSegments(topicPartition) - val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion) + val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion, "") val offsets = LogLoader.load(LoadLogParams( logDir, topicPartition, @@ -532,7 +532,7 @@ class LogLoaderTest { val maxProducerIdExpirationMs = 300000 val logDirFailureChannel = null val segments = new LogSegments(topicPartition) - val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion) + val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion, "") val offsets = LogLoader.load(LoadLogParams( logDir, topicPartition, @@ -595,7 +595,7 @@ class LogLoaderTest { val maxProducerIdExpirationMs = 300000 val logDirFailureChannel = null val segments = new LogSegments(topicPartition) - val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion) + val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, config.messageFormatVersion.recordVersion, "") val offsets = LogLoader.load(LoadLogParams( logDir, topicPartition, diff --git a/core/src/test/scala/unit/kafka/server/ReplicaManagerTest.scala b/core/src/test/scala/unit/kafka/server/ReplicaManagerTest.scala index 8081d0b994b32..d1d38601bda93 100644 --- a/core/src/test/scala/unit/kafka/server/ReplicaManagerTest.scala +++ b/core/src/test/scala/unit/kafka/server/ReplicaManagerTest.scala @@ -1486,7 +1486,7 @@ class ReplicaManagerTest { val tp = new TopicPartition(topic, topicPartition) val maxProducerIdExpirationMs = 30000 val segments = new LogSegments(tp) - val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, tp, mockLogDirFailureChannel, logConfig.messageFormatVersion.recordVersion) + val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, tp, mockLogDirFailureChannel, logConfig.messageFormatVersion.recordVersion, "") val producerStateManager = new ProducerStateManager(tp, logDir, maxProducerIdExpirationMs) val offsets = LogLoader.load(LoadLogParams( logDir, diff --git a/core/src/test/scala/unit/kafka/server/RequestQuotaTest.scala b/core/src/test/scala/unit/kafka/server/RequestQuotaTest.scala index 86a00b33b035d..757b82c13dbe8 100644 --- a/core/src/test/scala/unit/kafka/server/RequestQuotaTest.scala +++ b/core/src/test/scala/unit/kafka/server/RequestQuotaTest.scala @@ -131,7 +131,7 @@ class RequestQuotaTest extends BaseRequestTest { @Test def testResponseThrottleTime(): Unit = { - for (apiKey <- RequestQuotaTest.ClientActions) + for (apiKey <- RequestQuotaTest.ClientActions ++ RequestQuotaTest.ClusterActionsWithThrottle) submitTest(apiKey, () => checkRequestThrottleTime(apiKey)) waitAndCheckResults() @@ -160,7 +160,7 @@ class RequestQuotaTest extends BaseRequestTest { @Test def testExemptRequestTime(): Unit = { - for (apiKey <- RequestQuotaTest.ClusterActions) { + for (apiKey <- RequestQuotaTest.ClusterActions -- RequestQuotaTest.ClusterActionsWithThrottle) { submitTest(apiKey, () => checkExemptRequestMetric(apiKey)) } @@ -642,6 +642,8 @@ class RequestQuotaTest extends BaseRequestTest { case ApiKeys.LIST_TRANSACTIONS => new ListTransactionsRequest.Builder(new ListTransactionsRequestData()) + case ApiKeys.ALLOCATE_PRODUCER_IDS => + new AllocateProducerIdsRequest.Builder(new AllocateProducerIdsRequestData()) case _ => throw new IllegalArgumentException("Unsupported API key " + apiKey) } @@ -762,6 +764,7 @@ class RequestQuotaTest extends BaseRequestTest { object RequestQuotaTest { val ClusterActions = ApiKeys.zkBrokerApis.asScala.filter(_.clusterAction).toSet + val ClusterActionsWithThrottle = Set(ApiKeys.ALLOCATE_PRODUCER_IDS) val SaslActions = Set(ApiKeys.SASL_HANDSHAKE, ApiKeys.SASL_AUTHENTICATE) val ClientActions = ApiKeys.zkBrokerApis.asScala.toSet -- ClusterActions -- SaslActions diff --git a/core/src/test/scala/unit/kafka/utils/SchedulerTest.scala b/core/src/test/scala/unit/kafka/utils/SchedulerTest.scala index 876388d008012..93362e25a9d8c 100644 --- a/core/src/test/scala/unit/kafka/utils/SchedulerTest.scala +++ b/core/src/test/scala/unit/kafka/utils/SchedulerTest.scala @@ -123,7 +123,7 @@ class SchedulerTest { val topicPartition = Log.parseTopicPartitionName(logDir) val logDirFailureChannel = new LogDirFailureChannel(10) val segments = new LogSegments(topicPartition) - val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, logConfig.messageFormatVersion.recordVersion) + val leaderEpochCache = Log.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, logConfig.messageFormatVersion.recordVersion, "") val producerStateManager = new ProducerStateManager(topicPartition, logDir, maxProducerIdExpirationMs) val offsets = LogLoader.load(LoadLogParams( logDir, diff --git a/docs/streams/developer-guide/app-reset-tool.html b/docs/streams/developer-guide/app-reset-tool.html index d6d07c27c97f2..597b662a5296f 100644 --- a/docs/streams/developer-guide/app-reset-tool.html +++ b/docs/streams/developer-guide/app-reset-tool.html @@ -78,44 +78,43 @@

Step 1: Run the application reset tool

Invoke the application reset tool from the command line

Warning! This tool makes irreversible changes to your application. It is strongly recommended that you run this once with --dry-run to preview your changes before making them.

-
<path-to-kafka>/bin/kafka-streams-application-reset
-
+
<path-to-kafka>/bin/kafka-streams-application-reset

The tool accepts the following parameters:

-
Option (* = required)                 Description
+            
Option (* = required)                 Description
 ---------------------                 -----------
 * --application-id <String: id>       The Kafka Streams application ID
-                                        (application.id).
+                                        (application.id).
 --bootstrap-servers <String: urls>    Comma-separated list of broker urls with
                                         format: HOST1:PORT1,HOST2:PORT2
-                                        (default: localhost:9092)
---by-duration <String: urls>      Reset offsets to offset by duration from
-                                        current timestamp. Format: 'PnDTnHnMnS'
+                                        (default: localhost:9092)
+--by-duration <String: urls>          Reset offsets to offset by duration from
+                                        current timestamp. Format: 'PnDTnHnMnS'
 --config-file <String: file name>     Property file containing configs to be
                                         passed to admin clients and embedded
                                         consumer.
 --dry-run                             Display the actions that would be
                                         performed without executing the reset
                                         commands.
---from-file <String: urls>        Reset offsets to values defined in CSV
+--from-file <String: urls>            Reset offsets to values defined in CSV
                                         file.
 --input-topics <String: list>         Comma-separated list of user input
                                         topics. For these topics, the tool will
                                         reset the offset to the earliest
                                         available offset.
 --intermediate-topics <String: list>  Comma-separated list of intermediate user
-                                        topics (topics used in the through()
-                                        method). For these topics, the tool
+                                        topics (topics used in the through()
+                                        method). For these topics, the tool
                                         will skip to the end.
 --internal-topics <String: list>      Comma-separated list of internal topics
                                         to delete. Must be a subset of the
                                         internal topics marked for deletion by
                                         the default behaviour (do a dry-run without
                                         this option to view these topics).
---shift-by <Long: number-of-offsets> Reset offsets shifting current offset by
-                                        'n', where 'n' can be positive or
+--shift-by <Long: number-of-offsets>  Reset offsets shifting current offset by
+                                        'n', where 'n' can be positive or
                                         negative
 --to-datetime <String>                Reset offsets to offset from datetime.
-                                        Format: 'YYYY-MM-DDTHH:mm:SS.sss'
+                                        Format: 'YYYY-MM-DDTHH:mm:SS.sss'
 --to-earliest                         Reset offsets to earliest offset.
 --to-latest                           Reset offsets to latest offset.
 --to-offset <Long>                    Reset offsets to a specific offset.
@@ -125,8 +124,7 @@ 

Step 1: Run the application reset toolCreate a java.util.Properties instance.

  • Set the parameters. For example:

    -
    import java.util.Properties;
    -import org.apache.kafka.streams.StreamsConfig;
    -
    -Properties settings = new Properties();
    -// Set a few key parameters
    -settings.put(StreamsConfig.APPLICATION_ID_CONFIG, "my-first-streams-application");
    -settings.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:9092");
    -// Any further settings
    -settings.put(... , ...);
    -
    +
    import java.util.Properties;
    +import org.apache.kafka.streams.StreamsConfig;
    +
    +Properties settings = new Properties();
    +// Set a few key parameters
    +settings.put(StreamsConfig.APPLICATION_ID_CONFIG, "my-first-streams-application");
    +settings.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:9092");
    +// Any further settings
    +settings.put(... , ...);
  • @@ -396,31 +395,31 @@

    acceptable.recovery.lag -
                  public class SendToDeadLetterQueueExceptionHandler implements DeserializationExceptionHandler {
    -                  KafkaProducer<byte[], byte[]> dlqProducer;
    -                  String dlqTopic;
    +              
    public class SendToDeadLetterQueueExceptionHandler implements DeserializationExceptionHandler {
    +    KafkaProducer<byte[], byte[]> dlqProducer;
    +    String dlqTopic;
     
    -                  @Override
    -                  public DeserializationHandlerResponse handle(final ProcessorContext context,
    -                                                               final ConsumerRecord<byte[], byte[]> record,
    -                                                               final Exception exception) {
    +    @Override
    +    public DeserializationHandlerResponse handle(final ProcessorContext context,
    +                                                 final ConsumerRecord<byte[], byte[]> record,
    +                                                 final Exception exception) {
     
    -                      log.warn("Exception caught during Deserialization, sending to the dead queue topic; " +
    -                          "taskId: {}, topic: {}, partition: {}, offset: {}",
    -                          context.taskId(), record.topic(), record.partition(), record.offset(),
    -                          exception);
    +        log.warn("Exception caught during Deserialization, sending to the dead queue topic; " +
    +            "taskId: {}, topic: {}, partition: {}, offset: {}",
    +            context.taskId(), record.topic(), record.partition(), record.offset(),
    +            exception);
     
    -                      dlqProducer.send(new ProducerRecord<>(dlqTopic, record.timestamp(), record.key(), record.value(), record.headers())).get();
    +        dlqProducer.send(new ProducerRecord<>(dlqTopic, record.timestamp(), record.key(), record.value(), record.headers())).get();
     
    -                      return DeserializationHandlerResponse.CONTINUE;
    -                  }
    +        return DeserializationHandlerResponse.CONTINUE;
    +    }
     
    -                  @Override
    -                  public void configure(final Map<String, ?> configs) {
    -                      dlqProducer = .. // get a producer from the configs map
    -                      dlqTopic = .. // get the topic name from the configs map
    -                  }
    -              }
    + @Override + public void configure(final Map<String, ?> configs) { + dlqProducer = .. // get a producer from the configs map + dlqTopic = .. // get the topic name from the configs map + } +}

    @@ -434,32 +433,31 @@

    acceptable.recovery.lag

    acceptable.recovery.lagpreviousTimestamp (i.e., a Kafka Streams timestamp estimation). Here is an example of a custom TimestampExtractor implementation:

    -
    import org.apache.kafka.clients.consumer.ConsumerRecord;
    -import org.apache.kafka.streams.processor.TimestampExtractor;
    -
    -// Extracts the embedded timestamp of a record (giving you "event-time" semantics).
    -public class MyEventTimeExtractor implements TimestampExtractor {
    -
    -  @Override
    -  public long extract(final ConsumerRecord<Object, Object> record, final long previousTimestamp) {
    -    // `Foo` is your own custom class, which we assume has a method that returns
    -    // the embedded timestamp (milliseconds since midnight, January 1, 1970 UTC).
    -    long timestamp = -1;
    -    final Foo myPojo = (Foo) record.value();
    -    if (myPojo != null) {
    -      timestamp = myPojo.getTimestampInMillis();
    -    }
    -    if (timestamp < 0) {
    -      // Invalid timestamp!  Attempt to estimate a new timestamp,
    -      // otherwise fall back to wall-clock time (processing-time).
    -      if (previousTimestamp >= 0) {
    -        return previousTimestamp;
    -      } else {
    -        return System.currentTimeMillis();
    -      }
    -    }
    -  }
    -
    -}
    -
    -
    +
    import org.apache.kafka.clients.consumer.ConsumerRecord;
    +import org.apache.kafka.streams.processor.TimestampExtractor;
    +
    +// Extracts the embedded timestamp of a record (giving you "event-time" semantics).
    +public class MyEventTimeExtractor implements TimestampExtractor {
    +
    +  @Override
    +  public long extract(final ConsumerRecord<Object, Object> record, final long previousTimestamp) {
    +    // `Foo` is your own custom class, which we assume has a method that returns
    +    // the embedded timestamp (milliseconds since midnight, January 1, 1970 UTC).
    +    long timestamp = -1;
    +    final Foo myPojo = (Foo) record.value();
    +    if (myPojo != null) {
    +      timestamp = myPojo.getTimestampInMillis();
    +    }
    +    if (timestamp < 0) {
    +      // Invalid timestamp!  Attempt to estimate a new timestamp,
    +      // otherwise fall back to wall-clock time (processing-time).
    +      if (previousTimestamp >= 0) {
    +        return previousTimestamp;
    +      } else {
    +        return System.currentTimeMillis();
    +      }
    +    }
    +  }
    +
    +}

    You would then define the custom timestamp extractor in your Streams configuration as follows:

    -
    import java.util.Properties;
    -import org.apache.kafka.streams.StreamsConfig;
    +              
    import java.util.Properties;
    +import org.apache.kafka.streams.StreamsConfig;
     
    -Properties streamsConfiguration = new Properties();
    -streamsConfiguration.put(StreamsConfig.DEFAULT_TIMESTAMP_EXTRACTOR_CLASS_CONFIG, MyEventTimeExtractor.class);
    -
    +Properties streamsConfiguration = new Properties(); +streamsConfiguration.put(StreamsConfig.DEFAULT_TIMESTAMP_EXTRACTOR_CLASS_CONFIG, MyEventTimeExtractor.class);

    @@ -707,38 +702,33 @@

    probing.rebalance.interval.ms

    The RocksDB configuration. Kafka Streams uses RocksDB as the default storage engine for persistent stores. To change the default configuration for RocksDB, you can implement RocksDBConfigSetter and provide your custom class via rocksdb.config.setter.

    Here is an example that adjusts the memory size consumed by RocksDB.

    -
    -
    -public static class CustomRocksDBConfig implements RocksDBConfigSetter {
    -    // This object should be a member variable so it can be closed in RocksDBConfigSetter#close.
    -    private org.rocksdb.Cache cache = new org.rocksdb.LRUCache(16 * 1024L * 1024L);
    -
    -    @Override
    -    public void setConfig(final String storeName, final Options options, final Map<String, Object> configs) {
    -        // See #1 below.
    -        BlockBasedTableConfig tableConfig = (BlockBasedTableConfig) options.tableFormatConfig();
    -        tableConfig.setBlockCache(cache);
    -        // See #2 below.
    -        tableConfig.setBlockSize(16 * 1024L);
    -        // See #3 below.
    -        tableConfig.setCacheIndexAndFilterBlocks(true);
    -        options.setTableFormatConfig(tableConfig);
    -        // See #4 below.
    -        options.setMaxWriteBufferNumber(2);
    -    }
    -
    -    @Override
    -    public void close(final String storeName, final Options options) {
    -        // See #5 below.
    -        cache.close();
    -    }
    -}
    -
    -Properties streamsSettings = new Properties();
    -streamsConfig.put(StreamsConfig.ROCKSDB_CONFIG_SETTER_CLASS_CONFIG, CustomRocksDBConfig.class);
    -
    -
    -
    +
    public static class CustomRocksDBConfig implements RocksDBConfigSetter {
    +    // This object should be a member variable so it can be closed in RocksDBConfigSetter#close.
    +    private org.rocksdb.Cache cache = new org.rocksdb.LRUCache(16 * 1024L * 1024L);
    +
    +    @Override
    +    public void setConfig(final String storeName, final Options options, final Map<String, Object> configs) {
    +        // See #1 below.
    +        BlockBasedTableConfig tableConfig = (BlockBasedTableConfig) options.tableFormatConfig();
    +        tableConfig.setBlockCache(cache);
    +        // See #2 below.
    +        tableConfig.setBlockSize(16 * 1024L);
    +        // See #3 below.
    +        tableConfig.setCacheIndexAndFilterBlocks(true);
    +        options.setTableFormatConfig(tableConfig);
    +        // See #4 below.
    +        options.setMaxWriteBufferNumber(2);
    +    }
    +
    +    @Override
    +    public void close(final String storeName, final Options options) {
    +        // See #5 below.
    +        cache.close();
    +    }
    +}
    +
    +Properties streamsSettings = new Properties();
    +streamsConfig.put(StreamsConfig.ROCKSDB_CONFIG_SETTER_CLASS_CONFIG, CustomRocksDBConfig.class);
    Notes for example:
      @@ -798,12 +788,12 @@

      Kafka consumers, producer and admin clie and admin client that are used internally. The consumer, producer and admin client settings are defined by specifying parameters in a StreamsConfig instance.

      In this example, the Kafka consumer session timeout is configured to be 60000 milliseconds in the Streams settings:

      -
      Properties streamsSettings = new Properties();
      -// Example of a "normal" setting for Kafka Streams
      -streamsSettings.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker-01:9092");
      -// Customize the Kafka consumer settings of your Streams application
      -streamsSettings.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 60000);
      -
      +
      Properties streamsSettings = new Properties();
      +// Example of a "normal" setting for Kafka Streams
      +streamsSettings.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker-01:9092");
      +// Customize the Kafka consumer settings of your Streams application
      +streamsSettings.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 60000);
      +

      Naming

      @@ -811,18 +801,17 @@

      Namingreceive.buffer.bytes are used to configure TCP buffers; request.timeout.ms and retry.backoff.ms control retries for client request; retries are used to configure how many retries are allowed when handling retriable errors from broker request responses. You can avoid duplicate names by prefix parameter names with consumer., producer., or admin. (e.g., consumer.send.buffer.bytes and producer.send.buffer.bytes).

      -
      Properties streamsSettings = new Properties();
      -// same value for consumer, producer, and admin client
      -streamsSettings.put("PARAMETER_NAME", "value");
      -// different values for consumer and producer
      -streamsSettings.put("consumer.PARAMETER_NAME", "consumer-value");
      -streamsSettings.put("producer.PARAMETER_NAME", "producer-value");
      -streamsSettings.put("admin.PARAMETER_NAME", "admin-value");
      -// alternatively, you can use
      -streamsSettings.put(StreamsConfig.consumerPrefix("PARAMETER_NAME"), "consumer-value");
      -streamsSettings.put(StreamsConfig.producerPrefix("PARAMETER_NAME"), "producer-value");
      -streamsSettings.put(StreamsConfig.adminClientPrefix("PARAMETER_NAME"), "admin-value");
      -
      +
      Properties streamsSettings = new Properties();
      +// same value for consumer, producer, and admin client
      +streamsSettings.put("PARAMETER_NAME", "value");
      +// different values for consumer and producer
      +streamsSettings.put("consumer.PARAMETER_NAME", "consumer-value");
      +streamsSettings.put("producer.PARAMETER_NAME", "producer-value");
      +streamsSettings.put("admin.PARAMETER_NAME", "admin-value");
      +// alternatively, you can use
      +streamsSettings.put(StreamsConfig.consumerPrefix("PARAMETER_NAME"), "consumer-value");
      +streamsSettings.put(StreamsConfig.producerPrefix("PARAMETER_NAME"), "producer-value");
      +streamsSettings.put(StreamsConfig.adminClientPrefix("PARAMETER_NAME"), "admin-value");

      You could further separate consumer configuration by adding different prefixes:

      For example, if you only want to set restore consumer config without touching other consumers' settings, you could simply use restore.consumer. to set the config.

      -
      Properties streamsSettings = new Properties();
      -// same config value for all consumer types
      -streamsSettings.put("consumer.PARAMETER_NAME", "general-consumer-value");
      -// set a different restore consumer config. This would make restore consumer take restore-consumer-value,
      -// while main consumer and global consumer stay with general-consumer-value
      -streamsSettings.put("restore.consumer.PARAMETER_NAME", "restore-consumer-value");
      -// alternatively, you can use
      -streamsSettings.put(StreamsConfig.restoreConsumerPrefix("PARAMETER_NAME"), "restore-consumer-value");
      -
      -
      +
      Properties streamsSettings = new Properties();
      +// same config value for all consumer types
      +streamsSettings.put("consumer.PARAMETER_NAME", "general-consumer-value");
      +// set a different restore consumer config. This would make restore consumer take restore-consumer-value,
      +// while main consumer and global consumer stay with general-consumer-value
      +streamsSettings.put("restore.consumer.PARAMETER_NAME", "restore-consumer-value");
      +// alternatively, you can use
      +streamsSettings.put(StreamsConfig.restoreConsumerPrefix("PARAMETER_NAME"), "restore-consumer-value");

      Same applied to main.consumer. and main.consumer., if you only want to specify one consumer type config.

      Additionally, to configure the internal repartition/changelog topics, you could use the topic. prefix, followed by any of the standard topic configs.

      -
      Properties streamsSettings = new Properties();
      -// Override default for both changelog and repartition topics
      -streamsSettings.put("topic.PARAMETER_NAME", "topic-value");
      -// alternatively, you can use
      -streamsSettings.put(StreamsConfig.topicPrefix("PARAMETER_NAME"), "topic-value");
      -
      -
      +
      Properties streamsSettings = new Properties();
      +// Override default for both changelog and repartition topics
      +streamsSettings.put("topic.PARAMETER_NAME", "topic-value");
      +// alternatively, you can use
      +streamsSettings.put(StreamsConfig.topicPrefix("PARAMETER_NAME"), "topic-value");

      @@ -977,11 +962,11 @@

      acksreplication.factor

      -
      Properties streamsSettings = new Properties();
      -streamsSettings.put(StreamsConfig.REPLICATION_FACTOR_CONFIG, 3);
      -streamsSettings.put(StreamsConfig.topicPrefix(TopicConfig.MIN_IN_SYNC_REPLICAS_CONFIG), 2);
      -streamsSettings.put(StreamsConfig.producerPrefix(ProducerConfig.ACKS_CONFIG), "all");
      -
      +
      Properties streamsSettings = new Properties();
      +streamsSettings.put(StreamsConfig.REPLICATION_FACTOR_CONFIG, 3);
      +streamsSettings.put(StreamsConfig.topicPrefix(TopicConfig.MIN_IN_SYNC_REPLICAS_CONFIG), 2);
      +streamsSettings.put(StreamsConfig.producerPrefix(ProducerConfig.ACKS_CONFIG), "all");
      +
      diff --git a/docs/streams/developer-guide/datatypes.html b/docs/streams/developer-guide/datatypes.html index 2201b5b69d35a..f527021dc4911 100644 --- a/docs/streams/developer-guide/datatypes.html +++ b/docs/streams/developer-guide/datatypes.html @@ -55,40 +55,37 @@

      Configuring SerDes

      SerDes specified in the Streams configuration are used as the default in your Kafka Streams application.

      -
      import org.apache.kafka.common.serialization.Serdes;
      -import org.apache.kafka.streams.StreamsConfig;
      -
      -Properties settings = new Properties();
      -// Default serde for keys of data records (here: built-in serde for String type)
      -settings.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName());
      -// Default serde for values of data records (here: built-in serde for Long type)
      -settings.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.Long().getClass().getName());
      -
      +
      import org.apache.kafka.common.serialization.Serdes;
      +import org.apache.kafka.streams.StreamsConfig;
      +
      +Properties settings = new Properties();
      +// Default serde for keys of data records (here: built-in serde for String type)
      +settings.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName());
      +// Default serde for values of data records (here: built-in serde for Long type)
      +settings.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.Long().getClass().getName());

      Overriding default SerDes

      You can also specify SerDes explicitly by passing them to the appropriate API methods, which overrides the default serde settings:

      -
      import org.apache.kafka.common.serialization.Serde;
      -import org.apache.kafka.common.serialization.Serdes;
      +      
      import org.apache.kafka.common.serialization.Serde;
      +import org.apache.kafka.common.serialization.Serdes;
       
      -final Serde<String> stringSerde = Serdes.String();
      -final Serde<Long> longSerde = Serdes.Long();
      +final Serde<String> stringSerde = Serdes.String();
      +final Serde<Long> longSerde = Serdes.Long();
       
      -// The stream userCountByRegion has type `String` for record keys (for region)
      -// and type `Long` for record values (for user counts).
      -KStream<String, Long> userCountByRegion = ...;
      -userCountByRegion.to("RegionCountsTopic", Produced.with(stringSerde, longSerde));
      -
      +// The stream userCountByRegion has type `String` for record keys (for region) +// and type `Long` for record values (for user counts). +KStream<String, Long> userCountByRegion = ...; +userCountByRegion.to("RegionCountsTopic", Produced.with(stringSerde, longSerde));

      If you want to override serdes selectively, i.e., keep the defaults for some fields, then don’t specify the serde whenever you want to leverage the default settings:

      -
      import org.apache.kafka.common.serialization.Serde;
      -import org.apache.kafka.common.serialization.Serdes;
      -
      -// Use the default serializer for record keys (here: region as String) by not specifying the key serde,
      -// but override the default serializer for record values (here: userCount as Long).
      -final Serde<Long> longSerde = Serdes.Long();
      -KStream<String, Long> userCountByRegion = ...;
      -userCountByRegion.to("RegionCountsTopic", Produced.valueSerde(Serdes.Long()));
      -
      +
      import org.apache.kafka.common.serialization.Serde;
      +import org.apache.kafka.common.serialization.Serdes;
      +
      +// Use the default serializer for record keys (here: region as String) by not specifying the key serde,
      +// but override the default serializer for record values (here: userCount as Long).
      +final Serde<Long> longSerde = Serdes.Long();
      +KStream<String, Long> userCountByRegion = ...;
      +userCountByRegion.to("RegionCountsTopic", Produced.valueSerde(Serdes.Long()));

      If some of your incoming records are corrupted or ill-formatted, they will cause the deserializer class to report an error. Since 1.0.x we have introduced an DeserializationExceptionHandler interface which allows you to customize how to handle such records. The customized implementation of the interface can be specified via the StreamsConfig. @@ -101,12 +98,11 @@

      Overriding default SerDes

      Apache Kafka includes several built-in serde implementations for Java primitives and basic types such as byte[] in its kafka-clients Maven artifact:

      -
      <dependency>
      -    <groupId>org.apache.kafka</groupId>
      -    <artifactId>kafka-clients</artifactId>
      -    <version>{{fullDotVersion}}</version>
      -</dependency>
      -
      +
      <dependency>
      +    <groupId>org.apache.kafka</groupId>
      +    <artifactId>kafka-clients</artifactId>
      +    <version>2.8.0</version>
      +</dependency>

      This artifact provides the following serde implementations under the package org.apache.kafka.common.serialization, which you can leverage when e.g., defining default serializers in your Streams configuration.

      diff --git a/docs/streams/developer-guide/dsl-api.html b/docs/streams/developer-guide/dsl-api.html index 2add551d61137..d2bce047868c7 100644 --- a/docs/streams/developer-guide/dsl-api.html +++ b/docs/streams/developer-guide/dsl-api.html @@ -242,19 +242,18 @@

      import org.apache.kafka.common.serialization.Serdes;
      -import org.apache.kafka.streams.StreamsBuilder;
      -import org.apache.kafka.streams.kstream.KStream;
      -
      -StreamsBuilder builder = new StreamsBuilder();
      -
      -KStream<String, Long> wordCounts = builder.stream(
      -    "word-counts-input-topic", /* input topic */
      -    Consumed.with(
      -      Serdes.String(), /* key serde */
      -      Serdes.Long()   /* value serde */
      -    );
      - +
      import org.apache.kafka.common.serialization.Serdes;
      +import org.apache.kafka.streams.StreamsBuilder;
      +import org.apache.kafka.streams.kstream.KStream;
      +
      +StreamsBuilder builder = new StreamsBuilder();
      +
      +KStream<String, Long> wordCounts = builder.stream(
      +    "word-counts-input-topic", /* input topic */
      +    Consumed.with(
      +      Serdes.String(), /* key serde */
      +      Serdes.Long()   /* value serde */
      +    );

      If you do not specify SerDes explicitly, the default SerDes from the configuration are used.

      You must specify SerDes explicitly if the key or value types of the records in the Kafka input @@ -303,20 +302,19 @@

      state store that backs the table). This is required for supporting interactive queries against the table. When a name is not provided the table will not be queryable and an internal name will be provided for the state store.

      -
      import org.apache.kafka.common.serialization.Serdes;
      -import org.apache.kafka.streams.StreamsBuilder;
      -import org.apache.kafka.streams.kstream.GlobalKTable;
      -
      -StreamsBuilder builder = new StreamsBuilder();
      -
      -GlobalKTable<String, Long> wordCounts = builder.globalTable(
      -    "word-counts-input-topic",
      -    Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as(
      -      "word-counts-global-store" /* table/store name */)
      -      .withKeySerde(Serdes.String()) /* key serde */
      -      .withValueSerde(Serdes.Long()) /* value serde */
      -    );
      -
      +
      import org.apache.kafka.common.serialization.Serdes;
      +import org.apache.kafka.streams.StreamsBuilder;
      +import org.apache.kafka.streams.kstream.GlobalKTable;
      +
      +StreamsBuilder builder = new StreamsBuilder();
      +
      +GlobalKTable<String, Long> wordCounts = builder.globalTable(
      +    "word-counts-input-topic",
      +    Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as(
      +      "word-counts-global-store" /* table/store name */)
      +      .withKeySerde(Serdes.String()) /* key serde */
      +      .withValueSerde(Serdes.Long()) /* value serde */
      +    );

      You must specify SerDes explicitly if the key or value types of the records in the Kafka input topics do not match the configured default SerDes. For information about configuring default SerDes, available SerDes, and implementing your own custom SerDes see Data Types and Serialization.

      @@ -371,23 +369,21 @@

      -KStream stream = ...; -Map> branches = - stream.split(Named.as("Branch-")) - .branch((key, value) -> key.startsWith("A"), /* first predicate */ - Branched.as("A")) - .branch((key, value) -> key.startsWith("B"), /* second predicate */ - Branched.as("B")) -.defaultBranch(Branched.as("C")) +
      KStream<String, Long> stream = ...;
      +Map<String, KStream<String, Long>> branches =
      +    stream.split(Named.as("Branch-"))
      +        .branch((key, value) -> key.startsWith("A"),  /* first predicate  */
      +             Branched.as("A"))
      +        .branch((key, value) -> key.startsWith("B"),  /* second predicate */
      +             Branched.as("B"))
      +.defaultBranch(Branched.as("C"))
       );
       
      -// KStream branches.get("Branch-A") contains all records whose keys start with "A"
      -// KStream branches.get("Branch-B") contains all records whose keys start with "B"
      -// KStream branches.get("Branch-C") contains all other records
      +// KStream branches.get("Branch-A") contains all records whose keys start with "A"
      +// KStream branches.get("Branch-B") contains all records whose keys start with "B"
      +// KStream branches.get("Branch-C") contains all other records
       
      -// Java 7 example: cf. `filter` for how to create `Predicate` instances     
      -                            
      +// Java 7 example: cf. `filter` for how to create `Predicate` instances

      @@ -861,9 +838,8 @@

      repartition() operation always triggers repartitioning of the stream, as a result it can be used with embedded Processor API methods (like transform() et al.) that do not trigger auto repartitioning when key changing operation is performed beforehand. -
      KStream<byte[], String> stream = ... ;
      -KStream<byte[], String> repartitionedStream = stream.repartition(Repartitioned.numberOfPartitions(10));
      -
      +
      KStream<byte[], String> stream = ... ;
      +KStream<byte[], String> repartitionedStream = stream.repartition(Repartitioned.numberOfPartitions(10));

      @@ -898,45 +874,43 @@

      // Assume the record values represent lines of text.  For the sake of this example, you can ignore
      -// whatever may be stored in the record keys.
      -KStream<String, String> textLines = ...;
      -
      -KStream<String, Long> wordCounts = textLines
      -    // Split each text line, by whitespace, into words.  The text lines are the record
      -    // values, i.e. you can ignore whatever data is in the record keys and thus invoke
      -    // `flatMapValues` instead of the more generic `flatMap`.
      -    .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+")))
      -    // Group the stream by word to ensure the key of the record is the word.
      -    .groupBy((key, word) -> word)
      -    // Count the occurrences of each word (record key).
      -    //
      -    // This will change the stream type from `KGroupedStream<String, String>` to
      -    // `KTable<String, Long>` (word -> count).
      -    .count()
      -    // Convert the `KTable<String, Long>` into a `KStream<String, Long>`.
      -    .toStream();
      - +
      // Assume the record values represent lines of text.  For the sake of this example, you can ignore
      +// whatever may be stored in the record keys.
      +KStream<String, String> textLines = ...;
      +
      +KStream<String, Long> wordCounts = textLines
      +    // Split each text line, by whitespace, into words.  The text lines are the record
      +    // values, i.e. you can ignore whatever data is in the record keys and thus invoke
      +    // `flatMapValues` instead of the more generic `flatMap`.
      +    .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+")))
      +    // Group the stream by word to ensure the key of the record is the word.
      +    .groupBy((key, word) -> word)
      +    // Count the occurrences of each word (record key).
      +    //
      +    // This will change the stream type from `KGroupedStream<String, String>` to
      +    // `KTable<String, Long>` (word -> count).
      +    .count()
      +    // Convert the `KTable<String, Long>` into a `KStream<String, Long>`.
      +    .toStream();

      WordCount example in Java 7:

      -
      // Code below is equivalent to the previous Java 8+ example above.
      -KStream<String, String> textLines = ...;
      -
      -KStream<String, Long> wordCounts = textLines
      -    .flatMapValues(new ValueMapper<String, Iterable<String>>() {
      -        @Override
      -        public Iterable<String> apply(String value) {
      -            return Arrays.asList(value.toLowerCase().split("\\W+"));
      -        }
      -    })
      -    .groupBy(new KeyValueMapper<String, String, String>>() {
      -        @Override
      -        public String apply(String key, String word) {
      -            return word;
      -        }
      -    })
      -    .count()
      -    .toStream();
      -
      +
      // Code below is equivalent to the previous Java 8+ example above.
      +KStream<String, String> textLines = ...;
      +
      +KStream<String, Long> wordCounts = textLines
      +    .flatMapValues(new ValueMapper<String, Iterable<String>>() {
      +        @Override
      +        public Iterable<String> apply(String value) {
      +            return Arrays.asList(value.toLowerCase().split("\\W+"));
      +        }
      +    })
      +    .groupBy(new KeyValueMapper<String, String, String>>() {
      +        @Override
      +        public String apply(String key, String word) {
      +            return word;
      +        }
      +    })
      +    .count()
      +    .toStream();

      Aggregating

      After records are grouped by key via groupByKey or @@ -973,69 +947,68 @@

      aggValue = 0)

      Several variants of aggregate exist, see Javadocs for details.

      -
      KGroupedStream<byte[], String> groupedStream = ...;
      -KGroupedTable<byte[], String> groupedTable = ...;
      -
      -// Java 8+ examples, using lambda expressions
      -
      -// Aggregating a KGroupedStream (note how the value type changes from String to Long)
      -KTable<byte[], Long> aggregatedStream = groupedStream.aggregate(
      -    () -> 0L, /* initializer */
      -    (aggKey, newValue, aggValue) -> aggValue + newValue.length(), /* adder */
      -    Materialized.as("aggregated-stream-store") /* state store name */
      -        .withValueSerde(Serdes.Long()); /* serde for aggregate value */
      -
      -// Aggregating a KGroupedTable (note how the value type changes from String to Long)
      -KTable<byte[], Long> aggregatedTable = groupedTable.aggregate(
      -    () -> 0L, /* initializer */
      -    (aggKey, newValue, aggValue) -> aggValue + newValue.length(), /* adder */
      -    (aggKey, oldValue, aggValue) -> aggValue - oldValue.length(), /* subtractor */
      -    Materialized.as("aggregated-table-store") /* state store name */
      -	.withValueSerde(Serdes.Long()) /* serde for aggregate value */
      -
      -
      -// Java 7 examples
      -
      -// Aggregating a KGroupedStream (note how the value type changes from String to Long)
      -KTable<byte[], Long> aggregatedStream = groupedStream.aggregate(
      -    new Initializer<Long>() { /* initializer */
      -      @Override
      -      public Long apply() {
      -        return 0L;
      -      }
      -    },
      -    new Aggregator<byte[], String, Long>() { /* adder */
      -      @Override
      -      public Long apply(byte[] aggKey, String newValue, Long aggValue) {
      -        return aggValue + newValue.length();
      -      }
      -    },
      -    Materialized.as("aggregated-stream-store")
      -        .withValueSerde(Serdes.Long());
      -
      -// Aggregating a KGroupedTable (note how the value type changes from String to Long)
      -KTable<byte[], Long> aggregatedTable = groupedTable.aggregate(
      -    new Initializer<Long>() { /* initializer */
      -      @Override
      -      public Long apply() {
      -        return 0L;
      -      }
      -    },
      -    new Aggregator<byte[], String, Long>() { /* adder */
      -      @Override
      -      public Long apply(byte[] aggKey, String newValue, Long aggValue) {
      -        return aggValue + newValue.length();
      -      }
      -    },
      -    new Aggregator<byte[], String, Long>() { /* subtractor */
      -      @Override
      -      public Long apply(byte[] aggKey, String oldValue, Long aggValue) {
      -        return aggValue - oldValue.length();
      -      }
      -    },
      -    Materialized.as("aggregated-stream-store")
      -        .withValueSerde(Serdes.Long());
      -
      +
      KGroupedStream<byte[], String> groupedStream = ...;
      +KGroupedTable<byte[], String> groupedTable = ...;
      +
      +// Java 8+ examples, using lambda expressions
      +
      +// Aggregating a KGroupedStream (note how the value type changes from String to Long)
      +KTable<byte[], Long> aggregatedStream = groupedStream.aggregate(
      +    () -> 0L, /* initializer */
      +    (aggKey, newValue, aggValue) -> aggValue + newValue.length(), /* adder */
      +    Materialized.as("aggregated-stream-store") /* state store name */
      +        .withValueSerde(Serdes.Long()); /* serde for aggregate value */
      +
      +// Aggregating a KGroupedTable (note how the value type changes from String to Long)
      +KTable<byte[], Long> aggregatedTable = groupedTable.aggregate(
      +    () -> 0L, /* initializer */
      +    (aggKey, newValue, aggValue) -> aggValue + newValue.length(), /* adder */
      +    (aggKey, oldValue, aggValue) -> aggValue - oldValue.length(), /* subtractor */
      +    Materialized.as("aggregated-table-store") /* state store name */
      +	.withValueSerde(Serdes.Long()) /* serde for aggregate value */
      +
      +
      +// Java 7 examples
      +
      +// Aggregating a KGroupedStream (note how the value type changes from String to Long)
      +KTable<byte[], Long> aggregatedStream = groupedStream.aggregate(
      +    new Initializer<Long>() { /* initializer */
      +      @Override
      +      public Long apply() {
      +        return 0L;
      +      }
      +    },
      +    new Aggregator<byte[], String, Long>() { /* adder */
      +      @Override
      +      public Long apply(byte[] aggKey, String newValue, Long aggValue) {
      +        return aggValue + newValue.length();
      +      }
      +    },
      +    Materialized.as("aggregated-stream-store")
      +        .withValueSerde(Serdes.Long());
      +
      +// Aggregating a KGroupedTable (note how the value type changes from String to Long)
      +KTable<byte[], Long> aggregatedTable = groupedTable.aggregate(
      +    new Initializer<Long>() { /* initializer */
      +      @Override
      +      public Long apply() {
      +        return 0L;
      +      }
      +    },
      +    new Aggregator<byte[], String, Long>() { /* adder */
      +      @Override
      +      public Long apply(byte[] aggKey, String newValue, Long aggValue) {
      +        return aggValue + newValue.length();
      +      }
      +    },
      +    new Aggregator<byte[], String, Long>() { /* subtractor */
      +      @Override
      +      public Long apply(byte[] aggKey, String oldValue, Long aggValue) {
      +        return aggValue - oldValue.length();
      +      }
      +    },
      +    Materialized.as("aggregated-stream-store")
      +        .withValueSerde(Serdes.Long());

      Detailed behavior of KGroupedStream:

      Filter

      @@ -399,21 +395,20 @@

      Evaluates a boolean function for each element and retains those for which the function returns true. (KStream details, KTable details)

      -
      KStream<String, Long> stream = ...;
      -
      -// A filter that selects (keeps) only positive numbers
      -// Java 8+ example, using lambda expressions
      -KStream<String, Long> onlyPositives = stream.filter((key, value) -> value > 0);
      -
      -// Java 7 example
      -KStream<String, Long> onlyPositives = stream.filter(
      -    new Predicate<String, Long>() {
      -      @Override
      -      public boolean test(String key, Long value) {
      -        return value > 0;
      -      }
      -    });
      -
      +
      KStream<String, Long> stream = ...;
      +
      +// A filter that selects (keeps) only positive numbers
      +// Java 8+ example, using lambda expressions
      +KStream<String, Long> onlyPositives = stream.filter((key, value) -> value > 0);
      +
      +// Java 7 example
      +KStream<String, Long> onlyPositives = stream.filter(
      +    new Predicate<String, Long>() {
      +      @Override
      +      public boolean test(String key, Long value) {
      +        return value > 0;
      +      }
      +    });

      Inverse Filter

      @@ -425,21 +420,20 @@

      Evaluates a boolean function for each element and drops those for which the function returns true. (KStream details, KTable details)

      -
      KStream<String, Long> stream = ...;
      -
      -// An inverse filter that discards any negative numbers or zero
      -// Java 8+ example, using lambda expressions
      -KStream<String, Long> onlyPositives = stream.filterNot((key, value) -> value <= 0);
      -
      -// Java 7 example
      -KStream<String, Long> onlyPositives = stream.filterNot(
      -    new Predicate<String, Long>() {
      -      @Override
      -      public boolean test(String key, Long value) {
      -        return value <= 0;
      -      }
      -    });
      -
      +
      KStream<String, Long> stream = ...;
      +
      +// An inverse filter that discards any negative numbers or zero
      +// Java 8+ example, using lambda expressions
      +KStream<String, Long> onlyPositives = stream.filterNot((key, value) -> value <= 0);
      +
      +// Java 7 example
      +KStream<String, Long> onlyPositives = stream.filterNot(
      +    new Predicate<String, Long>() {
      +      @Override
      +      public boolean test(String key, Long value) {
      +        return value <= 0;
      +      }
      +    });

      FlatMap

      @@ -453,21 +447,20 @@

      flatMap will result in re-partitioning of the records. If possible use flatMapValues instead, which will not cause data re-partitioning.

      -
      KStream<Long, String> stream = ...;
      -KStream<String, Integer> transformed = stream.flatMap(
      -     // Here, we generate two output records for each input record.
      -     // We also change the key and value types.
      -     // Example: (345L, "Hello") -> ("HELLO", 1000), ("hello", 9000)
      -    (key, value) -> {
      -      List<KeyValue<String, Integer>> result = new LinkedList<>();
      -      result.add(KeyValue.pair(value.toUpperCase(), 1000));
      -      result.add(KeyValue.pair(value.toLowerCase(), 9000));
      -      return result;
      -    }
      -  );
      -
      -// Java 7 example: cf. `map` for how to create `KeyValueMapper` instances
      -
      +
      KStream<Long, String> stream = ...;
      +KStream<String, Integer> transformed = stream.flatMap(
      +     // Here, we generate two output records for each input record.
      +     // We also change the key and value types.
      +     // Example: (345L, "Hello") -> ("HELLO", 1000), ("hello", 9000)
      +    (key, value) -> {
      +      List<KeyValue<String, Integer>> result = new LinkedList<>();
      +      result.add(KeyValue.pair(value.toUpperCase(), 1000));
      +      result.add(KeyValue.pair(value.toLowerCase(), 9000));
      +      return result;
      +    }
      +  );
      +
      +// Java 7 example: cf. `map` for how to create `KeyValueMapper` instances

      FlatMap (values only)

      @@ -480,12 +473,11 @@

      details)

      flatMapValues is preferable to flatMap because it will not cause data re-partitioning. However, you cannot modify the key or key type like flatMap does.

      -
      // Split a sentence into words.
      -KStream<byte[], String> sentences = ...;
      -KStream<byte[], String> words = sentences.flatMapValues(value -> Arrays.asList(value.split("\\s+")));
      +                            
      // Split a sentence into words.
      +KStream<byte[], String> sentences = ...;
      +KStream<byte[], String> words = sentences.flatMapValues(value -> Arrays.asList(value.split("\\s+")));
       
      -// Java 7 example: cf. `mapValues` for how to create `ValueMapper` instances
      -
      +// Java 7 example: cf. `mapValues` for how to create `ValueMapper` instances

      Foreach

      @@ -501,21 +493,20 @@

      peek, which is not a terminal operation).

      Note on processing guarantees: Any side effects of an action (such as writing to external systems) are not trackable by Kafka, which means they will typically not benefit from Kafka’s processing guarantees.

      -
      KStream<String, Long> stream = ...;
      -
      -// Print the contents of the KStream to the local console.
      -// Java 8+ example, using lambda expressions
      -stream.foreach((key, value) -> System.out.println(key + " => " + value));
      -
      -// Java 7 example
      -stream.foreach(
      -    new ForeachAction<String, Long>() {
      -      @Override
      -      public void apply(String key, Long value) {
      -        System.out.println(key + " => " + value);
      -      }
      -    });
      -
      +
      KStream<String, Long> stream = ...;
      +
      +// Print the contents of the KStream to the local console.
      +// Java 8+ example, using lambda expressions
      +stream.foreach((key, value) -> System.out.println(key + " => " + value));
      +
      +// Java 7 example
      +stream.foreach(
      +    new ForeachAction<String, Long>() {
      +      @Override
      +      public void apply(String key, Long value) {
      +        System.out.println(key + " => " + value);
      +      }
      +    });

      GroupByKey

      @@ -543,20 +534,19 @@

      groupByKey is preferable to groupBy because it re-partitions data only if the stream was already marked for re-partitioning. However, groupByKey does not allow you to modify the key or key type like groupBy does.

      -
      KStream<byte[], String> stream = ...;
      -
      -// Group by the existing key, using the application's configured
      -// default serdes for keys and values.
      -KGroupedStream<byte[], String> groupedStream = stream.groupByKey();
      -
      -// When the key and/or value types do not match the configured
      -// default serdes, we must explicitly specify serdes.
      -KGroupedStream<byte[], String> groupedStream = stream.groupByKey(
      -    Grouped.with(
      -      Serdes.ByteArray(), /* key */
      -      Serdes.String())     /* value */
      -  );
      -
      +
      KStream<byte[], String> stream = ...;
      +
      +// Group by the existing key, using the application's configured
      +// default serdes for keys and values.
      +KGroupedStream<byte[], String> groupedStream = stream.groupByKey();
      +
      +// When the key and/or value types do not match the configured
      +// default serdes, we must explicitly specify serdes.
      +KGroupedStream<byte[], String> groupedStream = stream.groupByKey(
      +    Grouped.with(
      +      Serdes.ByteArray(), /* key */
      +      Serdes.String())     /* value */
      +  );

      GroupBy

      @@ -586,56 +576,55 @@

      groupBy always causes data re-partitioning. If possible use groupByKey instead, which will re-partition data only if required.

      -
      KStream<byte[], String> stream = ...;
      -KTable<byte[], String> table = ...;
      -
      -// Java 8+ examples, using lambda expressions
      -
      -// Group the stream by a new key and key type
      -KGroupedStream<String, String> groupedStream = stream.groupBy(
      -    (key, value) -> value,
      -    Grouped.with(
      -      Serdes.String(), /* key (note: type was modified) */
      -      Serdes.String())  /* value */
      -  );
      -
      -// Group the table by a new key and key type, and also modify the value and value type.
      -KGroupedTable<String, Integer> groupedTable = table.groupBy(
      -    (key, value) -> KeyValue.pair(value, value.length()),
      -    Grouped.with(
      -      Serdes.String(), /* key (note: type was modified) */
      -      Serdes.Integer()) /* value (note: type was modified) */
      -  );
      -
      -
      -// Java 7 examples
      -
      -// Group the stream by a new key and key type
      -KGroupedStream<String, String> groupedStream = stream.groupBy(
      -    new KeyValueMapper<byte[], String, String>>() {
      -      @Override
      -      public String apply(byte[] key, String value) {
      -        return value;
      -      }
      -    },
      -    Grouped.with(
      -      Serdes.String(), /* key (note: type was modified) */
      -      Serdes.String())  /* value */
      -  );
      -
      -// Group the table by a new key and key type, and also modify the value and value type.
      -KGroupedTable<String, Integer> groupedTable = table.groupBy(
      -    new KeyValueMapper<byte[], String, KeyValue<String, Integer>>() {
      -      @Override
      -      public KeyValue<String, Integer> apply(byte[] key, String value) {
      -        return KeyValue.pair(value, value.length());
      -      }
      -    },
      -    Grouped.with(
      -      Serdes.String(), /* key (note: type was modified) */
      -      Serdes.Integer()) /* value (note: type was modified) */
      -  );
      -
      +
      KStream<byte[], String> stream = ...;
      +KTable<byte[], String> table = ...;
      +
      +// Java 8+ examples, using lambda expressions
      +
      +// Group the stream by a new key and key type
      +KGroupedStream<String, String> groupedStream = stream.groupBy(
      +    (key, value) -> value,
      +    Grouped.with(
      +      Serdes.String(), /* key (note: type was modified) */
      +      Serdes.String())  /* value */
      +  );
      +
      +// Group the table by a new key and key type, and also modify the value and value type.
      +KGroupedTable<String, Integer> groupedTable = table.groupBy(
      +    (key, value) -> KeyValue.pair(value, value.length()),
      +    Grouped.with(
      +      Serdes.String(), /* key (note: type was modified) */
      +      Serdes.Integer()) /* value (note: type was modified) */
      +  );
      +
      +
      +// Java 7 examples
      +
      +// Group the stream by a new key and key type
      +KGroupedStream<String, String> groupedStream = stream.groupBy(
      +    new KeyValueMapper<byte[], String, String>>() {
      +      @Override
      +      public String apply(byte[] key, String value) {
      +        return value;
      +      }
      +    },
      +    Grouped.with(
      +      Serdes.String(), /* key (note: type was modified) */
      +      Serdes.String())  /* value */
      +  );
      +
      +// Group the table by a new key and key type, and also modify the value and value type.
      +KGroupedTable<String, Integer> groupedTable = table.groupBy(
      +    new KeyValueMapper<byte[], String, KeyValue<String, Integer>>() {
      +      @Override
      +      public KeyValue<String, Integer> apply(byte[] key, String value) {
      +        return KeyValue.pair(value, value.length());
      +      }
      +    },
      +    Grouped.with(
      +      Serdes.String(), /* key (note: type was modified) */
      +      Serdes.Integer()) /* value (note: type was modified) */
      +  );

      Cogroup

      @@ -650,19 +639,18 @@

      windowed before it is aggregated.

      Cogroup does not cause a repartition as it has the prerequisite that the input streams are grouped. In the process of creating these groups they will have already been repartitioned if the stream was already marked for repartitioning.

      -
      KStream<byte[], String> stream = ...;
      -                        KStream<byte[], String> stream2 = ...;
      +                            
      KStream<byte[], String> stream = ...;
      +                        KStream<byte[], String> stream2 = ...;
       
      -// Group by the existing key, using the application's configured
      -// default serdes for keys and values.
      -KGroupedStream<byte[], String> groupedStream = stream.groupByKey();
      -KGroupedStream<byte[], String> groupedStream2 = stream2.groupByKey();
      -CogroupedKStream<byte[], String> cogroupedStream = groupedStream.cogroup(aggregator1).cogroup(groupedStream2, aggregator2);
      +// Group by the existing key, using the application's configured
      +// default serdes for keys and values.
      +KGroupedStream<byte[], String> groupedStream = stream.groupByKey();
      +KGroupedStream<byte[], String> groupedStream2 = stream2.groupByKey();
      +CogroupedKStream<byte[], String> cogroupedStream = groupedStream.cogroup(aggregator1).cogroup(groupedStream2, aggregator2);
       
      -KTable<byte[], String> table = cogroupedStream.aggregate(initializer);
      +KTable<byte[], String> table = cogroupedStream.aggregate(initializer);
       
      -KTable<byte[], String> table2 = cogroupedStream.windowedBy(TimeWindows.duration(500ms)).aggregate(initializer);
      -
      +KTable<byte[], String> table2 = cogroupedStream.windowedBy(TimeWindows.duration(500ms)).aggregate(initializer);

      Map

      @@ -675,23 +663,22 @@

      map will result in re-partitioning of the records. If possible use mapValues instead, which will not cause data re-partitioning.

      -
      KStream<byte[], String> stream = ...;
      -
      -// Java 8+ example, using lambda expressions
      -// Note how we change the key and the key type (similar to `selectKey`)
      -// as well as the value and the value type.
      -KStream<String, Integer> transformed = stream.map(
      -    (key, value) -> KeyValue.pair(value.toLowerCase(), value.length()));
      -
      -// Java 7 example
      -KStream<String, Integer> transformed = stream.map(
      -    new KeyValueMapper<byte[], String, KeyValue<String, Integer>>() {
      -      @Override
      -      public KeyValue<String, Integer> apply(byte[] key, String value) {
      -        return new KeyValue<>(value.toLowerCase(), value.length());
      -      }
      -    });
      -
      +
      KStream<byte[], String> stream = ...;
      +
      +// Java 8+ example, using lambda expressions
      +// Note how we change the key and the key type (similar to `selectKey`)
      +// as well as the value and the value type.
      +KStream<String, Integer> transformed = stream.map(
      +    (key, value) -> KeyValue.pair(value.toLowerCase(), value.length()));
      +
      +// Java 7 example
      +KStream<String, Integer> transformed = stream.map(
      +    new KeyValueMapper<byte[], String, KeyValue<String, Integer>>() {
      +      @Override
      +      public KeyValue<String, Integer> apply(byte[] key, String value) {
      +        return new KeyValue<>(value.toLowerCase(), value.length());
      +      }
      +    });

      Map (values only)

      @@ -706,20 +693,19 @@

      KTable details)

      mapValues is preferable to map because it will not cause data re-partitioning. However, it does not allow you to modify the key or key type like map does.

      -
      KStream<byte[], String> stream = ...;
      -
      -// Java 8+ example, using lambda expressions
      -KStream<byte[], String> uppercased = stream.mapValues(value -> value.toUpperCase());
      -
      -// Java 7 example
      -KStream<byte[], String> uppercased = stream.mapValues(
      -    new ValueMapper<String>() {
      -      @Override
      -      public String apply(String s) {
      -        return s.toUpperCase();
      -      }
      -    });
      -
      +
      KStream<byte[], String> stream = ...;
      +
      +// Java 8+ example, using lambda expressions
      +KStream<byte[], String> uppercased = stream.mapValues(value -> value.toUpperCase());
      +
      +// Java 7 example
      +KStream<byte[], String> uppercased = stream.mapValues(
      +    new ValueMapper<String>() {
      +      @Override
      +      public String apply(String s) {
      +        return s.toUpperCase();
      +      }
      +    });

      Merge

      @@ -732,15 +718,11 @@

      details)

      There is no ordering guarantee between records from different streams in the merged stream. Relative order is preserved within each input stream though (ie, records within the same input stream are processed in order)

      -
      -
      -
      KStream<byte[], String> stream1 = ...;
      +                            
      KStream<byte[], String> stream1 = ...;
       
      -KStream<byte[], String> stream2 = ...;
      +KStream<byte[], String> stream2 = ...;
       
      -KStream<byte[], String> merged = stream1.merge(stream2);
      -
      -
      +KStream<byte[], String> merged = stream1.merge(stream2);

      Peek

      @@ -756,21 +738,20 @@

      peek is helpful for use cases such as logging or tracking metrics or for debugging and troubleshooting.

      Note on processing guarantees: Any side effects of an action (such as writing to external systems) are not trackable by Kafka, which means they will typically not benefit from Kafka’s processing guarantees.

      -
      KStream<byte[], String> stream = ...;
      -
      -// Java 8+ example, using lambda expressions
      -KStream<byte[], String> unmodifiedStream = stream.peek(
      -    (key, value) -> System.out.println("key=" + key + ", value=" + value));
      -
      -// Java 7 example
      -KStream<byte[], String> unmodifiedStream = stream.peek(
      -    new ForeachAction<byte[], String>() {
      -      @Override
      -      public void apply(byte[] key, String value) {
      -        System.out.println("key=" + key + ", value=" + value);
      -      }
      -    });
      -
      +
      KStream<byte[], String> stream = ...;
      +
      +// Java 8+ example, using lambda expressions
      +KStream<byte[], String> unmodifiedStream = stream.peek(
      +    (key, value) -> System.out.println("key=" + key + ", value=" + value));
      +
      +// Java 7 example
      +KStream<byte[], String> unmodifiedStream = stream.peek(
      +    new ForeachAction<byte[], String>() {
      +      @Override
      +      public void apply(byte[] key, String value) {
      +        System.out.println("key=" + key + ", value=" + value);
      +      }
      +    });

      Print

      @@ -783,13 +764,12 @@

      details)

      Calling print() is the same as calling foreach((key, value) -> System.out.println(key + ", " + value))

      print is mainly for debugging/testing purposes, and it will try to flush on each record print. Hence it should not be used for production usage if performance requirements are concerned.

      -
      KStream<byte[], String> stream = ...;
      -// print to sysout
      -stream.print();
      +                            
      KStream<byte[], String> stream = ...;
      +// print to sysout
      +stream.print();
       
      -// print to file with a custom label
      -stream.print(Printed.toFile("streams.out").withLabel("streams"));
      -
      +// print to file with a custom label +stream.print(Printed.toFile("streams.out").withLabel("streams"));

      SelectKey

      @@ -802,21 +782,20 @@

      selectKey(mapper) is the same as calling map((key, value) -> mapper(key, value), value).

      Marks the stream for data re-partitioning: Applying a grouping or a join after selectKey will result in re-partitioning of the records.

      -
      KStream<byte[], String> stream = ...;
      -
      -// Derive a new record key from the record's value.  Note how the key type changes, too.
      -// Java 8+ example, using lambda expressions
      -KStream<String, String> rekeyed = stream.selectKey((key, value) -> value.split(" ")[0])
      -
      -// Java 7 example
      -KStream<String, String> rekeyed = stream.selectKey(
      -    new KeyValueMapper<byte[], String, String>() {
      -      @Override
      -      public String apply(byte[] key, String value) {
      -        return value.split(" ")[0];
      -      }
      -    });
      -
      +
      KStream<byte[], String> stream = ...;
      +
      +// Derive a new record key from the record's value.  Note how the key type changes, too.
      +// Java 8+ example, using lambda expressions
      +KStream<String, String> rekeyed = stream.selectKey((key, value) -> value.split(" ")[0])
      +
      +// Java 7 example
      +KStream<String, String> rekeyed = stream.selectKey(
      +    new KeyValueMapper<byte[], String, String>() {
      +      @Override
      +      public String apply(byte[] key, String value) {
      +        return value.split(" ")[0];
      +      }
      +    });

      Table to Stream

      @@ -826,12 +805,11 @@

      Get the changelog stream of this table. (details)

      -
      KTable<byte[], String> table = ...;
      +                            
      KTable<byte[], String> table = ...;
       
      -// Also, a variant of `toStream` exists that allows you
      -// to select a new key for the resulting stream.
      -KStream<byte[], String> stream = table.toStream();
      -
      +// Also, a variant of `toStream` exists that allows you +// to select a new key for the resulting stream. +KStream<byte[], String> stream = table.toStream();

      Stream to Table

      @@ -841,10 +819,9 @@

      Convert an event stream into a table, or say a changelog stream. (details)

      -
      KStream<byte[], String> stream = ...;
      +                            
      KStream<byte[], String> stream = ...;
       
      -KTable<byte[], String> table = stream.toTable();
      -
      +KTable<byte[], String> table = stream.toTable();

      @@ -1827,35 +1792,34 @@

      co-partitioned.

      Causes data re-partitioning of a stream if and only if the stream was marked for re-partitioning (if both are marked, both are re-partitioned).

      Several variants of join exists, see the Javadocs for details.

      -
      import java.time.Duration;
      -KStream<String, Long> left = ...;
      -KStream<String, Double> right = ...;
      -
      -// Java 8+ example, using lambda expressions
      -KStream<String, String> joined = left.join(right,
      -    (leftValue, rightValue) -> "left=" + leftValue + ", right=" + rightValue, /* ValueJoiner */
      -    JoinWindows.of(Duration.ofMinutes(5)),
      -    Joined.with(
      -      Serdes.String(), /* key */
      -      Serdes.Long(),   /* left value */
      -      Serdes.Double())  /* right value */
      -  );
      -
      -// Java 7 example
      -KStream<String, String> joined = left.join(right,
      -    new ValueJoiner<Long, Double, String>() {
      -      @Override
      -      public String apply(Long leftValue, Double rightValue) {
      -        return "left=" + leftValue + ", right=" + rightValue;
      -      }
      -    },
      -    JoinWindows.of(Duration.ofMinutes(5)),
      -    Joined.with(
      -      Serdes.String(), /* key */
      -      Serdes.Long(),   /* left value */
      -      Serdes.Double())  /* right value */
      -  );
      -
      +
      import java.time.Duration;
      +KStream<String, Long> left = ...;
      +KStream<String, Double> right = ...;
      +
      +// Java 8+ example, using lambda expressions
      +KStream<String, String> joined = left.join(right,
      +    (leftValue, rightValue) -> "left=" + leftValue + ", right=" + rightValue, /* ValueJoiner */
      +    JoinWindows.of(Duration.ofMinutes(5)),
      +    Joined.with(
      +      Serdes.String(), /* key */
      +      Serdes.Long(),   /* left value */
      +      Serdes.Double())  /* right value */
      +  );
      +
      +// Java 7 example
      +KStream<String, String> joined = left.join(right,
      +    new ValueJoiner<Long, Double, String>() {
      +      @Override
      +      public String apply(Long leftValue, Double rightValue) {
      +        return "left=" + leftValue + ", right=" + rightValue;
      +      }
      +    },
      +    JoinWindows.of(Duration.ofMinutes(5)),
      +    Joined.with(
      +      Serdes.String(), /* key */
      +      Serdes.Long(),   /* left value */
      +      Serdes.Double())  /* right value */
      +  );

      Detailed behavior:

      • The join is key-based, i.e. with the join predicate leftRecord.key == rightRecord.key, and window-based, i.e. two input records are joined if and only if their @@ -1885,35 +1849,34 @@

        co-partitioned.

        Causes data re-partitioning of a stream if and only if the stream was marked for re-partitioning (if both are marked, both are re-partitioned).

        Several variants of leftJoin exists, see the Javadocs for details.

        -
        import java.time.Duration;
        -KStream<String, Long> left = ...;
        -KStream<String, Double> right = ...;
        -
        -// Java 8+ example, using lambda expressions
        -KStream<String, String> joined = left.leftJoin(right,
        -    (leftValue, rightValue) -> "left=" + leftValue + ", right=" + rightValue, /* ValueJoiner */
        -    JoinWindows.of(Duration.ofMinutes(5)),
        -    Joined.with(
        -      Serdes.String(), /* key */
        -      Serdes.Long(),   /* left value */
        -      Serdes.Double())  /* right value */
        -  );
        -
        -// Java 7 example
        -KStream<String, String> joined = left.leftJoin(right,
        -    new ValueJoiner<Long, Double, String>() {
        -      @Override
        -      public String apply(Long leftValue, Double rightValue) {
        -        return "left=" + leftValue + ", right=" + rightValue;
        -      }
        -    },
        -    JoinWindows.of(Duration.ofMinutes(5)),
        -    Joined.with(
        -      Serdes.String(), /* key */
        -      Serdes.Long(),   /* left value */
        -      Serdes.Double())  /* right value */
        -  );
        -
        +
        import java.time.Duration;
        +KStream<String, Long> left = ...;
        +KStream<String, Double> right = ...;
        +
        +// Java 8+ example, using lambda expressions
        +KStream<String, String> joined = left.leftJoin(right,
        +    (leftValue, rightValue) -> "left=" + leftValue + ", right=" + rightValue, /* ValueJoiner */
        +    JoinWindows.of(Duration.ofMinutes(5)),
        +    Joined.with(
        +      Serdes.String(), /* key */
        +      Serdes.Long(),   /* left value */
        +      Serdes.Double())  /* right value */
        +  );
        +
        +// Java 7 example
        +KStream<String, String> joined = left.leftJoin(right,
        +    new ValueJoiner<Long, Double, String>() {
        +      @Override
        +      public String apply(Long leftValue, Double rightValue) {
        +        return "left=" + leftValue + ", right=" + rightValue;
        +      }
        +    },
        +    JoinWindows.of(Duration.ofMinutes(5)),
        +    Joined.with(
        +      Serdes.String(), /* key */
        +      Serdes.Long(),   /* left value */
        +      Serdes.Double())  /* right value */
        +  );

        Detailed behavior:

        • The join is key-based, i.e. with the join predicate leftRecord.key == rightRecord.key, and window-based, i.e. two input records are joined if and only if their @@ -1946,35 +1909,34 @@

          co-partitioned.

          Causes data re-partitioning of a stream if and only if the stream was marked for re-partitioning (if both are marked, both are re-partitioned).

          Several variants of outerJoin exists, see the Javadocs for details.

          -
          import java.time.Duration;
          -KStream<String, Long> left = ...;
          -KStream<String, Double> right = ...;
          -
          -// Java 8+ example, using lambda expressions
          -KStream<String, String> joined = left.outerJoin(right,
          -    (leftValue, rightValue) -> "left=" + leftValue + ", right=" + rightValue, /* ValueJoiner */
          -    JoinWindows.of(Duration.ofMinutes(5)),
          -    Joined.with(
          -      Serdes.String(), /* key */
          -      Serdes.Long(),   /* left value */
          -      Serdes.Double())  /* right value */
          -  );
          -
          -// Java 7 example
          -KStream<String, String> joined = left.outerJoin(right,
          -    new ValueJoiner<Long, Double, String>() {
          -      @Override
          -      public String apply(Long leftValue, Double rightValue) {
          -        return "left=" + leftValue + ", right=" + rightValue;
          -      }
          -    },
          -    JoinWindows.of(Duration.ofMinutes(5)),
          -    Joined.with(
          -      Serdes.String(), /* key */
          -      Serdes.Long(),   /* left value */
          -      Serdes.Double())  /* right value */
          -  );
          -
          +
          import java.time.Duration;
          +KStream<String, Long> left = ...;
          +KStream<String, Double> right = ...;
          +
          +// Java 8+ example, using lambda expressions
          +KStream<String, String> joined = left.outerJoin(right,
          +    (leftValue, rightValue) -> "left=" + leftValue + ", right=" + rightValue, /* ValueJoiner */
          +    JoinWindows.of(Duration.ofMinutes(5)),
          +    Joined.with(
          +      Serdes.String(), /* key */
          +      Serdes.Long(),   /* left value */
          +      Serdes.Double())  /* right value */
          +  );
          +
          +// Java 7 example
          +KStream<String, String> joined = left.outerJoin(right,
          +    new ValueJoiner<Long, Double, String>() {
          +      @Override
          +      public String apply(Long leftValue, Double rightValue) {
          +        return "left=" + leftValue + ", right=" + rightValue;
          +      }
          +    },
          +    JoinWindows.of(Duration.ofMinutes(5)),
          +    Joined.with(
          +      Serdes.String(), /* key */
          +      Serdes.Long(),   /* left value */
          +      Serdes.Double())  /* right value */
          +  );

          Detailed behavior:

          • The join is key-based, i.e. with the join predicate leftRecord.key == rightRecord.key, and window-based, i.e. two input records are joined if and only if their @@ -2131,15 +2093,14 @@

            table duals. The join result is a new KTable that represents the changelog stream of the join operation.

            Join output records are effectively created as follows, leveraging the user-supplied ValueJoiner:

            -
            KeyValue<K, LV> leftRecord = ...;
            -KeyValue<K, RV> rightRecord = ...;
            -ValueJoiner<LV, RV, JV> joiner = ...;
            -
            -KeyValue<K, JV> joinOutputRecord = KeyValue.pair(
            -    leftRecord.key, /* by definition, leftRecord.key == rightRecord.key */
            -    joiner.apply(leftRecord.value, rightRecord.value)
            -  );
            -
            +
            KeyValue<K, LV> leftRecord = ...;
            +KeyValue<K, RV> rightRecord = ...;
            +ValueJoiner<LV, RV, JV> joiner = ...;
            +
            +KeyValue<K, JV> joinOutputRecord = KeyValue.pair(
            +    leftRecord.key, /* by definition, leftRecord.key == rightRecord.key */
            +    joiner.apply(leftRecord.value, rightRecord.value)
            +  );

      @@ -2161,23 +2122,22 @@

      (details)

      Data must be co-partitioned: The input data for both sides must be co-partitioned.

      -
      KTable<String, Long> left = ...;
      -KTable<String, Double> right = ...;
      -
      -// Java 8+ example, using lambda expressions
      -KTable<String, String> joined = left.join(right,
      -    (leftValue, rightValue) -> "left=" + leftValue + ", right=" + rightValue /* ValueJoiner */
      -  );
      -
      -// Java 7 example
      -KTable<String, String> joined = left.join(right,
      -    new ValueJoiner<Long, Double, String>() {
      -      @Override
      -      public String apply(Long leftValue, Double rightValue) {
      -        return "left=" + leftValue + ", right=" + rightValue;
      -      }
      -    });
      -
      +
      KTable<String, Long> left = ...;
      +KTable<String, Double> right = ...;
      +
      +// Java 8+ example, using lambda expressions
      +KTable<String, String> joined = left.join(right,
      +    (leftValue, rightValue) -> "left=" + leftValue + ", right=" + rightValue /* ValueJoiner */
      +  );
      +
      +// Java 7 example
      +KTable<String, String> joined = left.join(right,
      +    new ValueJoiner<Long, Double, String>() {
      +      @Override
      +      public String apply(Long leftValue, Double rightValue) {
      +        return "left=" + leftValue + ", right=" + rightValue;
      +      }
      +    });

      Detailed behavior:

      @@ -2770,28 +2727,27 @@
      KTable-KTable Foreign-Key

      Data must be co-partitioned: The input data for both sides must be co-partitioned.

      Causes data re-partitioning of the stream if and only if the stream was marked for re-partitioning.

      Several variants of join exists, see the Javadocs for details.

      -
      KStream<String, Long> left = ...;
      -KTable<String, Double> right = ...;
      -
      -// Java 8+ example, using lambda expressions
      -KStream<String, String> joined = left.join(right,
      -    (leftValue, rightValue) -> "left=" + leftValue + ", right=" + rightValue, /* ValueJoiner */
      -    Joined.keySerde(Serdes.String()) /* key */
      -      .withValueSerde(Serdes.Long()) /* left value */
      -  );
      -
      -// Java 7 example
      -KStream<String, String> joined = left.join(right,
      -    new ValueJoiner<Long, Double, String>() {
      -      @Override
      -      public String apply(Long leftValue, Double rightValue) {
      -        return "left=" + leftValue + ", right=" + rightValue;
      -      }
      -    },
      -    Joined.keySerde(Serdes.String()) /* key */
      -      .withValueSerde(Serdes.Long()) /* left value */
      -  );
      -
      +
      KStream<String, Long> left = ...;
      +KTable<String, Double> right = ...;
      +
      +// Java 8+ example, using lambda expressions
      +KStream<String, String> joined = left.join(right,
      +    (leftValue, rightValue) -> "left=" + leftValue + ", right=" + rightValue, /* ValueJoiner */
      +    Joined.keySerde(Serdes.String()) /* key */
      +      .withValueSerde(Serdes.Long()) /* left value */
      +  );
      +
      +// Java 7 example
      +KStream<String, String> joined = left.join(right,
      +    new ValueJoiner<Long, Double, String>() {
      +      @Override
      +      public String apply(Long leftValue, Double rightValue) {
      +        return "left=" + leftValue + ", right=" + rightValue;
      +      }
      +    },
      +    Joined.keySerde(Serdes.String()) /* key */
      +      .withValueSerde(Serdes.Long()) /* left value */
      +  );

      Detailed behavior:

      @@ -3038,30 +2992,29 @@
      KTable-KTable Foreign-Key

      The GlobalKTable is fully bootstrapped upon (re)start of a KafkaStreams instance, which means the table is fully populated with all the data in the underlying topic that is available at the time of the startup. The actual data processing begins only once the bootstrapping has completed.

      Causes data re-partitioning of the stream if and only if the stream was marked for re-partitioning.

      -
      KStream<String, Long> left = ...;
      -GlobalKTable<Integer, Double> right = ...;
      -
      -// Java 8+ example, using lambda expressions
      -KStream<String, String> joined = left.join(right,
      -    (leftKey, leftValue) -> leftKey.length(), /* derive a (potentially) new key by which to lookup against the table */
      -    (leftValue, rightValue) -> "left=" + leftValue + ", right=" + rightValue /* ValueJoiner */
      -  );
      -
      -// Java 7 example
      -KStream<String, String> joined = left.join(right,
      -    new KeyValueMapper<String, Long, Integer>() { /* derive a (potentially) new key by which to lookup against the table */
      -      @Override
      -      public Integer apply(String key, Long value) {
      -        return key.length();
      -      }
      -    },
      -    new ValueJoiner<Long, Double, String>() {
      -      @Override
      -      public String apply(Long leftValue, Double rightValue) {
      -        return "left=" + leftValue + ", right=" + rightValue;
      -      }
      -    });
      -
      +
      KStream<String, Long> left = ...;
      +GlobalKTable<Integer, Double> right = ...;
      +
      +// Java 8+ example, using lambda expressions
      +KStream<String, String> joined = left.join(right,
      +    (leftKey, leftValue) -> leftKey.length(), /* derive a (potentially) new key by which to lookup against the table */
      +    (leftValue, rightValue) -> "left=" + leftValue + ", right=" + rightValue /* ValueJoiner */
      +  );
      +
      +// Java 7 example
      +KStream<String, String> joined = left.join(right,
      +    new KeyValueMapper<String, Long, Integer>() { /* derive a (potentially) new key by which to lookup against the table */
      +      @Override
      +      public Integer apply(String key, Long value) {
      +        return key.length();
      +      }
      +    },
      +    new ValueJoiner<Long, Double, String>() {
      +      @Override
      +      public String apply(Long leftValue, Double rightValue) {
      +        return "left=" + leftValue + ", right=" + rightValue;
      +      }
      +    });

      Detailed behavior:

      The class/interface hierarchy for your custom store might look something like:

      -
      public class MyCustomStore<K,V> implements StateStore, MyWriteableCustomStore<K,V> {
      -  // implementation of the actual store
      -}
      -
      -// Read-write interface for MyCustomStore
      -public interface MyWriteableCustomStore<K,V> extends MyReadableCustomStore<K,V> {
      -  void write(K Key, V value);
      -}
      -
      -// Read-only interface for MyCustomStore
      -public interface MyReadableCustomStore<K,V> {
      -  V read(K key);
      -}
      -
      -public class MyCustomStoreBuilder implements StoreBuilder {
      -  // implementation of the supplier for MyCustomStore
      -}
      -
      +
      public class MyCustomStore<K,V> implements StateStore, MyWriteableCustomStore<K,V> {
      +  // implementation of the actual store
      +}
      +
      +// Read-write interface for MyCustomStore
      +public interface MyWriteableCustomStore<K,V> extends MyReadableCustomStore<K,V> {
      +  void write(K Key, V value);
      +}
      +
      +// Read-only interface for MyCustomStore
      +public interface MyReadableCustomStore<K,V> {
      +  V read(K key);
      +}
      +
      +public class MyCustomStoreBuilder implements StoreBuilder {
      +  // implementation of the supplier for MyCustomStore
      +}

      To make this store queryable you must:

      • Provide an implementation of QueryableStoreType.
      • Provide a wrapper class that has access to all of the underlying instances of the store and is used for querying.

      Here is how to implement QueryableStoreType:

      -
      public class MyCustomStoreType<K,V> implements QueryableStoreType<MyReadableCustomStore<K,V>> {
      +                
      public class MyCustomStoreType<K,V> implements QueryableStoreType<MyReadableCustomStore<K,V>> {
       
      -  // Only accept StateStores that are of type MyCustomStore
      -  public boolean accepts(final StateStore stateStore) {
      -    return stateStore instanceOf MyCustomStore;
      -  }
      +  // Only accept StateStores that are of type MyCustomStore
      +  public boolean accepts(final StateStore stateStore) {
      +    return stateStore instanceOf MyCustomStore;
      +  }
       
      -  public MyReadableCustomStore<K,V> create(final StateStoreProvider storeProvider, final String storeName) {
      -      return new MyCustomStoreTypeWrapper(storeProvider, storeName, this);
      -  }
      +  public MyReadableCustomStore<K,V> create(final StateStoreProvider storeProvider, final String storeName) {
      +      return new MyCustomStoreTypeWrapper(storeProvider, storeName, this);
      +  }
       
      -}
      -
      +}

      A wrapper class is required because each instance of a Kafka Streams application may run multiple stream tasks and manage multiple local instances of a particular state store. The wrapper class hides this complexity and lets you query a “logical” state store by name without having to know about all of the underlying local instances of that state store.

      @@ -279,56 +269,53 @@ StateStoreProvider#stores(String storeName, QueryableStoreType<T> queryableStoreType) returns a List of state stores with the given storeName and of the type as defined by queryableStoreType.

      Here is an example implementation of the wrapper follows (Java 8+):

      -
      // We strongly recommended implementing a read-only interface
      -// to restrict usage of the store to safe read operations!
      -public class MyCustomStoreTypeWrapper<K,V> implements MyReadableCustomStore<K,V> {
      -
      -  private final QueryableStoreType<MyReadableCustomStore<K, V>> customStoreType;
      -  private final String storeName;
      -  private final StateStoreProvider provider;
      -
      -  public CustomStoreTypeWrapper(final StateStoreProvider provider,
      -                              final String storeName,
      -                              final QueryableStoreType<MyReadableCustomStore<K, V>> customStoreType) {
      -
      -    // ... assign fields ...
      -  }
      -
      -  // Implement a safe read method
      -  @Override
      -  public V read(final K key) {
      -    // Get all the stores with storeName and of customStoreType
      -    final List<MyReadableCustomStore<K, V>> stores = provider.getStores(storeName, customStoreType);
      -    // Try and find the value for the given key
      -    final Optional<V> value = stores.stream().filter(store -> store.read(key) != null).findFirst();
      -    // Return the value if it exists
      -    return value.orElse(null);
      -  }
      -
      -}
      -
      +
      // We strongly recommended implementing a read-only interface
      +// to restrict usage of the store to safe read operations!
      +public class MyCustomStoreTypeWrapper<K,V> implements MyReadableCustomStore<K,V> {
      +
      +  private final QueryableStoreType<MyReadableCustomStore<K, V>> customStoreType;
      +  private final String storeName;
      +  private final StateStoreProvider provider;
      +
      +  public CustomStoreTypeWrapper(final StateStoreProvider provider,
      +                              final String storeName,
      +                              final QueryableStoreType<MyReadableCustomStore<K, V>> customStoreType) {
      +
      +    // ... assign fields ...
      +  }
      +
      +  // Implement a safe read method
      +  @Override
      +  public V read(final K key) {
      +    // Get all the stores with storeName and of customStoreType
      +    final List<MyReadableCustomStore<K, V>> stores = provider.getStores(storeName, customStoreType);
      +    // Try and find the value for the given key
      +    final Optional<V> value = stores.stream().filter(store -> store.read(key) != null).findFirst();
      +    // Return the value if it exists
      +    return value.orElse(null);
      +  }
      +
      +}

      You can now find and query your custom store:

      -
      
      -Topology topology = ...;
      -ProcessorSupplier processorSuppler = ...;
      -
      -// Create CustomStoreSupplier for store name the-custom-store
      -MyCustomStoreBuilder customStoreBuilder = new MyCustomStoreBuilder("the-custom-store") //...;
      -// Add the source topic
      -topology.addSource("input", "inputTopic");
      -// Add a custom processor that reads from the source topic
      -topology.addProcessor("the-processor", processorSupplier, "input");
      -// Connect your custom state store to the custom processor above
      -topology.addStateStore(customStoreBuilder, "the-processor");
      -
      -KafkaStreams streams = new KafkaStreams(topology, config);
      -streams.start();
      -
      -// Get access to the custom store
      -MyReadableCustomStore<String,String> store = streams.store("the-custom-store", new MyCustomStoreType<String,String>());
      -// Query the store
      -String value = store.read("key");
      -
      +
      Topology topology = ...;
      +ProcessorSupplier processorSuppler = ...;
      +
      +// Create CustomStoreSupplier for store name the-custom-store
      +MyCustomStoreBuilder customStoreBuilder = new MyCustomStoreBuilder("the-custom-store") //...;
      +// Add the source topic
      +topology.addSource("input", "inputTopic");
      +// Add a custom processor that reads from the source topic
      +topology.addProcessor("the-processor", processorSupplier, "input");
      +// Connect your custom state store to the custom processor above
      +topology.addStateStore(customStoreBuilder, "the-processor");
      +
      +KafkaStreams streams = new KafkaStreams(topology, config);
      +streams.start();
      +
      +// Get access to the custom store
      +MyReadableCustomStore<String,String> store = streams.store("the-custom-store", new MyCustomStoreType<String,String>());
      +// Query the store
      +String value = store.read("key");
      @@ -369,41 +356,39 @@ piggybacking additional inter-application communication that goes beyond interactive queries.

      This example shows how to configure and run a Kafka Streams application that supports the discovery of its state stores.

      -
      Properties props = new Properties();
      -// Set the unique RPC endpoint of this application instance through which it
      -// can be interactively queried.  In a real application, the value would most
      -// probably not be hardcoded but derived dynamically.
      -String rpcEndpoint = "host1:4460";
      -props.put(StreamsConfig.APPLICATION_SERVER_CONFIG, rpcEndpoint);
      -// ... further settings may follow here ...
      -
      -StreamsBuilder builder = new StreamsBuilder();
      -
      -KStream<String, String> textLines = builder.stream(stringSerde, stringSerde, "word-count-input");
      -
      -final KGroupedStream<String, String> groupedByWord = textLines
      -    .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+")))
      -    .groupBy((key, word) -> word, Grouped.with(stringSerde, stringSerde));
      -
      -// This call to `count()` creates a state store named "word-count".
      -// The state store is discoverable and can be queried interactively.
      -groupedByWord.count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>as("word-count"));
      -
      -// Start an instance of the topology
      -KafkaStreams streams = new KafkaStreams(builder, props);
      -streams.start();
      -
      -// Then, create and start the actual RPC service for remote access to this
      -// application instance's local state stores.
      -//
      -// This service should be started on the same host and port as defined above by
      -// the property `StreamsConfig.APPLICATION_SERVER_CONFIG`.  The example below is
      -// fictitious, but we provide end-to-end demo applications (such as KafkaMusicExample)
      -// that showcase how to implement such a service to get you started.
      -MyRPCService rpcService = ...;
      -rpcService.listenAt(rpcEndpoint);
      -
      - +
      Properties props = new Properties();
      +// Set the unique RPC endpoint of this application instance through which it
      +// can be interactively queried.  In a real application, the value would most
      +// probably not be hardcoded but derived dynamically.
      +String rpcEndpoint = "host1:4460";
      +props.put(StreamsConfig.APPLICATION_SERVER_CONFIG, rpcEndpoint);
      +// ... further settings may follow here ...
      +
      +StreamsBuilder builder = new StreamsBuilder();
      +
      +KStream<String, String> textLines = builder.stream(stringSerde, stringSerde, "word-count-input");
      +
      +final KGroupedStream<String, String> groupedByWord = textLines
      +    .flatMapValues(value -> Arrays.asList(value.toLowerCase().split("\\W+")))
      +    .groupBy((key, word) -> word, Grouped.with(stringSerde, stringSerde));
      +
      +// This call to `count()` creates a state store named "word-count".
      +// The state store is discoverable and can be queried interactively.
      +groupedByWord.count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>as("word-count"));
      +
      +// Start an instance of the topology
      +KafkaStreams streams = new KafkaStreams(builder, props);
      +streams.start();
      +
      +// Then, create and start the actual RPC service for remote access to this
      +// application instance's local state stores.
      +//
      +// This service should be started on the same host and port as defined above by
      +// the property `StreamsConfig.APPLICATION_SERVER_CONFIG`.  The example below is
      +// fictitious, but we provide end-to-end demo applications (such as KafkaMusicExample)
      +// that showcase how to implement such a service to get you started.
      +MyRPCService rpcService = ...;
      +rpcService.listenAt(rpcEndpoint);

      Discovering and accessing application instances and their local state stores

      The following methods return StreamsMetadata objects, which provide meta-information about application instances such as their RPC endpoint and locally available state stores.

      @@ -419,39 +404,38 @@

      For example, we can now find the StreamsMetadata for the state store named “word-count” that we defined in the code example shown in the previous section:

      -
      KafkaStreams streams = ...;
      -// Find all the locations of local instances of the state store named "word-count"
      -Collection<StreamsMetadata> wordCountHosts = streams.allMetadataForStore("word-count");
      -
      -// For illustrative purposes, we assume using an HTTP client to talk to remote app instances.
      -HttpClient http = ...;
      -
      -// Get the word count for word (aka key) 'alice': Approach 1
      -//
      -// We first find the one app instance that manages the count for 'alice' in its local state stores.
      -StreamsMetadata metadata = streams.metadataForKey("word-count", "alice", Serdes.String().serializer());
      -// Then, we query only that single app instance for the latest count of 'alice'.
      -// Note: The RPC URL shown below is fictitious and only serves to illustrate the idea.  Ultimately,
      -// the URL (or, in general, the method of communication) will depend on the RPC layer you opted to
      -// implement.  Again, we provide end-to-end demo applications (such as KafkaMusicExample) that showcase
      -// how to implement such an RPC layer.
      -Long result = http.getLong("http://" + metadata.host() + ":" + metadata.port() + "/word-count/alice");
      -
      -// Get the word count for word (aka key) 'alice': Approach 2
      -//
      -// Alternatively, we could also choose (say) a brute-force approach where we query every app instance
      -// until we find the one that happens to know about 'alice'.
      -Optional<Long> result = streams.allMetadataForStore("word-count")
      -    .stream()
      -    .map(streamsMetadata -> {
      -        // Construct the (fictituous) full endpoint URL to query the current remote application instance
      -        String url = "http://" + streamsMetadata.host() + ":" + streamsMetadata.port() + "/word-count/alice";
      -        // Read and return the count for 'alice', if any.
      -        return http.getLong(url);
      -    })
      -    .filter(s -> s != null)
      -    .findFirst();
      -
      +
      KafkaStreams streams = ...;
      +// Find all the locations of local instances of the state store named "word-count"
      +Collection<StreamsMetadata> wordCountHosts = streams.allMetadataForStore("word-count");
      +
      +// For illustrative purposes, we assume using an HTTP client to talk to remote app instances.
      +HttpClient http = ...;
      +
      +// Get the word count for word (aka key) 'alice': Approach 1
      +//
      +// We first find the one app instance that manages the count for 'alice' in its local state stores.
      +StreamsMetadata metadata = streams.metadataForKey("word-count", "alice", Serdes.String().serializer());
      +// Then, we query only that single app instance for the latest count of 'alice'.
      +// Note: The RPC URL shown below is fictitious and only serves to illustrate the idea.  Ultimately,
      +// the URL (or, in general, the method of communication) will depend on the RPC layer you opted to
      +// implement.  Again, we provide end-to-end demo applications (such as KafkaMusicExample) that showcase
      +// how to implement such an RPC layer.
      +Long result = http.getLong("http://" + metadata.host() + ":" + metadata.port() + "/word-count/alice");
      +
      +// Get the word count for word (aka key) 'alice': Approach 2
      +//
      +// Alternatively, we could also choose (say) a brute-force approach where we query every app instance
      +// until we find the one that happens to know about 'alice'.
      +Optional<Long> result = streams.allMetadataForStore("word-count")
      +    .stream()
      +    .map(streamsMetadata -> {
      +        // Construct the (fictituous) full endpoint URL to query the current remote application instance
      +        String url = "http://" + streamsMetadata.host() + ":" + streamsMetadata.port() + "/word-count/alice";
      +        // Read and return the count for 'alice', if any.
      +        return http.getLong(url);
      +    })
      +    .filter(s -> s != null)
      +    .findFirst();

      At this point the full state of the application is interactively queryable:

      • You can discover the running instances of the application and the state stores they manage locally.
      • diff --git a/docs/streams/developer-guide/memory-mgmt.html b/docs/streams/developer-guide/memory-mgmt.html index 9aa382e089a20..9a39ce1e78a5c 100644 --- a/docs/streams/developer-guide/memory-mgmt.html +++ b/docs/streams/developer-guide/memory-mgmt.html @@ -80,10 +80,9 @@

      The cache size is specified through the cache.max.bytes.buffering parameter, which is a global setting per processing topology:

      -
      // Enable record cache of size 10 MB.
      -Properties props = new Properties();
      -props.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, 10 * 1024 * 1024L);
      -
      +
      // Enable record cache of size 10 MB.
      +Properties props = new Properties();
      +props.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, 10 * 1024 * 1024L);

      This parameter controls the number of bytes allocated for caching. Specifically, for a processor topology instance with T threads and C bytes allocated for caching, each thread will have an even C/T bytes to construct its own cache and use as it sees fit among its tasks. This means that there are as many caches as there are threads, but no sharing of @@ -103,27 +102,16 @@

      Here are example settings for both parameters based on desired scenarios.

      • To turn off caching the cache size can be set to zero:

        -
        -
        // Disable record cache
        -Properties props = new Properties();
        -props.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, 0);
        -
        -

        Turning off caching might result in high write traffic for the underlying RocksDB store. - With default settings caching is enabled within Kafka Streams but RocksDB caching is disabled. - Thus, to avoid high write traffic it is recommended to enable RocksDB caching if Kafka Streams caching is turned off.

        -

        For example, the RocksDB Block Cache could be set to 100MB and Write Buffer size to 32 MB. For more information, see - the RocksDB config.

        -
        +
        // Disable record cache
        +Properties props = new Properties();
        +props.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, 0);
      • To enable caching but still have an upper bound on how long records will be cached, you can set the commit interval. In this example, it is set to 1000 milliseconds:

        -
        -
        Properties props = new Properties();
        -// Enable record cache of size 10 MB.
        -props.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, 10 * 1024 * 1024L);
        -// Set commit interval to 1 second.
        -props.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 1000);
        -
        -
        +
        Properties props = new Properties();
        +// Enable record cache of size 10 MB.
        +props.put(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, 10 * 1024 * 1024L);
        +// Set commit interval to 1 second.
        +props.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, 1000);

      The effect of these two configurations is described in the figure below. The records are shown using 4 keys: blue, red, yellow, and green. Assume the cache has space for only 3 keys.

      @@ -156,13 +144,12 @@

      Following from the example first shown in section State Stores, to disable caching, you can add the withCachingDisabled call (note that caches are enabled by default, however there is an explicit withCachingEnabled call).

      -
      StoreBuilder countStoreBuilder =
      -  Stores.keyValueStoreBuilder(
      -    Stores.persistentKeyValueStore("Counts"),
      -    Serdes.String(),
      -    Serdes.Long())
      -  .withCachingEnabled()
      -
      +
      StoreBuilder countStoreBuilder =
      +  Stores.keyValueStoreBuilder(
      +    Stores.persistentKeyValueStore("Counts"),
      +    Serdes.String(),
      +    Serdes.Long())
      +  .withCachingEnabled();

      RocksDB

      @@ -171,44 +158,42 @@

      RocksDBrocksdb.config.setter configuration.

      Also, we recommend changing RocksDB's default memory allocator, because the default allocator may lead to increased memory consumption. To change the memory allocator to jemalloc, you need to set the environment variable LD_PRELOADbefore you start your Kafka Streams application:

      -
      -# example: install jemalloc (on Debian)
      +      
      # example: install jemalloc (on Debian)
       $ apt install -y libjemalloc-dev
       # set LD_PRELOAD before you start your Kafka Streams application
       $ export LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libjemalloc.so”
      -      
      +

      As of 2.3.0 the memory usage across all instances can be bounded, limiting the total off-heap memory of your Kafka Streams application. To do so you must configure RocksDB to cache the index and filter blocks in the block cache, limit the memtable memory through a shared WriteBufferManager and count its memory against the block cache, and then pass the same Cache object to each instance. See RocksDB Memory Usage for details. An example RocksDBConfigSetter implementing this is shown below:

      +
      public static class BoundedMemoryRocksDBConfig implements RocksDBConfigSetter {
       
      -      
          public static class BoundedMemoryRocksDBConfig implements RocksDBConfigSetter {
      -
      -       private static org.rocksdb.Cache cache = new org.rocksdb.LRUCache(TOTAL_OFF_HEAP_MEMORY, -1, false, INDEX_FILTER_BLOCK_RATIO);1
      -       private static org.rocksdb.WriteBufferManager writeBufferManager = new org.rocksdb.WriteBufferManager(TOTAL_MEMTABLE_MEMORY, cache);
      +   private static org.rocksdb.Cache cache = new org.rocksdb.LRUCache(TOTAL_OFF_HEAP_MEMORY, -1, false, INDEX_FILTER_BLOCK_RATIO);1
      +   private static org.rocksdb.WriteBufferManager writeBufferManager = new org.rocksdb.WriteBufferManager(TOTAL_MEMTABLE_MEMORY, cache);
       
      -       @Override
      -       public void setConfig(final String storeName, final Options options, final Map<String, Object> configs) {
      +   @Override
      +   public void setConfig(final String storeName, final Options options, final Map<String, Object> configs) {
       
      -         BlockBasedTableConfig tableConfig = (BlockBasedTableConfig) options.tableFormatConfig();
      +     BlockBasedTableConfig tableConfig = (BlockBasedTableConfig) options.tableFormatConfig();
       
      -          // These three options in combination will limit the memory used by RocksDB to the size passed to the block cache (TOTAL_OFF_HEAP_MEMORY)
      -         tableConfig.setBlockCache(cache);
      -         tableConfig.setCacheIndexAndFilterBlocks(true);
      -         options.setWriteBufferManager(writeBufferManager);
      +      // These three options in combination will limit the memory used by RocksDB to the size passed to the block cache (TOTAL_OFF_HEAP_MEMORY)
      +     tableConfig.setBlockCache(cache);
      +     tableConfig.setCacheIndexAndFilterBlocks(true);
      +     options.setWriteBufferManager(writeBufferManager);
       
      -          // These options are recommended to be set when bounding the total memory
      -         tableConfig.setCacheIndexAndFilterBlocksWithHighPriority(true);2
      -         tableConfig.setPinTopLevelIndexAndFilter(true);
      -         tableConfig.setBlockSize(BLOCK_SIZE);3
      -         options.setMaxWriteBufferNumber(N_MEMTABLES);
      -         options.setWriteBufferSize(MEMTABLE_SIZE);
      +      // These options are recommended to be set when bounding the total memory
      +     tableConfig.setCacheIndexAndFilterBlocksWithHighPriority(true);2
      +     tableConfig.setPinTopLevelIndexAndFilter(true);
      +     tableConfig.setBlockSize(BLOCK_SIZE);3
      +     options.setMaxWriteBufferNumber(N_MEMTABLES);
      +     options.setWriteBufferSize(MEMTABLE_SIZE);
       
      -         options.setTableFormatConfig(tableConfig);
      -       }
      +     options.setTableFormatConfig(tableConfig);
      +   }
       
      -       @Override
      -       public void close(final String storeName, final Options options) {
      -         // Cache and WriteBufferManager should not be closed here, as the same objects are shared by every store instance.
      -       }
      -    }
      +   @Override
      +   public void close(final String storeName, final Options options) {
      +     // Cache and WriteBufferManager should not be closed here, as the same objects are shared by every store instance.
      +   }
      +}
      1. INDEX_FILTER_BLOCK_RATIO can be used to set a fraction of the block cache to set aside for "high priority" (aka index and filter) blocks, preventing them from being evicted by data blocks. See the full signature of the LRUCache constructor. NOTE: the boolean parameter in the cache constructor lets you control whether the cache should enforce a strict memory limit by failing the read or iteration in the rare cases where it might go larger than its capacity. Due to a diff --git a/docs/streams/developer-guide/processor-api.html b/docs/streams/developer-guide/processor-api.html index 9cabac030b90c..589a3ff0aa40e 100644 --- a/docs/streams/developer-guide/processor-api.html +++ b/docs/streams/developer-guide/processor-api.html @@ -119,47 +119,46 @@

      Overviewprocess() method, upon each received record, split the value string into words, and update their counts into the state store (we will talk about this later in this section).
    1. In the punctuate() method, iterate the local state store and send the aggregated counts to the downstream processor (we will talk about downstream processors later in this section), and commit the current stream state.
    2. -
      public class WordCountProcessor implements Processor<String, String> {
      +            
      public class WordCountProcessor implements Processor<String, String> {
       
      -  private ProcessorContext context;
      -  private KeyValueStore<String, Long> kvStore;
      +  private ProcessorContext context;
      +  private KeyValueStore<String, Long> kvStore;
       
      -  @Override
      -  @SuppressWarnings("unchecked")
      -  public void init(ProcessorContext context) {
      -      // keep the processor context locally because we need it in punctuate() and commit()
      -      this.context = context;
      +  @Override
      +  @SuppressWarnings("unchecked")
      +  public void init(ProcessorContext context) {
      +      // keep the processor context locally because we need it in punctuate() and commit()
      +      this.context = context;
       
      -      // retrieve the key-value store named "Counts"
      -      kvStore = (KeyValueStore) context.getStateStore("Counts");
      +      // retrieve the key-value store named "Counts"
      +      kvStore = (KeyValueStore) context.getStateStore("Counts");
       
      -      // schedule a punctuate() method every second based on stream-time
      -      this.context.schedule(Duration.ofSeconds(1000), PunctuationType.STREAM_TIME, (timestamp) -> {
      -          KeyValueIterator<String, Long> iter = this.kvStore.all();
      -          while (iter.hasNext()) {
      -              KeyValue<String, Long> entry = iter.next();
      -              context.forward(entry.key, entry.value.toString());
      -          }
      -          iter.close();
      +      // schedule a punctuate() method every second based on stream-time
      +      this.context.schedule(Duration.ofSeconds(1000), PunctuationType.STREAM_TIME, (timestamp) -> {
      +          KeyValueIterator<String, Long> iter = this.kvStore.all();
      +          while (iter.hasNext()) {
      +              KeyValue<String, Long> entry = iter.next();
      +              context.forward(entry.key, entry.value.toString());
      +          }
      +          iter.close();
       
      -          // commit the current processing progress
      -          context.commit();
      -      });
      -  }
      +          // commit the current processing progress
      +          context.commit();
      +      });
      +  }
       
      -  @Override
      -  public void punctuate(long timestamp) {
      -      // this method is deprecated and should not be used anymore
      -  }
      +  @Override
      +  public void punctuate(long timestamp) {
      +      // this method is deprecated and should not be used anymore
      +  }
       
      -  @Override
      -  public void close() {
      -      // close any resources managed by this processor
      -      // Note: Do not close any StateStores as these are managed by the library
      -  }
      +  @Override
      +  public void close() {
      +      // close any resources managed by this processor
      +      // Note: Do not close any StateStores as these are managed by the library
      +  }
       
      -}
      -
      +}

      Note

      Stateful processing with state stores: @@ -234,19 +233,18 @@

    3. Use persistentTimestampedWindowStore when you need a persistent windowedKey-(value/timestamp) store.
    4. -
      // Creating a persistent key-value store:
      -// here, we create a `KeyValueStore<String, Long>` named "persistent-counts".
      -import org.apache.kafka.streams.state.StoreBuilder;
      -import org.apache.kafka.streams.state.Stores;
      +                            
      // Creating a persistent key-value store:
      +// here, we create a `KeyValueStore<String, Long>` named "persistent-counts".
      +import org.apache.kafka.streams.state.StoreBuilder;
      +import org.apache.kafka.streams.state.Stores;
       
      -// Using a `KeyValueStoreBuilder` to build a `KeyValueStore`.
      -StoreBuilder<KeyValueStore<String, Long>> countStoreSupplier =
      -  Stores.keyValueStoreBuilder(
      -    Stores.persistentKeyValueStore("persistent-counts"),
      -    Serdes.String(),
      -    Serdes.Long());
      -KeyValueStore<String, Long> countStore = countStoreSupplier.build();
      -
      +// Using a `KeyValueStoreBuilder` to build a `KeyValueStore`. +StoreBuilder<KeyValueStore<String, Long>> countStoreSupplier = + Stores.keyValueStoreBuilder( + Stores.persistentKeyValueStore("persistent-counts"), + Serdes.String(), + Serdes.Long()); +KeyValueStore<String, Long> countStore = countStoreSupplier.build();

      @@ -317,15 +314,14 @@

      of the store through enableLogging() and disableLogging(). You can also fine-tune the associated topic’s configuration if needed.

      Example for disabling fault-tolerance:

      -
      import org.apache.kafka.streams.state.StoreBuilder;
      -import org.apache.kafka.streams.state.Stores;
      +                
      import org.apache.kafka.streams.state.StoreBuilder;
      +import org.apache.kafka.streams.state.Stores;
       
      -StoreBuilder<KeyValueStore<String, Long>> countStoreSupplier = Stores.keyValueStoreBuilder(
      -  Stores.persistentKeyValueStore("Counts"),
      -    Serdes.String(),
      -    Serdes.Long())
      -  .withLoggingDisabled(); // disable backing up the store to a changelog topic
      -
      +StoreBuilder<KeyValueStore<String, Long>> countStoreSupplier = Stores.keyValueStoreBuilder( + Stores.persistentKeyValueStore("Counts"), + Serdes.String(), + Serdes.Long()) + .withLoggingDisabled(); // disable backing up the store to a changelog topic

      Attention

      If the changelog is disabled then the attached state store is no longer fault tolerant and it can’t have any standby replicas.

      @@ -333,19 +329,18 @@

      Here is an example for enabling fault tolerance, with additional changelog-topic configuration: You can add any log config from kafka.log.LogConfig. Unrecognized configs will be ignored.

      -
      import org.apache.kafka.streams.state.StoreBuilder;
      -import org.apache.kafka.streams.state.Stores;
      +                
      import org.apache.kafka.streams.state.StoreBuilder;
      +import org.apache.kafka.streams.state.Stores;
       
      -Map<String, String> changelogConfig = new HashMap();
      -// override min.insync.replicas
      -changelogConfig.put(TopicConfig.MIN_IN_SYNC_REPLICAS_CONFIG, "1")
      +Map<String, String> changelogConfig = new HashMap();
      +// override min.insync.replicas
      +changelogConfig.put(TopicConfig.MIN_IN_SYNC_REPLICAS_CONFIG, "1")
       
      -StoreBuilder<KeyValueStore<String, Long>> countStoreSupplier = Stores.keyValueStoreBuilder(
      -  Stores.persistentKeyValueStore("Counts"),
      -    Serdes.String(),
      -    Serdes.Long())
      -  .withLoggingEnabled(changlogConfig); // enable changelogging, with custom changelog settings
      -
      +StoreBuilder<KeyValueStore<String, Long>> countStoreSupplier = Stores.keyValueStoreBuilder( + Stores.persistentKeyValueStore("Counts"), + Serdes.String(), + Serdes.Long()) + .withLoggingEnabled(changlogConfig); // enable changelogging, with custom changelog settings

      Timestamped State Stores

      @@ -389,12 +384,11 @@

      Accessing Processor Contextpartition, offset, timestamp and headers.

      Here is an example implementation of how to add a new header to the record:

      -
      public void process(String key, String value) {
      +            
      public void process(String key, String value) {
       
      -    // add a header to the elements
      -    context().headers().add.("key", "key"
      -}
      -
      + // add a header to the elements + context().headers().add.("key", "value"); +}

      Connecting Processors and State Stores

      Now that a processor (WordCountProcessor) and the @@ -403,16 +397,16 @@

      Connecting Processors and State Stores

      Here is an example implementation:

      -
                      Topology builder = new Topology();
      -                // add the source processor node that takes Kafka topic "source-topic" as input
      -                builder.addSource("Source", "source-topic")
      -                    // add the WordCountProcessor node which takes the source processor as its upstream processor
      -                    .addProcessor("Process", () -> new WordCountProcessor(), "Source")
      -                    // add the count store associated with the WordCountProcessor processor
      -                    .addStateStore(countStoreBuilder, "Process")
      -                    // add the sink processor node that takes Kafka topic "sink-topic" as output
      -                    // and the WordCountProcessor node as its upstream processor
      -                    .addSink("Sink", "sink-topic", "Process");
      +
      Topology builder = new Topology();
      +// add the source processor node that takes Kafka topic "source-topic" as input
      +builder.addSource("Source", "source-topic")
      +    // add the WordCountProcessor node which takes the source processor as its upstream processor
      +    .addProcessor("Process", () -> new WordCountProcessor(), "Source")
      +    // add the count store associated with the WordCountProcessor processor
      +    .addStateStore(countStoreBuilder, "Process")
      +    // add the sink processor node that takes Kafka topic "sink-topic" as output
      +    // and the WordCountProcessor node as its upstream processor
      +    .addSink("Sink", "sink-topic", "Process");

      Here is a quick explanation of this example:

      • A source processor node named "Source" is added to the topology using the addSource method, with one Kafka topic @@ -429,22 +423,22 @@

        Connecting Processors and State StoresConnectedStoreProvider#stores() on the ProcessorSupplier instead of calling Topology#addStateStore(), like this:

        -
                        Topology builder = new Topology();
        -                // add the source processor node that takes Kafka "source-topic" as input
        -                builder.addSource("Source", "source-topic")
        -                    // add the WordCountProcessor node which takes the source processor as its upstream processor.
        -                    // the ProcessorSupplier provides the count store associated with the WordCountProcessor
        -                    .addProcessor("Process", new ProcessorSupplier<String, String>() {
        -                        public Processor<String, String> get() {
        -                            return new WordCountProcessor();
        -                        }
        -                        public Set<StoreBuilder<?>> stores() {
        -                            return countStoreBuilder;
        -                        }
        -                    }, "Source")
        -                    // add the sink processor node that takes Kafka topic "sink-topic" as output
        -                    // and the WordCountProcessor node as its upstream processor
        -                    .addSink("Sink", "sink-topic", "Process");
        +
        Topology builder = new Topology();
        +// add the source processor node that takes Kafka "source-topic" as input
        +builder.addSource("Source", "source-topic")
        +    // add the WordCountProcessor node which takes the source processor as its upstream processor.
        +    // the ProcessorSupplier provides the count store associated with the WordCountProcessor
        +    .addProcessor("Process", new ProcessorSupplier<String, String>() {
        +        public Processor<String, String> get() {
        +            return new WordCountProcessor();
        +        }
        +        public Set<StoreBuilder<?>> stores() {
        +            return countStoreBuilder;
        +        }
        +    }, "Source")
        +    // add the sink processor node that takes Kafka topic "sink-topic" as output
        +    // and the WordCountProcessor node as its upstream processor
        +    .addSink("Sink", "sink-topic", "Process");

        This allows for a processor to "own" state stores, effectively encapsulating their usage from the user wiring the topology. Multiple processors that share a state store may provide the same store with this technique, as long as the StoreBuilder is the same instance.

        In these topologies, the "Process" stream processor node is considered a downstream processor of the "Source" node, and an diff --git a/docs/streams/developer-guide/running-app.html b/docs/streams/developer-guide/running-app.html index 87ee8f0a9da30..ff3ed75d29010 100644 --- a/docs/streams/developer-guide/running-app.html +++ b/docs/streams/developer-guide/running-app.html @@ -51,10 +51,9 @@

        Starting a Kafka Streams application

        You can package your Java application as a fat JAR file and then start the application like this:

        -
        # Start the application in class `com.example.MyStreamsApp`
        -# from the fat JAR named `path-to-app-fatjar.jar`.
        -$ java -cp path-to-app-fatjar.jar com.example.MyStreamsApp
        -
        +
        # Start the application in class `com.example.MyStreamsApp`
        +# from the fat JAR named `path-to-app-fatjar.jar`.
        +$ java -cp path-to-app-fatjar.jar com.example.MyStreamsApp

        When you start your application you are launching a Kafka Streams instance of your application. You can run multiple instances of your application. A common scenario is that there are multiple instances of your application running in parallel. For more information, see Parallelism Model.

        diff --git a/docs/streams/developer-guide/security.html b/docs/streams/developer-guide/security.html index 05de0794d63a9..63bc942f7f5a0 100644 --- a/docs/streams/developer-guide/security.html +++ b/docs/streams/developer-guide/security.html @@ -98,47 +98,44 @@ then you must also include these SSL certificates in the correct locations within the Docker image.

        The snippet below shows the settings to enable client authentication and SSL encryption for data-in-transit between your Kafka Streams application and the Kafka cluster it is reading and writing from:

        -
        # Essential security settings to enable client authentication and SSL encryption
        -bootstrap.servers=kafka.example.com:9093
        -security.protocol=SSL
        -ssl.truststore.location=/etc/security/tls/kafka.client.truststore.jks
        -ssl.truststore.password=test1234
        -ssl.keystore.location=/etc/security/tls/kafka.client.keystore.jks
        -ssl.keystore.password=test1234
        -ssl.key.password=test1234
        -
        +
        # Essential security settings to enable client authentication and SSL encryption
        +bootstrap.servers=kafka.example.com:9093
        +security.protocol=SSL
        +ssl.truststore.location=/etc/security/tls/kafka.client.truststore.jks
        +ssl.truststore.password=test1234
        +ssl.keystore.location=/etc/security/tls/kafka.client.keystore.jks
        +ssl.keystore.password=test1234
        +ssl.key.password=test1234

        Configure these settings in the application for your Properties instance. These settings will encrypt any data-in-transit that is being read from or written to Kafka, and your application will authenticate itself against the Kafka brokers that it is communicating with. Note that this example does not cover client authorization.

        -
        // Code of your Java application that uses the Kafka Streams library
        -Properties settings = new Properties();
        -settings.put(StreamsConfig.APPLICATION_ID_CONFIG, "secure-kafka-streams-app");
        -// Where to find secure Kafka brokers.  Here, it's on port 9093.
        -settings.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka.example.com:9093");
        -//
        -// ...further non-security related settings may follow here...
        -//
        -// Security settings.
        -// 1. These settings must match the security settings of the secure Kafka cluster.
        -// 2. The SSL trust store and key store files must be locally accessible to the application.
        -settings.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SSL");
        -settings.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, "/etc/security/tls/kafka.client.truststore.jks");
        -settings.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, "test1234");
        -settings.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, "/etc/security/tls/kafka.client.keystore.jks");
        -settings.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, "test1234");
        -settings.put(SslConfigs.SSL_KEY_PASSWORD_CONFIG, "test1234");
        -
        +
        // Code of your Java application that uses the Kafka Streams library
        +Properties settings = new Properties();
        +settings.put(StreamsConfig.APPLICATION_ID_CONFIG, "secure-kafka-streams-app");
        +// Where to find secure Kafka brokers.  Here, it's on port 9093.
        +settings.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka.example.com:9093");
        +//
        +// ...further non-security related settings may follow here...
        +//
        +// Security settings.
        +// 1. These settings must match the security settings of the secure Kafka cluster.
        +// 2. The SSL trust store and key store files must be locally accessible to the application.
        +settings.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SSL");
        +settings.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, "/etc/security/tls/kafka.client.truststore.jks");
        +settings.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, "test1234");
        +settings.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, "/etc/security/tls/kafka.client.keystore.jks");
        +settings.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, "test1234");
        +settings.put(SslConfigs.SSL_KEY_PASSWORD_CONFIG, "test1234");

        If you incorrectly configure a security setting in your application, it will fail at runtime, typically right after you start it. For example, if you enter an incorrect password for the ssl.keystore.password setting, an error message similar to this would be logged and then the application would terminate:

        -
        # Misconfigured ssl.keystore.password
        -Exception in thread "main" org.apache.kafka.common.KafkaException: Failed to construct kafka producer
        -[...snip...]
        +            
        # Misconfigured ssl.keystore.password
        +Exception in thread "main" org.apache.kafka.common.KafkaException: Failed to construct kafka producer
        +[...snip...]
         Caused by: org.apache.kafka.common.KafkaException: org.apache.kafka.common.KafkaException:
            java.io.IOException: Keystore was tampered with, or password was incorrect
        -[...snip...]
        -Caused by: java.security.UnrecoverableKeyException: Password verification failed
        -
        +[...snip...] +Caused by: java.security.UnrecoverableKeyException: Password verification failed

        Monitor your Kafka Streams application log files for such error messages to spot any misconfigured applications quickly.

      diff --git a/docs/streams/developer-guide/testing.html b/docs/streams/developer-guide/testing.html index ceef648d9c4d8..b5fadb12b3d3e 100644 --- a/docs/streams/developer-guide/testing.html +++ b/docs/streams/developer-guide/testing.html @@ -71,15 +71,15 @@

      // Processor API +
      // Processor API
       Topology topology = new Topology();
      -topology.addSource("sourceProcessor", "input-topic");
      -topology.addProcessor("processor", ..., "sourceProcessor");
      -topology.addSink("sinkProcessor", "output-topic", "processor");
      +topology.addSource("sourceProcessor", "input-topic");
      +topology.addProcessor("processor", ..., "sourceProcessor");
      +topology.addSink("sinkProcessor", "output-topic", "processor");
       // or
       // using DSL
       StreamsBuilder builder = new StreamsBuilder();
      -builder.stream("input-topic").filter(...).to("output-topic");
      +builder.stream("input-topic").filter(...).to("output-topic");
       Topology topology = builder.build();
       
       // create test driver
      @@ -88,7 +88,7 @@ 

      TestInputTopic<String, Long> inputTopic = testDriver.createInputTopic("input-topic", stringSerde.serializer(), longSerde.serializer()); +
      TestInputTopic<String, Long> inputTopic = testDriver.createInputTopic("input-topic", stringSerde.serializer(), longSerde.serializer());
       inputTopic.pipeInput("key", 42L);

      To verify the output, you can use TestOutputTopic @@ -97,7 +97,7 @@

      TestOutputTopic<String, Long> outputTopic = testDriver.createOutputTopic("output-topic", stringSerde.deserializer(), longSerde.deserializer()); +
      TestOutputTopic<String, Long> outputTopic = testDriver.createOutputTopic("output-topic", stringSerde.deserializer(), longSerde.deserializer());
       assertThat(outputTopic.readKeyValue(), equalTo(new KeyValue<>("key", 42L)));

      TopologyTestDriver supports punctuations, too. @@ -105,18 +105,18 @@

      testDriver.advanceWallClockTime(Duration.ofSeconds(20));

      +
      testDriver.advanceWallClockTime(Duration.ofSeconds(20));

      Additionally, you can access state stores via the test driver before or after a test. Accessing stores before a test is useful to pre-populate a store with some initial values. After data was processed, expected updates to the store can be verified.

      -
      KeyValueStore store = testDriver.getKeyValueStore("store-name");
      +
      KeyValueStore store = testDriver.getKeyValueStore("store-name");

      Note, that you should always close the test driver at the end to make sure all resources are release properly.

      -
      testDriver.close();
      +
      testDriver.close();

      Example

      @@ -125,7 +125,7 @@

      Example

      While processing, no output is generated, but only the store is updated. Output is only sent downstream based on event-time and wall-clock punctuations.

      -
      private TopologyTestDriver testDriver;
      +            
      private TopologyTestDriver testDriver;
       private TestInputTopic<String, Long> inputTopic;
       private TestOutputTopic<String, Long> outputTopic;
       private KeyValueStore<String, Long> store;
      @@ -275,21 +275,21 @@ 

      Construction

      To begin with, instantiate your processor and initialize it with the mock context: -

      final Processor processorUnderTest = ...;
      +            
      final Processor processorUnderTest = ...;
       final MockProcessorContext context = new MockProcessorContext();
       processorUnderTest.init(context);
      If you need to pass configuration to your processor or set the default serdes, you can create the mock with config: -
      final Properties props = new Properties();
      +            
      final Properties props = new Properties();
       props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
       props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.Long().getClass());
      -props.put("some.other.config", "some config value");
      +props.put("some.other.config", "some config value");
       final MockProcessorContext context = new MockProcessorContext(props);

      Captured data

      The mock will capture any values that your processor forwards. You can make assertions on them: -

      processorUnderTest.process("key", "value");
      +            
      processorUnderTest.process("key", "value");
       
       final Iterator<CapturedForward> forwarded = context.forwarded().iterator();
       assertEquals(forwarded.next().keyValue(), new KeyValue<>(..., ...));
      @@ -301,9 +301,9 @@ 

      assertEquals(context.forwarded().size(), 0);

      If your processor forwards to specific child processors, you can query the context for captured data by child name: -
      final List<CapturedForward> captures = context.forwarded("childProcessorName");
      +
      final List<CapturedForward> captures = context.forwarded("childProcessorName");
      The mock also captures whether your processor has called commit() on the context: -
      assertTrue(context.committed());
      +            
      assertTrue(context.committed());
       
       // commit captures can also be reset.
       context.resetCommit();
      @@ -314,8 +314,8 @@ 

      In case your processor logic depends on the record metadata (topic, partition, offset, or timestamp), you can set them on the context, either all together or individually: -

      context.setRecordMetadata("topicName", /*partition*/ 0, /*offset*/ 0L, /*timestamp*/ 0L);
      -context.setTopic("topicName");
      +            
      context.setRecordMetadata("topicName", /*partition*/ 0, /*offset*/ 0L, /*timestamp*/ 0L);
      +context.setTopic("topicName");
       context.setPartition(0);
       context.setOffset(0L);
       context.setTimestamp(0L);
      @@ -327,7 +327,7 @@

      You're encouraged to use a simple in-memory store of the appropriate type (KeyValue, Windowed, or Session), since the mock context does not manage changelogs, state directories, etc.

      -
      final KeyValueStore<String, Integer> store =
      +            
      final KeyValueStore<String, Integer> store =
           Stores.keyValueStoreBuilder(
                   Stores.inMemoryKeyValueStore("myStore"),
                   Serdes.String(),
      @@ -342,7 +342,7 @@ 

      Processors can schedule punctuators to handle periodic tasks. The mock context does not automatically execute punctuators, but it does capture them to allow you to unit test them as well: -
      final MockProcessorContext.CapturedPunctuator capturedPunctuator = context.scheduledPunctuators().get(0);
      +            
      final MockProcessorContext.CapturedPunctuator capturedPunctuator = context.scheduledPunctuators().get(0);
       final long interval = capturedPunctuator.getIntervalMs();
       final PunctuationType type = capturedPunctuator.getType();
       final boolean cancelled = capturedPunctuator.cancelled();
      diff --git a/docs/streams/developer-guide/write-streams.html b/docs/streams/developer-guide/write-streams.html
      index 720b0c376756c..03bd16328efbe 100644
      --- a/docs/streams/developer-guide/write-streams.html
      +++ b/docs/streams/developer-guide/write-streams.html
      @@ -90,22 +90,22 @@
                     

      See the section Data Types and Serialization for more information about Serializers/Deserializers.

      Example pom.xml snippet when using Maven:

      -
      
      -    org.apache.kafka
      -    kafka-streams
      -    {{fullDotVersion}}
      -
      -
      -    org.apache.kafka
      -    kafka-clients
      -    {{fullDotVersion}}
      -
      -
      -
      -    org.apache.kafka
      -    kafka-streams-scala_{{scalaVersion}}
      -    {{fullDotVersion}}
      -
      +
      <dependency>
      +    <groupId>org.apache.kafka</groupId>
      +    <artifactId>kafka-streams</artifactId>
      +    <version>{{fullDotVersion}}</version>
      +</dependency>
      +<dependency>
      +    <groupId>org.apache.kafka</groupId>
      +    <artifactId>kafka-clients</artifactId>
      +    <version>{{fullDotVersion}}</version>
      +</dependency>
      +<!-- Optionally include Kafka Streams DSL for Scala for Scala {{scalaVersion}} -->
      +<dependency>
      +    <groupId>org.apache.kafka</groupId>
      +    <artifactId>kafka-streams-scala_{{scalaVersion}}</artifactId>
      +    <version>{{fullDotVersion}}</version>
      +</dependency>

      Using Kafka Streams within your application code

      @@ -120,79 +120,69 @@

      Using Kafka Streams within your application codejava.util.Properties, which defines the configuration for this specific topology.

      Code example:

      -
      import org.apache.kafka.streams.KafkaStreams;
      -import org.apache.kafka.streams.kstream.StreamsBuilder;
      -import org.apache.kafka.streams.processor.Topology;
      -
      -// Use the builders to define the actual processing topology, e.g. to specify
      -// from which input topics to read, which stream operations (filter, map, etc.)
      -// should be called, and so on.  We will cover this in detail in the subsequent
      -// sections of this Developer Guide.
      -
      -StreamsBuilder builder = ...;  // when using the DSL
      -Topology topology = builder.build();
      -//
      -// OR
      -//
      -Topology topology = ...; // when using the Processor API
      -
      -// Use the configuration to tell your application where the Kafka cluster is,
      -// which Serializers/Deserializers to use by default, to specify security settings,
      -// and so on.
      -Properties props = ...;
      -
      -KafkaStreams streams = new KafkaStreams(topology, props);
      -
      +
      import org.apache.kafka.streams.KafkaStreams;
      +import org.apache.kafka.streams.kstream.StreamsBuilder;
      +import org.apache.kafka.streams.processor.Topology;
      +
      +// Use the builders to define the actual processing topology, e.g. to specify
      +// from which input topics to read, which stream operations (filter, map, etc.)
      +// should be called, and so on.  We will cover this in detail in the subsequent
      +// sections of this Developer Guide.
      +
      +StreamsBuilder builder = ...;  // when using the DSL
      +Topology topology = builder.build();
      +//
      +// OR
      +//
      +Topology topology = ...; // when using the Processor API
      +
      +// Use the configuration to tell your application where the Kafka cluster is,
      +// which Serializers/Deserializers to use by default, to specify security settings,
      +// and so on.
      +Properties props = ...;
      +
      +KafkaStreams streams = new KafkaStreams(topology, props);

      At this point, internal structures are initialized, but the processing is not started yet. You have to explicitly start the Kafka Streams thread by calling the KafkaStreams#start() method:

      -
      // Start the Kafka Streams threads
      -streams.start();
      -
      +
      // Start the Kafka Streams threads
      +streams.start();

      If there are other instances of this stream processing application running elsewhere (e.g., on another machine), Kafka Streams transparently re-assigns tasks from the existing instances to the new instance that you just started. For more information, see Stream Partitions and Tasks and Threading Model.

      To catch any unexpected exceptions, you can set an java.lang.Thread.UncaughtExceptionHandler before you start the application. This handler is called whenever a stream thread is terminated by an unexpected exception:

      -
      // Java 8+, using lambda expressions
      -streams.setUncaughtExceptionHandler((Thread thread, Throwable throwable) -> {
      -  // here you should examine the throwable/exception and perform an appropriate action!
      -});
      -
      -
      -// Java 7
      -streams.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
      -  public void uncaughtException(Thread thread, Throwable throwable) {
      -    // here you should examine the throwable/exception and perform an appropriate action!
      -  }
      -});
      -
      +
      // Java 8+, using lambda expressions
      +streams.setUncaughtExceptionHandler((Thread thread, Throwable throwable) -> {
      +  // here you should examine the throwable/exception and perform an appropriate action!
      +});
      +
      +
      +// Java 7
      +streams.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
      +  public void uncaughtException(Thread thread, Throwable throwable) {
      +    // here you should examine the throwable/exception and perform an appropriate action!
      +  }
      +});

      To stop the application instance, call the KafkaStreams#close() method:

      -
      // Stop the Kafka Streams threads
      -streams.close();
      -
      +
      // Stop the Kafka Streams threads
      +streams.close();

      To allow your application to gracefully shutdown in response to SIGTERM, it is recommended that you add a shutdown hook and call KafkaStreams#close.

      • Here is a shutdown hook example in Java 8+:

        -
        -
        // Add shutdown hook to stop the Kafka Streams threads.
        -// You can optionally provide a timeout to `close`.
        -Runtime.getRuntime().addShutdownHook(new Thread(streams::close));
        -
        -
        +
        // Add shutdown hook to stop the Kafka Streams threads.
        +// You can optionally provide a timeout to `close`.
        +Runtime.getRuntime().addShutdownHook(new Thread(streams::close));
      • Here is a shutdown hook example in Java 7:

        -
        -
        // Add shutdown hook to stop the Kafka Streams threads.
        -// You can optionally provide a timeout to `close`.
        -Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
        -  @Override
        -  public void run() {
        -      streams.close();
        -  }
        -}));
        -
        -
        +
        // Add shutdown hook to stop the Kafka Streams threads.
        +// You can optionally provide a timeout to `close`.
        +Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
        +  @Override
        +  public void run() {
        +      streams.close();
        +  }
        +}));

      After an application is stopped, Kafka Streams will migrate any tasks that had been running in this instance to available remaining diff --git a/docs/streams/index.html b/docs/streams/index.html index 3d84bbfee7802..e38b3890af9ce 100644 --- a/docs/streams/index.html +++ b/docs/streams/index.html @@ -154,95 +154,95 @@

      Hello Kafka Streams

      -
                         import org.apache.kafka.common.serialization.Serdes;
      -                   import org.apache.kafka.common.utils.Bytes;
      -                   import org.apache.kafka.streams.KafkaStreams;
      -                   import org.apache.kafka.streams.StreamsBuilder;
      -                   import org.apache.kafka.streams.StreamsConfig;
      -                   import org.apache.kafka.streams.kstream.KStream;
      -                   import org.apache.kafka.streams.kstream.KTable;
      -                   import org.apache.kafka.streams.kstream.Materialized;
      -                   import org.apache.kafka.streams.kstream.Produced;
      -                   import org.apache.kafka.streams.state.KeyValueStore;
      -
      -                   import java.util.Arrays;
      -                   import java.util.Properties;
      -
      -                   public class WordCountApplication {
      -
      -                       public static void main(final String[] args) throws Exception {
      -                           Properties props = new Properties();
      -                           props.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-application");
      -                           props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:9092");
      -                           props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      -                           props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      -
      -                           StreamsBuilder builder = new StreamsBuilder();
      -                           KStream<String, String> textLines = builder.stream("TextLinesTopic");
      -                           KTable<String, Long> wordCounts = textLines
      -                               .flatMapValues(textLine -> Arrays.asList(textLine.toLowerCase().split("\\W+")))
      -                               .groupBy((key, word) -> word)
      -                               .count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as("counts-store"));
      -                           wordCounts.toStream().to("WordsWithCountsTopic", Produced.with(Serdes.String(), Serdes.Long()));
      -
      -                           KafkaStreams streams = new KafkaStreams(builder.build(), props);
      -                           streams.start();
      -                       }
      -
      -                   }
      +
      import org.apache.kafka.common.serialization.Serdes;
      +import org.apache.kafka.common.utils.Bytes;
      +import org.apache.kafka.streams.KafkaStreams;
      +import org.apache.kafka.streams.StreamsBuilder;
      +import org.apache.kafka.streams.StreamsConfig;
      +import org.apache.kafka.streams.kstream.KStream;
      +import org.apache.kafka.streams.kstream.KTable;
      +import org.apache.kafka.streams.kstream.Materialized;
      +import org.apache.kafka.streams.kstream.Produced;
      +import org.apache.kafka.streams.state.KeyValueStore;
      +
      +import java.util.Arrays;
      +import java.util.Properties;
      +
      +public class WordCountApplication {
      +
      +   public static void main(final String[] args) throws Exception {
      +       Properties props = new Properties();
      +       props.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-application");
      +       props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:9092");
      +       props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      +       props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      +
      +       StreamsBuilder builder = new StreamsBuilder();
      +       KStream<String, String> textLines = builder.stream("TextLinesTopic");
      +       KTable<String, Long> wordCounts = textLines
      +           .flatMapValues(textLine -> Arrays.asList(textLine.toLowerCase().split("\\W+")))
      +           .groupBy((key, word) -> word)
      +           .count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as("counts-store"));
      +       wordCounts.toStream().to("WordsWithCountsTopic", Produced.with(Serdes.String(), Serdes.Long()));
      +
      +       KafkaStreams streams = new KafkaStreams(builder.build(), props);
      +       streams.start();
      +   }
      +
      +}
      -
                         import org.apache.kafka.common.serialization.Serdes;
      -                   import org.apache.kafka.common.utils.Bytes;
      -                   import org.apache.kafka.streams.KafkaStreams;
      -                   import org.apache.kafka.streams.StreamsBuilder;
      -                   import org.apache.kafka.streams.StreamsConfig;
      -                   import org.apache.kafka.streams.kstream.KStream;
      -                   import org.apache.kafka.streams.kstream.KTable;
      -                   import org.apache.kafka.streams.kstream.ValueMapper;
      -                   import org.apache.kafka.streams.kstream.KeyValueMapper;
      -                   import org.apache.kafka.streams.kstream.Materialized;
      -                   import org.apache.kafka.streams.kstream.Produced;
      -                   import org.apache.kafka.streams.state.KeyValueStore;
      -
      -                   import java.util.Arrays;
      -                   import java.util.Properties;
      -
      -                   public class WordCountApplication {
      -
      -                       public static void main(final String[] args) throws Exception {
      -                           Properties props = new Properties();
      -                           props.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-application");
      -                           props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:9092");
      -                           props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      -                           props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      -
      -                           StreamsBuilder builder = new StreamsBuilder();
      -                           KStream<String, String> textLines = builder.stream("TextLinesTopic");
      -                           KTable<String, Long> wordCounts = textLines
      -                               .flatMapValues(new ValueMapper<String, Iterable<String>>() {
      -                                   @Override
      -                                   public Iterable<String> apply(String textLine) {
      -                                       return Arrays.asList(textLine.toLowerCase().split("\\W+"));
      -                                   }
      -                               })
      -                               .groupBy(new KeyValueMapper<String, String, String>() {
      -                                   @Override
      -                                   public String apply(String key, String word) {
      -                                       return word;
      -                                   }
      -                               })
      -                               .count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as("counts-store"));
      -
      -
      -                           wordCounts.toStream().to("WordsWithCountsTopic", Produced.with(Serdes.String(), Serdes.Long()));
      -
      -                           KafkaStreams streams = new KafkaStreams(builder.build(), props);
      -                           streams.start();
      -                       }
      -
      -                   }
      +
      import org.apache.kafka.common.serialization.Serdes;
      +import org.apache.kafka.common.utils.Bytes;
      +import org.apache.kafka.streams.KafkaStreams;
      +import org.apache.kafka.streams.StreamsBuilder;
      +import org.apache.kafka.streams.StreamsConfig;
      +import org.apache.kafka.streams.kstream.KStream;
      +import org.apache.kafka.streams.kstream.KTable;
      +import org.apache.kafka.streams.kstream.ValueMapper;
      +import org.apache.kafka.streams.kstream.KeyValueMapper;
      +import org.apache.kafka.streams.kstream.Materialized;
      +import org.apache.kafka.streams.kstream.Produced;
      +import org.apache.kafka.streams.state.KeyValueStore;
      +
      +import java.util.Arrays;
      +import java.util.Properties;
      +
      +public class WordCountApplication {
      +
      +   public static void main(final String[] args) throws Exception {
      +       Properties props = new Properties();
      +       props.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-application");
      +       props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:9092");
      +       props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      +       props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      +
      +       StreamsBuilder builder = new StreamsBuilder();
      +       KStream<String, String> textLines = builder.stream("TextLinesTopic");
      +       KTable<String, Long> wordCounts = textLines
      +           .flatMapValues(new ValueMapper<String, Iterable<String>>() {
      +               @Override
      +               public Iterable<String> apply(String textLine) {
      +                   return Arrays.asList(textLine.toLowerCase().split("\\W+"));
      +               }
      +           })
      +           .groupBy(new KeyValueMapper<String, String, String>() {
      +               @Override
      +               public String apply(String key, String word) {
      +                   return word;
      +               }
      +           })
      +           .count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as("counts-store"));
      +
      +
      +       wordCounts.toStream().to("WordsWithCountsTopic", Produced.with(Serdes.String(), Serdes.Long()));
      +
      +       KafkaStreams streams = new KafkaStreams(builder.build(), props);
      +       streams.start();
      +   }
      +
      +}
      diff --git a/docs/streams/tutorial.html b/docs/streams/tutorial.html index 2fa4dc3ec5a0c..a526de568abb1 100644 --- a/docs/streams/tutorial.html +++ b/docs/streams/tutorial.html @@ -42,32 +42,31 @@

      We are going to use a Kafka Streams Maven Archetype for creating a Streams project structure with the following commands:

      -
              mvn archetype:generate \
      -            -DarchetypeGroupId=org.apache.kafka \
      -            -DarchetypeArtifactId=streams-quickstart-java \
      -            -DarchetypeVersion={{fullDotVersion}} \
      -            -DgroupId=streams.examples \
      -            -DartifactId=streams.examples \
      -            -Dversion=0.1 \
      -            -Dpackage=myapps
      - +
      mvn archetype:generate \
      +    -DarchetypeGroupId=org.apache.kafka \
      +    -DarchetypeArtifactId=streams-quickstart-java \
      +    -DarchetypeVersion={{fullDotVersion}} \
      +    -DgroupId=streams.examples \
      +    -DartifactId=streams.examples \
      +    -Dversion=0.1 \
      +    -Dpackage=myapps

      You can use a different value for groupId, artifactId and package parameters if you like. Assuming the above parameter values are used, this command will create a project structure that looks like this:

      -
              > tree streams.examples
      -        streams-quickstart
      -        |-- pom.xml
      -        |-- src
      -            |-- main
      -                |-- java
      -                |   |-- myapps
      -                |       |-- LineSplit.java
      -                |       |-- Pipe.java
      -                |       |-- WordCount.java
      -                |-- resources
      -                    |-- log4j.properties
      +
      > tree streams.examples
      +    streams-quickstart
      +    |-- pom.xml
      +    |-- src
      +        |-- main
      +            |-- java
      +            |   |-- myapps
      +            |       |-- LineSplit.java
      +            |       |-- Pipe.java
      +            |       |-- WordCount.java
      +            |-- resources
      +                |-- log4j.properties

      The pom.xml file included in the project already has the Streams dependency defined. @@ -79,22 +78,22 @@

      Since we are going to start writing such programs from scratch, we can now delete these examples:

      -
              > cd streams-quickstart
      -        > rm src/main/java/myapps/*.java
      +
      > cd streams-quickstart
      +> rm src/main/java/myapps/*.java

      Writing a first Streams application: Pipe

      It's coding time now! Feel free to open your favorite IDE and import this Maven project, or simply open a text editor and create a java file under src/main/java/myapps. Let's name it Pipe.java: -
              package myapps;
      +    
      package myapps;
       
      -        public class Pipe {
      +public class Pipe {
       
      -            public static void main(String[] args) throws Exception {
      +    public static void main(String[] args) throws Exception {
       
      -            }
      -        }
      + } +}

      We are going to fill in the main function to write this pipe program. Note that we will not list the import statements as we go since IDEs can usually add them automatically. @@ -107,16 +106,16 @@

      Writing a first Stream and StreamsConfig.APPLICATION_ID_CONFIG, which gives the unique identifier of your Streams application to distinguish itself with other applications talking to the same Kafka cluster:

      -
              Properties props = new Properties();
      -        props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-pipe");
      -        props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");    // assuming that the Kafka broker this application is talking to runs on local machine with port 9092
      +
      Properties props = new Properties();
      +props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-pipe");
      +props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");    // assuming that the Kafka broker this application is talking to runs on local machine with port 9092

      In addition, you can customize other configurations in the same map, for example, default serialization and deserialization libraries for the record key-value pairs:

      -
              props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      -        props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      +
      props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      +props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());

      For a full list of configurations of Kafka Streams please refer to this table. @@ -128,13 +127,13 @@

      Writing a first Stream We can use a topology builder to construct such a topology,

      -
              final StreamsBuilder builder = new StreamsBuilder();
      +
      final StreamsBuilder builder = new StreamsBuilder();

      And then create a source stream from a Kafka topic named streams-plaintext-input using this topology builder:

      -
              KStream<String, String> source = builder.stream("streams-plaintext-input");
      +
      KStream<String, String> source = builder.stream("streams-plaintext-input");

      Now we get a KStream that is continuously generating records from its source Kafka topic streams-plaintext-input. @@ -142,38 +141,38 @@

      Writing a first Stream The simplest thing we can do with this stream is to write it into another Kafka topic, say it's named streams-pipe-output:

      -
              source.to("streams-pipe-output");
      +
      source.to("streams-pipe-output");

      Note that we can also concatenate the above two lines into a single line as:

      -
              builder.stream("streams-plaintext-input").to("streams-pipe-output");
      +
      builder.stream("streams-plaintext-input").to("streams-pipe-output");

      We can inspect what kind of topology is created from this builder by doing the following:

      -
              final Topology topology = builder.build();
      +
      final Topology topology = builder.build();

      And print its description to standard output as:

      -
              System.out.println(topology.describe());
      +
      System.out.println(topology.describe());

      If we just stop here, compile and run the program, it will output the following information:

      -
              > mvn clean package
      -        > mvn exec:java -Dexec.mainClass=myapps.Pipe
      -        Sub-topologies:
      -          Sub-topology: 0
      -            Source: KSTREAM-SOURCE-0000000000(topics: streams-plaintext-input) --> KSTREAM-SINK-0000000001
      -            Sink: KSTREAM-SINK-0000000001(topic: streams-pipe-output) <-- KSTREAM-SOURCE-0000000000
      -        Global Stores:
      -          none
      +
      > mvn clean package
      +> mvn exec:java -Dexec.mainClass=myapps.Pipe
      +Sub-topologies:
      +  Sub-topology: 0
      +    Source: KSTREAM-SOURCE-0000000000(topics: streams-plaintext-input) --> KSTREAM-SINK-0000000001
      +    Sink: KSTREAM-SINK-0000000001(topic: streams-pipe-output) <-- KSTREAM-SOURCE-0000000000
      +Global Stores:
      +  none

      As shown above, it illustrates that the constructed topology has two processor nodes, a source node KSTREAM-SOURCE-0000000000 and a sink node KSTREAM-SINK-0000000001. @@ -189,7 +188,7 @@

      Writing a first Stream we can now construct the Streams client with the two components we have just constructed above: the configuration map specified in a java.util.Properties instance and the Topology object.

      -
              final KafkaStreams streams = new KafkaStreams(topology, props);
      +
      final KafkaStreams streams = new KafkaStreams(topology, props);

      By calling its start() function we can trigger the execution of this client. @@ -197,76 +196,76 @@

      Writing a first Stream We can, for example, add a shutdown hook with a countdown latch to capture a user interrupt and close the client upon terminating this program:

      -
              final CountDownLatch latch = new CountDownLatch(1);
      +    
      final CountDownLatch latch = new CountDownLatch(1);
       
      -        // attach shutdown handler to catch control-c
      -        Runtime.getRuntime().addShutdownHook(new Thread("streams-shutdown-hook") {
      -            @Override
      -            public void run() {
      -                streams.close();
      -                latch.countDown();
      -            }
      -        });
      +// attach shutdown handler to catch control-c
      +Runtime.getRuntime().addShutdownHook(new Thread("streams-shutdown-hook") {
      +    @Override
      +    public void run() {
      +        streams.close();
      +        latch.countDown();
      +    }
      +});
       
      -        try {
      -            streams.start();
      -            latch.await();
      -        } catch (Throwable e) {
      -            System.exit(1);
      -        }
      -        System.exit(0);
      +try { + streams.start(); + latch.await(); +} catch (Throwable e) { + System.exit(1); +} +System.exit(0);

      The complete code so far looks like this:

      -
              package myapps;
      -
      -        import org.apache.kafka.common.serialization.Serdes;
      -        import org.apache.kafka.streams.KafkaStreams;
      -        import org.apache.kafka.streams.StreamsBuilder;
      -        import org.apache.kafka.streams.StreamsConfig;
      -        import org.apache.kafka.streams.Topology;
      +    
      package myapps;
       
      -        import java.util.Properties;
      -        import java.util.concurrent.CountDownLatch;
      +import org.apache.kafka.common.serialization.Serdes;
      +import org.apache.kafka.streams.KafkaStreams;
      +import org.apache.kafka.streams.StreamsBuilder;
      +import org.apache.kafka.streams.StreamsConfig;
      +import org.apache.kafka.streams.Topology;
       
      -        public class Pipe {
      +import java.util.Properties;
      +import java.util.concurrent.CountDownLatch;
       
      -            public static void main(String[] args) throws Exception {
      -                Properties props = new Properties();
      -                props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-pipe");
      -                props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
      -                props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      -                props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      +public class Pipe {
       
      -                final StreamsBuilder builder = new StreamsBuilder();
      +    public static void main(String[] args) throws Exception {
      +        Properties props = new Properties();
      +        props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-pipe");
      +        props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
      +        props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      +        props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
       
      -                builder.stream("streams-plaintext-input").to("streams-pipe-output");
      +        final StreamsBuilder builder = new StreamsBuilder();
       
      -                final Topology topology = builder.build();
      +        builder.stream("streams-plaintext-input").to("streams-pipe-output");
       
      -                final KafkaStreams streams = new KafkaStreams(topology, props);
      -                final CountDownLatch latch = new CountDownLatch(1);
      +        final Topology topology = builder.build();
       
      -                // attach shutdown handler to catch control-c
      -                Runtime.getRuntime().addShutdownHook(new Thread("streams-shutdown-hook") {
      -                    @Override
      -                    public void run() {
      -                        streams.close();
      -                        latch.countDown();
      -                    }
      -                });
      +        final KafkaStreams streams = new KafkaStreams(topology, props);
      +        final CountDownLatch latch = new CountDownLatch(1);
       
      -                try {
      -                    streams.start();
      -                    latch.await();
      -                } catch (Throwable e) {
      -                    System.exit(1);
      -                }
      -                System.exit(0);
      +        // attach shutdown handler to catch control-c
      +        Runtime.getRuntime().addShutdownHook(new Thread("streams-shutdown-hook") {
      +            @Override
      +            public void run() {
      +                streams.close();
      +                latch.countDown();
                   }
      -        }
      + }); + + try { + streams.start(); + latch.await(); + } catch (Throwable e) { + System.exit(1); + } + System.exit(0); + } +}

      If you already have the Kafka broker up and running at localhost:9092, @@ -274,8 +273,8 @@

      Writing a first Stream you can run this code in your IDE or on the command line, using Maven:

      -
              > mvn clean package
      -        > mvn exec:java -Dexec.mainClass=myapps.Pipe
      +
      > mvn clean package
      +> mvn exec:java -Dexec.mainClass=myapps.Pipe

      For detailed instructions on how to run a Streams application and observe its computing results, @@ -291,33 +290,33 @@

      Writing a se We can first create another program by first copy the existing Pipe.java class:

      -
              > cp src/main/java/myapps/Pipe.java src/main/java/myapps/LineSplit.java
      +
      > cp src/main/java/myapps/Pipe.java src/main/java/myapps/LineSplit.java

      And change its class name as well as the application id config to distinguish with the original program:

      -
              public class LineSplit {
      +    
      public class LineSplit {
       
      -            public static void main(String[] args) throws Exception {
      -                Properties props = new Properties();
      -                props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-linesplit");
      -                // ...
      -            }
      -        }
      + public static void main(String[] args) throws Exception { + Properties props = new Properties(); + props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-linesplit"); + // ... + } +}

      Since each of the source stream's record is a String typed key-value pair, let's treat the value string as a text line and split it into words with a FlatMapValues operator:

      -
              KStream<String, String> source = builder.stream("streams-plaintext-input");
      -        KStream<String, String> words = source.flatMapValues(new ValueMapper<String, Iterable<String>>() {
      -                    @Override
      -                    public Iterable<String> apply(String value) {
      -                        return Arrays.asList(value.split("\\W+"));
      -                    }
      -                });
      +
      KStream<String, String> source = builder.stream("streams-plaintext-input");
      +KStream<String, String> words = source.flatMapValues(new ValueMapper<String, Iterable<String>>() {
      +            @Override
      +            public Iterable<String> apply(String value) {
      +                return Arrays.asList(value.split("\\W+"));
      +            }
      +        });

      The operator will take the source stream as its input, and generate a new stream named words @@ -327,31 +326,31 @@

      Writing a se Note if you are using JDK 8 you can use lambda expression and simplify the above code as:

      -
              KStream<String, String> source = builder.stream("streams-plaintext-input");
      -        KStream<String, String> words = source.flatMapValues(value -> Arrays.asList(value.split("\\W+")));
      +
      KStream<String, String> source = builder.stream("streams-plaintext-input");
      +KStream<String, String> words = source.flatMapValues(value -> Arrays.asList(value.split("\\W+")));

      And finally we can write the word stream back into another Kafka topic, say streams-linesplit-output. Again, these two steps can be concatenated as the following (assuming lambda expression is used):

      -
              KStream<String, String> source = builder.stream("streams-plaintext-input");
      -        source.flatMapValues(value -> Arrays.asList(value.split("\\W+")))
      -              .to("streams-linesplit-output");
      +
      KStream<String, String> source = builder.stream("streams-plaintext-input");
      +source.flatMapValues(value -> Arrays.asList(value.split("\\W+")))
      +      .to("streams-linesplit-output");

      If we now describe this augmented topology as System.out.println(topology.describe()), we will get the following:

      -
              > mvn clean package
      -        > mvn exec:java -Dexec.mainClass=myapps.LineSplit
      -        Sub-topologies:
      -          Sub-topology: 0
      -            Source: KSTREAM-SOURCE-0000000000(topics: streams-plaintext-input) --> KSTREAM-FLATMAPVALUES-0000000001
      -            Processor: KSTREAM-FLATMAPVALUES-0000000001(stores: []) --> KSTREAM-SINK-0000000002 <-- KSTREAM-SOURCE-0000000000
      -            Sink: KSTREAM-SINK-0000000002(topic: streams-linesplit-output) <-- KSTREAM-FLATMAPVALUES-0000000001
      -          Global Stores:
      -            none
      +
      > mvn clean package
      +> mvn exec:java -Dexec.mainClass=myapps.LineSplit
      +Sub-topologies:
      +  Sub-topology: 0
      +    Source: KSTREAM-SOURCE-0000000000(topics: streams-plaintext-input) --> KSTREAM-FLATMAPVALUES-0000000001
      +    Processor: KSTREAM-FLATMAPVALUES-0000000001(stores: []) --> KSTREAM-SINK-0000000002 <-- KSTREAM-SOURCE-0000000000
      +    Sink: KSTREAM-SINK-0000000002(topic: streams-linesplit-output) <-- KSTREAM-FLATMAPVALUES-0000000001
      +  Global Stores:
      +    none

      As we can see above, a new processor node KSTREAM-FLATMAPVALUES-0000000001 is injected into the topology between the original source and sink nodes. @@ -365,41 +364,41 @@

      Writing a se The complete code looks like this (assuming lambda expression is used):

      -
              package myapps;
      +    
      package myapps;
       
      -        import org.apache.kafka.common.serialization.Serdes;
      -        import org.apache.kafka.streams.KafkaStreams;
      -        import org.apache.kafka.streams.StreamsBuilder;
      -        import org.apache.kafka.streams.StreamsConfig;
      -        import org.apache.kafka.streams.Topology;
      -        import org.apache.kafka.streams.kstream.KStream;
      +import org.apache.kafka.common.serialization.Serdes;
      +import org.apache.kafka.streams.KafkaStreams;
      +import org.apache.kafka.streams.StreamsBuilder;
      +import org.apache.kafka.streams.StreamsConfig;
      +import org.apache.kafka.streams.Topology;
      +import org.apache.kafka.streams.kstream.KStream;
       
      -        import java.util.Arrays;
      -        import java.util.Properties;
      -        import java.util.concurrent.CountDownLatch;
      +import java.util.Arrays;
      +import java.util.Properties;
      +import java.util.concurrent.CountDownLatch;
       
      -        public class LineSplit {
      +public class LineSplit {
       
      -            public static void main(String[] args) throws Exception {
      -                Properties props = new Properties();
      -                props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-linesplit");
      -                props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
      -                props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      -                props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      +    public static void main(String[] args) throws Exception {
      +        Properties props = new Properties();
      +        props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-linesplit");
      +        props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
      +        props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      +        props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
       
      -                final StreamsBuilder builder = new StreamsBuilder();
      +        final StreamsBuilder builder = new StreamsBuilder();
       
      -                KStream<String, String> source = builder.stream("streams-plaintext-input");
      -                source.flatMapValues(value -> Arrays.asList(value.split("\\W+")))
      -                      .to("streams-linesplit-output");
      +        KStream<String, String> source = builder.stream("streams-plaintext-input");
      +        source.flatMapValues(value -> Arrays.asList(value.split("\\W+")))
      +              .to("streams-linesplit-output");
       
      -                final Topology topology = builder.build();
      -                final KafkaStreams streams = new KafkaStreams(topology, props);
      -                final CountDownLatch latch = new CountDownLatch(1);
      +        final Topology topology = builder.build();
      +        final KafkaStreams streams = new KafkaStreams(topology, props);
      +        final CountDownLatch latch = new CountDownLatch(1);
       
      -                // ... same as Pipe.java above
      -            }
      -        }
      + // ... same as Pipe.java above + } +}

      Writing a third Streams application: Wordcount

      @@ -408,47 +407,47 @@

      Writing a th Following similar steps let's create another program based on the LineSplit.java class:

      -
              public class WordCount {
      +    
      public class WordCount {
       
      -            public static void main(String[] args) throws Exception {
      -                Properties props = new Properties();
      -                props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-wordcount");
      -                // ...
      -            }
      -        }
      + public static void main(String[] args) throws Exception { + Properties props = new Properties(); + props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-wordcount"); + // ... + } +}

      In order to count the words we can first modify the flatMapValues operator to treat all of them as lower case (assuming lambda expression is used):

      -
              source.flatMapValues(new ValueMapper<String, Iterable<String>>() {
      -                    @Override
      -                    public Iterable<String> apply(String value) {
      -                        return Arrays.asList(value.toLowerCase(Locale.getDefault()).split("\\W+"));
      -                    }
      -                });
      +
      source.flatMapValues(new ValueMapper<String, Iterable<String>>() {
      +    @Override
      +    public Iterable<String> apply(String value) {
      +        return Arrays.asList(value.toLowerCase(Locale.getDefault()).split("\\W+"));
      +    }
      +});

      In order to do the counting aggregation we have to first specify that we want to key the stream on the value string, i.e. the lower cased word, with a groupBy operator. This operator generate a new grouped stream, which can then be aggregated by a count operator, which generates a running count on each of the grouped keys:

      -
              KTable<String, Long> counts =
      -        source.flatMapValues(new ValueMapper<String, Iterable<String>>() {
      -                    @Override
      -                    public Iterable<String> apply(String value) {
      -                        return Arrays.asList(value.toLowerCase(Locale.getDefault()).split("\\W+"));
      -                    }
      -                })
      -              .groupBy(new KeyValueMapper<String, String, String>() {
      -                   @Override
      -                   public String apply(String key, String value) {
      -                       return value;
      -                   }
      -                })
      -              // Materialize the result into a KeyValueStore named "counts-store".
      -              // The Materialized store is always of type <Bytes, byte[]> as this is the format of the inner most store.
      -              .count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>> as("counts-store"));
      +
      KTable<String, Long> counts =
      +source.flatMapValues(new ValueMapper<String, Iterable<String>>() {
      +            @Override
      +            public Iterable<String> apply(String value) {
      +                return Arrays.asList(value.toLowerCase(Locale.getDefault()).split("\\W+"));
      +            }
      +        })
      +      .groupBy(new KeyValueMapper<String, String, String>() {
      +           @Override
      +           public String apply(String key, String value) {
      +               return value;
      +           }
      +        })
      +      // Materialize the result into a KeyValueStore named "counts-store".
      +      // The Materialized store is always of type <Bytes, byte[]> as this is the format of the inner most store.
      +      .count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>> as("counts-store"));

      Note that the count operator has a Materialized parameter that specifies that the @@ -463,7 +462,7 @@

      Writing a th We need to provide overridden serialization methods for Long types, otherwise a runtime exception will be thrown:

      -
              counts.toStream().to("streams-wordcount-output", Produced.with(Serdes.String(), Serdes.Long()));
      +
      counts.toStream().to("streams-wordcount-output", Produced.with(Serdes.String(), Serdes.Long()));

      Note that in order to read the changelog stream from topic streams-wordcount-output, @@ -472,33 +471,33 @@

      Writing a th Assuming lambda expression from JDK 8 can be used, the above code can be simplified as:

      -
              KStream<String, String> source = builder.stream("streams-plaintext-input");
      -        source.flatMapValues(value -> Arrays.asList(value.toLowerCase(Locale.getDefault()).split("\\W+")))
      -              .groupBy((key, value) -> value)
      -              .count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as("counts-store"))
      -              .toStream()
      -              .to("streams-wordcount-output", Produced.with(Serdes.String(), Serdes.Long()));
      +
      KStream<String, String> source = builder.stream("streams-plaintext-input");
      +source.flatMapValues(value -> Arrays.asList(value.toLowerCase(Locale.getDefault()).split("\\W+")))
      +      .groupBy((key, value) -> value)
      +      .count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as("counts-store"))
      +      .toStream()
      +      .to("streams-wordcount-output", Produced.with(Serdes.String(), Serdes.Long()));

      If we again describe this augmented topology as System.out.println(topology.describe()), we will get the following:

      -
              > mvn clean package
      -        > mvn exec:java -Dexec.mainClass=myapps.WordCount
      -        Sub-topologies:
      -          Sub-topology: 0
      -            Source: KSTREAM-SOURCE-0000000000(topics: streams-plaintext-input) --> KSTREAM-FLATMAPVALUES-0000000001
      -            Processor: KSTREAM-FLATMAPVALUES-0000000001(stores: []) --> KSTREAM-KEY-SELECT-0000000002 <-- KSTREAM-SOURCE-0000000000
      -            Processor: KSTREAM-KEY-SELECT-0000000002(stores: []) --> KSTREAM-FILTER-0000000005 <-- KSTREAM-FLATMAPVALUES-0000000001
      -            Processor: KSTREAM-FILTER-0000000005(stores: []) --> KSTREAM-SINK-0000000004 <-- KSTREAM-KEY-SELECT-0000000002
      -            Sink: KSTREAM-SINK-0000000004(topic: Counts-repartition) <-- KSTREAM-FILTER-0000000005
      -          Sub-topology: 1
      -            Source: KSTREAM-SOURCE-0000000006(topics: Counts-repartition) --> KSTREAM-AGGREGATE-0000000003
      -            Processor: KSTREAM-AGGREGATE-0000000003(stores: [Counts]) --> KTABLE-TOSTREAM-0000000007 <-- KSTREAM-SOURCE-0000000006
      -            Processor: KTABLE-TOSTREAM-0000000007(stores: []) --> KSTREAM-SINK-0000000008 <-- KSTREAM-AGGREGATE-0000000003
      -            Sink: KSTREAM-SINK-0000000008(topic: streams-wordcount-output) <-- KTABLE-TOSTREAM-0000000007
      -        Global Stores:
      -          none
      +
      > mvn clean package
      +> mvn exec:java -Dexec.mainClass=myapps.WordCount
      +Sub-topologies:
      +  Sub-topology: 0
      +    Source: KSTREAM-SOURCE-0000000000(topics: streams-plaintext-input) --> KSTREAM-FLATMAPVALUES-0000000001
      +    Processor: KSTREAM-FLATMAPVALUES-0000000001(stores: []) --> KSTREAM-KEY-SELECT-0000000002 <-- KSTREAM-SOURCE-0000000000
      +    Processor: KSTREAM-KEY-SELECT-0000000002(stores: []) --> KSTREAM-FILTER-0000000005 <-- KSTREAM-FLATMAPVALUES-0000000001
      +    Processor: KSTREAM-FILTER-0000000005(stores: []) --> KSTREAM-SINK-0000000004 <-- KSTREAM-KEY-SELECT-0000000002
      +    Sink: KSTREAM-SINK-0000000004(topic: Counts-repartition) <-- KSTREAM-FILTER-0000000005
      +  Sub-topology: 1
      +    Source: KSTREAM-SOURCE-0000000006(topics: Counts-repartition) --> KSTREAM-AGGREGATE-0000000003
      +    Processor: KSTREAM-AGGREGATE-0000000003(stores: [Counts]) --> KTABLE-TOSTREAM-0000000007 <-- KSTREAM-SOURCE-0000000006
      +    Processor: KTABLE-TOSTREAM-0000000007(stores: []) --> KSTREAM-SINK-0000000008 <-- KSTREAM-AGGREGATE-0000000003
      +    Sink: KSTREAM-SINK-0000000008(topic: streams-wordcount-output) <-- KTABLE-TOSTREAM-0000000007
      +Global Stores:
      +  none

      As we can see above, the topology now contains two disconnected sub-topologies. @@ -517,49 +516,49 @@

      Writing a th The complete code looks like this (assuming lambda expression is used):

      -
              package myapps;
      +    
      package myapps;
       
      -        import org.apache.kafka.common.serialization.Serdes;
      -        import org.apache.kafka.common.utils.Bytes;
      -        import org.apache.kafka.streams.KafkaStreams;
      -        import org.apache.kafka.streams.StreamsBuilder;
      -        import org.apache.kafka.streams.StreamsConfig;
      -        import org.apache.kafka.streams.Topology;
      -        import org.apache.kafka.streams.kstream.KStream;
      -        import org.apache.kafka.streams.kstream.Materialized;
      -        import org.apache.kafka.streams.kstream.Produced;
      -        import org.apache.kafka.streams.state.KeyValueStore;
      +import org.apache.kafka.common.serialization.Serdes;
      +import org.apache.kafka.common.utils.Bytes;
      +import org.apache.kafka.streams.KafkaStreams;
      +import org.apache.kafka.streams.StreamsBuilder;
      +import org.apache.kafka.streams.StreamsConfig;
      +import org.apache.kafka.streams.Topology;
      +import org.apache.kafka.streams.kstream.KStream;
      +import org.apache.kafka.streams.kstream.Materialized;
      +import org.apache.kafka.streams.kstream.Produced;
      +import org.apache.kafka.streams.state.KeyValueStore;
       
      -        import java.util.Arrays;
      -        import java.util.Locale;
      -        import java.util.Properties;
      -        import java.util.concurrent.CountDownLatch;
      +import java.util.Arrays;
      +import java.util.Locale;
      +import java.util.Properties;
      +import java.util.concurrent.CountDownLatch;
       
      -        public class WordCount {
      +public class WordCount {
       
      -            public static void main(String[] args) throws Exception {
      -                Properties props = new Properties();
      -                props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-wordcount");
      -                props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
      -                props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      -                props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      +    public static void main(String[] args) throws Exception {
      +        Properties props = new Properties();
      +        props.put(StreamsConfig.APPLICATION_ID_CONFIG, "streams-wordcount");
      +        props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
      +        props.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
      +        props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
       
      -                final StreamsBuilder builder = new StreamsBuilder();
      +        final StreamsBuilder builder = new StreamsBuilder();
       
      -                KStream<String, String> source = builder.stream("streams-plaintext-input");
      -                source.flatMapValues(value -> Arrays.asList(value.toLowerCase(Locale.getDefault()).split("\\W+")))
      -                      .groupBy((key, value) -> value)
      -                      .count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as("counts-store"))
      -                      .toStream()
      -                      .to("streams-wordcount-output", Produced.with(Serdes.String(), Serdes.Long()));
      +        KStream<String, String> source = builder.stream("streams-plaintext-input");
      +        source.flatMapValues(value -> Arrays.asList(value.toLowerCase(Locale.getDefault()).split("\\W+")))
      +              .groupBy((key, value) -> value)
      +              .count(Materialized.<String, Long, KeyValueStore<Bytes, byte[]>>as("counts-store"))
      +              .toStream()
      +              .to("streams-wordcount-output", Produced.with(Serdes.String(), Serdes.Long()));
       
      -                final Topology topology = builder.build();
      -                final KafkaStreams streams = new KafkaStreams(topology, props);
      -                final CountDownLatch latch = new CountDownLatch(1);
      +        final Topology topology = builder.build();
      +        final KafkaStreams streams = new KafkaStreams(topology, props);
      +        final CountDownLatch latch = new CountDownLatch(1);
       
      -                // ... same as Pipe.java above
      -            }
      -        }
      + // ... same as Pipe.java above + } +}

      In-memory @@ -268,19 +266,18 @@

    5. Use TimestampedWindowStore when you need to store windowedKey-(value/timestamp) pairs.
    6. -
      // Creating an in-memory key-value store:
      -// here, we create a `KeyValueStore<String, Long>` named "inmemory-counts".
      -import org.apache.kafka.streams.state.StoreBuilder;
      -import org.apache.kafka.streams.state.Stores;
      +                            
      // Creating an in-memory key-value store:
      +// here, we create a `KeyValueStore<String, Long>` named "inmemory-counts".
      +import org.apache.kafka.streams.state.StoreBuilder;
      +import org.apache.kafka.streams.state.Stores;
       
      -// Using a `KeyValueStoreBuilder` to build a `KeyValueStore`.
      -StoreBuilder<KeyValueStore<String, Long>> countStoreSupplier =
      -  Stores.keyValueStoreBuilder(
      -    Stores.inMemoryKeyValueStore("inmemory-counts"),
      -    Serdes.String(),
      -    Serdes.Long());
      -KeyValueStore<String, Long> countStore = countStoreSupplier.build();
      -
      +// Using a `KeyValueStoreBuilder` to build a `KeyValueStore`. +StoreBuilder<KeyValueStore<String, Long>> countStoreSupplier = + Stores.keyValueStoreBuilder( + Stores.inMemoryKeyValueStore("inmemory-counts"), + Serdes.String(), + Serdes.Long()); +KeyValueStore<String, Long> countStore = countStoreSupplier.build();