Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -307,12 +307,22 @@ class TransactionCoordinator(brokerId: Int,
}

def handleTxnImmigration(txnTopicPartitionId: Int, coordinatorEpoch: Int): Unit = {
// The operations performed during immigration must be resilient to any previous errors we saw or partial state we
// left off during the unloading phase. Ensure we remove all associated state for this partition before we continue
// loading it.
completeEmigration(txnTopicPartitionId)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@junrao I added this in after discussing with @hachikuji. Let me know what you think.


// Now load the partition.
txnManager.loadTransactionsForTxnTopicPartition(txnTopicPartitionId, coordinatorEpoch, txnMarkerChannelManager.addTxnMarkersToSend)
}

def handleTxnEmigration(txnTopicPartitionId: Int, coordinatorEpoch: Int): Unit = {
txnManager.removeTransactionsForTxnTopicPartition(txnTopicPartitionId, coordinatorEpoch)
txnMarkerChannelManager.removeMarkersForTxnTopicPartition(txnTopicPartitionId)
txnManager.removeTransactionsForTxnTopicPartition(txnTopicPartitionId, coordinatorEpoch)
completeEmigration(txnTopicPartitionId)
}

private def completeEmigration(txnTopicPartitionId: Int): Unit = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this function doesn't seem to add much value

txnMarkerChannelManager.removeMarkersForTxnTopicPartition(txnTopicPartitionId)
}

