Skip to content

Commit 54cf2eb

Browse files
authored
MINOR: Initialize fetchPartitionStatus as a Map type to reduce collection conversions (#20768)
see #19876 (comment) Initialize `fetchPartitionStatus` as a `Map` type to reduce unnecessary collection conversions. Reviewers: Ismael Juma <ismael@juma.me.uk>, Kamal Chandraprakash <kamal.chandraprakash@gmail.com>, Chia-Ping Tsai <chia7712@gmail.com>
1 parent 28ff2a4 commit 54cf2eb

File tree

4 files changed

+95
-79
lines changed

4 files changed

+95
-79
lines changed

core/src/main/scala/kafka/server/DelayedFetch.scala

Lines changed: 66 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import org.apache.kafka.server.purgatory.DelayedOperation
3030
import org.apache.kafka.server.storage.log.{FetchIsolation, FetchParams, FetchPartitionData}
3131
import org.apache.kafka.storage.internals.log.{FetchPartitionStatus, LogOffsetMetadata}
3232

33+
import java.util
3334
import scala.collection._
3435
import scala.jdk.CollectionConverters._
3536

@@ -39,7 +40,7 @@ import scala.jdk.CollectionConverters._
3940
*/
4041
class DelayedFetch(
4142
params: FetchParams,
42-
fetchPartitionStatus: Seq[(TopicIdPartition, FetchPartitionStatus)],
43+
fetchPartitionStatus: util.LinkedHashMap[TopicIdPartition, FetchPartitionStatus],
4344
replicaManager: ReplicaManager,
4445
quota: ReplicaQuota,
4546
responseCallback: Seq[(TopicIdPartition, FetchPartitionData)] => Unit
@@ -66,79 +67,78 @@ class DelayedFetch(
6667
*/
6768
override def tryComplete(): Boolean = {
6869
var accumulatedSize = 0
69-
fetchPartitionStatus.foreach {
70-
case (topicIdPartition, fetchStatus) =>
71-
val fetchOffset = fetchStatus.startOffsetMetadata
72-
val fetchLeaderEpoch = fetchStatus.fetchInfo.currentLeaderEpoch
73-
try {
74-
if (fetchOffset != LogOffsetMetadata.UNKNOWN_OFFSET_METADATA) {
75-
val partition = replicaManager.getPartitionOrException(topicIdPartition.topicPartition)
76-
val offsetSnapshot = partition.fetchOffsetSnapshot(fetchLeaderEpoch, params.fetchOnlyLeader)
77-
78-
val endOffset = params.isolation match {
79-
case FetchIsolation.LOG_END => offsetSnapshot.logEndOffset
80-
case FetchIsolation.HIGH_WATERMARK => offsetSnapshot.highWatermark
81-
case FetchIsolation.TXN_COMMITTED => offsetSnapshot.lastStableOffset
82-
}
70+
fetchPartitionStatus.forEach { (topicIdPartition, fetchStatus) =>
71+
val fetchOffset = fetchStatus.startOffsetMetadata
72+
val fetchLeaderEpoch = fetchStatus.fetchInfo.currentLeaderEpoch
73+
try {
74+
if (fetchOffset != LogOffsetMetadata.UNKNOWN_OFFSET_METADATA) {
75+
val partition = replicaManager.getPartitionOrException(topicIdPartition.topicPartition)
76+
val offsetSnapshot = partition.fetchOffsetSnapshot(fetchLeaderEpoch, params.fetchOnlyLeader)
77+
78+
val endOffset = params.isolation match {
79+
case FetchIsolation.LOG_END => offsetSnapshot.logEndOffset
80+
case FetchIsolation.HIGH_WATERMARK => offsetSnapshot.highWatermark
81+
case FetchIsolation.TXN_COMMITTED => offsetSnapshot.lastStableOffset
82+
}
8383

84-
// Go directly to the check for Case G if the message offsets are the same. If the log segment
85-
// has just rolled, then the high watermark offset will remain the same but be on the old segment,
86-
// which would incorrectly be seen as an instance of Case F.
87-
if (fetchOffset.messageOffset > endOffset.messageOffset) {
88-
// Case F, this can happen when the new fetch operation is on a truncated leader
89-
debug(s"Satisfying fetch $this since it is fetching later segments of partition $topicIdPartition.")
90-
return forceComplete()
91-
} else if (fetchOffset.messageOffset < endOffset.messageOffset) {
92-
if (fetchOffset.onOlderSegment(endOffset)) {
93-
// Case F, this can happen when the fetch operation is falling behind the current segment
94-
// or the partition has just rolled a new segment
95-
debug(s"Satisfying fetch $this immediately since it is fetching older segments.")
96-
// We will not force complete the fetch request if a replica should be throttled.
97-
if (!params.isFromFollower || !replicaManager.shouldLeaderThrottle(quota, partition, params.replicaId))
98-
return forceComplete()
99-
} else if (fetchOffset.onSameSegment(endOffset)) {
100-
// we take the partition fetch size as upper bound when accumulating the bytes (skip if a throttled partition)
101-
val bytesAvailable = math.min(endOffset.positionDiff(fetchOffset), fetchStatus.fetchInfo.maxBytes)
102-
if (!params.isFromFollower || !replicaManager.shouldLeaderThrottle(quota, partition, params.replicaId))
103-
accumulatedSize += bytesAvailable
104-
}
84+
// Go directly to the check for Case G if the message offsets are the same. If the log segment
85+
// has just rolled, then the high watermark offset will remain the same but be on the old segment,
86+
// which would incorrectly be seen as an instance of Case F.
87+
if (fetchOffset.messageOffset > endOffset.messageOffset) {
88+
// Case F, this can happen when the new fetch operation is on a truncated leader
89+
debug(s"Satisfying fetch $this since it is fetching later segments of partition $topicIdPartition.")
90+
return forceComplete()
91+
} else if (fetchOffset.messageOffset < endOffset.messageOffset) {
92+
if (fetchOffset.onOlderSegment(endOffset)) {
93+
// Case F, this can happen when the fetch operation is falling behind the current segment
94+
// or the partition has just rolled a new segment
95+
debug(s"Satisfying fetch $this immediately since it is fetching older segments.")
96+
// We will not force complete the fetch request if a replica should be throttled.
97+
if (!params.isFromFollower || !replicaManager.shouldLeaderThrottle(quota, partition, params.replicaId))
98+
return forceComplete()
99+
} else if (fetchOffset.onSameSegment(endOffset)) {
100+
// we take the partition fetch size as upper bound when accumulating the bytes (skip if a throttled partition)
101+
val bytesAvailable = math.min(endOffset.positionDiff(fetchOffset), fetchStatus.fetchInfo.maxBytes)
102+
if (!params.isFromFollower || !replicaManager.shouldLeaderThrottle(quota, partition, params.replicaId))
103+
accumulatedSize += bytesAvailable
105104
}
105+
}
106106

107-
// Case H: If truncation has caused diverging epoch while this request was in purgatory, return to trigger truncation
108-
fetchStatus.fetchInfo.lastFetchedEpoch.ifPresent { fetchEpoch =>
109-
val epochEndOffset = partition.lastOffsetForLeaderEpoch(fetchLeaderEpoch, fetchEpoch, fetchOnlyFromLeader = false)
110-
if (epochEndOffset.errorCode != Errors.NONE.code()
111-
|| epochEndOffset.endOffset == UNDEFINED_EPOCH_OFFSET
112-
|| epochEndOffset.leaderEpoch == UNDEFINED_EPOCH) {
113-
debug(s"Could not obtain last offset for leader epoch for partition $topicIdPartition, epochEndOffset=$epochEndOffset.")
114-
return forceComplete()
115-
} else if (epochEndOffset.leaderEpoch < fetchEpoch || epochEndOffset.endOffset < fetchStatus.fetchInfo.fetchOffset) {
116-
debug(s"Satisfying fetch $this since it has diverging epoch requiring truncation for partition " +
117-
s"$topicIdPartition epochEndOffset=$epochEndOffset fetchEpoch=$fetchEpoch fetchOffset=${fetchStatus.fetchInfo.fetchOffset}.")
118-
return forceComplete()
119-
}
107+
// Case H: If truncation has caused diverging epoch while this request was in purgatory, return to trigger truncation
108+
fetchStatus.fetchInfo.lastFetchedEpoch.ifPresent { fetchEpoch =>
109+
val epochEndOffset = partition.lastOffsetForLeaderEpoch(fetchLeaderEpoch, fetchEpoch, fetchOnlyFromLeader = false)
110+
if (epochEndOffset.errorCode != Errors.NONE.code()
111+
|| epochEndOffset.endOffset == UNDEFINED_EPOCH_OFFSET
112+
|| epochEndOffset.leaderEpoch == UNDEFINED_EPOCH) {
113+
debug(s"Could not obtain last offset for leader epoch for partition $topicIdPartition, epochEndOffset=$epochEndOffset.")
114+
return forceComplete()
115+
} else if (epochEndOffset.leaderEpoch < fetchEpoch || epochEndOffset.endOffset < fetchStatus.fetchInfo.fetchOffset) {
116+
debug(s"Satisfying fetch $this since it has diverging epoch requiring truncation for partition " +
117+
s"$topicIdPartition epochEndOffset=$epochEndOffset fetchEpoch=$fetchEpoch fetchOffset=${fetchStatus.fetchInfo.fetchOffset}.")
118+
return forceComplete()
120119
}
121120
}
122-
} catch {
123-
case _: NotLeaderOrFollowerException => // Case A or Case B
124-
debug(s"Broker is no longer the leader or follower of $topicIdPartition, satisfy $this immediately")
125-
return forceComplete()
126-
case _: UnknownTopicOrPartitionException => // Case C
127-
debug(s"Broker no longer knows of partition $topicIdPartition, satisfy $this immediately")
128-
return forceComplete()
129-
case _: KafkaStorageException => // Case D
130-
debug(s"Partition $topicIdPartition is in an offline log directory, satisfy $this immediately")
131-
return forceComplete()
132-
case _: FencedLeaderEpochException => // Case E
133-
debug(s"Broker is the leader of partition $topicIdPartition, but the requested epoch " +
134-
s"$fetchLeaderEpoch is fenced by the latest leader epoch, satisfy $this immediately")
135-
return forceComplete()
136121
}
122+
} catch {
123+
case _: NotLeaderOrFollowerException => // Case A or Case B
124+
debug(s"Broker is no longer the leader or follower of $topicIdPartition, satisfy $this immediately")
125+
return forceComplete()
126+
case _: UnknownTopicOrPartitionException => // Case C
127+
debug(s"Broker no longer knows of partition $topicIdPartition, satisfy $this immediately")
128+
return forceComplete()
129+
case _: KafkaStorageException => // Case D
130+
debug(s"Partition $topicIdPartition is in an offline log directory, satisfy $this immediately")
131+
return forceComplete()
132+
case _: FencedLeaderEpochException => // Case E
133+
debug(s"Broker is the leader of partition $topicIdPartition, but the requested epoch " +
134+
s"$fetchLeaderEpoch is fenced by the latest leader epoch, satisfy $this immediately")
135+
return forceComplete()
136+
}
137137
}
138138

139139
// Case G
140140
if (accumulatedSize >= params.minBytes)
141-
forceComplete()
141+
forceComplete()
142142
else
143143
false
144144
}
@@ -154,9 +154,9 @@ class DelayedFetch(
154154
* Upon completion, read whatever data is available and pass to the complete callback
155155
*/
156156
override def onComplete(): Unit = {
157-
val fetchInfos = fetchPartitionStatus.map { case (tp, status) =>
157+
val fetchInfos = fetchPartitionStatus.asScala.iterator.map { case (tp, status) =>
158158
tp -> status.fetchInfo
159-
}
159+
}.toBuffer
160160

161161
val logReadResults = replicaManager.readFromLog(
162162
params,

core/src/main/scala/kafka/server/ReplicaManager.scala

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1628,7 +1628,7 @@ class ReplicaManager(val config: KafkaConfig,
16281628
params: FetchParams,
16291629
responseCallback: Seq[(TopicIdPartition, FetchPartitionData)] => Unit,
16301630
logReadResults: util.LinkedHashMap[TopicIdPartition, LogReadResult],
1631-
fetchPartitionStatus: Seq[(TopicIdPartition, FetchPartitionStatus)]): Unit = {
1631+
fetchPartitionStatus: util.LinkedHashMap[TopicIdPartition, FetchPartitionStatus]): Unit = {
16321632
val remoteFetchTasks = new util.HashMap[TopicIdPartition, Future[Void]]
16331633
val remoteFetchResults = new util.HashMap[TopicIdPartition, CompletableFuture[RemoteLogReadResult]]
16341634

@@ -1643,7 +1643,7 @@ class ReplicaManager(val config: KafkaConfig,
16431643
remoteFetchResults,
16441644
remoteFetchInfos,
16451645
remoteFetchMaxWaitMs,
1646-
fetchPartitionStatus.toMap.asJava,
1646+
fetchPartitionStatus,
16471647
params,
16481648
logReadResults,
16491649
tp => getPartitionOrException(tp),
@@ -1710,17 +1710,17 @@ class ReplicaManager(val config: KafkaConfig,
17101710
responseCallback(fetchPartitionData)
17111711
} else {
17121712
// construct the fetch results from the read results
1713-
val fetchPartitionStatus = new mutable.ArrayBuffer[(TopicIdPartition, FetchPartitionStatus)]
1713+
val fetchPartitionStatus = new util.LinkedHashMap[TopicIdPartition, FetchPartitionStatus]
17141714
fetchInfos.foreach { case (topicIdPartition, partitionData) =>
17151715
val logReadResult = logReadResultMap.get(topicIdPartition)
17161716
if (logReadResult != null) {
17171717
val logOffsetMetadata = logReadResult.info.fetchOffsetMetadata
1718-
fetchPartitionStatus += (topicIdPartition -> new FetchPartitionStatus(logOffsetMetadata, partitionData))
1718+
fetchPartitionStatus.put(topicIdPartition, new FetchPartitionStatus(logOffsetMetadata, partitionData))
17191719
}
17201720
}
17211721

17221722
if (!remoteFetchInfos.isEmpty) {
1723-
processRemoteFetches(remoteFetchInfos, params, responseCallback, logReadResultMap, fetchPartitionStatus.toSeq)
1723+
processRemoteFetches(remoteFetchInfos, params, responseCallback, logReadResultMap, fetchPartitionStatus)
17241724
} else {
17251725
// If there is not enough data to respond and there is no remote data, we will let the fetch request
17261726
// wait for new data.
@@ -1733,12 +1733,15 @@ class ReplicaManager(val config: KafkaConfig,
17331733
)
17341734

17351735
// create a list of (topic, partition) pairs to use as keys for this delayed fetch operation
1736-
val delayedFetchKeys = fetchPartitionStatus.map { case (tp, _) => new TopicPartitionOperationKey(tp) }.toList
1736+
val delayedFetchKeys = fetchPartitionStatus.keySet()
1737+
.stream()
1738+
.map(new TopicPartitionOperationKey(_))
1739+
.toList()
17371740

17381741
// try to complete the request immediately, otherwise put it into the purgatory;
17391742
// this is because while the delayed fetch operation is being created, new requests
17401743
// may arrive and hence make this operation completable.
1741-
delayedFetchPurgatory.tryCompleteElseWatch(delayedFetch, delayedFetchKeys.asJava)
1744+
delayedFetchPurgatory.tryCompleteElseWatch(delayedFetch, delayedFetchKeys)
17421745
}
17431746
}
17441747
}

core/src/test/scala/integration/kafka/server/DelayedFetchTest.scala

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import org.junit.jupiter.params.provider.ValueSource
3434
import org.mockito.ArgumentMatchers.{any, anyInt}
3535
import org.mockito.Mockito.{mock, when}
3636

37+
import java.util
38+
3739
class DelayedFetchTest {
3840
private val maxBytes = 1024
3941
private val replicaManager: ReplicaManager = mock(classOf[ReplicaManager])
@@ -59,7 +61,7 @@ class DelayedFetchTest {
5961

6062
val delayedFetch = new DelayedFetch(
6163
params = fetchParams,
62-
fetchPartitionStatus = Seq(topicIdPartition -> fetchStatus),
64+
fetchPartitionStatus = createFetchPartitionStatusMap(topicIdPartition, fetchStatus),
6365
replicaManager = replicaManager,
6466
quota = replicaQuota,
6567
responseCallback = callback
@@ -105,7 +107,7 @@ class DelayedFetchTest {
105107

106108
val delayedFetch = new DelayedFetch(
107109
params = fetchParams,
108-
fetchPartitionStatus = Seq(topicIdPartition -> fetchStatus),
110+
fetchPartitionStatus = createFetchPartitionStatusMap(topicIdPartition, fetchStatus),
109111
replicaManager = replicaManager,
110112
quota = replicaQuota,
111113
responseCallback = callback
@@ -145,7 +147,7 @@ class DelayedFetchTest {
145147

146148
val delayedFetch = new DelayedFetch(
147149
params = fetchParams,
148-
fetchPartitionStatus = Seq(topicIdPartition -> fetchStatus),
150+
fetchPartitionStatus = createFetchPartitionStatusMap(topicIdPartition, fetchStatus),
149151
replicaManager = replicaManager,
150152
quota = replicaQuota,
151153
responseCallback = callback
@@ -196,7 +198,7 @@ class DelayedFetchTest {
196198

197199
val delayedFetch = new DelayedFetch(
198200
params = fetchParams,
199-
fetchPartitionStatus = Seq(topicIdPartition -> fetchStatus),
201+
fetchPartitionStatus = createFetchPartitionStatusMap(topicIdPartition, fetchStatus),
200202
replicaManager = replicaManager,
201203
quota = replicaQuota,
202204
responseCallback = callback
@@ -267,4 +269,9 @@ class DelayedFetchTest {
267269
error)
268270
}
269271

272+
private def createFetchPartitionStatusMap(tpId: TopicIdPartition, status: FetchPartitionStatus): util.LinkedHashMap[TopicIdPartition, FetchPartitionStatus] = {
273+
val statusMap = new util.LinkedHashMap[TopicIdPartition, FetchPartitionStatus]
274+
statusMap.put(tpId, status)
275+
statusMap
276+
}
270277
}

core/src/test/scala/unit/kafka/server/ReplicaManagerQuotasTest.scala

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import org.mockito.ArgumentMatchers.{any, anyBoolean, anyInt, anyLong}
4040
import org.mockito.Mockito.{mock, when}
4141
import org.mockito.{AdditionalMatchers, ArgumentMatchers}
4242

43+
import java.util
4344
import scala.jdk.CollectionConverters._
4445

4546
class ReplicaManagerQuotasTest {
@@ -186,7 +187,7 @@ class ReplicaManagerQuotasTest {
186187

187188
new DelayedFetch(
188189
params = fetchParams,
189-
fetchPartitionStatus = Seq(tp -> fetchPartitionStatus),
190+
fetchPartitionStatus = createFetchPartitionStatusMap(tp, fetchPartitionStatus),
190191
replicaManager = replicaManager,
191192
quota = null,
192193
responseCallback = null
@@ -237,7 +238,7 @@ class ReplicaManagerQuotasTest {
237238

238239
new DelayedFetch(
239240
params = fetchParams,
240-
fetchPartitionStatus = Seq(tidp -> fetchPartitionStatus),
241+
fetchPartitionStatus = createFetchPartitionStatusMap(tidp, fetchPartitionStatus),
241242
replicaManager = replicaManager,
242243
quota = null,
243244
responseCallback = null
@@ -341,4 +342,9 @@ class ReplicaManagerQuotasTest {
341342
quota
342343
}
343344

345+
private def createFetchPartitionStatusMap(tpId: TopicIdPartition, status: FetchPartitionStatus): util.LinkedHashMap[TopicIdPartition, FetchPartitionStatus] = {
346+
val statusMap = new util.LinkedHashMap[TopicIdPartition, FetchPartitionStatus]
347+
statusMap.put(tpId, status)
348+
statusMap
349+
}
344350
}

0 commit comments

Comments
 (0)