-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-16452: Don't throw OOORE when converting the offset to metadata #15825
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 21 commits
33bf18b
5fc04f6
9143e45
5345bd3
d23f426
a4e31e0
afca50e
ff3ab0e
5945dec
9f22047
f26d381
14efe65
503732a
06ee554
56604d6
ce09bcd
7e97904
eb75573
f5ce2a6
6e3f113
7ce211e
5d080ad
34d8ca0
59e6a7b
21a6d27
91bd96b
d4668be
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 |
|---|---|---|
|
|
@@ -91,19 +91,23 @@ class DelayedFetch( | |
| // Go directly to the check for Case G if the message offsets are the same. If the log segment | ||
| // has just rolled, then the high watermark offset will remain the same but be on the old segment, | ||
| // which would incorrectly be seen as an instance of Case F. | ||
| if (endOffset.messageOffset != fetchOffset.messageOffset) { | ||
| if (endOffset.onOlderSegment(fetchOffset)) { | ||
| // Case F, this can happen when the new fetch operation is on a truncated leader | ||
| debug(s"Satisfying fetch $this since it is fetching later segments of partition $topicIdPartition.") | ||
| return forceComplete() | ||
| if (fetchOffset.messageOffset > endOffset.messageOffset) { | ||
| // Case F, this can happen when the new fetch operation is on a truncated leader | ||
| debug(s"Satisfying fetch $this since it is fetching later segments of partition $topicIdPartition.") | ||
| return forceComplete() | ||
| } else if (fetchOffset.messageOffset < endOffset.messageOffset) { | ||
| if (fetchOffset.messageOffsetOnly() || endOffset.messageOffsetOnly()) { | ||
|
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. Could we fold this into the condition above?
Contributor
Author
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. Not clear on this one. The current
Contributor
Author
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. With the latest changes in LogOffsetMetadata, it seems we only have to add one check in DelayedFetch.scala#106 compared to trunk, that would suffice:
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. Yes, since |
||
| // If fetchOffset or endOffset is message only, we return empty records when reading from the log. | ||
| // So, to be consistent, we want to avoid accumulating new bytes in this case. This can happen when the | ||
| // high-watermark is stale, but should be rare. | ||
| } else if (fetchOffset.onOlderSegment(endOffset)) { | ||
| // Case F, this can happen when the fetch operation is falling behind the current segment | ||
| // or the partition has just rolled a new segment | ||
| debug(s"Satisfying fetch $this immediately since it is fetching older segments.") | ||
| // We will not force complete the fetch request if a replica should be throttled. | ||
| if (!params.isFromFollower || !replicaManager.shouldLeaderThrottle(quota, partition, params.replicaId)) | ||
| return forceComplete() | ||
| } else if (fetchOffset.messageOffset < endOffset.messageOffset) { | ||
| } else if (fetchOffset.onSameSegment(endOffset)) { | ||
| // we take the partition fetch size as upper bound when accumulating the bytes (skip if a throttled partition) | ||
| val bytesAvailable = math.min(endOffset.positionDiff(fetchOffset), fetchStatus.fetchInfo.maxBytes) | ||
| if (!params.isFromFollower || !replicaManager.shouldLeaderThrottle(quota, partition, params.replicaId)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,8 @@ import org.apache.kafka.common.requests.FetchRequest | |
| import org.apache.kafka.storage.internals.log.{FetchDataInfo, FetchIsolation, FetchParams, FetchPartitionData, LogOffsetMetadata, LogOffsetSnapshot} | ||
| import org.junit.jupiter.api.Test | ||
| import org.junit.jupiter.api.Assertions._ | ||
| import org.junit.jupiter.params.ParameterizedTest | ||
| import org.junit.jupiter.params.provider.ValueSource | ||
| import org.mockito.ArgumentMatchers.{any, anyInt} | ||
| import org.mockito.Mockito.{mock, when} | ||
|
|
||
|
|
@@ -46,7 +48,7 @@ class DelayedFetchTest { | |
|
|
||
| val fetchStatus = FetchPartitionStatus( | ||
| startOffsetMetadata = new LogOffsetMetadata(fetchOffset), | ||
| fetchInfo = new FetchRequest.PartitionData(Uuid.ZERO_UUID, fetchOffset, logStartOffset, maxBytes, currentLeaderEpoch)) | ||
| fetchInfo = new FetchRequest.PartitionData(topicIdPartition.topicId(), fetchOffset, logStartOffset, maxBytes, currentLeaderEpoch)) | ||
| val fetchParams = buildFollowerFetchParams(replicaId, maxWaitMs = 500) | ||
|
|
||
| var fetchResultOpt: Option[FetchPartitionData] = None | ||
|
|
@@ -92,7 +94,7 @@ class DelayedFetchTest { | |
|
|
||
| val fetchStatus = FetchPartitionStatus( | ||
| startOffsetMetadata = new LogOffsetMetadata(fetchOffset), | ||
| fetchInfo = new FetchRequest.PartitionData(Uuid.ZERO_UUID, fetchOffset, logStartOffset, maxBytes, currentLeaderEpoch)) | ||
| fetchInfo = new FetchRequest.PartitionData(topicIdPartition.topicId(), fetchOffset, logStartOffset, maxBytes, currentLeaderEpoch)) | ||
| val fetchParams = buildFollowerFetchParams(replicaId, maxWaitMs = 500) | ||
|
|
||
| var fetchResultOpt: Option[FetchPartitionData] = None | ||
|
|
@@ -116,6 +118,9 @@ class DelayedFetchTest { | |
| assertTrue(delayedFetch.tryComplete()) | ||
| assertTrue(delayedFetch.isCompleted) | ||
| assertTrue(fetchResultOpt.isDefined) | ||
|
|
||
| val fetchResult = fetchResultOpt.get | ||
| assertEquals(Errors.NOT_LEADER_OR_FOLLOWER, fetchResult.error) | ||
| } | ||
|
|
||
| @Test | ||
|
|
@@ -164,18 +169,71 @@ class DelayedFetchTest { | |
| assertTrue(delayedFetch.tryComplete()) | ||
| assertTrue(delayedFetch.isCompleted) | ||
| assertTrue(fetchResultOpt.isDefined) | ||
|
|
||
| val fetchResult = fetchResultOpt.get | ||
| assertEquals(Errors.NONE, fetchResult.error) | ||
| } | ||
|
|
||
| @ParameterizedTest(name = "testDelayedFetchWithMessageOnlyHighWatermark endOffset={0}") | ||
| @ValueSource(longs = Array(0, 500)) | ||
| def testDelayedFetchWithMessageOnlyHighWatermark(endOffset: Long): Unit = { | ||
| val topicIdPartition = new TopicIdPartition(Uuid.randomUuid(), 0, "topic") | ||
| val fetchOffset = 450L | ||
| val logStartOffset = 5L | ||
| val currentLeaderEpoch = Optional.of[Integer](10) | ||
| val replicaId = 1 | ||
|
|
||
| val fetchStatus = FetchPartitionStatus( | ||
| startOffsetMetadata = new LogOffsetMetadata(fetchOffset), | ||
| fetchInfo = new FetchRequest.PartitionData(topicIdPartition.topicId, fetchOffset, logStartOffset, maxBytes, currentLeaderEpoch)) | ||
| val fetchParams = buildFollowerFetchParams(replicaId, maxWaitMs = 500) | ||
|
|
||
| var fetchResultOpt: Option[FetchPartitionData] = None | ||
| def callback(responses: Seq[(TopicIdPartition, FetchPartitionData)]): Unit = { | ||
| fetchResultOpt = Some(responses.head._2) | ||
| } | ||
|
|
||
| val delayedFetch = new DelayedFetch( | ||
| params = fetchParams, | ||
| fetchPartitionStatus = Seq(topicIdPartition -> fetchStatus), | ||
| replicaManager = replicaManager, | ||
| quota = replicaQuota, | ||
| responseCallback = callback | ||
| ) | ||
|
|
||
| val partition: Partition = mock(classOf[Partition]) | ||
| when(replicaManager.getPartitionOrException(topicIdPartition.topicPartition)).thenReturn(partition) | ||
| // Note that the high-watermark does not contain the complete metadata | ||
| val endOffsetMetadata = new LogOffsetMetadata(endOffset, -1L, -1) | ||
| when(partition.fetchOffsetSnapshot( | ||
| currentLeaderEpoch, | ||
| fetchOnlyFromLeader = true)) | ||
| .thenReturn(new LogOffsetSnapshot(0L, endOffsetMetadata, endOffsetMetadata, endOffsetMetadata)) | ||
| when(replicaManager.isAddingReplica(any(), anyInt())).thenReturn(false) | ||
| expectReadFromReplica(fetchParams, topicIdPartition, fetchStatus.fetchInfo, Errors.NONE) | ||
|
|
||
| // 1. When `endOffset` is 0, it refers to the truncation case | ||
| // 2. When `endOffset` is 500, it refers to the normal case | ||
| val expected = endOffset == 0 | ||
|
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, in both cases, we are not forcing the completion of the delayed fetch, right?
Contributor
Author
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. when fetchOffset > endOffset, This is the only behavior change post the refactor. Previously when fetchOffset > endOffset:
We can retain the same behavior if required.
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. Got it. We won't complete when |
||
| assertEquals(expected, delayedFetch.tryComplete()) | ||
|
kamalcph marked this conversation as resolved.
|
||
| assertEquals(expected, delayedFetch.isCompleted) | ||
|
kamalcph marked this conversation as resolved.
|
||
| assertEquals(expected, fetchResultOpt.isDefined) | ||
| if (fetchResultOpt.isDefined) { | ||
| assertEquals(Errors.NONE, fetchResultOpt.get.error) | ||
| } | ||
| } | ||
|
|
||
| private def buildFollowerFetchParams( | ||
| replicaId: Int, | ||
| maxWaitMs: Int | ||
| maxWaitMs: Int, | ||
| minBytes: Int = 1, | ||
| ): FetchParams = { | ||
| new FetchParams( | ||
| ApiKeys.FETCH.latestVersion, | ||
| replicaId, | ||
| 1, | ||
| maxWaitMs, | ||
| 1, | ||
| minBytes, | ||
| maxBytes, | ||
| FetchIsolation.LOG_END, | ||
| Optional.empty() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.