private def logInvalidStateTransitionAndReturnError(transactionalId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,13 @@ class TransactionStateManager(brokerId: Int,
private val stateLock = new ReentrantReadWriteLock()

/** partitions of transaction topic that are being loaded, state lock should be called BEFORE accessing this set */
private val loadingPartitions: mutable.Set[TransactionPartitionAndLeaderEpoch] = mutable.Set()
private[transaction] val loadingPartitions: mutable.Set[TransactionPartitionAndLeaderEpoch] = mutable.Set()

/** partitions of transaction topic that are being removed, state lock should be called BEFORE accessing this set */
private val leavingPartitions: mutable.Set[TransactionPartitionAndLeaderEpoch] = mutable.Set()
private[transaction] val leavingPartitions: mutable.Set[TransactionPartitionAndLeaderEpoch] = mutable.Set()

/** transaction metadata cache indexed by assigned transaction topic partition ids */
private val transactionMetadataCache: mutable.Map[Int, TxnMetadataCacheEntry] = mutable.Map()
private[transaction] val transactionMetadataCache: mutable.Map[Int, TxnMetadataCacheEntry] = mutable.Map()

/** number of partitions for the transaction log topic */
private val transactionTopicPartitionCount = getTransactionTopicPartitionCount
Expand Down Expand Up @@ -371,27 +371,22 @@ class TransactionStateManager(brokerId: Int,

/**
* Add a transaction topic partition into the cache
*
* Make it package-private to be used only for unit tests.
*/
private[transaction] def addLoadedTransactionsToCache(txnTopicPartition: Int, coordinatorEpoch: Int, metadataPerTransactionalId: Pool[String, TransactionMetadata]): Unit = {
val txnMetadataCacheEntry = TxnMetadataCacheEntry(coordinatorEpoch, metadataPerTransactionalId)
private[transaction] def addLoadedTransactionsToCache(txnTopicPartition: Int,
coordinatorEpoch: Int,
loadedTransactions: Pool[String, TransactionMetadata]): Unit = {
val txnMetadataCacheEntry = TxnMetadataCacheEntry(coordinatorEpoch, loadedTransactions)
val currentTxnMetadataCacheEntry = transactionMetadataCache.put(txnTopicPartition, txnMetadataCacheEntry)

if (currentTxnMetadataCacheEntry.isDefined) {
val coordinatorEpoch = currentTxnMetadataCacheEntry.get.coordinatorEpoch
val metadataPerTxnId = currentTxnMetadataCacheEntry.get.metadataPerTransactionalId
val errorMsg = s"The metadata cache for txn partition $txnTopicPartition has already exist with epoch $coordinatorEpoch " +
s"and ${metadataPerTxnId.size} entries while trying to add to it; " +
s"this should not happen"
fatal(errorMsg)
throw new IllegalStateException(errorMsg)
}
if (currentTxnMetadataCacheEntry.isDefined)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we can make this a foreach

info(s"Unloaded transaction metadata ${currentTxnMetadataCacheEntry.get} from $txnTopicPartition as part of " +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering whether it is worth making this a warn.. I think it is still an unexpected state in the system.

s"loading metadata at epoch $coordinatorEpoch")
}

/**
* When this broker becomes a leader for a transaction log partition, load this partition and
* populate the transaction metadata cache with the transactional ids.
* When this broker becomes a leader for a transaction log partition, load this partition and populate the transaction
* metadata cache with the transactional ids. This operation must be resilient to any partial state left off from
* the previous loading / unloading operation.
*/
def loadTransactionsForTxnTopicPartition(partitionId: Int, coordinatorEpoch: Int, sendTxnMarkers: SendTxnMarkersCallback): Unit = {
validateTransactionTopicPartitionCountIsStable()
Expand All @@ -405,7 +400,7 @@ class TransactionStateManager(brokerId: Int,
}

def loadTransactions(): Unit = {
info(s"Loading transaction metadata from $topicPartition")
info(s"Loading transaction metadata from $topicPartition at epoch $coordinatorEpoch")
val loadedTransactions = loadTransactionMetadata(topicPartition, coordinatorEpoch)

inWriteLock(stateLock) {
Expand Down Expand Up @@ -440,6 +435,8 @@ class TransactionStateManager(brokerId: Int,
}
}
}

info(s"Completed loading transaciton metadata from $topicPartition at epoch $coordinatorEpoch")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "at epoch" -> "for coordinator epoch"?

Also, typo: transaciton

}

scheduler.schedule(s"load-txns-for-partition-$topicPartition", () => loadTransactions)
Expand All @@ -465,11 +462,10 @@ class TransactionStateManager(brokerId: Int,
if (leavingPartitions.contains(partitionAndLeaderEpoch)) {
transactionMetadataCache.remove(partitionId) match {
case Some(txnMetadataCacheEntry) =>
info(s"Removed ${txnMetadataCacheEntry.metadataPerTransactionalId.size} cached transaction metadata for $topicPartition on follower transition")
info(s"Unloaded transaction metadata $txnMetadataCacheEntry for $topicPartition on become-follower transition")

case None =>
info(s"Trying to remove cached transaction metadata for $topicPartition on follower transition but there is no entries remaining; " +
s"it is likely that another process for removing the cached entries has just executed earlier before")
info(s"No cached transaction metadata found for $topicPartition during become-follower transition")
}

leavingPartitions.remove(partitionAndLeaderEpoch)
Expand Down Expand Up @@ -662,7 +658,12 @@ class TransactionStateManager(brokerId: Int,
}


private[transaction] case class TxnMetadataCacheEntry(coordinatorEpoch: Int, metadataPerTransactionalId: Pool[String, TransactionMetadata])
private[transaction] case class TxnMetadataCacheEntry(coordinatorEpoch: Int,
metadataPerTransactionalId: Pool[String, TransactionMetadata]) {
override def toString: String = {
s"TxnMetadataCacheEntry(coordinatorEpoch=$coordinatorEpoch, numTransactionalEntries=${metadataPerTransactionalId.size})"
}
}

private[transaction] case class CoordinatorEpochAndTxnMetadata(coordinatorEpoch: Int,
transactionMetadata: TransactionMetadata)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import org.apache.kafka.common.utils.{LogContext, MockTime, ProducerIdAndEpoch}
import org.easymock.{Capture, EasyMock, IAnswer}
import org.junit.Assert._
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mockito._

import scala.collection.mutable

Expand Down Expand Up @@ -858,6 +860,34 @@ class TransactionCoordinatorTest {
EasyMock.verify(transactionManager, transactionMarkerChannelManager)
}

@Test
def shouldRemoveTransactionsForPartitionOnImmigration(): Unit = {
val txnTopicPartitionId = 1
val transactionManager = mock(classOf[TransactionStateManager])
val transactionMarkerChannelManager = mock(classOf[TransactionMarkerChannelManager])
val coordinator = new TransactionCoordinator(brokerId,
new TransactionConfig(),
scheduler,
pidManager,
transactionManager,
transactionMarkerChannelManager,
time,
new LogContext)

doNothing().when(transactionMarkerChannelManager).removeMarkersForTxnTopicPartition(txnTopicPartitionId)
doNothing().when(transactionManager).loadTransactionsForTxnTopicPartition(ArgumentMatchers.eq(txnTopicPartitionId),
ArgumentMatchers.eq(coordinatorEpoch), ArgumentMatchers.any())

val inOrderVerifier = inOrder(transactionManager, transactionMarkerChannelManager)

coordinator.handleTxnImmigration(txnTopicPartitionId, coordinatorEpoch)

// first call must be to remove transaction markers, and then we start loading transactions
inOrderVerifier.verify(transactionMarkerChannelManager).removeMarkersForTxnTopicPartition(txnTopicPartitionId)
inOrderVerifier.verify(transactionManager).loadTransactionsForTxnTopicPartition(ArgumentMatchers.eq(txnTopicPartitionId),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is there any value in this test? We are just verifying ordering of 2 methods in the code.

ArgumentMatchers.eq(coordinatorEpoch), ArgumentMatchers.any())
}

@Test
def shouldAbortExpiredTransactionsInOngoingStateAndBumpEpoch(): Unit = {
val now = time.milliseconds()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import org.apache.kafka.common.utils.MockTime
import org.junit.Assert.{assertEquals, assertFalse, assertTrue}
import org.junit.{After, Before, Test}
import org.easymock.{Capture, EasyMock, IAnswer}
import org.scalatest.Assertions.assertThrows

import scala.collection.Map
import scala.collection.mutable
Expand Down Expand Up @@ -469,6 +470,44 @@ class TransactionStateManagerTest {
verifyMetadataDoesExistAndIsUsable(transactionalId2)
}

@Test
def testReloadMetadataAfterException(): Unit = {
txnMetadata1.state = PrepareCommit
txnMetadata1.addPartitions(Set[TopicPartition](new TopicPartition("topic1", 0),
new TopicPartition("topic1", 1)))

txnRecords += new SimpleRecord(txnMessageKeyBytes1, TransactionLog.valueToBytes(txnMetadata1.prepareNoTransit()))
val startOffset = 0L
val records = MemoryRecords.withRecords(startOffset, CompressionType.NONE, txnRecords.toArray: _*)

prepareTxnLog(topicPartition, 0, records)

// immigrate partition at epoch 0
transactionManager.loadTransactionsForTxnTopicPartition(partitionId, coordinatorEpoch = 0, (_, _, _, _, _) => ())
assertEquals(0, transactionManager.loadingPartitions.size)
assertEquals(0, transactionManager.leavingPartitions.size)

// emigrate partition; simulate ZK session timeout
EasyMock.reset(zkClient)
EasyMock.expect(zkClient.getTopicPartitionCount(TRANSACTION_STATE_TOPIC_NAME))
.andThrow(new RuntimeException)
.once()
.andReturn(Some(numPartitions))
.anyTimes()
EasyMock.replay(zkClient)

assertThrows[RuntimeException] {
transactionManager.removeTransactionsForTxnTopicPartition(partitionId, 0)
}

// immigrate partition at epoch 1
prepareTxnLog(topicPartition, 0, records)
transactionManager.loadTransactionsForTxnTopicPartition(partitionId, coordinatorEpoch = 1, (_, _, _, _, _) => ())
assertEquals(0, transactionManager.loadingPartitions.size)
assertEquals(0, transactionManager.leavingPartitions.size)
assertTrue(transactionManager.transactionMetadataCache.get(partitionId).isDefined)
}

private def verifyMetadataDoesExistAndIsUsable(transactionalId: String): Unit = {
transactionManager.getTransactionState(transactionalId) match {
case Left(_) => fail("shouldn't have been any errors")
Expand Down