-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-9307: Make transaction metadata loading resilient to previous errors #7840
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
df8687f
9a28b70
6534bd7
865f5f8
49cafdd
7d6ebb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
|
||
| // 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 = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: we can make this a |
||
| info(s"Unloaded transaction metadata ${currentTxnMetadataCacheEntry.get} from $txnTopicPartition as part of " + | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
|
@@ -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) { | ||
|
|
@@ -440,6 +435,8 @@ class TransactionStateManager(brokerId: Int, | |
| } | ||
| } | ||
| } | ||
|
|
||
| info(s"Completed loading transaciton metadata from $topicPartition at epoch $coordinatorEpoch") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: "at epoch" -> "for coordinator epoch"? Also, typo: |
||
| } | ||
|
|
||
| scheduler.schedule(s"load-txns-for-partition-$topicPartition", () => loadTransactions) | ||
|
|
@@ -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) | ||
|
|
@@ -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) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
|
||
There was a problem hiding this comment.
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.