You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kafka.apache.org by gu...@apache.org on 2017/04/26 21:11:05 UTC
[02/10] kafka git commit: KAFKA-5059: Implement Transactional
Coordinator
http://git-wip-us.apache.org/repos/asf/kafka/blob/f69d9415/core/src/test/scala/unit/kafka/coordinator/group/GroupMetadataTest.scala
----------------------------------------------------------------------
diff --git a/core/src/test/scala/unit/kafka/coordinator/group/GroupMetadataTest.scala b/core/src/test/scala/unit/kafka/coordinator/group/GroupMetadataTest.scala
new file mode 100644
index 0000000..ab893cd
--- /dev/null
+++ b/core/src/test/scala/unit/kafka/coordinator/group/GroupMetadataTest.scala
@@ -0,0 +1,362 @@
+/**
+ * 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.group
+
+import kafka.common.OffsetAndMetadata
+
+import org.apache.kafka.common.TopicPartition
+
+import org.junit.Assert._
+import org.junit.{Before, Test}
+import org.scalatest.junit.JUnitSuite
+
+/**
+ * Test group state transitions and other GroupMetadata functionality
+ */
+class GroupMetadataTest extends JUnitSuite {
+ private val protocolType = "consumer"
+ private val groupId = "groupId"
+ private val clientId = "clientId"
+ private val clientHost = "clientHost"
+ private val rebalanceTimeoutMs = 60000
+ private val sessionTimeoutMs = 10000
+
+ private var group: GroupMetadata = null
+
+ @Before
+ def setUp() {
+ group = new GroupMetadata("groupId")
+ }
+
+ @Test
+ def testCanRebalanceWhenStable() {
+ assertTrue(group.canRebalance)
+ }
+
+ @Test
+ def testCanRebalanceWhenAwaitingSync() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(AwaitingSync)
+ assertTrue(group.canRebalance)
+ }
+
+ @Test
+ def testCannotRebalanceWhenPreparingRebalance() {
+ group.transitionTo(PreparingRebalance)
+ assertFalse(group.canRebalance)
+ }
+
+ @Test
+ def testCannotRebalanceWhenDead() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(Empty)
+ group.transitionTo(Dead)
+ assertFalse(group.canRebalance)
+ }
+
+ @Test
+ def testStableToPreparingRebalanceTransition() {
+ group.transitionTo(PreparingRebalance)
+ assertState(group, PreparingRebalance)
+ }
+
+ @Test
+ def testStableToDeadTransition() {
+ group.transitionTo(Dead)
+ assertState(group, Dead)
+ }
+
+ @Test
+ def testAwaitingSyncToPreparingRebalanceTransition() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(AwaitingSync)
+ group.transitionTo(PreparingRebalance)
+ assertState(group, PreparingRebalance)
+ }
+
+ @Test
+ def testPreparingRebalanceToDeadTransition() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(Dead)
+ assertState(group, Dead)
+ }
+
+ @Test
+ def testPreparingRebalanceToEmptyTransition() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(Empty)
+ assertState(group, Empty)
+ }
+
+ @Test
+ def testEmptyToDeadTransition() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(Empty)
+ group.transitionTo(Dead)
+ assertState(group, Dead)
+ }
+
+ @Test
+ def testAwaitingSyncToStableTransition() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(AwaitingSync)
+ group.transitionTo(Stable)
+ assertState(group, Stable)
+ }
+
+ @Test(expected = classOf[IllegalStateException])
+ def testStableToStableIllegalTransition() {
+ group.transitionTo(Stable)
+ }
+
+ @Test(expected = classOf[IllegalStateException])
+ def testStableToAwaitingSyncIllegalTransition() {
+ group.transitionTo(AwaitingSync)
+ }
+
+ @Test(expected = classOf[IllegalStateException])
+ def testPreparingRebalanceToPreparingRebalanceIllegalTransition() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(PreparingRebalance)
+ }
+
+ @Test(expected = classOf[IllegalStateException])
+ def testPreparingRebalanceToStableIllegalTransition() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(Stable)
+ }
+
+ @Test(expected = classOf[IllegalStateException])
+ def testAwaitingSyncToAwaitingSyncIllegalTransition() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(AwaitingSync)
+ group.transitionTo(AwaitingSync)
+ }
+
+ def testDeadToDeadIllegalTransition() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(Dead)
+ group.transitionTo(Dead)
+ assertState(group, Dead)
+ }
+
+ @Test(expected = classOf[IllegalStateException])
+ def testDeadToStableIllegalTransition() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(Dead)
+ group.transitionTo(Stable)
+ }
+
+ @Test(expected = classOf[IllegalStateException])
+ def testDeadToPreparingRebalanceIllegalTransition() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(Dead)
+ group.transitionTo(PreparingRebalance)
+ }
+
+ @Test(expected = classOf[IllegalStateException])
+ def testDeadToAwaitingSyncIllegalTransition() {
+ group.transitionTo(PreparingRebalance)
+ group.transitionTo(Dead)
+ group.transitionTo(AwaitingSync)
+ }
+
+ @Test
+ def testSelectProtocol() {
+ val memberId = "memberId"
+ val member = new MemberMetadata(memberId, groupId, clientId, clientHost, rebalanceTimeoutMs, sessionTimeoutMs,
+ protocolType, List(("range", Array.empty[Byte]), ("roundrobin", Array.empty[Byte])))
+
+ group.add(member)
+ assertEquals("range", group.selectProtocol)
+
+ val otherMemberId = "otherMemberId"
+ val otherMember = new MemberMetadata(otherMemberId, groupId, clientId, clientHost, rebalanceTimeoutMs,
+ sessionTimeoutMs, protocolType, List(("roundrobin", Array.empty[Byte]), ("range", Array.empty[Byte])))
+
+ group.add(otherMember)
+ // now could be either range or robin since there is no majority preference
+ assertTrue(Set("range", "roundrobin")(group.selectProtocol))
+
+ val lastMemberId = "lastMemberId"
+ val lastMember = new MemberMetadata(lastMemberId, groupId, clientId, clientHost, rebalanceTimeoutMs,
+ sessionTimeoutMs, protocolType, List(("roundrobin", Array.empty[Byte]), ("range", Array.empty[Byte])))
+
+ group.add(lastMember)
+ // now we should prefer 'roundrobin'
+ assertEquals("roundrobin", group.selectProtocol)
+ }
+
+ @Test(expected = classOf[IllegalStateException])
+ def testSelectProtocolRaisesIfNoMembers() {
+ group.selectProtocol
+ fail()
+ }
+
+ @Test
+ def testSelectProtocolChoosesCompatibleProtocol() {
+ val memberId = "memberId"
+ val member = new MemberMetadata(memberId, groupId, clientId, clientHost, rebalanceTimeoutMs, sessionTimeoutMs,
+ protocolType, List(("range", Array.empty[Byte]), ("roundrobin", Array.empty[Byte])))
+
+ val otherMemberId = "otherMemberId"
+ val otherMember = new MemberMetadata(otherMemberId, groupId, clientId, clientHost, rebalanceTimeoutMs,
+ sessionTimeoutMs, protocolType, List(("roundrobin", Array.empty[Byte]), ("blah", Array.empty[Byte])))
+
+ group.add(member)
+ group.add(otherMember)
+ assertEquals("roundrobin", group.selectProtocol)
+ }
+
+ @Test
+ def testSupportsProtocols() {
+ // by default, the group supports everything
+ assertTrue(group.supportsProtocols(Set("roundrobin", "range")))
+
+ val memberId = "memberId"
+ val member = new MemberMetadata(memberId, groupId, clientId, clientHost, rebalanceTimeoutMs,
+ sessionTimeoutMs, protocolType, List(("range", Array.empty[Byte]), ("roundrobin", Array.empty[Byte])))
+
+ group.add(member)
+ assertTrue(group.supportsProtocols(Set("roundrobin", "foo")))
+ assertTrue(group.supportsProtocols(Set("range", "foo")))
+ assertFalse(group.supportsProtocols(Set("foo", "bar")))
+
+ val otherMemberId = "otherMemberId"
+ val otherMember = new MemberMetadata(otherMemberId, groupId, clientId, clientHost, rebalanceTimeoutMs,
+ sessionTimeoutMs, protocolType, List(("roundrobin", Array.empty[Byte]), ("blah", Array.empty[Byte])))
+
+ group.add(otherMember)
+
+ assertTrue(group.supportsProtocols(Set("roundrobin", "foo")))
+ assertFalse(group.supportsProtocols(Set("range", "foo")))
+ }
+
+ @Test
+ def testInitNextGeneration() {
+ val memberId = "memberId"
+ val member = new MemberMetadata(memberId, groupId, clientId, clientHost, rebalanceTimeoutMs, sessionTimeoutMs,
+ protocolType, List(("roundrobin", Array.empty[Byte])))
+
+ group.transitionTo(PreparingRebalance)
+ member.awaitingJoinCallback = _ => ()
+ group.add(member)
+
+ assertEquals(0, group.generationId)
+ assertNull(group.protocol)
+
+ group.initNextGeneration()
+
+ assertEquals(1, group.generationId)
+ assertEquals("roundrobin", group.protocol)
+ }
+
+ @Test
+ def testInitNextGenerationEmptyGroup() {
+ assertEquals(Empty, group.currentState)
+ assertEquals(0, group.generationId)
+ assertNull(group.protocol)
+
+ group.transitionTo(PreparingRebalance)
+ group.initNextGeneration()
+
+ assertEquals(1, group.generationId)
+ assertNull(group.protocol)
+ }
+
+ @Test
+ def testOffsetCommit(): Unit = {
+ val partition = new TopicPartition("foo", 0)
+ val offset = OffsetAndMetadata(37)
+
+ group.prepareOffsetCommit(Map(partition -> offset))
+ assertTrue(group.hasOffsets)
+ assertEquals(None, group.offset(partition))
+
+ group.completePendingOffsetWrite(partition, offset)
+ assertTrue(group.hasOffsets)
+ assertEquals(Some(offset), group.offset(partition))
+ }
+
+ @Test
+ def testOffsetCommitFailure(): Unit = {
+ val partition = new TopicPartition("foo", 0)
+ val offset = OffsetAndMetadata(37)
+
+ group.prepareOffsetCommit(Map(partition -> offset))
+ assertTrue(group.hasOffsets)
+ assertEquals(None, group.offset(partition))
+
+ group.failPendingOffsetWrite(partition, offset)
+ assertFalse(group.hasOffsets)
+ assertEquals(None, group.offset(partition))
+ }
+
+ @Test
+ def testOffsetCommitFailureWithAnotherPending(): Unit = {
+ val partition = new TopicPartition("foo", 0)
+ val firstOffset = OffsetAndMetadata(37)
+ val secondOffset = OffsetAndMetadata(57)
+
+ group.prepareOffsetCommit(Map(partition -> firstOffset))
+ assertTrue(group.hasOffsets)
+ assertEquals(None, group.offset(partition))
+
+ group.prepareOffsetCommit(Map(partition -> secondOffset))
+ assertTrue(group.hasOffsets)
+
+ group.failPendingOffsetWrite(partition, firstOffset)
+ assertTrue(group.hasOffsets)
+ assertEquals(None, group.offset(partition))
+
+ group.completePendingOffsetWrite(partition, secondOffset)
+ assertTrue(group.hasOffsets)
+ assertEquals(Some(secondOffset), group.offset(partition))
+ }
+
+ @Test
+ def testOffsetCommitWithAnotherPending(): Unit = {
+ val partition = new TopicPartition("foo", 0)
+ val firstOffset = OffsetAndMetadata(37)
+ val secondOffset = OffsetAndMetadata(57)
+
+ group.prepareOffsetCommit(Map(partition -> firstOffset))
+ assertTrue(group.hasOffsets)
+ assertEquals(None, group.offset(partition))
+
+ group.prepareOffsetCommit(Map(partition -> secondOffset))
+ assertTrue(group.hasOffsets)
+
+ group.completePendingOffsetWrite(partition, firstOffset)
+ assertTrue(group.hasOffsets)
+ assertEquals(Some(firstOffset), group.offset(partition))
+
+ group.completePendingOffsetWrite(partition, secondOffset)
+ assertTrue(group.hasOffsets)
+ assertEquals(Some(secondOffset), group.offset(partition))
+ }
+
+ private def assertState(group: GroupMetadata, targetState: GroupState) {
+ val states: Set[GroupState] = Set(Stable, PreparingRebalance, AwaitingSync, Dead)
+ val otherStates = states - targetState
+ otherStates.foreach { otherState =>
+ assertFalse(group.is(otherState))
+ }
+ assertTrue(group.is(targetState))
+ }
+}
http://git-wip-us.apache.org/repos/asf/kafka/blob/f69d9415/core/src/test/scala/unit/kafka/coordinator/group/MemberMetadataTest.scala
----------------------------------------------------------------------
diff --git a/core/src/test/scala/unit/kafka/coordinator/group/MemberMetadataTest.scala b/core/src/test/scala/unit/kafka/coordinator/group/MemberMetadataTest.scala
new file mode 100644
index 0000000..48c9270
--- /dev/null
+++ b/core/src/test/scala/unit/kafka/coordinator/group/MemberMetadataTest.scala
@@ -0,0 +1,88 @@
+/**
+ * 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.group
+
+import java.util.Arrays
+
+import org.junit.Assert._
+import org.junit.Test
+import org.scalatest.junit.JUnitSuite
+
+class MemberMetadataTest extends JUnitSuite {
+ val groupId = "groupId"
+ val clientId = "clientId"
+ val clientHost = "clientHost"
+ val memberId = "memberId"
+ val protocolType = "consumer"
+ val rebalanceTimeoutMs = 60000
+ val sessionTimeoutMs = 10000
+
+
+ @Test
+ def testMatchesSupportedProtocols() {
+ val protocols = List(("range", Array.empty[Byte]))
+
+ val member = new MemberMetadata(memberId, groupId, clientId, clientHost, rebalanceTimeoutMs, sessionTimeoutMs,
+ protocolType, protocols)
+ assertTrue(member.matches(protocols))
+ assertFalse(member.matches(List(("range", Array[Byte](0)))))
+ assertFalse(member.matches(List(("roundrobin", Array.empty[Byte]))))
+ assertFalse(member.matches(List(("range", Array.empty[Byte]), ("roundrobin", Array.empty[Byte]))))
+ }
+
+ @Test
+ def testVoteForPreferredProtocol() {
+ val protocols = List(("range", Array.empty[Byte]), ("roundrobin", Array.empty[Byte]))
+
+ val member = new MemberMetadata(memberId, groupId, clientId, clientHost, rebalanceTimeoutMs, sessionTimeoutMs,
+ protocolType, protocols)
+ assertEquals("range", member.vote(Set("range", "roundrobin")))
+ assertEquals("roundrobin", member.vote(Set("blah", "roundrobin")))
+ }
+
+ @Test
+ def testMetadata() {
+ val protocols = List(("range", Array[Byte](0)), ("roundrobin", Array[Byte](1)))
+
+ val member = new MemberMetadata(memberId, groupId, clientId, clientHost, rebalanceTimeoutMs, sessionTimeoutMs,
+ protocolType, protocols)
+ assertTrue(Arrays.equals(Array[Byte](0), member.metadata("range")))
+ assertTrue(Arrays.equals(Array[Byte](1), member.metadata("roundrobin")))
+ }
+
+ @Test(expected = classOf[IllegalArgumentException])
+ def testMetadataRaisesOnUnsupportedProtocol() {
+ val protocols = List(("range", Array.empty[Byte]), ("roundrobin", Array.empty[Byte]))
+
+ val member = new MemberMetadata(memberId, groupId, clientId, clientHost, rebalanceTimeoutMs, sessionTimeoutMs,
+ protocolType, protocols)
+ member.metadata("blah")
+ fail()
+ }
+
+ @Test(expected = classOf[IllegalArgumentException])
+ def testVoteRaisesOnNoSupportedProtocols() {
+ val protocols = List(("range", Array.empty[Byte]), ("roundrobin", Array.empty[Byte]))
+
+ val member = new MemberMetadata(memberId, groupId, clientId, clientHost, rebalanceTimeoutMs, sessionTimeoutMs,
+ protocolType, protocols)
+ member.vote(Set("blah"))
+ fail()
+ }
+
+
+}
http://git-wip-us.apache.org/repos/asf/kafka/blob/f69d9415/core/src/test/scala/unit/kafka/coordinator/transaction/ProducerIdManagerTest.scala
----------------------------------------------------------------------
diff --git a/core/src/test/scala/unit/kafka/coordinator/transaction/ProducerIdManagerTest.scala b/core/src/test/scala/unit/kafka/coordinator/transaction/ProducerIdManagerTest.scala
new file mode 100644
index 0000000..85c631c
--- /dev/null
+++ b/core/src/test/scala/unit/kafka/coordinator/transaction/ProducerIdManagerTest.scala
@@ -0,0 +1,104 @@
+/*
+ * 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.common.KafkaException
+import kafka.utils.ZkUtils
+import org.easymock.{Capture, EasyMock, IAnswer}
+import org.junit.{After, Test}
+import org.junit.Assert._
+
+class ProducerIdManagerTest {
+
+ val zkUtils: ZkUtils = EasyMock.createNiceMock(classOf[ZkUtils])
+
+ @After
+ def tearDown(): Unit = {
+ EasyMock.reset(zkUtils)
+ }
+
+ @Test
+ def testGetPID() {
+ var zkVersion: Int = -1
+ var data: String = null
+ EasyMock.expect(zkUtils.readDataAndVersionMaybeNull(EasyMock.anyString()))
+ .andAnswer(new IAnswer[(Option[String], Int)] {
+ override def answer(): (Option[String], Int) = {
+ if (zkVersion == -1) {
+ (None.asInstanceOf[Option[String]], 0)
+ } else {
+ (Some(data), zkVersion)
+ }
+ }
+ })
+ .anyTimes()
+
+ val capturedVersion: Capture[Int] = EasyMock.newCapture()
+ val capturedData: Capture[String] = EasyMock.newCapture()
+ EasyMock.expect(zkUtils.conditionalUpdatePersistentPath(EasyMock.anyString(),
+ EasyMock.capture(capturedData),
+ EasyMock.capture(capturedVersion),
+ EasyMock.anyObject().asInstanceOf[Option[(ZkUtils, String, String) => (Boolean,Int)]]))
+ .andAnswer(new IAnswer[(Boolean, Int)] {
+ override def answer(): (Boolean, Int) = {
+ zkVersion = capturedVersion.getValue + 1
+ data = capturedData.getValue
+
+ (true, zkVersion)
+ }
+ })
+ .anyTimes()
+
+ EasyMock.replay(zkUtils)
+
+ val manager1: ProducerIdManager = new ProducerIdManager(0, zkUtils)
+ val manager2: ProducerIdManager = new ProducerIdManager(1, zkUtils)
+
+ val pid1 = manager1.nextPid()
+ val pid2 = manager2.nextPid()
+
+ assertEquals(0, pid1)
+ assertEquals(ProducerIdManager.PidBlockSize, pid2)
+
+ for (i <- 1 until ProducerIdManager.PidBlockSize.asInstanceOf[Int]) {
+ assertEquals(pid1 + i, manager1.nextPid())
+ }
+
+ for (i <- 1 until ProducerIdManager.PidBlockSize.asInstanceOf[Int]) {
+ assertEquals(pid2 + i, manager2.nextPid())
+ }
+
+ assertEquals(pid2 + ProducerIdManager.PidBlockSize, manager1.nextPid())
+ assertEquals(pid2 + ProducerIdManager.PidBlockSize * 2, manager2.nextPid())
+ }
+
+ @Test(expected = classOf[KafkaException])
+ def testExceedPIDLimit() {
+ EasyMock.expect(zkUtils.readDataAndVersionMaybeNull(EasyMock.anyString()))
+ .andAnswer(new IAnswer[(Option[String], Int)] {
+ override def answer(): (Option[String], Int) = {
+ (Some(ProducerIdManager.generatePidBlockJson(ProducerIdBlock(0,
+ Long.MaxValue - ProducerIdManager.PidBlockSize,
+ Long.MaxValue))), 0)
+ }
+ })
+ .anyTimes()
+ EasyMock.replay(zkUtils)
+ new ProducerIdManager(0, zkUtils)
+ }
+}
+
http://git-wip-us.apache.org/repos/asf/kafka/blob/f69d9415/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorIntegrationTest.scala
----------------------------------------------------------------------
diff --git a/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorIntegrationTest.scala b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorIntegrationTest.scala
new file mode 100644
index 0000000..80ef687
--- /dev/null
+++ b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorIntegrationTest.scala
@@ -0,0 +1,98 @@
+/*
+ * 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 unit.kafka.coordinator.transaction
+
+import java.util.Properties
+
+import kafka.common.Topic
+import kafka.coordinator.transaction.InitPidResult
+import kafka.integration.KafkaServerTestHarness
+import kafka.server.KafkaConfig
+import kafka.utils.TestUtils
+import org.apache.kafka.common.TopicPartition
+import org.apache.kafka.common.protocol.Errors
+import org.apache.kafka.common.record.CompressionType
+import org.apache.kafka.common.requests.TransactionResult
+import org.apache.kafka.common.utils.Utils
+import org.junit.{Assert, Test}
+
+class TransactionCoordinatorIntegrationTest extends KafkaServerTestHarness {
+ val offsetsTopicCompressionCodec = CompressionType.GZIP
+ val overridingProps = new Properties()
+ overridingProps.put(KafkaConfig.OffsetsTopicPartitionsProp, "1")
+ overridingProps.put(KafkaConfig.TransactionsTopicPartitionsProp, "1")
+ overridingProps.put(KafkaConfig.RequestTimeoutMsProp, "100")
+
+ override def generateConfigs = TestUtils.createBrokerConfigs(1, zkConnect, enableControlledShutdown = false).map {
+ KafkaConfig.fromProps(_, overridingProps)
+ }
+
+ @Test
+ def shouldCommitTransaction(): Unit = {
+ TestUtils.createTopic(zkUtils, Topic.TransactionStateTopicName, 1, 1, servers, servers.head.groupCoordinator.offsetsTopicConfigs)
+ val topic = "foo"
+ TestUtils.createTopic(this.zkUtils, topic, 1, 1, servers)
+
+ val tc = servers.head.transactionCoordinator
+
+ var initPidResult: InitPidResult = null
+ def callback(result: InitPidResult): Unit = {
+ initPidResult = result
+ }
+
+ val txnId = "txn"
+ tc.handleInitPid(txnId, 900000, callback)
+
+ @volatile var addPartitionErrors: Errors = null
+ def addPartitionsCallback(errors: Errors): Unit = {
+ addPartitionErrors = errors
+ }
+
+ tc.handleAddPartitionsToTransaction(txnId,
+ initPidResult.pid,
+ initPidResult.epoch,
+ Set[TopicPartition](new TopicPartition(topic, 0)),
+ addPartitionsCallback
+ )
+
+ while(addPartitionErrors == null) {
+ Utils.sleep(1)
+ }
+
+ Assert.assertEquals(Errors.NONE, addPartitionErrors)
+
+ /**
+ * TODO: Can't do this until the Broker side changes are done
+ @volatile var commitErrors: Errors = null
+ def commitCallback(errors: Errors): Unit ={
+ commitErrors = errors
+ }
+
+ tc.handleEndTransaction(txnId,
+ initPidResult.pid,
+ initPidResult.epoch,
+ TransactionResult.COMMIT,
+ commitCallback)
+
+ while(commitErrors == null) {
+ Utils.sleep(1)
+ }
+
+ Assert.assertEquals(Errors.NONE, commitErrors)
+ */
+ }
+}
http://git-wip-us.apache.org/repos/asf/kafka/blob/f69d9415/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorTest.scala
----------------------------------------------------------------------
diff --git a/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorTest.scala b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorTest.scala
new file mode 100644
index 0000000..4d8d1c9
--- /dev/null
+++ b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionCoordinatorTest.scala
@@ -0,0 +1,787 @@
+/*
+ * 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.server.DelayedOperationPurgatory
+import kafka.utils.timer.MockTimer
+import org.apache.kafka.common.TopicPartition
+import org.apache.kafka.common.protocol.Errors
+import org.apache.kafka.common.requests.TransactionResult
+import org.apache.kafka.common.utils.MockTime
+import org.easymock.{Capture, EasyMock, IAnswer}
+import org.junit.Assert._
+import org.junit.Test
+
+import scala.collection.mutable
+
+class TransactionCoordinatorTest {
+
+ val time = new MockTime()
+
+ var nextPid: Long = 0L
+ val pidManager: 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()
+ val capturedErrorsCallback: Capture[Errors => Unit] = EasyMock.newCapture()
+ val brokerId = 0
+ private val transactionalId = "known"
+ private val pid = 10
+ private val epoch:Short = 1
+ private val txnTimeoutMs = 1
+
+ private val txnMarkerPurgatory = new DelayedOperationPurgatory[DelayedTxnMarker]("test", new MockTimer, reaperEnabled = false)
+ private val partitions = mutable.Set[TopicPartition](new TopicPartition("topic1", 0))
+
+ val coordinator: TransactionCoordinator = new TransactionCoordinator(brokerId,
+ pidManager,
+ transactionManager,
+ transactionMarkerChannelManager,
+ txnMarkerPurgatory,
+ time)
+
+ var result: InitPidResult = _
+ var error: Errors = Errors.NONE
+
+ private def mockPidManager(): Unit = {
+ EasyMock.expect(pidManager.nextPid())
+ .andAnswer(new IAnswer[Long] {
+ override def answer(): Long = {
+ nextPid += 1
+ nextPid - 1
+ }
+ })
+ .anyTimes()
+ }
+
+ private def initPidGenericMocks(transactionalId: String): Unit = {
+ mockPidManager()
+ EasyMock.expect(transactionManager.isCoordinatorFor(EasyMock.eq(transactionalId)))
+ .andReturn(true)
+ .anyTimes()
+
+ EasyMock.expect(transactionManager.isCoordinatorLoadingInProgress(EasyMock.anyString()))
+ .andReturn(false)
+ .anyTimes()
+ EasyMock.expect(transactionManager.validateTransactionTimeoutMs(EasyMock.anyInt()))
+ .andReturn(true)
+ .anyTimes()
+ }
+
+
+ @Test
+ def shouldAcceptInitPidAndReturnNextPidWhenTransactionalIdIsEmpty(): Unit = {
+ mockPidManager()
+ EasyMock.replay(pidManager)
+
+ coordinator.handleInitPid("", txnTimeoutMs, initPidMockCallback)
+ assertEquals(InitPidResult(0L, 0, Errors.NONE), result)
+ coordinator.handleInitPid("", txnTimeoutMs, initPidMockCallback)
+ assertEquals(InitPidResult(1L, 0, Errors.NONE), result)
+ }
+
+ @Test
+ def shouldAcceptInitPidAndReturnNextPidWhenTransactionalIdIsNull(): Unit = {
+ mockPidManager()
+ EasyMock.replay(pidManager)
+
+ coordinator.handleInitPid(null, txnTimeoutMs, initPidMockCallback)
+ assertEquals(InitPidResult(0L, 0, Errors.NONE), result)
+ coordinator.handleInitPid(null, txnTimeoutMs, initPidMockCallback)
+ assertEquals(InitPidResult(1L, 0, Errors.NONE), result)
+ }
+
+ @Test
+ def shouldInitPidWithEpochZeroForNewTransactionalId(): Unit = {
+ initPidGenericMocks(transactionalId)
+ EasyMock.expect(transactionManager.addTransaction(EasyMock.eq(transactionalId), EasyMock.capture(capturedTxn)))
+ .andAnswer(new IAnswer[TransactionMetadata] {
+ override def answer(): TransactionMetadata = {
+ capturedTxn.getValue
+ }
+ })
+ .once()
+ EasyMock.expect(transactionManager.getTransactionState(EasyMock.eq(transactionalId)))
+ .andAnswer(new IAnswer[Option[TransactionMetadata]] {
+ override def answer(): Option[TransactionMetadata] = {
+ if (capturedTxn.hasCaptured) {
+ Some(capturedTxn.getValue)
+ } else {
+ None
+ }
+ }
+ })
+ .once()
+
+ EasyMock.expect(transactionManager.appendTransactionToLog(
+ EasyMock.eq(transactionalId),
+ EasyMock.capture(capturedTxn),
+ EasyMock.capture(capturedErrorsCallback)))
+ .andAnswer(new IAnswer[Unit] {
+ override def answer(): Unit = {
+ capturedErrorsCallback.getValue.apply(Errors.NONE)
+ }
+ })
+ .anyTimes()
+ EasyMock.replay(pidManager, transactionManager)
+
+ coordinator.handleInitPid(transactionalId, txnTimeoutMs, initPidMockCallback)
+ assertEquals(InitPidResult(0L, 0, Errors.NONE), result)
+ }
+
+ @Test
+ def shouldRespondWithNotCoordinatorOnInitPidWhenNotCoordinatorForId(): Unit = {
+ mockPidManager()
+ EasyMock.replay(pidManager)
+ coordinator.handleInitPid("some-pid", txnTimeoutMs, initPidMockCallback)
+ assertEquals(InitPidResult(-1, -1, Errors.NOT_COORDINATOR), result)
+ }
+
+ @Test
+ def shouldRespondWithInvalidPidMappingOnAddPartitionsToTransactionWhenTransactionalIdNotPresent(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+
+ EasyMock.expect(transactionManager.getTransactionState(EasyMock.eq(transactionalId)))
+ .andReturn(None)
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleAddPartitionsToTransaction(transactionalId, 0L, 1, partitions, errorsCallback)
+ assertEquals(Errors.INVALID_PID_MAPPING, error)
+ }
+
+ @Test
+ def shouldRespondWithInvalidRequestAddPartitionsToTransactionWhenTransactionalIdIsEmpty(): Unit = {
+ coordinator.handleAddPartitionsToTransaction("", 0L, 1, partitions, errorsCallback)
+ assertEquals(Errors.INVALID_REQUEST, error)
+ }
+
+ @Test
+ def shouldRespondWithInvalidRequestAddPartitionsToTransactionWhenTransactionalIdIsNull(): Unit = {
+ coordinator.handleAddPartitionsToTransaction(null, 0L, 1, partitions, errorsCallback)
+ assertEquals(Errors.INVALID_REQUEST, error)
+ }
+
+ @Test
+ def shouldRespondWithNotCoordinatorOnAddPartitionsWhenNotCoordinator(): Unit = {
+ coordinator.handleAddPartitionsToTransaction("txn", 0L, 1, partitions, errorsCallback)
+ assertEquals(Errors.NOT_COORDINATOR, error)
+ }
+
+ @Test
+ def shouldRespondWithCoordinatorLoadInProgressOnAddPartitionsWhenCoordintorLoading(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.isCoordinatorLoadingInProgress(transactionalId))
+ .andReturn(true)
+
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleAddPartitionsToTransaction(transactionalId, 0L, 1, partitions, errorsCallback)
+ assertEquals(Errors.COORDINATOR_LOAD_IN_PROGRESS, error)
+ }
+
+ @Test
+ def shouldRespondWithInvalidTnxStateOnAddPartitionsWhenStateIsPrepareCommit(): Unit = {
+ validateInvalidTxnState(PrepareCommit)
+ }
+
+ @Test
+ def shouldRespondWithInvalidTnxStateOnAddPartitionsWhenStateIsPrepareAbort(): Unit = {
+ validateInvalidTxnState(PrepareAbort)
+ }
+
+ @Test
+ def shouldRespondWithInvalidTnxStateOnAddPartitionsWhenStateIsCompleteCommit(): Unit = {
+ validateInvalidTxnState(CompleteCommit)
+ }
+
+ @Test
+ def shouldRespondWithInvalidTnxStateOnAddPartitionsWhenStateIsCompleteAbort(): Unit = {
+ validateInvalidTxnState(CompleteAbort)
+ }
+
+ def validateInvalidTxnState(state: TransactionState): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(new TransactionMetadata(0, 0, 0, state, mutable.Set.empty, 0, 0)))
+
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleAddPartitionsToTransaction(transactionalId, 0L, 0, partitions, errorsCallback)
+ assertEquals(Errors.INVALID_TXN_STATE, error)
+ }
+
+ @Test
+ def shouldRespondWithInvalidTnxProduceEpochOnAddPartitionsWhenEpochsAreDifferent(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(new TransactionMetadata(0, 10, 0, PrepareCommit, mutable.Set.empty, 0, 0)))
+
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleAddPartitionsToTransaction(transactionalId, 0L, 0, partitions, errorsCallback)
+ assertEquals(Errors.INVALID_PRODUCER_EPOCH, error)
+ }
+
+ @Test
+ def shouldAppendNewMetadataToLogOnAddPartitionsWhenPartitionsAdded(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(new TransactionMetadata(0, 0, 0, Empty, mutable.Set.empty, 0, 0)))
+
+ EasyMock.expect(transactionManager.appendTransactionToLog(
+ EasyMock.eq(transactionalId),
+ EasyMock.eq(new TransactionMetadata(0, 0, 0, Ongoing, partitions, time.milliseconds(), time.milliseconds())),
+ EasyMock.capture(capturedErrorsCallback)
+ ))
+
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleAddPartitionsToTransaction(transactionalId, 0L, 0, partitions, errorsCallback)
+
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldRespondWithErrorsNoneOnAddPartitionWhenNoErrorsAndPartitionsTheSame(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(new TransactionMetadata(0, 0, 0, Empty, partitions, 0, 0)))
+
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleAddPartitionsToTransaction(transactionalId, 0L, 0, partitions, errorsCallback)
+ assertEquals(Errors.NONE, error)
+ EasyMock.verify(transactionManager)
+
+ }
+
+ @Test
+ def shouldReplyWithInvalidPidMappingOnEndTxnWhenTxnIdDoesntExist(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId)).andReturn(None)
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleEndTransaction(transactionalId, 0, 0, TransactionResult.COMMIT, errorsCallback)
+ assertEquals(Errors.INVALID_PID_MAPPING, error)
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldReplyWithInvalidPidMappingOnEndTxnWhenPidDosentMatchMapped(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(new TransactionMetadata(10, 0, 0, Ongoing, collection.mutable.Set.empty[TopicPartition], 0, time.milliseconds())))
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleEndTransaction(transactionalId, 0, 0, TransactionResult.COMMIT, errorsCallback)
+ assertEquals(Errors.INVALID_PID_MAPPING, error)
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldReplyWithProducerFencedOnEndTxnWhenEpochIsNotSameAsTransaction(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(new TransactionMetadata(pid, 1, 1, Ongoing, collection.mutable.Set.empty[TopicPartition], 0, time.milliseconds())))
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, 0, TransactionResult.COMMIT, errorsCallback)
+ assertEquals(Errors.INVALID_PRODUCER_EPOCH, error)
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldReturnOkOnEndTxnWhenStatusIsCompleteCommitAndResultIsCommit(): Unit ={
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(new TransactionMetadata(pid, 1, 1, CompleteCommit, collection.mutable.Set.empty[TopicPartition], 0, time.milliseconds())))
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, 1, TransactionResult.COMMIT, errorsCallback)
+ assertEquals(Errors.NONE, error)
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldReturnOkOnEndTxnWhenStatusIsCompleteAbortAndResultIsAbort(): Unit ={
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(new TransactionMetadata(pid, 1, 1, CompleteAbort, collection.mutable.Set.empty[TopicPartition], 0, time.milliseconds())))
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, 1, TransactionResult.ABORT, errorsCallback)
+ assertEquals(Errors.NONE, error)
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldReturnInvalidTxnRequestOnEndTxnRequestWhenStatusIsCompleteAbortAndResultIsNotAbort(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(new TransactionMetadata(pid, 1, 1, CompleteAbort, collection.mutable.Set.empty[TopicPartition], 0, time.milliseconds())))
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, 1, TransactionResult.COMMIT, errorsCallback)
+ assertEquals(Errors.INVALID_TXN_STATE, error)
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldReturnInvalidTxnRequestOnEndTxnRequestWhenStatusIsCompleteCommitAndResultIsNotCommit(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(new TransactionMetadata(pid, 1, 1, CompleteCommit, collection.mutable.Set.empty[TopicPartition], 0, time.milliseconds())))
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, 1, TransactionResult.ABORT, errorsCallback)
+ assertEquals(Errors.INVALID_TXN_STATE, error)
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldReturnInvalidTxnRequestOnEndTxnRequestWhenStatusIsPrepareCommit(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(new TransactionMetadata(pid, 1, 1, PrepareCommit, collection.mutable.Set.empty[TopicPartition], 0, time.milliseconds())))
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, 1, TransactionResult.COMMIT, errorsCallback)
+ assertEquals(Errors.INVALID_TXN_STATE, error)
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldReturnInvalidTxnRequestOnEndTxnRequestWhenStatusIsPrepareAbort(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(new TransactionMetadata(pid, 1, 1, PrepareAbort, collection.mutable.Set.empty[TopicPartition], 0, time.milliseconds())))
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, 1, TransactionResult.COMMIT, errorsCallback)
+ assertEquals(Errors.INVALID_TXN_STATE, error)
+ EasyMock.verify(transactionManager)
+ }
+
+
+ @Test
+ def shouldAppendPrepareCommitToLogOnEndTxnWhenStatusIsOngoingAndResultIsCommit(): Unit = {
+ mockPrepare(PrepareCommit)
+
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, epoch, TransactionResult.COMMIT, errorsCallback)
+
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldAppendPrepareAbortToLogOnEndTxnWhenStatusIsOngoingAndResultIsAbort(): Unit = {
+ mockPrepare(PrepareAbort)
+
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, epoch, TransactionResult.ABORT, errorsCallback)
+ EasyMock.verify(transactionManager)
+ }
+
+
+ @Test
+ def shouldAppendCompleteAbortToLogOnEndTxnWhenStatusIsOngoingAndResultIsAbort(): Unit = {
+ mockComplete(PrepareAbort)
+
+ EasyMock.replay(transactionManager, transactionMarkerChannelManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, epoch, TransactionResult.ABORT, errorsCallback)
+
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldAppendCompleteCommitToLogOnEndTxnWhenStatusIsOngoingAndResultIsCommit(): Unit = {
+ mockComplete(PrepareCommit)
+
+ EasyMock.replay(transactionManager, transactionMarkerChannelManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, epoch, TransactionResult.COMMIT, errorsCallback)
+
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldRespondWithInvalidRequestOnEndTxnWhenTransactionalIdIsNull(): Unit = {
+ coordinator.handleEndTransaction(null, 0, 0, TransactionResult.COMMIT, errorsCallback)
+ assertEquals(Errors.INVALID_REQUEST, error)
+ }
+
+ @Test
+ def shouldRespondWithInvalidRequestOnEndTxnWhenTransactionalIdIsEmpty(): Unit = {
+ coordinator.handleEndTransaction("", 0, 0, TransactionResult.COMMIT, errorsCallback)
+ assertEquals(Errors.INVALID_REQUEST, error)
+ }
+
+ @Test
+ def shouldRespondWithNotCoordinatorOnEndTxnWhenIsNotCoordinatorForId(): Unit = {
+ coordinator.handleEndTransaction("id", 0, 0, TransactionResult.COMMIT, errorsCallback)
+ assertEquals(Errors.NOT_COORDINATOR, error)
+ }
+
+ @Test
+ def shouldRespondWithCoordinatorLoadInProgressOnEndTxnWhenCoordinatorIsLoading(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(EasyMock.anyString()))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.isCoordinatorLoadingInProgress(EasyMock.anyString()))
+ .andReturn(true)
+
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleEndTransaction("id", 0, 0, TransactionResult.COMMIT, errorsCallback)
+ assertEquals(Errors.COORDINATOR_LOAD_IN_PROGRESS, error)
+ }
+
+ @Test
+ def shouldIncrementEpochAndUpdateMetadataOnHandleInitPidWhenExistingEmptyTransaction(): Unit = {
+ validateIncrementEpochAndUpdateMetadata(Empty)
+ }
+
+ @Test
+ def shouldIncrementEpochAndUpdateMetadataOnHandleInitPidWhenExistingCompleteTransaction(): Unit = {
+ validateIncrementEpochAndUpdateMetadata(CompleteAbort)
+ }
+
+ @Test
+ def shouldIncrementEpochAndUpdateMetadataOnHandleInitPidWhenExistingCompleteCommitTransaction(): Unit = {
+ validateIncrementEpochAndUpdateMetadata(CompleteCommit)
+ }
+
+ @Test
+ def shouldWaitForCommitToCompleteOnHandleInitPidAndExistingTransactionInPrepareCommitState(): Unit ={
+ validateWaitsForCompletionBeforeRespondingWithIncrementedEpoch(PrepareCommit)
+ }
+
+ @Test
+ def shouldWaitForCommitToCompleteOnHandleInitPidAndExistingTransactionInPrepareAbortState(): Unit ={
+ validateWaitsForCompletionBeforeRespondingWithIncrementedEpoch(PrepareAbort)
+ }
+
+ @Test
+ def shouldAbortTransactionOnHandleInitPidWhenExistingTransactionInOngoingState(): Unit = {
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.validateTransactionTimeoutMs(EasyMock.anyInt()))
+ .andReturn(true)
+
+ val metadata = new TransactionMetadata(pid, epoch, txnTimeoutMs, Ongoing, mutable.Set[TopicPartition](new TopicPartition("topic", 1)), 0, 0)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(metadata))
+ .once()
+
+ mockComplete(PrepareAbort)
+
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true).anyTimes()
+ EasyMock.expect(transactionManager.validateTransactionTimeoutMs(EasyMock.anyInt()))
+ .andReturn(true).anyTimes()
+
+ val completedMetadata = new TransactionMetadata(pid, epoch, txnTimeoutMs, CompleteAbort, mutable.Set.empty[TopicPartition], 0, 0)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(completedMetadata))
+ .anyTimes()
+
+ EasyMock.expect(transactionManager.appendTransactionToLog(
+ EasyMock.eq(transactionalId),
+ EasyMock.anyObject(classOf[TransactionMetadata]),
+ EasyMock.capture(capturedErrorsCallback)
+ )).andAnswer(new IAnswer[Unit] {
+ override def answer(): Unit = {
+ capturedErrorsCallback.getValue.apply(Errors.NONE)
+ }
+ })
+
+ EasyMock.replay(transactionManager, transactionMarkerChannelManager)
+
+ coordinator.handleInitPid(transactionalId, txnTimeoutMs, initPidMockCallback)
+
+ assertEquals(InitPidResult(10, 2, Errors.NONE), result)
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldRemoveTransactionsForPartitionOnEmigration(): Unit = {
+ EasyMock.expect(transactionManager.removeTransactionsForPartition(0))
+ EasyMock.expect(transactionMarkerChannelManager.removeStateForPartition(0))
+ EasyMock.replay(transactionManager, transactionMarkerChannelManager)
+
+ coordinator.handleTxnEmigration(0)
+
+ EasyMock.verify(transactionManager, transactionMarkerChannelManager)
+ }
+
+ @Test
+ def shouldRetryOnCommitWhenTxnMarkerRequestFailsWithErrorOtherThanNotCoordinator(): Unit = {
+ val prepareMetadata = mockPrepare(PrepareCommit, runCallback = true)
+
+ EasyMock.expect(transactionManager.coordinatorEpochFor(transactionalId))
+ .andReturn(Some(0))
+
+ EasyMock.expect(transactionMarkerChannelManager.addTxnMarkerRequest(
+ EasyMock.eq(0),
+ EasyMock.anyObject(),
+ EasyMock.anyInt(),
+ EasyMock.capture(capturedErrorsCallback)
+ )).andAnswer(new IAnswer[Unit] {
+ override def answer(): Unit = {
+ capturedErrorsCallback.getValue.apply(Errors.NETWORK_EXCEPTION)
+ }
+ }).andAnswer(new IAnswer[Unit] {
+ override def answer(): Unit = {
+ capturedErrorsCallback.getValue.apply(Errors.NONE)
+ }
+ })
+
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(prepareMetadata))
+ .once()
+
+ EasyMock.replay(transactionManager, transactionMarkerChannelManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, epoch, TransactionResult.COMMIT, errorsCallback)
+
+ EasyMock.verify(transactionMarkerChannelManager)
+ }
+
+ @Test
+ def shouldNotRetryOnCommitWhenTxnMarkerRequestFailsWithNotCoordinator(): Unit = {
+ val prepareMetadata = mockPrepare(PrepareCommit, runCallback = true)
+
+ EasyMock.expect(transactionManager.coordinatorEpochFor(transactionalId))
+ .andReturn(Some(0))
+
+ EasyMock.expect(transactionMarkerChannelManager.addTxnMarkerRequest(
+ EasyMock.eq(0),
+ EasyMock.anyObject(),
+ EasyMock.anyInt(),
+ EasyMock.capture(capturedErrorsCallback)
+ )).andAnswer(new IAnswer[Unit] {
+ override def answer(): Unit = {
+ capturedErrorsCallback.getValue.apply(Errors.NOT_COORDINATOR)
+ }
+ })
+
+ EasyMock.replay(transactionManager, transactionMarkerChannelManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, epoch, TransactionResult.COMMIT, errorsCallback)
+
+ EasyMock.verify(transactionMarkerChannelManager)
+ }
+
+ @Test
+ def shouldNotRetryOnCommitWhenAppendToLogFailsWithNotCoordinator(): Unit = {
+ mockComplete(PrepareCommit, Errors.NOT_COORDINATOR)
+ EasyMock.replay(transactionManager, transactionMarkerChannelManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, epoch, TransactionResult.COMMIT, errorsCallback)
+
+ EasyMock.verify(transactionManager)
+ }
+
+ @Test
+ def shouldRetryOnCommitWhenAppendToLogFailsErrorsOtherThanNotCoordinator(): Unit = {
+ mockComplete(PrepareCommit, Errors.ILLEGAL_GENERATION)
+ EasyMock.replay(transactionManager, transactionMarkerChannelManager)
+
+ coordinator.handleEndTransaction(transactionalId, pid, epoch, TransactionResult.COMMIT, errorsCallback)
+
+ EasyMock.verify(transactionManager)
+ }
+
+ private def validateWaitsForCompletionBeforeRespondingWithIncrementedEpoch(state: TransactionState) = {
+ val transactionId = "tid"
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionId))
+ .andReturn(true).anyTimes()
+ EasyMock.expect(transactionManager.validateTransactionTimeoutMs(EasyMock.anyInt()))
+ .andReturn(true).anyTimes()
+
+ val metadata = new TransactionMetadata(0, 0, 0, state, mutable.Set[TopicPartition](new TopicPartition("topic", 1)), 0, 0)
+ EasyMock.expect(transactionManager.getTransactionState(transactionId))
+ .andReturn(Some(metadata)).anyTimes()
+
+ EasyMock.expect(transactionManager.appendTransactionToLog(
+ EasyMock.eq(transactionId),
+ EasyMock.anyObject(classOf[TransactionMetadata]),
+ EasyMock.capture(capturedErrorsCallback)
+ )).andAnswer(new IAnswer[Unit] {
+ override def answer(): Unit = {
+ capturedErrorsCallback.getValue.apply(Errors.NONE)
+ }
+ })
+
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleInitPid(transactionId, 10, initPidMockCallback)
+ // no result yet as hasn't completed
+ assertNull(result)
+ // complete the transaction
+ metadata.topicPartitions.clear()
+ metadata.state = if (state == PrepareCommit) CompleteCommit else CompleteAbort
+ txnMarkerPurgatory.checkAndComplete(0L)
+
+ assertEquals(InitPidResult(0, 1, Errors.NONE), result)
+ assertEquals(new TransactionMetadata(0, 1, 10, Empty, mutable.Set.empty, 0, time.milliseconds()), metadata)
+ }
+
+ private def validateIncrementEpochAndUpdateMetadata(state: TransactionState) = {
+ val transactionId = "tid"
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.validateTransactionTimeoutMs(EasyMock.anyInt()))
+ .andReturn(true)
+
+ val metadata = new TransactionMetadata(0, 0, 0, state, mutable.Set.empty[TopicPartition], 0, 0)
+ EasyMock.expect(transactionManager.getTransactionState(transactionId))
+ .andReturn(Some(metadata))
+
+ EasyMock.expect(transactionManager.appendTransactionToLog(
+ EasyMock.eq(transactionId),
+ EasyMock.anyObject(classOf[TransactionMetadata]),
+ EasyMock.capture(capturedErrorsCallback)
+ )).andAnswer(new IAnswer[Unit] {
+ override def answer(): Unit = {
+ capturedErrorsCallback.getValue.apply(Errors.NONE)
+ }
+ })
+
+ EasyMock.replay(transactionManager)
+
+ coordinator.handleInitPid(transactionId, 10, initPidMockCallback)
+
+ assertEquals(InitPidResult(0, 1, Errors.NONE), result)
+ assertEquals(10, metadata.txnTimeoutMs)
+ assertEquals(time.milliseconds(), metadata.lastUpdateTimestamp)
+ assertEquals(1, metadata.producerEpoch)
+ assertEquals(0, metadata.pid)
+ }
+
+ private def mockPrepare(transactionState: TransactionState, runCallback: Boolean = false) = {
+ val originalMetadata = new TransactionMetadata(pid,
+ epoch,
+ txnTimeoutMs,
+ Ongoing,
+ collection.mutable.Set.empty[TopicPartition],
+ 0,
+ time.milliseconds())
+
+ val prepareCommitMetadata = new TransactionMetadata(pid,
+ epoch,
+ txnTimeoutMs,
+ transactionState,
+ collection.mutable.Set.empty[TopicPartition],
+ 0,
+ time.milliseconds())
+
+ EasyMock.expect(transactionManager.isCoordinatorFor(transactionalId))
+ .andReturn(true)
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(originalMetadata))
+ .once()
+
+ EasyMock.expect(transactionManager.appendTransactionToLog(EasyMock.eq(transactionalId),
+ EasyMock.eq(prepareCommitMetadata),
+ EasyMock.capture(capturedErrorsCallback)))
+ .andAnswer(new IAnswer[Unit] {
+ override def answer(): Unit = {
+ if (runCallback) capturedErrorsCallback.getValue.apply(Errors.NONE)
+ }
+ }).once()
+ prepareCommitMetadata
+ }
+
+ private def mockComplete(transactionState: TransactionState, appendError: Errors = Errors.NONE) = {
+
+
+ val prepareMetadata: TransactionMetadata = mockPrepare(transactionState, true)
+ val finalState = if (transactionState == PrepareAbort) CompleteAbort else CompleteCommit
+
+ EasyMock.expect(transactionManager.coordinatorEpochFor(transactionalId))
+ .andReturn(Some(0))
+
+ EasyMock.expect(transactionMarkerChannelManager.addTxnMarkerRequest(
+ EasyMock.eq(0),
+ EasyMock.anyObject(),
+ EasyMock.anyInt(),
+ EasyMock.capture(capturedErrorsCallback)
+ )).andAnswer(new IAnswer[Unit] {
+ override def answer(): Unit = {
+ capturedErrorsCallback.getValue.apply(Errors.NONE)
+ }
+ })
+
+ EasyMock.expect(transactionManager.getTransactionState(transactionalId))
+ .andReturn(Some(prepareMetadata))
+ .once()
+
+ val completedMetadata = new TransactionMetadata(pid,
+ epoch,
+ txnTimeoutMs,
+ finalState,
+ prepareMetadata.topicPartitions,
+ prepareMetadata.transactionStartTime,
+ prepareMetadata.lastUpdateTimestamp)
+
+ val firstAnswer = EasyMock.expect(transactionManager.appendTransactionToLog(EasyMock.eq(transactionalId),
+ EasyMock.eq(completedMetadata),
+ EasyMock.capture(capturedErrorsCallback)))
+ .andAnswer(new IAnswer[Unit] {
+ override def answer(): Unit = {
+ capturedErrorsCallback.getValue.apply(appendError)
+ }
+ })
+
+ if(appendError != Errors.NONE && appendError != Errors.NOT_COORDINATOR) {
+ firstAnswer.andAnswer(new IAnswer[Unit] {
+ override def answer(): Unit = {
+ capturedErrorsCallback.getValue.apply(Errors.NONE)
+ }
+ })
+ }
+
+
+ }
+
+
+ def initPidMockCallback(ret: InitPidResult): Unit = {
+ result = ret
+ }
+
+ def errorsCallback(ret: Errors): Unit = {
+ error = ret
+ }
+}
http://git-wip-us.apache.org/repos/asf/kafka/blob/f69d9415/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionLogTest.scala
----------------------------------------------------------------------
diff --git a/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionLogTest.scala b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionLogTest.scala
new file mode 100644
index 0000000..cfb4a99
--- /dev/null
+++ b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionLogTest.scala
@@ -0,0 +1,109 @@
+/*
+ * 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 org.apache.kafka.common.TopicPartition
+import org.apache.kafka.common.record.{CompressionType, SimpleRecord, MemoryRecords}
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.scalatest.junit.JUnitSuite
+
+import scala.collection.JavaConverters._
+
+class TransactionLogTest extends JUnitSuite {
+
+ val epoch: Short = 0
+ val transactionTimeoutMs: Int = 1000
+
+ val topicPartitions: Set[TopicPartition] = Set[TopicPartition](new TopicPartition("topic1", 0),
+ new TopicPartition("topic1", 1),
+ new TopicPartition("topic2", 0),
+ new TopicPartition("topic2", 1),
+ new TopicPartition("topic2", 2))
+
+ @Test
+ def shouldThrowExceptionWriteInvalidTxn() {
+ val txnMetadata = TransactionMetadata(0L, epoch, transactionTimeoutMs, 0)
+ txnMetadata.addPartitions(topicPartitions)
+
+ intercept[IllegalStateException] {
+ TransactionLog.valueToBytes(txnMetadata)
+ }
+ }
+
+ @Test
+ def shouldReadWriteMessages() {
+ val pidMappings = Map[String, Long]("zero" -> 0L,
+ "one" -> 1L,
+ "two" -> 2L,
+ "three" -> 3L,
+ "four" -> 4L,
+ "five" -> 5L)
+
+ val transactionStates = Map[Long, TransactionState](0L -> Empty,
+ 1L -> Ongoing,
+ 2L -> PrepareCommit,
+ 3L -> CompleteCommit,
+ 4L -> PrepareAbort,
+ 5L -> CompleteAbort)
+
+ // generate transaction log messages
+ val txnRecords = pidMappings.map { case (transactionalId, pid) =>
+ val txnMetadata = TransactionMetadata(pid, epoch, transactionTimeoutMs, transactionStates(pid), 0)
+
+ if (!txnMetadata.state.equals(Empty))
+ txnMetadata.addPartitions(topicPartitions)
+
+ val keyBytes = TransactionLog.keyToBytes(transactionalId)
+ val valueBytes = TransactionLog.valueToBytes(txnMetadata)
+
+ new SimpleRecord(keyBytes, valueBytes)
+ }.toSeq
+
+ val records = MemoryRecords.withRecords(0, CompressionType.NONE, txnRecords: _*)
+
+ var count = 0
+ for (record <- records.records.asScala) {
+ val key = TransactionLog.readMessageKey(record.key())
+
+ key match {
+ case pidKey: TxnKey =>
+ val transactionalId = pidKey.transactionalId
+ val txnMetadata = TransactionLog.readMessageValue(record.value())
+
+ assertEquals(pidMappings(transactionalId), txnMetadata.pid)
+ assertEquals(epoch, txnMetadata.producerEpoch)
+ assertEquals(transactionTimeoutMs, txnMetadata.txnTimeoutMs)
+ assertEquals(transactionStates(txnMetadata.pid), txnMetadata.state)
+
+ if (txnMetadata.state.equals(Empty))
+ assertEquals(Set.empty[TopicPartition], txnMetadata.topicPartitions)
+ else
+ assertEquals(topicPartitions, txnMetadata.topicPartitions)
+
+ count = count + 1
+
+ case _ => fail(s"Unexpected transaction topic message key $key")
+ }
+ }
+
+ assertEquals(pidMappings.size, count)
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/kafka/blob/f69d9415/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionMarkerChannelManagerTest.scala
----------------------------------------------------------------------
diff --git a/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionMarkerChannelManagerTest.scala b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionMarkerChannelManagerTest.scala
new file mode 100644
index 0000000..352daa2
--- /dev/null
+++ b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionMarkerChannelManagerTest.scala
@@ -0,0 +1,283 @@
+/*
+ * 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.api.{LeaderAndIsr, PartitionStateInfo}
+import kafka.common.{BrokerEndPointNotAvailableException, BrokerNotAvailableException, InterBrokerSendThread}
+import kafka.controller.LeaderIsrAndControllerEpoch
+import kafka.server.{DelayedOperationPurgatory, KafkaConfig, MetadataCache}
+import kafka.utils.{MockTime, TestUtils}
+import kafka.utils.timer.MockTimer
+import org.apache.kafka.clients.NetworkClient
+import org.apache.kafka.common.network.ListenerName
+import org.apache.kafka.common.protocol.{Errors, SecurityProtocol}
+import org.apache.kafka.common.requests.{TransactionResult, WriteTxnMarkersRequest}
+import org.apache.kafka.common.utils.Utils
+import org.apache.kafka.common.{Node, TopicPartition}
+import org.easymock.EasyMock
+import org.junit.Assert._
+import org.junit.Test
+
+import scala.collection.mutable
+
+class TransactionMarkerChannelManagerTest {
+ private val metadataCache = EasyMock.createNiceMock(classOf[MetadataCache])
+ private val interBrokerSendThread = EasyMock.createNiceMock(classOf[InterBrokerSendThread])
+ private val networkClient = EasyMock.createNiceMock(classOf[NetworkClient])
+ private val channel = new TransactionMarkerChannel(ListenerName.forSecurityProtocol(SecurityProtocol.PLAINTEXT),
+ metadataCache,
+ networkClient,
+ new MockTime())
+ private val purgatory = new DelayedOperationPurgatory[DelayedTxnMarker]("txn-purgatory-name",
+ new MockTimer,
+ reaperEnabled = false)
+ private val requestGenerator = TransactionMarkerChannelManager.requestGenerator(channel, purgatory)
+ private val partition1 = new TopicPartition("topic1", 0)
+ private val partition2 = new TopicPartition("topic1", 1)
+ private val broker1 = new Node(1, "host", 10)
+ private val broker2 = new Node(2, "otherhost", 10)
+ private val metadataPartition = 0
+ private val channelManager = new TransactionMarkerChannelManager(
+ KafkaConfig.fromProps(TestUtils.createBrokerConfig(1, "localhost:2181")),
+ metadataCache,
+ purgatory,
+ interBrokerSendThread,
+ channel)
+
+ @Test
+ def shouldGenerateEmptyMapWhenNoRequestsOutstanding(): Unit = {
+ assertTrue(requestGenerator().isEmpty)
+ }
+
+ @Test
+ def shouldGenerateRequestPerBroker(): Unit ={
+ EasyMock.expect(metadataCache.getPartitionInfo(partition1.topic(), partition1.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, 0, List.empty, 0), 0), Set.empty)))
+
+ EasyMock.expect(metadataCache.getPartitionInfo(partition2.topic(), partition2.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(2, 0, List.empty, 0), 0), Set.empty)))
+
+ EasyMock.expect(metadataCache.getAliveEndpoint(EasyMock.eq(1), EasyMock.anyObject())).andReturn(Some(broker1))
+ EasyMock.expect(metadataCache.getAliveEndpoint(EasyMock.eq(2), EasyMock.anyObject())).andReturn(Some(broker2))
+
+ EasyMock.replay(metadataCache)
+
+ channel.addRequestToSend(0, 0, 0, TransactionResult.COMMIT, 0, Set[TopicPartition](partition1, partition2))
+
+
+ val expectedBroker1Request = new WriteTxnMarkersRequest.Builder(0,
+ Utils.mkList(new WriteTxnMarkersRequest.TxnMarkerEntry(0, 0, TransactionResult.COMMIT, Utils.mkList(partition1)))).build()
+ val expectedBroker2Request = new WriteTxnMarkersRequest.Builder(0,
+ Utils.mkList(new WriteTxnMarkersRequest.TxnMarkerEntry(0, 0, TransactionResult.COMMIT, Utils.mkList(partition2)))).build()
+
+ val requests: Map[Node, WriteTxnMarkersRequest] = requestGenerator().map{ result =>
+ (result.destination, result.request.asInstanceOf[WriteTxnMarkersRequest.Builder].build())
+ }.toMap
+
+ val broker1Request = requests(broker1)
+ val broker2Request = requests(broker2)
+
+ assertEquals(2, requests.size)
+ assertEquals(expectedBroker1Request, broker1Request)
+ assertEquals(expectedBroker2Request, broker2Request)
+
+ }
+
+ @Test
+ def shouldGenerateRequestPerPartitionPerBroker(): Unit ={
+ val partitionOneEpoch = 0
+ val partitionTwoEpoch = 1
+
+ EasyMock.expect(metadataCache.getPartitionInfo(partition1.topic(), partition1.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, partitionOneEpoch, List.empty, 0), 0), Set.empty)))
+
+
+ EasyMock.expect(metadataCache.getPartitionInfo(partition2.topic(), partition2.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, partitionTwoEpoch, List.empty, 0), 0), Set.empty)))
+ EasyMock.expect(metadataCache.getAliveEndpoint(EasyMock.eq(1), EasyMock.anyObject())).andReturn(Some(broker1)).anyTimes()
+ EasyMock.replay(metadataCache)
+
+ channel.addRequestToSend(0, 0, 0, TransactionResult.COMMIT, partitionOneEpoch, Set[TopicPartition](partition1))
+ channel.addRequestToSend(0, 0, 0, TransactionResult.COMMIT, partitionTwoEpoch, Set[TopicPartition](partition2))
+
+ val expectedPartition1Request = new WriteTxnMarkersRequest.Builder(0,
+ Utils.mkList(new WriteTxnMarkersRequest.TxnMarkerEntry(0, 0, TransactionResult.COMMIT, Utils.mkList(partition1)))).build()
+ val expectedPartition2Request = new WriteTxnMarkersRequest.Builder(1,
+ Utils.mkList(new WriteTxnMarkersRequest.TxnMarkerEntry(0, 0, TransactionResult.COMMIT, Utils.mkList(partition2)))).build()
+
+ val requests = requestGenerator().map { result =>
+ val markersRequest = result.request.asInstanceOf[WriteTxnMarkersRequest.Builder].build()
+ (markersRequest.coordinatorEpoch(), (result.destination, markersRequest))
+ }.toMap
+
+ val request1 = requests(partitionOneEpoch)
+ val request2 = requests(partitionTwoEpoch)
+ assertEquals(broker1, request1._1)
+ assertEquals(broker1, request2._1)
+ assertEquals(2, requests.size)
+ assertEquals(expectedPartition1Request, request1._2)
+ assertEquals(expectedPartition2Request, request2._2)
+ }
+
+ @Test
+ def shouldDrainBrokerQueueWhenGeneratingRequests(): Unit = {
+ EasyMock.expect(metadataCache.getPartitionInfo(partition1.topic(), partition1.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, 0, List.empty, 0), 0), Set.empty)))
+ EasyMock.expect(metadataCache.getAliveEndpoint(EasyMock.eq(1), EasyMock.anyObject())).andReturn(Some(broker1))
+ EasyMock.replay(metadataCache)
+
+ channel.addRequestToSend(0, 0, 0, TransactionResult.COMMIT, 0, Set[TopicPartition](partition1))
+
+ assertTrue(requestGenerator().nonEmpty)
+ assertTrue(requestGenerator().isEmpty)
+ }
+
+ @Test
+ def shouldRetryGettingLeaderWhenNotFound(): Unit = {
+ EasyMock.expect(metadataCache.getPartitionInfo(partition1.topic(), partition1.partition()))
+ .andReturn(None)
+ .andReturn(None)
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, 0, List.empty, 0), 0), Set.empty)))
+
+ EasyMock.expect(metadataCache.getAliveEndpoint(EasyMock.eq(1), EasyMock.anyObject())).andReturn(Some(broker1))
+ EasyMock.replay(metadataCache)
+
+ channel.addRequestToSend(0, 0, 0, TransactionResult.COMMIT, 0, Set[TopicPartition](partition1))
+
+ EasyMock.verify(metadataCache)
+ }
+
+ @Test
+ def shouldRetryGettingLeaderWhenBrokerEndPointNotAvailableException(): Unit = {
+ EasyMock.expect(metadataCache.getPartitionInfo(partition1.topic(), partition1.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, 0, List.empty, 0), 0), Set.empty)))
+ .times(2)
+ EasyMock.expect(metadataCache.getAliveEndpoint(EasyMock.eq(1), EasyMock.anyObject()))
+ .andThrow(new BrokerEndPointNotAvailableException())
+ .andReturn(Some(broker1))
+ EasyMock.replay(metadataCache)
+
+ channel.addRequestToSend(0, 0, 0, TransactionResult.COMMIT, 0, Set[TopicPartition](partition1))
+
+ EasyMock.verify(metadataCache)
+ }
+
+ @Test
+ def shouldRetryGettingLeaderWhenLeaderDoesntExist(): Unit = {
+ EasyMock.expect(metadataCache.getPartitionInfo(partition1.topic(), partition1.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, 0, List.empty, 0), 0), Set.empty)))
+ .times(2)
+
+ EasyMock.expect(metadataCache.getAliveEndpoint(EasyMock.eq(1), EasyMock.anyObject()))
+ .andReturn(None)
+ .andReturn(Some(broker1))
+
+ EasyMock.replay(metadataCache)
+
+ channel.addRequestToSend(0, 0, 0, TransactionResult.COMMIT, 0, Set[TopicPartition](partition1))
+
+ EasyMock.verify(metadataCache)
+ }
+
+ @Test
+ def shouldAddPendingTxnRequest(): Unit = {
+ val metadata = new TransactionMetadata(1, 0, 0, PrepareCommit, mutable.Set[TopicPartition](partition1, partition2), 0, 0L)
+ EasyMock.expect(metadataCache.getPartitionInfo(partition1.topic(), partition1.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, 0, List.empty, 0), 0), Set.empty)))
+ EasyMock.expect(metadataCache.getPartitionInfo(partition2.topic(), partition2.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(2, 0, List.empty, 0), 0), Set.empty)))
+ EasyMock.expect(metadataCache.getAliveEndpoint(EasyMock.eq(1), EasyMock.anyObject())).andReturn(Some(broker1))
+ EasyMock.expect(metadataCache.getAliveEndpoint(EasyMock.eq(2), EasyMock.anyObject())).andReturn(Some(broker2))
+
+ EasyMock.replay(metadataCache)
+
+ channelManager.addTxnMarkerRequest(metadataPartition, metadata, 0, completionCallback)
+
+ assertEquals(Some(metadata), channel.pendingTxnMetadata(metadataPartition, 1))
+
+ }
+
+ @Test
+ def shouldAddRequestToBrokerQueue(): Unit = {
+ val metadata = new TransactionMetadata(1, 0, 0, PrepareCommit, mutable.Set[TopicPartition](partition1), 0, 0L)
+
+ EasyMock.expect(metadataCache.getPartitionInfo(partition1.topic(), partition1.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, 0, List.empty, 0), 0), Set.empty)))
+ EasyMock.expect(metadataCache.getAliveEndpoint(EasyMock.eq(1), EasyMock.anyObject())).andReturn(Some(broker1))
+ EasyMock.replay(metadataCache)
+
+ channelManager.addTxnMarkerRequest(metadataPartition, metadata, 0, completionCallback)
+ assertEquals(1, requestGenerator().size)
+ }
+
+ @Test
+ def shouldAddDelayedTxnMarkerToPurgatory(): Unit = {
+ val metadata = new TransactionMetadata(1, 0, 0, PrepareCommit, mutable.Set[TopicPartition](partition1), 0, 0L)
+ EasyMock.expect(metadataCache.getPartitionInfo(partition1.topic(), partition1.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, 0, List.empty, 0), 0), Set.empty)))
+ EasyMock.expect(metadataCache.getAliveEndpoint(EasyMock.eq(1), EasyMock.anyObject())).andReturn(Some(broker1))
+
+ EasyMock.replay(metadataCache)
+
+ channelManager.addTxnMarkerRequest(metadataPartition, metadata, 0, completionCallback)
+ assertEquals(1,purgatory.watched)
+ }
+
+ @Test
+ def shouldStartInterBrokerThreadOnStartup(): Unit = {
+ EasyMock.expect(interBrokerSendThread.start())
+ EasyMock.replay(interBrokerSendThread)
+ channelManager.start()
+ EasyMock.verify(interBrokerSendThread)
+ }
+
+
+ @Test
+ def shouldStopInterBrokerThreadOnShutdown(): Unit = {
+ EasyMock.expect(interBrokerSendThread.shutdown())
+ EasyMock.replay(interBrokerSendThread)
+ channelManager.shutdown()
+ EasyMock.verify(interBrokerSendThread)
+ }
+
+ @Test
+ def shouldClearPurgatoryForPartitionWhenPartitionEmigrated(): Unit = {
+ val metadata1 = new TransactionMetadata(0, 0, 0, PrepareCommit, mutable.Set[TopicPartition](partition1), 0, 0)
+ purgatory.tryCompleteElseWatch(new DelayedTxnMarker(metadata1, (error:Errors) => {}),Seq(0L))
+ channel.maybeAddPendingRequest(0, metadata1)
+
+ val metadata2 = new TransactionMetadata(1, 0, 0, PrepareCommit, mutable.Set[TopicPartition](partition1), 0, 0)
+ purgatory.tryCompleteElseWatch(new DelayedTxnMarker(metadata2, (error:Errors) => {}),Seq(1L))
+ channel.maybeAddPendingRequest(0, metadata2)
+
+ val metadata3 = new TransactionMetadata(2, 0, 0, PrepareCommit, mutable.Set[TopicPartition](partition1), 0, 0)
+ purgatory.tryCompleteElseWatch(new DelayedTxnMarker(metadata3, (error:Errors) => {}),Seq(2L))
+ channel.maybeAddPendingRequest(1, metadata3)
+
+ channelManager.removeStateForPartition(0)
+
+ assertEquals(1, purgatory.watched)
+ // should not complete as they've been removed
+ purgatory.checkAndComplete(0L)
+ purgatory.checkAndComplete(1L)
+
+ assertEquals(1, purgatory.watched)
+ }
+
+ def completionCallback(errors: Errors): Unit = {
+ }
+}
http://git-wip-us.apache.org/repos/asf/kafka/blob/f69d9415/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionMarkerChannelTest.scala
----------------------------------------------------------------------
diff --git a/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionMarkerChannelTest.scala b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionMarkerChannelTest.scala
new file mode 100644
index 0000000..6bfeb9b
--- /dev/null
+++ b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionMarkerChannelTest.scala
@@ -0,0 +1,179 @@
+/*
+ * 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.api.{LeaderAndIsr, PartitionStateInfo}
+import kafka.controller.LeaderIsrAndControllerEpoch
+import kafka.server.{DelayedOperationPurgatory, MetadataCache}
+import kafka.utils.MockTime
+import kafka.utils.timer.MockTimer
+import org.apache.kafka.clients.NetworkClient
+import org.apache.kafka.common.network.ListenerName
+import org.apache.kafka.common.protocol.{Errors, SecurityProtocol}
+import org.apache.kafka.common.requests.{TransactionResult, WriteTxnMarkersRequest}
+import org.apache.kafka.common.utils.Utils
+import org.apache.kafka.common.{Node, TopicPartition}
+import org.easymock.EasyMock
+import org.junit.Assert._
+import org.junit.Test
+
+import scala.collection.mutable
+
+class TransactionMarkerChannelTest {
+
+ private val metadataCache = EasyMock.createNiceMock(classOf[MetadataCache])
+ private val networkClient = EasyMock.createNiceMock(classOf[NetworkClient])
+ private val purgatory = new DelayedOperationPurgatory[DelayedTxnMarker]("name", new MockTimer, reaperEnabled = false)
+ private val listenerName = ListenerName.forSecurityProtocol(SecurityProtocol.PLAINTEXT)
+ private val channel = new TransactionMarkerChannel(listenerName, metadataCache, networkClient, new MockTime())
+ private val partition1 = new TopicPartition("topic1", 0)
+
+
+ @Test
+ def shouldAddEmptyBrokerQueueWhenAddingNewBroker(): Unit = {
+ channel.addOrUpdateBroker(new Node(1, "host", 10))
+ channel.addOrUpdateBroker(new Node(2, "host", 10))
+ assertEquals(0, channel.queueForBroker(1).get.markersQueue.size())
+ assertEquals(0, channel.queueForBroker(2).get.markersQueue.size())
+ }
+
+ @Test
+ def shouldUpdateDestinationBrokerNodeWhenUpdatingBroker(): Unit = {
+ val newDestination = new Node(1, "otherhost", 100)
+
+ EasyMock.expect(metadataCache.getPartitionInfo(partition1.topic(), partition1.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, 0, List.empty, 0), 0), Set.empty)))
+
+ // getAliveEndpoint returns an updated node
+ EasyMock.expect(metadataCache.getAliveEndpoint(1, listenerName)).andReturn(Some(newDestination))
+ EasyMock.replay(metadataCache)
+
+ channel.addOrUpdateBroker(new Node(1, "host", 10))
+ channel.addRequestToSend(0, 0, 0, TransactionResult.COMMIT, 0, Set[TopicPartition](partition1))
+
+ val destinationAndQueue = channel.queueForBroker(1).get
+ assertEquals(newDestination, destinationAndQueue.destBrokerNode)
+ assertEquals(1, destinationAndQueue.markersQueue.size())
+ }
+
+ @Test
+ def shouldQueueRequestsByBrokerId(): Unit = {
+ channel.addOrUpdateBroker(new Node(1, "host", 10))
+ channel.addOrUpdateBroker(new Node(2, "otherhost", 10))
+ channel.addRequestForBroker(1, CoordinatorEpochAndMarkers(0, 0, Utils.mkList(new WriteTxnMarkersRequest.TxnMarkerEntry(0, 0, TransactionResult.COMMIT, Utils.mkList()))))
+ channel.addRequestForBroker(1, CoordinatorEpochAndMarkers(0, 0, Utils.mkList(new WriteTxnMarkersRequest.TxnMarkerEntry(0, 0, TransactionResult.COMMIT, Utils.mkList()))))
+ channel.addRequestForBroker(2, CoordinatorEpochAndMarkers(0, 0, Utils.mkList(new WriteTxnMarkersRequest.TxnMarkerEntry(0, 0, TransactionResult.COMMIT, Utils.mkList()))))
+
+ assertEquals(2, channel.queueForBroker(1).get.markersQueue.size())
+ assertEquals(1, channel.queueForBroker(2).get.markersQueue.size())
+ }
+
+ @Test
+ def shouldNotAddPendingTxnIfOneAlreadyExistsForPid(): Unit = {
+ channel.maybeAddPendingRequest(0, new TransactionMetadata(0, 0, 0, PrepareCommit, mutable.Set.empty, 0, 0))
+ assertFalse(channel.maybeAddPendingRequest(0, new TransactionMetadata(0, 0, 0, PrepareCommit, mutable.Set.empty, 0, 0)))
+ }
+
+ @Test
+ def shouldAddRequestsToCorrectBrokerQueues(): Unit = {
+ val partition2 = new TopicPartition("topic1", 1)
+
+ EasyMock.expect(metadataCache.getPartitionInfo(partition1.topic(), partition1.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, 0, List.empty, 0), 0), Set.empty)))
+
+ EasyMock.expect(metadataCache.getPartitionInfo(partition2.topic(), partition2.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(2, 0, List.empty, 0), 0), Set.empty)))
+
+ EasyMock.expect(metadataCache.getAliveEndpoint(1, listenerName)).andReturn(Some(new Node(1, "host", 10)))
+ EasyMock.expect(metadataCache.getAliveEndpoint(2, listenerName)).andReturn(Some(new Node(2, "otherhost", 10)))
+
+ EasyMock.replay(metadataCache)
+ channel.addRequestToSend(0, 0, 0, TransactionResult.COMMIT, 0, Set[TopicPartition](partition1, partition2))
+
+ assertEquals(1, channel.queueForBroker(1).get.markersQueue.size)
+ assertEquals(1, channel.queueForBroker(2).get.markersQueue.size)
+ }
+ @Test
+ def shouldWakeupNetworkClientWhenRequestsQueued(): Unit = {
+ EasyMock.expect(metadataCache.getPartitionInfo(partition1.topic(), partition1.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, 0, List.empty, 0), 0), Set.empty)))
+ EasyMock.expect(metadataCache.getAliveEndpoint(1, listenerName)).andReturn(Some(new Node(1, "host", 10)))
+
+ EasyMock.expect(networkClient.wakeup())
+
+ EasyMock.replay(metadataCache, networkClient)
+ channel.addRequestToSend(0, 0, 0, TransactionResult.COMMIT, 0, Set[TopicPartition](partition1))
+
+ EasyMock.verify(networkClient)
+ }
+
+ @Test
+ def shouldAddNewBrokerQueueIfDoesntAlreadyExistWhenAddingRequest(): Unit = {
+ EasyMock.expect(metadataCache.getPartitionInfo(partition1.topic(), partition1.partition()))
+ .andReturn(Some(PartitionStateInfo(LeaderIsrAndControllerEpoch(LeaderAndIsr(1, 0, List.empty, 0), 0), Set.empty)))
+ EasyMock.expect(metadataCache.getAliveEndpoint(1, listenerName)).andReturn(Some(new Node(1, "host", 10)))
+
+ EasyMock.replay(metadataCache)
+ channel.addRequestToSend(0, 0, 0, TransactionResult.COMMIT, 0, Set[TopicPartition](partition1))
+
+ assertEquals(1, channel.queueForBroker(1).get.markersQueue.size)
+ EasyMock.verify(metadataCache)
+ }
+
+ @Test
+ def shouldGetPendingTxnMetadataByPid(): Unit = {
+ val metadataPartition = 0
+ val transaction = new TransactionMetadata(1, 0, 0, PrepareCommit, mutable.Set.empty, 0, 0)
+ channel.maybeAddPendingRequest(metadataPartition, transaction)
+ channel.maybeAddPendingRequest(metadataPartition, new TransactionMetadata(2, 0, 0, PrepareCommit, mutable.Set.empty, 0, 0))
+ assertEquals(Some(transaction), channel.pendingTxnMetadata(metadataPartition, 1))
+ }
+
+ @Test
+ def shouldRemovePendingRequestsForPartitionWhenPartitionEmigrated(): Unit = {
+ channel.maybeAddPendingRequest(0, new TransactionMetadata(0, 0, 0, PrepareCommit, mutable.Set.empty, 0, 0))
+ channel.maybeAddPendingRequest(0, new TransactionMetadata(1, 0, 0, PrepareCommit, mutable.Set.empty, 0, 0))
+ val metadata = new TransactionMetadata(2, 0, 0, PrepareCommit, mutable.Set.empty, 0, 0)
+ channel.maybeAddPendingRequest(1, metadata)
+
+ channel.removeStateForPartition(0)
+
+ assertEquals(None, channel.pendingTxnMetadata(0, 0))
+ assertEquals(None, channel.pendingTxnMetadata(0, 1))
+ assertEquals(Some(metadata), channel.pendingTxnMetadata(1, 2))
+ }
+
+ @Test
+ def shouldRemoveBrokerRequestsForPartitionWhenPartitionEmigrated(): Unit = {
+ channel.addOrUpdateBroker(new Node(1, "host", 10))
+ channel.addRequestForBroker(1, CoordinatorEpochAndMarkers(0, 0, Utils.mkList(new WriteTxnMarkersRequest.TxnMarkerEntry(0, 0, TransactionResult.COMMIT, Utils.mkList()))))
+ channel.addRequestForBroker(1, CoordinatorEpochAndMarkers(1, 0, Utils.mkList(new WriteTxnMarkersRequest.TxnMarkerEntry(0, 0, TransactionResult.COMMIT, Utils.mkList()))))
+ channel.addRequestForBroker(1, CoordinatorEpochAndMarkers(1, 0, Utils.mkList(new WriteTxnMarkersRequest.TxnMarkerEntry(0, 0, TransactionResult.COMMIT, Utils.mkList()))))
+
+ channel.removeStateForPartition(1)
+
+
+ val markersQueue = channel.queueForBroker(1).get.markersQueue
+ assertEquals(1, markersQueue.size())
+ assertEquals(0, markersQueue.peek().metadataPartition)
+ }
+
+
+
+ def errorCallback(error: Errors): Unit = {}
+}