This is an automated email from the ASF dual-hosted git repository.

chia7712 pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new cb69d5716e5 MINOR: Rewrite TransactionMetadataTest from Scala to Java 
(#22811)
cb69d5716e5 is described below

commit cb69d5716e5924001ddc269ec95ae5a09e1f80f2
Author: Ken Huang <[email protected]>
AuthorDate: Tue Jul 21 02:20:44 2026 +0800

    MINOR: Rewrite TransactionMetadataTest from Scala to Java (#22811)
    
    Rewrite it, and move it to transaction coordinator module
    
    Reviewers: Chia-Ping Tsai <[email protected]>
---
 .../transaction/TransactionMetadataTest.scala      | 794 --------------------
 .../transaction/TransactionMetadataTest.java       | 822 +++++++++++++++++++++
 2 files changed, 822 insertions(+), 794 deletions(-)

diff --git 
a/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionMetadataTest.scala
 
b/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionMetadataTest.scala
deleted file mode 100644
index cab8d3e90de..00000000000
--- 
a/core/src/test/scala/unit/kafka/coordinator/transaction/TransactionMetadataTest.scala
+++ /dev/null
@@ -1,794 +0,0 @@
-/*
- * 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.protocol.Errors
-import org.apache.kafka.common.record.internal.RecordBatch
-import org.apache.kafka.coordinator.transaction.{TransactionMetadata, 
TransactionState, TxnTransitMetadata}
-import org.apache.kafka.server.common.TransactionVersion
-import org.apache.kafka.server.common.TransactionVersion.{TV_0, TV_2}
-import org.apache.kafka.server.util.MockTime
-import org.junit.jupiter.api.Assertions._
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.params.ParameterizedTest
-import org.junit.jupiter.params.provider.ValueSource
-
-import java.util
-import java.util.Optional
-
-import scala.collection.mutable
-import scala.jdk.CollectionConverters._
-
-class TransactionMetadataTest {
-
-  val time = new MockTime()
-  val producerId = 23423L
-  val transactionalId = "txnlId"
-
-  @Test
-  def testInitializeEpoch(): Unit = {
-    val producerEpoch = RecordBatch.NO_PRODUCER_EPOCH
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.EMPTY,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      TV_0)
-
-    val transitMetadata = prepareSuccessfulIncrementProducerEpoch(txnMetadata, 
Optional.empty())
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(0, txnMetadata.producerEpoch)
-    assertEquals(RecordBatch.NO_PRODUCER_EPOCH, txnMetadata.lastProducerEpoch)
-  }
-
-  @Test
-  def testNormalEpochBump(): Unit = {
-    val producerEpoch = 735.toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.EMPTY,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      TV_0)
-
-    val transitMetadata = prepareSuccessfulIncrementProducerEpoch(txnMetadata, 
Optional.empty())
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(producerEpoch + 1, txnMetadata.producerEpoch)
-    assertEquals(RecordBatch.NO_PRODUCER_EPOCH, txnMetadata.lastProducerEpoch)
-  }
-
-  @Test
-  def testBumpEpochNotAllowedIfEpochsExhausted(): Unit = {
-    val producerEpoch = (Short.MaxValue - 1).toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.EMPTY,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      TV_0)
-    assertTrue(txnMetadata.isProducerEpochExhausted)
-
-    assertThrows(classOf[IllegalStateException], () => 
txnMetadata.prepareIncrementProducerEpoch(30000,
-      Optional.empty, time.milliseconds()))
-  }
-
-  @Test
-  def testTransitFromEmptyToPrepareAbortInV2(): Unit = {
-    val producerEpoch = 735.toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.EMPTY,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      TV_2)
-
-    val transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_ABORT, TV_2, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds() + 1, true)
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(producerEpoch + 1, txnMetadata.producerEpoch)
-    assertEquals(time.milliseconds() + 1, txnMetadata.txnStartTimestamp)
-  }
-
-  @Test
-  def testTransitFromCompleteAbortToPrepareAbortInV2(): Unit = {
-    val producerEpoch = 735.toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.COMPLETE_ABORT,
-      util.Set.of,
-      time.milliseconds() - 1,
-      time.milliseconds(),
-      TV_2)
-
-    val transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_ABORT, TV_2, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds() + 1, true)
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(producerEpoch + 1, txnMetadata.producerEpoch)
-    assertEquals(time.milliseconds() + 1, txnMetadata.txnStartTimestamp)
-  }
-
-  @Test
-  def testTransitFromCompleteCommitToPrepareAbortInV2(): Unit = {
-    val producerEpoch = 735.toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.COMPLETE_COMMIT,
-      util.Set.of,
-      time.milliseconds() - 1,
-      time.milliseconds(),
-      TV_2)
-
-    val transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_ABORT, TV_2, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds() + 1, true)
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(producerEpoch + 1, txnMetadata.producerEpoch)
-    assertEquals(time.milliseconds() + 1, txnMetadata.txnStartTimestamp)
-  }
-
-  @Test
-  def testTolerateUpdateTimeShiftDuringEpochBump(): Unit = {
-    val producerEpoch: Short = 1
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.EMPTY,
-      util.Set.of,
-      1L,
-      time.milliseconds(),
-      TV_0)
-
-    // let new time be smaller
-    val transitMetadata = prepareSuccessfulIncrementProducerEpoch(txnMetadata, 
Optional.of(producerEpoch),
-      Some(time.milliseconds() - 1))
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(producerEpoch + 1, txnMetadata.producerEpoch)
-    assertEquals(producerEpoch, txnMetadata.lastProducerEpoch)
-    assertEquals(-1L, txnMetadata.txnStartTimestamp)
-    assertEquals(time.milliseconds() - 1, txnMetadata.txnLastUpdateTimestamp)
-  }
-
-  @Test
-  def testTolerateUpdateTimeResetDuringProducerIdRotation(): Unit = {
-    val producerEpoch: Short = 1
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.EMPTY,
-      util.Set.of,
-      1L,
-      time.milliseconds(),
-      TV_0)
-
-    // let new time be smaller
-    val transitMetadata = txnMetadata.prepareProducerIdRotation(producerId + 
1, 30000, time.milliseconds() - 1, true)
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId + 1, txnMetadata.producerId)
-    assertEquals(producerEpoch, txnMetadata.lastProducerEpoch)
-    assertEquals(0, txnMetadata.producerEpoch)
-    assertEquals(-1L, txnMetadata.txnStartTimestamp)
-    assertEquals(time.milliseconds() - 1, txnMetadata.txnLastUpdateTimestamp)
-  }
-
-  @Test
-  def testTolerateTimeShiftDuringAddPartitions(): Unit = {
-    val producerEpoch: Short = 1
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.EMPTY,
-      util.Set.of,
-      time.milliseconds(),
-      time.milliseconds(),
-      TV_0)
-
-    // let new time be smaller; when transiting from TransactionState.EMPTY 
the start time would be updated to the update-time
-    var transitMetadata = txnMetadata.prepareAddPartitions(util.Set.of(new 
TopicPartition("topic1", 0)), time.milliseconds() - 1, TV_0)
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(util.Set.of(new TopicPartition("topic1", 0)), 
txnMetadata.topicPartitions)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(RecordBatch.NO_PRODUCER_EPOCH, txnMetadata.lastProducerEpoch)
-    assertEquals(producerEpoch, txnMetadata.producerEpoch)
-    assertEquals(time.milliseconds() - 1, txnMetadata.txnStartTimestamp)
-    assertEquals(time.milliseconds() - 1, txnMetadata.txnLastUpdateTimestamp)
-
-    // add another partition, check that in TransactionState.ONGOING state the 
start timestamp would not change to update time
-    transitMetadata = txnMetadata.prepareAddPartitions(util.Set.of(new 
TopicPartition("topic2", 0)), time.milliseconds() - 2, TV_0)
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(util.Set.of(new TopicPartition("topic1", 0), new 
TopicPartition("topic2", 0)), txnMetadata.topicPartitions)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(RecordBatch.NO_PRODUCER_EPOCH, txnMetadata.lastProducerEpoch)
-    assertEquals(producerEpoch, txnMetadata.producerEpoch)
-    assertEquals(time.milliseconds() - 1, txnMetadata.txnStartTimestamp)
-    assertEquals(time.milliseconds() - 2, txnMetadata.txnLastUpdateTimestamp)
-  }
-
-  @Test
-  def testTolerateTimeShiftDuringPrepareCommit(): Unit = {
-    val producerEpoch: Short = 1
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.ONGOING,
-      util.Set.of,
-      1L,
-      time.milliseconds(),
-      TV_0)
-
-    // let new time be smaller
-    val transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_COMMIT, TV_0, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds() - 1, false)
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(TransactionState.PREPARE_COMMIT, txnMetadata.state)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(RecordBatch.NO_PRODUCER_EPOCH, txnMetadata.lastProducerEpoch)
-    assertEquals(producerEpoch, txnMetadata.producerEpoch)
-    assertEquals(1L, txnMetadata.txnStartTimestamp)
-    assertEquals(time.milliseconds() - 1, txnMetadata.txnLastUpdateTimestamp)
-  }
-
-  @Test
-  def testTolerateTimeShiftDuringPrepareAbort(): Unit = {
-    val producerEpoch: Short = 1
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.ONGOING,
-      util.Set.of,
-      1L,
-      time.milliseconds(),
-      TV_0)
-
-    // let new time be smaller
-    val transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_ABORT, TV_0, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds() - 1, false)
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(TransactionState.PREPARE_ABORT, txnMetadata.state)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(RecordBatch.NO_PRODUCER_EPOCH, txnMetadata.lastProducerEpoch)
-    assertEquals(producerEpoch, txnMetadata.producerEpoch)
-    assertEquals(1L, txnMetadata.txnStartTimestamp)
-    assertEquals(time.milliseconds() - 1, txnMetadata.txnLastUpdateTimestamp)
-  }
-
-  @ParameterizedTest
-  @ValueSource(shorts = Array(0, 2))
-  def testTolerateTimeShiftDuringCompleteCommit(transactionVersion: Short): 
Unit = {
-    val clientTransactionVersion = 
TransactionVersion.fromFeatureLevel(transactionVersion)
-    val producerEpoch: Short = 1
-    val lastProducerEpoch: Short = 0
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      lastProducerEpoch,
-      30000,
-      TransactionState.PREPARE_COMMIT,
-      util.Set.of(),
-      1L,
-      time.milliseconds(),
-      clientTransactionVersion
-    )
-
-    // let new time be smaller
-    val transitMetadata = txnMetadata.prepareComplete(time.milliseconds() - 1)
-    txnMetadata.completeTransitionTo(transitMetadata)
-
-    assertEquals(TransactionState.COMPLETE_COMMIT, txnMetadata.state)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(lastProducerEpoch, txnMetadata.lastProducerEpoch)
-    assertEquals(producerEpoch, txnMetadata.producerEpoch)
-    assertEquals(1L, txnMetadata.txnStartTimestamp)
-    assertEquals(time.milliseconds() - 1, txnMetadata.txnLastUpdateTimestamp)
-  }
-
-  @ParameterizedTest
-  @ValueSource(shorts = Array(0, 2))
-  def testTolerateTimeShiftDuringCompleteAbort(transactionVersion: Short): 
Unit = {
-    val clientTransactionVersion = 
TransactionVersion.fromFeatureLevel(transactionVersion)
-    val producerEpoch: Short = 1
-    val lastProducerEpoch: Short = 0
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      lastProducerEpoch,
-      30000,
-      TransactionState.PREPARE_ABORT,
-      util.Set.of,
-      1L,
-      time.milliseconds(),
-      clientTransactionVersion
-    )
-
-    // let new time be smaller
-    val transitMetadata = txnMetadata.prepareComplete(time.milliseconds() - 1)
-    txnMetadata.completeTransitionTo(transitMetadata)
-
-    assertEquals(TransactionState.COMPLETE_ABORT, txnMetadata.state)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(lastProducerEpoch, txnMetadata.lastProducerEpoch)
-    assertEquals(producerEpoch, txnMetadata.producerEpoch)
-    assertEquals(1L, txnMetadata.txnStartTimestamp)
-    assertEquals(time.milliseconds() - 1, txnMetadata.txnLastUpdateTimestamp)
-  }
-
-  @Test
-  def testFenceProducerAfterEpochsExhausted(): Unit = {
-    val producerEpoch = (Short.MaxValue - 1).toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.ONGOING,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      TV_0)
-    assertTrue(txnMetadata.isProducerEpochExhausted)
-
-    val fencingTransitMetadata = txnMetadata.prepareFenceProducerEpoch()
-    assertEquals(Short.MaxValue, fencingTransitMetadata.producerEpoch)
-    assertEquals(RecordBatch.NO_PRODUCER_EPOCH, 
fencingTransitMetadata.lastProducerEpoch)
-    assertEquals(Optional.of(TransactionState.PREPARE_EPOCH_FENCE), 
txnMetadata.pendingState)
-
-    // We should reset the pending state to make way for the abort transition.
-    txnMetadata.pendingState(Optional.empty())
-
-    val transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_ABORT, TV_0, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds(), false)
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId, transitMetadata.producerId)
-  }
-
-  @Test
-  def testInvalidTransitionFromCompleteCommitToFence(): Unit = {
-    val producerEpoch = (Short.MaxValue - 1).toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.COMPLETE_COMMIT,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      TV_0)
-    assertTrue(txnMetadata.isProducerEpochExhausted)
-
-    assertThrows(classOf[IllegalStateException], () => 
txnMetadata.prepareFenceProducerEpoch())
-  }
-
-  @Test
-  def testInvalidTransitionFromCompleteAbortToFence(): Unit = {
-    val producerEpoch = (Short.MaxValue - 1).toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.COMPLETE_ABORT,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      TV_0)
-    assertTrue(txnMetadata.isProducerEpochExhausted)
-
-    assertThrows(classOf[IllegalStateException], () => 
txnMetadata.prepareFenceProducerEpoch())
-  }
-
-  @Test
-  def testFenceProducerNotAllowedIfItWouldOverflow(): Unit = {
-    val producerEpoch = Short.MaxValue
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.ONGOING,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      TV_0)
-    assertTrue(txnMetadata.isProducerEpochExhausted)
-
-    // When epoch is at max, prepareFenceProducerEpoch logs an error but 
doesn't throw
-    // This allows graceful recovery through producer ID rotation
-    val preparedMetadata = txnMetadata.prepareFenceProducerEpoch()
-
-    // Epoch should remain at Short.MaxValue (not overflow to negative)
-    assertEquals(Short.MaxValue, preparedMetadata.producerEpoch)
-    assertEquals(TransactionState.PREPARE_EPOCH_FENCE, 
preparedMetadata.txnState)
-  }
-
-  @Test
-  def testRotateProducerId(): Unit = {
-    val producerEpoch = (Short.MaxValue - 1).toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.EMPTY,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      TV_0)
-
-    val newProducerId = 9893L
-    val transitMetadata = txnMetadata.prepareProducerIdRotation(newProducerId, 
30000, time.milliseconds(), true)
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(newProducerId, txnMetadata.producerId)
-    assertEquals(producerId, txnMetadata.prevProducerId)
-    assertEquals(0, txnMetadata.producerEpoch)
-    assertEquals(producerEpoch, txnMetadata.lastProducerEpoch)
-  }
-
-  @Test
-  def testEpochBumpOnEndTxn(): Unit = {
-    time.sleep(100)
-    val producerEpoch = 10.toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.ONGOING,
-      util.Set.of,
-      time.milliseconds(),
-      time.milliseconds(),
-      TV_2)
-
-    var transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_COMMIT, TV_2, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds() - 1, false)
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals((producerEpoch + 1).toShort, txnMetadata.producerEpoch)
-    assertEquals(TV_2, txnMetadata.clientTransactionVersion)
-
-    transitMetadata = txnMetadata.prepareComplete(time.milliseconds())
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals((producerEpoch + 1).toShort, txnMetadata.producerEpoch)
-    assertEquals(TV_2, txnMetadata.clientTransactionVersion)
-  }
-
-  @Test
-  def testEpochBumpOnEndTxnOverflow(): Unit = {
-    time.sleep(100)
-    val producerEpoch = (Short.MaxValue - 1).toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.ONGOING,
-      util.Set.of,
-      time.milliseconds(),
-      time.milliseconds(),
-      TV_2)
-    assertTrue(txnMetadata.isProducerEpochExhausted)
-
-    val newProducerId = 9893L
-    var transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_COMMIT, TV_2, 
newProducerId, time.milliseconds() - 1, false)
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(Short.MaxValue, txnMetadata.producerEpoch)
-    assertEquals(producerEpoch, txnMetadata.lastProducerEpoch)
-    assertEquals(TV_2, txnMetadata.clientTransactionVersion)
-
-    transitMetadata = txnMetadata.prepareComplete(time.milliseconds())
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(newProducerId, txnMetadata.producerId)
-    assertEquals(0, txnMetadata.producerEpoch)
-    assertEquals(producerEpoch, txnMetadata.lastProducerEpoch)
-    assertEquals(TV_2, txnMetadata.clientTransactionVersion)
-  }
-
-  @Test
-  def testRotateProducerIdInOngoingState(): Unit = {
-    assertThrows(classOf[IllegalStateException], () => 
testRotateProducerIdInOngoingState(TransactionState.ONGOING, TV_0))
-  }
-
-  @ParameterizedTest
-  @ValueSource(shorts = Array(0, 2))
-  def testRotateProducerIdInPrepareAbortState(transactionVersion: Short): Unit 
= {
-    val clientTransactionVersion = 
TransactionVersion.fromFeatureLevel(transactionVersion)
-    assertThrows(classOf[IllegalStateException], () => 
testRotateProducerIdInOngoingState(TransactionState.PREPARE_ABORT, 
clientTransactionVersion))
-  }
-
-  @ParameterizedTest
-  @ValueSource(shorts = Array(0, 2))
-  def testRotateProducerIdInPrepareCommitState(transactionVersion: Short): 
Unit = {
-    val clientTransactionVersion = 
TransactionVersion.fromFeatureLevel(transactionVersion)
-    assertThrows(classOf[IllegalStateException], () => 
testRotateProducerIdInOngoingState(TransactionState.PREPARE_COMMIT, 
clientTransactionVersion))
-  }
-
-  @Test
-  def testAttemptedEpochBumpWithNewlyCreatedMetadata(): Unit = {
-    val producerEpoch = 735.toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.EMPTY,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      TV_0)
-
-    val transitMetadata = prepareSuccessfulIncrementProducerEpoch(txnMetadata, 
Optional.of(producerEpoch))
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(0, txnMetadata.producerEpoch)
-    assertEquals(RecordBatch.NO_PRODUCER_EPOCH, txnMetadata.lastProducerEpoch)
-  }
-
-  @Test
-  def testEpochBumpWithCurrentEpochProvided(): Unit = {
-    val producerEpoch = 735.toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      TransactionState.EMPTY,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      TV_0)
-
-    val transitMetadata = prepareSuccessfulIncrementProducerEpoch(txnMetadata, 
Optional.of(producerEpoch))
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(producerEpoch + 1, txnMetadata.producerEpoch)
-    assertEquals(producerEpoch, txnMetadata.lastProducerEpoch)
-  }
-
-  @Test
-  def testAttemptedEpochBumpWithLastEpoch(): Unit = {
-    val producerEpoch = 735.toShort
-    val lastProducerEpoch = (producerEpoch - 1).toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      lastProducerEpoch,
-      30000,
-      TransactionState.EMPTY,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      TV_0)
-
-    val transitMetadata = prepareSuccessfulIncrementProducerEpoch(txnMetadata, 
Optional.of(lastProducerEpoch))
-    txnMetadata.completeTransitionTo(transitMetadata)
-    assertEquals(producerId, txnMetadata.producerId)
-    assertEquals(producerEpoch, txnMetadata.producerEpoch)
-    assertEquals(lastProducerEpoch, txnMetadata.lastProducerEpoch)
-  }
-
-  @Test
-  def testAttemptedEpochBumpWithFencedEpoch(): Unit = {
-    val producerEpoch = 735.toShort
-    val lastProducerEpoch = (producerEpoch - 1).toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      lastProducerEpoch,
-      30000,
-      TransactionState.EMPTY,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      TV_0)
-
-    assertThrows(Errors.PRODUCER_FENCED.exception().getClass, () =>
-      txnMetadata.prepareIncrementProducerEpoch(30000, 
Optional.of((lastProducerEpoch - 1).toShort),
-        time.milliseconds())
-    )
-  }
-
-  @Test
-  def testTransactionStateIdAndNameMapping(): Unit = {
-    for (state <- TransactionState.ALL_STATES.asScala) {
-      assertEquals(state, TransactionState.fromId(state.id))
-      assertEquals(Optional.of(state), 
TransactionState.fromName(state.stateName))
-
-      if (state != TransactionState.DEAD) {
-        val clientTransactionState = 
org.apache.kafka.clients.admin.TransactionState.parse(state.stateName)
-        assertEquals(state.stateName, clientTransactionState.toString)
-        
assertNotEquals(org.apache.kafka.clients.admin.TransactionState.UNKNOWN, 
clientTransactionState)
-      }
-    }
-  }
-
-  @Test
-  def testAllTransactionStatesAreMapped(): Unit = {
-    val unmatchedStates = mutable.Set(
-      TransactionState.EMPTY,
-      TransactionState.ONGOING,
-      TransactionState.PREPARE_COMMIT,
-      TransactionState.PREPARE_ABORT,
-      TransactionState.COMPLETE_COMMIT,
-      TransactionState.COMPLETE_ABORT,
-      TransactionState.PREPARE_EPOCH_FENCE,
-      TransactionState.DEAD
-    )
-
-    // The exhaustive match is intentional here to ensure that we are
-    // forced to update the test case if a new state is added.
-    TransactionState.ALL_STATES.asScala.foreach {
-      case TransactionState.EMPTY => 
assertTrue(unmatchedStates.remove(TransactionState.EMPTY))
-      case TransactionState.ONGOING => 
assertTrue(unmatchedStates.remove(TransactionState.ONGOING))
-      case TransactionState.PREPARE_COMMIT => 
assertTrue(unmatchedStates.remove(TransactionState.PREPARE_COMMIT))
-      case TransactionState.PREPARE_ABORT => 
assertTrue(unmatchedStates.remove(TransactionState.PREPARE_ABORT))
-      case TransactionState.COMPLETE_COMMIT => 
assertTrue(unmatchedStates.remove(TransactionState.COMPLETE_COMMIT))
-      case TransactionState.COMPLETE_ABORT => 
assertTrue(unmatchedStates.remove(TransactionState.COMPLETE_ABORT))
-      case TransactionState.PREPARE_EPOCH_FENCE => 
assertTrue(unmatchedStates.remove(TransactionState.PREPARE_EPOCH_FENCE))
-      case TransactionState.DEAD => 
assertTrue(unmatchedStates.remove(TransactionState.DEAD))
-    }
-
-    assertEquals(Set.empty, unmatchedStates)
-  }
-
-  private def testRotateProducerIdInOngoingState(state: TransactionState, 
clientTransactionVersion: TransactionVersion): Unit = {
-    val producerEpoch = (Short.MaxValue - 1).toShort
-
-    val txnMetadata = new TransactionMetadata(
-      transactionalId,
-      producerId,
-      producerId,
-      RecordBatch.NO_PRODUCER_ID,
-      producerEpoch,
-      RecordBatch.NO_PRODUCER_EPOCH,
-      30000,
-      state,
-      util.Set.of,
-      -1,
-      time.milliseconds(),
-      clientTransactionVersion)
-    val newProducerId = 9893L
-    txnMetadata.prepareProducerIdRotation(newProducerId, 30000, 
time.milliseconds(), false)
-  }
-
-  private def prepareSuccessfulIncrementProducerEpoch(txnMetadata: 
TransactionMetadata,
-                                                      expectedProducerEpoch: 
Optional[java.lang.Short],
-                                                      now: Option[Long] = 
None): TxnTransitMetadata = {
-    txnMetadata.prepareIncrementProducerEpoch(30000, expectedProducerEpoch, 
now.getOrElse(time.milliseconds()))
-  }
-
-}
diff --git 
a/transaction-coordinator/src/test/java/org/apache/kafka/coordinator/transaction/TransactionMetadataTest.java
 
b/transaction-coordinator/src/test/java/org/apache/kafka/coordinator/transaction/TransactionMetadataTest.java
new file mode 100644
index 00000000000..9c33f25dbfd
--- /dev/null
+++ 
b/transaction-coordinator/src/test/java/org/apache/kafka/coordinator/transaction/TransactionMetadataTest.java
@@ -0,0 +1,822 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.kafka.coordinator.transaction;
+
+import org.apache.kafka.common.TopicPartition;
+import org.apache.kafka.common.errors.ProducerFencedException;
+import org.apache.kafka.common.record.internal.RecordBatch;
+import org.apache.kafka.common.utils.MockTime;
+import org.apache.kafka.server.common.TransactionVersion;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.apache.kafka.server.common.TransactionVersion.TV_0;
+import static org.apache.kafka.server.common.TransactionVersion.TV_2;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class TransactionMetadataTest {
+
+    private final MockTime time = new MockTime();
+    private final long producerId = 23423L;
+    private final String transactionalId = "txnlId";
+
+    @Test
+    public void testInitializeEpoch() {
+        short producerEpoch = RecordBatch.NO_PRODUCER_EPOCH;
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.EMPTY,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            TV_0
+        );
+
+        TxnTransitMetadata transitMetadata = 
prepareSuccessfulIncrementProducerEpoch(txnMetadata, Optional.empty());
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(0, txnMetadata.producerEpoch());
+        assertEquals(RecordBatch.NO_PRODUCER_EPOCH, 
txnMetadata.lastProducerEpoch());
+    }
+
+    @Test
+    public void testNormalEpochBump() {
+        short producerEpoch = 735;
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.EMPTY,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            TV_0
+        );
+
+        TxnTransitMetadata transitMetadata = 
prepareSuccessfulIncrementProducerEpoch(txnMetadata, Optional.empty());
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(producerEpoch + 1, txnMetadata.producerEpoch());
+        assertEquals(RecordBatch.NO_PRODUCER_EPOCH, 
txnMetadata.lastProducerEpoch());
+    }
+
+    @Test
+    public void testBumpEpochNotAllowedIfEpochsExhausted() {
+        short producerEpoch = (short) (Short.MAX_VALUE - 1);
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.EMPTY,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            TV_0
+        );
+
+        assertTrue(txnMetadata.isProducerEpochExhausted());
+        assertThrows(IllegalStateException.class, () -> 
txnMetadata.prepareIncrementProducerEpoch(30000,
+            Optional.empty(), time.milliseconds()));
+    }
+
+    @Test
+    public void testTransitFromEmptyToPrepareAbortInV2() {
+        short producerEpoch = 735;
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.EMPTY,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            TV_2
+        );
+
+        TxnTransitMetadata transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_ABORT, TV_2, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds() + 1, true);
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(producerEpoch + 1, txnMetadata.producerEpoch());
+        assertEquals(time.milliseconds() + 1, txnMetadata.txnStartTimestamp());
+    }
+
+    @Test
+    public void testTransitFromCompleteAbortToPrepareAbortInV2() {
+        short producerEpoch = 735;
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.COMPLETE_ABORT,
+            Set.of(),
+            time.milliseconds() - 1,
+            time.milliseconds(),
+            TV_2
+        );
+
+        TxnTransitMetadata transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_ABORT, TV_2, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds() + 1, true);
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(producerEpoch + 1, txnMetadata.producerEpoch());
+        assertEquals(time.milliseconds() + 1, txnMetadata.txnStartTimestamp());
+    }
+
+    @Test
+    public void testTransitFromCompleteCommitToPrepareAbortInV2() {
+        short producerEpoch = 735;
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.COMPLETE_COMMIT,
+            Set.of(),
+            time.milliseconds() - 1,
+            time.milliseconds(),
+            TV_2
+        );
+
+        TxnTransitMetadata transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_ABORT, TV_2, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds() + 1, true);
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(producerEpoch + 1, txnMetadata.producerEpoch());
+        assertEquals(time.milliseconds() + 1, txnMetadata.txnStartTimestamp());
+    }
+
+    @Test
+    public void testTolerateUpdateTimeShiftDuringEpochBump() {
+        short producerEpoch = 1;
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.EMPTY,
+            Set.of(),
+            1L,
+            time.milliseconds(),
+            TV_0
+        );
+
+        // let new time be smaller
+        TxnTransitMetadata transitMetadata = 
prepareSuccessfulIncrementProducerEpoch(txnMetadata, Optional.of(producerEpoch),
+            Optional.of(time.milliseconds() - 1));
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(producerEpoch + 1, txnMetadata.producerEpoch());
+        assertEquals(producerEpoch, txnMetadata.lastProducerEpoch());
+        assertEquals(-1L, txnMetadata.txnStartTimestamp());
+        assertEquals(time.milliseconds() - 1, 
txnMetadata.txnLastUpdateTimestamp());
+    }
+
+    @Test
+    public void testTolerateUpdateTimeResetDuringProducerIdRotation() {
+        short producerEpoch = 1;
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.EMPTY,
+            Set.of(),
+            1L,
+            time.milliseconds(),
+            TV_0
+        );
+
+        // let new time be smaller
+        TxnTransitMetadata transitMetadata = 
txnMetadata.prepareProducerIdRotation(producerId + 1, 30000, 
time.milliseconds() - 1, true);
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId + 1, txnMetadata.producerId());
+        assertEquals(producerEpoch, txnMetadata.lastProducerEpoch());
+        assertEquals(0, txnMetadata.producerEpoch());
+        assertEquals(-1L, txnMetadata.txnStartTimestamp());
+        assertEquals(time.milliseconds() - 1, 
txnMetadata.txnLastUpdateTimestamp());
+    }
+
+    @Test
+    public void testTolerateTimeShiftDuringAddPartitions() {
+        short producerEpoch = 1;
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.EMPTY,
+            Set.of(),
+            time.milliseconds(),
+            time.milliseconds(),
+            TV_0
+        );
+
+        // let new time be smaller; when transiting from 
TransactionState.EMPTY the start time would be updated to the update-time
+        TxnTransitMetadata transitMetadata = 
txnMetadata.prepareAddPartitions(Set.of(new TopicPartition("topic1", 0)), 
time.milliseconds() - 1, TV_0);
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(Set.of(new TopicPartition("topic1", 0)), 
txnMetadata.topicPartitions());
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(RecordBatch.NO_PRODUCER_EPOCH, 
txnMetadata.lastProducerEpoch());
+        assertEquals(producerEpoch, txnMetadata.producerEpoch());
+        assertEquals(time.milliseconds() - 1, txnMetadata.txnStartTimestamp());
+        assertEquals(time.milliseconds() - 1, 
txnMetadata.txnLastUpdateTimestamp());
+
+        // add another partition, check that in TransactionState.ONGOING state 
the start timestamp would not change to update time
+        transitMetadata = txnMetadata.prepareAddPartitions(Set.of(new 
TopicPartition("topic2", 0)), time.milliseconds() - 2, TV_0);
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(Set.of(new TopicPartition("topic1", 0), new 
TopicPartition("topic2", 0)), txnMetadata.topicPartitions());
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(RecordBatch.NO_PRODUCER_EPOCH, 
txnMetadata.lastProducerEpoch());
+        assertEquals(producerEpoch, txnMetadata.producerEpoch());
+        assertEquals(time.milliseconds() - 1, txnMetadata.txnStartTimestamp());
+        assertEquals(time.milliseconds() - 2, 
txnMetadata.txnLastUpdateTimestamp());
+    }
+
+    @Test
+    public void testTolerateTimeShiftDuringPrepareCommit() {
+        short producerEpoch = 1;
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.ONGOING,
+            Set.of(),
+            1L,
+            time.milliseconds(),
+            TV_0
+        );
+
+        // let new time be smaller
+        TxnTransitMetadata transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_COMMIT, TV_0, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds() - 1, false);
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(TransactionState.PREPARE_COMMIT, txnMetadata.state());
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(RecordBatch.NO_PRODUCER_EPOCH, 
txnMetadata.lastProducerEpoch());
+        assertEquals(producerEpoch, txnMetadata.producerEpoch());
+        assertEquals(1L, txnMetadata.txnStartTimestamp());
+        assertEquals(time.milliseconds() - 1, 
txnMetadata.txnLastUpdateTimestamp());
+    }
+
+    @Test
+    public void testTolerateTimeShiftDuringPrepareAbort() {
+        short producerEpoch = 1;
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.ONGOING,
+            Set.of(),
+            1L,
+            time.milliseconds(),
+            TV_0
+        );
+
+        // let new time be smaller
+        TxnTransitMetadata transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_ABORT, TV_0, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds() - 1, false);
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(TransactionState.PREPARE_ABORT, txnMetadata.state());
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(RecordBatch.NO_PRODUCER_EPOCH, 
txnMetadata.lastProducerEpoch());
+        assertEquals(producerEpoch, txnMetadata.producerEpoch());
+        assertEquals(1L, txnMetadata.txnStartTimestamp());
+        assertEquals(time.milliseconds() - 1, 
txnMetadata.txnLastUpdateTimestamp());
+    }
+
+    @ParameterizedTest
+    @ValueSource(shorts = {0, 2})
+    public void testTolerateTimeShiftDuringCompleteCommit(short 
transactionVersion) {
+        TransactionVersion clientTransactionVersion = 
TransactionVersion.fromFeatureLevel(transactionVersion);
+        short producerEpoch = 1;
+        short lastProducerEpoch = 0;
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            lastProducerEpoch,
+            30000,
+            TransactionState.PREPARE_COMMIT,
+            Set.of(),
+            1L,
+            time.milliseconds(),
+            clientTransactionVersion
+        );
+
+        // let new time be smaller
+        TxnTransitMetadata transitMetadata = 
txnMetadata.prepareComplete(time.milliseconds() - 1);
+        txnMetadata.completeTransitionTo(transitMetadata);
+
+        assertEquals(TransactionState.COMPLETE_COMMIT, txnMetadata.state());
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(lastProducerEpoch, txnMetadata.lastProducerEpoch());
+        assertEquals(producerEpoch, txnMetadata.producerEpoch());
+        assertEquals(1L, txnMetadata.txnStartTimestamp());
+        assertEquals(time.milliseconds() - 1, 
txnMetadata.txnLastUpdateTimestamp());
+    }
+
+    @ParameterizedTest
+    @ValueSource(shorts = {0, 2})
+    public void testTolerateTimeShiftDuringCompleteAbort(short 
transactionVersion) {
+        TransactionVersion clientTransactionVersion = 
TransactionVersion.fromFeatureLevel(transactionVersion);
+        short producerEpoch = 1;
+        short lastProducerEpoch = 0;
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            lastProducerEpoch,
+            30000,
+            TransactionState.PREPARE_ABORT,
+            Set.of(),
+            1L,
+            time.milliseconds(),
+            clientTransactionVersion
+        );
+
+        // let new time be smaller
+        TxnTransitMetadata transitMetadata = 
txnMetadata.prepareComplete(time.milliseconds() - 1);
+        txnMetadata.completeTransitionTo(transitMetadata);
+
+        assertEquals(TransactionState.COMPLETE_ABORT, txnMetadata.state());
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(lastProducerEpoch, txnMetadata.lastProducerEpoch());
+        assertEquals(producerEpoch, txnMetadata.producerEpoch());
+        assertEquals(1L, txnMetadata.txnStartTimestamp());
+        assertEquals(time.milliseconds() - 1, 
txnMetadata.txnLastUpdateTimestamp());
+    }
+
+    @Test
+    public void testFenceProducerAfterEpochsExhausted() {
+        short producerEpoch = (short) (Short.MAX_VALUE - 1);
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.ONGOING,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            TV_0);
+        assertTrue(txnMetadata.isProducerEpochExhausted());
+
+        TxnTransitMetadata fencingTransitMetadata = 
txnMetadata.prepareFenceProducerEpoch();
+        assertEquals(Short.MAX_VALUE, fencingTransitMetadata.producerEpoch());
+        assertEquals(RecordBatch.NO_PRODUCER_EPOCH, 
fencingTransitMetadata.lastProducerEpoch());
+        assertEquals(Optional.of(TransactionState.PREPARE_EPOCH_FENCE), 
txnMetadata.pendingState());
+
+        // We should reset the pending state to make way for the abort 
transition.
+        txnMetadata.pendingState(Optional.empty());
+
+        TxnTransitMetadata transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_ABORT, TV_0, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds(), false);
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId, transitMetadata.producerId());
+    }
+
+    @Test
+    public void testInvalidTransitionFromCompleteCommitToFence() {
+        short producerEpoch = (short) (Short.MAX_VALUE - 1);
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.COMPLETE_COMMIT,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            TV_0);
+        assertTrue(txnMetadata.isProducerEpochExhausted());
+
+        assertThrows(IllegalStateException.class, 
txnMetadata::prepareFenceProducerEpoch);
+    }
+
+    @Test
+    public void testInvalidTransitionFromCompleteAbortToFence() {
+        short producerEpoch = (short) (Short.MAX_VALUE - 1);
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.COMPLETE_ABORT,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            TV_0);
+        assertTrue(txnMetadata.isProducerEpochExhausted());
+
+        assertThrows(IllegalStateException.class, 
txnMetadata::prepareFenceProducerEpoch);
+    }
+
+    @Test
+    public void testFenceProducerNotAllowedIfItWouldOverflow() {
+        short producerEpoch = Short.MAX_VALUE;
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.ONGOING,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            TV_0);
+        assertTrue(txnMetadata.isProducerEpochExhausted());
+
+        // When epoch is at max, prepareFenceProducerEpoch logs an error but 
doesn't throw
+        // This allows graceful recovery through producer ID rotation
+        TxnTransitMetadata preparedMetadata = 
txnMetadata.prepareFenceProducerEpoch();
+
+        // Epoch should remain at Short.MaxValue (not overflow to negative)
+        assertEquals(Short.MAX_VALUE, preparedMetadata.producerEpoch());
+        assertEquals(TransactionState.PREPARE_EPOCH_FENCE, 
preparedMetadata.txnState());
+    }
+
+    @Test
+    public void testRotateProducerId() {
+        short producerEpoch = (short) (Short.MAX_VALUE - 1);
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.EMPTY,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            TV_0);
+
+        long newProducerId = 9893L;
+        TxnTransitMetadata transitMetadata = 
txnMetadata.prepareProducerIdRotation(newProducerId, 30000, 
time.milliseconds(), true);
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(newProducerId, txnMetadata.producerId());
+        assertEquals(producerId, txnMetadata.prevProducerId());
+        assertEquals(0, txnMetadata.producerEpoch());
+        assertEquals(producerEpoch, txnMetadata.lastProducerEpoch());
+    }
+
+    @Test
+    public void testEpochBumpOnEndTxn() {
+        time.sleep(100);
+        short producerEpoch = 10;
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.ONGOING,
+            Set.of(),
+            time.milliseconds(),
+            time.milliseconds(),
+            TV_2);
+
+        TxnTransitMetadata transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_COMMIT, TV_2, 
RecordBatch.NO_PRODUCER_ID, time.milliseconds() - 1, false);
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals((short) (producerEpoch + 1), txnMetadata.producerEpoch());
+        assertEquals(TV_2, txnMetadata.clientTransactionVersion());
+
+        transitMetadata = txnMetadata.prepareComplete(time.milliseconds());
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals((short) (producerEpoch + 1), txnMetadata.producerEpoch());
+        assertEquals(TV_2, txnMetadata.clientTransactionVersion());
+    }
+
+    @Test
+    public void testEpochBumpOnEndTxnOverflow() {
+        time.sleep(100);
+        short producerEpoch = (short) (Short.MAX_VALUE - 1);
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.ONGOING,
+            Set.of(),
+            time.milliseconds(),
+            time.milliseconds(),
+            TV_2);
+        assertTrue(txnMetadata.isProducerEpochExhausted());
+
+        long newProducerId = 9893L;
+        TxnTransitMetadata transitMetadata = 
txnMetadata.prepareAbortOrCommit(TransactionState.PREPARE_COMMIT, TV_2, 
newProducerId, time.milliseconds() - 1, false);
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(Short.MAX_VALUE, txnMetadata.producerEpoch());
+        assertEquals(producerEpoch, txnMetadata.lastProducerEpoch());
+        assertEquals(TV_2, txnMetadata.clientTransactionVersion());
+
+        transitMetadata = txnMetadata.prepareComplete(time.milliseconds());
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(newProducerId, txnMetadata.producerId());
+        assertEquals(0, txnMetadata.producerEpoch());
+        assertEquals(producerEpoch, txnMetadata.lastProducerEpoch());
+        assertEquals(TV_2, txnMetadata.clientTransactionVersion());
+    }
+
+    @Test
+    public void testRotateProducerIdInOngoingState() {
+        assertThrows(IllegalStateException.class, () -> 
testRotateProducerIdInOngoingState(TransactionState.ONGOING, TV_0));
+    }
+
+    @ParameterizedTest
+    @ValueSource(shorts = {0, 2})
+    public void testRotateProducerIdInPrepareAbortState(short 
transactionVersion) {
+        TransactionVersion clientTransactionVersion = 
TransactionVersion.fromFeatureLevel(transactionVersion);
+        assertThrows(IllegalStateException.class, () -> 
testRotateProducerIdInOngoingState(TransactionState.PREPARE_ABORT, 
clientTransactionVersion));
+    }
+
+    @ParameterizedTest
+    @ValueSource(shorts = {0, 2})
+    public void testRotateProducerIdInPrepareCommitState(short 
transactionVersion) {
+        TransactionVersion clientTransactionVersion = 
TransactionVersion.fromFeatureLevel(transactionVersion);
+        assertThrows(IllegalStateException.class, () -> 
testRotateProducerIdInOngoingState(TransactionState.PREPARE_COMMIT, 
clientTransactionVersion));
+    }
+
+    @Test
+    public void testAttemptedEpochBumpWithNewlyCreatedMetadata() {
+        short producerEpoch = 735;
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.EMPTY,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            TV_0);
+
+        TxnTransitMetadata transitMetadata = 
prepareSuccessfulIncrementProducerEpoch(txnMetadata, 
Optional.of(producerEpoch));
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(0, txnMetadata.producerEpoch());
+        assertEquals(RecordBatch.NO_PRODUCER_EPOCH, 
txnMetadata.lastProducerEpoch());
+    }
+
+    @Test
+    public void testEpochBumpWithCurrentEpochProvided() {
+        short producerEpoch = 735;
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            TransactionState.EMPTY,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            TV_0);
+
+        TxnTransitMetadata transitMetadata = 
prepareSuccessfulIncrementProducerEpoch(txnMetadata, 
Optional.of(producerEpoch));
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(producerEpoch + 1, txnMetadata.producerEpoch());
+        assertEquals(producerEpoch, txnMetadata.lastProducerEpoch());
+    }
+
+    @Test
+    public void testAttemptedEpochBumpWithLastEpoch() {
+        short producerEpoch = 735;
+        short lastProducerEpoch = (short) (producerEpoch - 1);
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            lastProducerEpoch,
+            30000,
+            TransactionState.EMPTY,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            TV_0);
+
+        TxnTransitMetadata transitMetadata = 
prepareSuccessfulIncrementProducerEpoch(txnMetadata, 
Optional.of(lastProducerEpoch));
+        txnMetadata.completeTransitionTo(transitMetadata);
+        assertEquals(producerId, txnMetadata.producerId());
+        assertEquals(producerEpoch, txnMetadata.producerEpoch());
+        assertEquals(lastProducerEpoch, txnMetadata.lastProducerEpoch());
+    }
+
+    @Test
+    public void testAttemptedEpochBumpWithFencedEpoch() {
+        short producerEpoch = 735;
+        short lastProducerEpoch = (short) (producerEpoch - 1);
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            lastProducerEpoch,
+            30000,
+            TransactionState.EMPTY,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            TV_0);
+
+        assertThrows(
+            ProducerFencedException.class, 
+            () -> txnMetadata.prepareIncrementProducerEpoch(30000, 
Optional.of((short) (lastProducerEpoch - 1)), time.milliseconds())
+        );
+    }
+
+    @Test
+    public void testTransactionStateIdAndNameMapping() {
+        for (TransactionState state : TransactionState.ALL_STATES) {
+            assertEquals(state, TransactionState.fromId(state.id()));
+            assertEquals(Optional.of(state), 
TransactionState.fromName(state.stateName()));
+
+            if (state != TransactionState.DEAD) {
+                org.apache.kafka.clients.admin.TransactionState 
clientTransactionState =
+                    
org.apache.kafka.clients.admin.TransactionState.parse(state.stateName());
+                assertEquals(state.stateName(), 
clientTransactionState.toString());
+                
assertNotEquals(org.apache.kafka.clients.admin.TransactionState.UNKNOWN, 
clientTransactionState);
+            }
+        }
+    }
+
+    @Test
+    public void testAllTransactionStatesAreMapped() {
+        Set<TransactionState> unmatchedStates = new HashSet<>(Set.of(
+            TransactionState.EMPTY,
+            TransactionState.ONGOING,
+            TransactionState.PREPARE_COMMIT,
+            TransactionState.PREPARE_ABORT,
+            TransactionState.COMPLETE_COMMIT,
+            TransactionState.COMPLETE_ABORT,
+            TransactionState.PREPARE_EPOCH_FENCE,
+            TransactionState.DEAD
+        ));
+
+        // The exhaustive match is intentional here to ensure that we are
+        // forced to update the test case if a new state is added.
+        for (TransactionState state : TransactionState.ALL_STATES) {
+            switch (state) {
+                case EMPTY -> 
assertTrue(unmatchedStates.remove(TransactionState.EMPTY));
+                case ONGOING -> 
assertTrue(unmatchedStates.remove(TransactionState.ONGOING));
+                case PREPARE_COMMIT -> 
assertTrue(unmatchedStates.remove(TransactionState.PREPARE_COMMIT));
+                case PREPARE_ABORT -> 
assertTrue(unmatchedStates.remove(TransactionState.PREPARE_ABORT));
+                case COMPLETE_COMMIT -> 
assertTrue(unmatchedStates.remove(TransactionState.COMPLETE_COMMIT));
+                case COMPLETE_ABORT -> 
assertTrue(unmatchedStates.remove(TransactionState.COMPLETE_ABORT));
+                case PREPARE_EPOCH_FENCE -> 
assertTrue(unmatchedStates.remove(TransactionState.PREPARE_EPOCH_FENCE));
+                case DEAD -> 
assertTrue(unmatchedStates.remove(TransactionState.DEAD));
+                default -> fail("Unexpected transaction state " + state);
+            }
+        }
+
+        assertEquals(Set.of(), unmatchedStates);
+    }
+
+    private void testRotateProducerIdInOngoingState(TransactionState state, 
TransactionVersion clientTransactionVersion) {
+        short producerEpoch = (short) (Short.MAX_VALUE - 1);
+
+        TransactionMetadata txnMetadata = new TransactionMetadata(
+            transactionalId,
+            producerId,
+            producerId,
+            RecordBatch.NO_PRODUCER_ID,
+            producerEpoch,
+            RecordBatch.NO_PRODUCER_EPOCH,
+            30000,
+            state,
+            Set.of(),
+            -1,
+            time.milliseconds(),
+            clientTransactionVersion
+        );
+        long newProducerId = 9893L;
+        txnMetadata.prepareProducerIdRotation(newProducerId, 30000, 
time.milliseconds(), false);
+    }
+
+    private TxnTransitMetadata prepareSuccessfulIncrementProducerEpoch(
+        TransactionMetadata txnMetadata,
+        Optional<Short> expectedProducerEpoch
+    ) {
+        return prepareSuccessfulIncrementProducerEpoch(txnMetadata, 
expectedProducerEpoch, Optional.empty());
+    }
+
+    private TxnTransitMetadata prepareSuccessfulIncrementProducerEpoch(
+        TransactionMetadata txnMetadata,
+        Optional<Short> expectedProducerEpoch,
+        Optional<Long> now
+    ) {
+        return txnMetadata.prepareIncrementProducerEpoch(30000, 
expectedProducerEpoch, now.orElse(time.milliseconds()));
+    }
+}

Reply via email to