junrao commented on code in PR #23780:
URL: https://github.com/apache/kafka/pull/23780#discussion_r4298114868


##########
storage/src/main/java/org/apache/kafka/storage/internals/log/UnifiedLog.java:
##########
@@ -1770,10 +1785,11 @@ public OffsetResultHolder fetchOffsetByTimestamp(long 
targetTimestamp, Optional<
 
                             
AsyncOffsetReadFutureHolder<OffsetResultHolder.FileRecordsOrError> 
asyncOffsetReadFutureHolder =
                                     
remoteOffsetReader.get().asyncOffsetRead(topicPartition(), targetTimestamp,
-                                    logStartOffset, leaderEpochCache, () -> 
searchOffsetInLocalLog(targetTimestamp, localLogStartOffset()));
+                                    logStartOffset, leaderEpochCache,
+                                        () -> 
searchOffsetInLocalLog(targetTimestamp, localLogStartOffset(), 
lastFetchableOffset));

Review Comment:
   indentation



##########
storage/src/main/java/org/apache/kafka/storage/internals/log/LogSegment.java:
##########
@@ -750,12 +750,121 @@ public long getFirstBatchTimestamp() {
      * @return the timestamp and offset of the first message that meets the 
requirements. Empty will be returned if there is no such message.
      */
     public Optional<FileRecords.TimestampAndOffset> findOffsetByTimestamp(long 
timestampMs, long startingOffset, int maxRecordBodySize) throws IOException {
+        return findOffsetByTimestamp(timestampMs, startingOffset, 
Long.MAX_VALUE, maxRecordBodySize);
+    }
+
+    /**
+     * Search for the first message whose timestamp is at least {@code 
timestampMs} and whose offset is at least
+     * {@code startingOffset} and strictly less than {@code 
exclusiveEndOffset}.
+     * {@code exclusiveEndOffset} is a batch boundary, such as the high 
watermark or last stable offset.
+     * Pass {@link Long#MAX_VALUE} to search the whole segment.
+     */
+    public Optional<FileRecords.TimestampAndOffset> findOffsetByTimestamp(long 
timestampMs, long startingOffset,
+                                                                          long 
exclusiveEndOffset, int maxRecordBodySize) throws IOException {
+        if (exclusiveEndOffset <= baseOffset)
+            return Optional.empty();
+
         // Get the index entry with a timestamp less than or equal to the 
target timestamp
         TimestampOffset timestampOffset = timeIndex().lookup(timestampMs);
         int position = offsetIndex().lookup(Math.max(timestampOffset.offset(), 
startingOffset)).position();
+        int endPosition = endPosition(exclusiveEndOffset);
+        if (position >= endPosition)
+            return Optional.empty();
+
+        return Optional.ofNullable(log.searchForTimestamp(timestampMs, 
position, endPosition, startingOffset, maxRecordBodySize));
+    }
+
+    /**
+     * Largest timestamp in this segment, and the offset of the record that 
holds it.
+     * Empty when the segment has no batches or the batch does not expose a 
timestamp offset.
+     */
+    public Optional<FileRecords.TimestampAndOffset> 
fetchMaxTimestampAndOffset(int maxRecordBodySize) throws IOException {

Review Comment:
   Could this be private?



##########
clients/src/main/java/org/apache/kafka/common/record/internal/FileRecords.java:
##########
@@ -348,7 +348,21 @@ public LogOffsetPosition searchForOffsetFromPosition(long 
targetOffset, int star
      */
     public TimestampAndOffset searchForTimestamp(long targetTimestamp, int 
startingPosition, long startingOffset,
                                                  int maxRecordBodySize) {
-        for (RecordBatch batch : batchesFrom(startingPosition)) {
+        int endPosition = isSlice ? end : sizeInBytes();

Review Comment:
   Could we add a comment that this searchForTimestamp() is for test only?



##########
storage/src/main/java/org/apache/kafka/storage/internals/log/UnifiedLog.java:
##########
@@ -1694,6 +1694,16 @@ public List<AbortedTxn> collectAbortedTransactions(long 
startOffset, long upperB
      *         </ul>
      */
     public OffsetResultHolder fetchOffsetByTimestamp(long targetTimestamp, 
Optional<AsyncOffsetReader> remoteOffsetReader) {
+        return fetchOffsetByTimestamp(targetTimestamp, remoteOffsetReader, 
Long.MAX_VALUE);
+    }
+
+    /**
+     * Same as {@link #fetchOffsetByTimestamp(long, Optional)} but ignores 
offsets at or after {@code lastFetchableOffset}.

Review Comment:
   This method ignores offsets after lastFetchableOffset only for maxTimestamp.



##########
storage/src/main/java/org/apache/kafka/storage/internals/log/LogSegment.java:
##########
@@ -750,12 +750,121 @@ public long getFirstBatchTimestamp() {
      * @return the timestamp and offset of the first message that meets the 
requirements. Empty will be returned if there is no such message.
      */
     public Optional<FileRecords.TimestampAndOffset> findOffsetByTimestamp(long 
timestampMs, long startingOffset, int maxRecordBodySize) throws IOException {
+        return findOffsetByTimestamp(timestampMs, startingOffset, 
Long.MAX_VALUE, maxRecordBodySize);
+    }
+
+    /**
+     * Search for the first message whose timestamp is at least {@code 
timestampMs} and whose offset is at least
+     * {@code startingOffset} and strictly less than {@code 
exclusiveEndOffset}.
+     * {@code exclusiveEndOffset} is a batch boundary, such as the high 
watermark or last stable offset.
+     * Pass {@link Long#MAX_VALUE} to search the whole segment.
+     */
+    public Optional<FileRecords.TimestampAndOffset> findOffsetByTimestamp(long 
timestampMs, long startingOffset,
+                                                                          long 
exclusiveEndOffset, int maxRecordBodySize) throws IOException {
+        if (exclusiveEndOffset <= baseOffset)
+            return Optional.empty();
+
         // Get the index entry with a timestamp less than or equal to the 
target timestamp
         TimestampOffset timestampOffset = timeIndex().lookup(timestampMs);
         int position = offsetIndex().lookup(Math.max(timestampOffset.offset(), 
startingOffset)).position();
+        int endPosition = endPosition(exclusiveEndOffset);
+        if (position >= endPosition)
+            return Optional.empty();
+
+        return Optional.ofNullable(log.searchForTimestamp(timestampMs, 
position, endPosition, startingOffset, maxRecordBodySize));
+    }
+
+    /**
+     * Largest timestamp in this segment, and the offset of the record that 
holds it.
+     * Empty when the segment has no batches or the batch does not expose a 
timestamp offset.
+     */
+    public Optional<FileRecords.TimestampAndOffset> 
fetchMaxTimestampAndOffset(int maxRecordBodySize) throws IOException {
+        if (size() == 0)
+            return Optional.empty();
+
+        TimestampOffset maxTimestampSoFar = readMaxTimestampAndOffsetSoFar();
+        int position = 
offsetIndex().lookup(maxTimestampSoFar.offset()).position();
+        for (RecordBatch batch : log.batchesFrom(position)) {
+            if (batch.maxTimestamp() == maxTimestampSoFar.timestamp())
+                return toTimestampAndOffset(batch, maxRecordBodySize);
+        }
+        return Optional.empty();
+    }
+
+    /**
+     * Largest timestamp among batches whose base offset is strictly less than 
{@code exclusiveEndOffset}.
+     * The high watermark and last stable offset are batch boundaries, so a 
batch is either entirely included or not.
+     * When this segment's own max timestamp is already below the bound, that 
value is used. Otherwise the time index
+     * names the last indexed timestamp below the bound and the records after 
that entry are scanned up to the bound.
+     */
+    public Optional<FileRecords.TimestampAndOffset> maxTimestampBelow(long 
exclusiveEndOffset, int maxRecordBodySize) throws IOException {
+        if (exclusiveEndOffset <= baseOffset || size() == 0)
+            return Optional.empty();
+
+        TimestampOffset segmentMax = readMaxTimestampAndOffsetSoFar();
+        if (segmentMax.offset() < exclusiveEndOffset)
+            return fetchMaxTimestampAndOffset(maxRecordBodySize);

Review Comment:
   We call readMaxTimestampAndOffsetSoFar() again inside 
fetchMaxTimestampAndOffset(), at that point, maxTimestamp could have moved to 
>= exclusiveEndOffset. It's safer to pass in segmentMax.



##########
clients/src/main/java/org/apache/kafka/common/record/internal/FileRecords.java:
##########
@@ -434,7 +456,12 @@ private AbstractIterator<FileChannelRecordBatch> 
batchIterator(int start) {
             end = this.end;
         else
             end = this.sizeInBytes();
-        FileLogInputStream inputStream = new FileLogInputStream(this, start, 
end);
+        return batchIterator(start, end);
+    }
+
+    private AbstractIterator<FileChannelRecordBatch> batchIterator(int start, 
int end) {
+        int boundedEnd = isSlice ? Math.min(end, this.end) : end;

Review Comment:
   We probably want to bound the end for non-sliced FileRecords too. Claude 
suggested the following.
   
   `Math.min(end, isSlice ? this.end : sizeInBytes())`



##########
storage/src/main/java/org/apache/kafka/storage/internals/log/LogSegment.java:
##########
@@ -750,12 +750,121 @@ public long getFirstBatchTimestamp() {
      * @return the timestamp and offset of the first message that meets the 
requirements. Empty will be returned if there is no such message.
      */
     public Optional<FileRecords.TimestampAndOffset> findOffsetByTimestamp(long 
timestampMs, long startingOffset, int maxRecordBodySize) throws IOException {
+        return findOffsetByTimestamp(timestampMs, startingOffset, 
Long.MAX_VALUE, maxRecordBodySize);
+    }
+
+    /**
+     * Search for the first message whose timestamp is at least {@code 
timestampMs} and whose offset is at least
+     * {@code startingOffset} and strictly less than {@code 
exclusiveEndOffset}.
+     * {@code exclusiveEndOffset} is a batch boundary, such as the high 
watermark or last stable offset.
+     * Pass {@link Long#MAX_VALUE} to search the whole segment.
+     */
+    public Optional<FileRecords.TimestampAndOffset> findOffsetByTimestamp(long 
timestampMs, long startingOffset,
+                                                                          long 
exclusiveEndOffset, int maxRecordBodySize) throws IOException {
+        if (exclusiveEndOffset <= baseOffset)
+            return Optional.empty();
+
         // Get the index entry with a timestamp less than or equal to the 
target timestamp
         TimestampOffset timestampOffset = timeIndex().lookup(timestampMs);
         int position = offsetIndex().lookup(Math.max(timestampOffset.offset(), 
startingOffset)).position();
+        int endPosition = endPosition(exclusiveEndOffset);
+        if (position >= endPosition)
+            return Optional.empty();
+
+        return Optional.ofNullable(log.searchForTimestamp(timestampMs, 
position, endPosition, startingOffset, maxRecordBodySize));
+    }
+
+    /**
+     * Largest timestamp in this segment, and the offset of the record that 
holds it.
+     * Empty when the segment has no batches or the batch does not expose a 
timestamp offset.
+     */
+    public Optional<FileRecords.TimestampAndOffset> 
fetchMaxTimestampAndOffset(int maxRecordBodySize) throws IOException {
+        if (size() == 0)
+            return Optional.empty();
+
+        TimestampOffset maxTimestampSoFar = readMaxTimestampAndOffsetSoFar();
+        int position = 
offsetIndex().lookup(maxTimestampSoFar.offset()).position();
+        for (RecordBatch batch : log.batchesFrom(position)) {
+            if (batch.maxTimestamp() == maxTimestampSoFar.timestamp())
+                return toTimestampAndOffset(batch, maxRecordBodySize);
+        }
+        return Optional.empty();
+    }
+
+    /**
+     * Largest timestamp among batches whose base offset is strictly less than 
{@code exclusiveEndOffset}.
+     * The high watermark and last stable offset are batch boundaries, so a 
batch is either entirely included or not.
+     * When this segment's own max timestamp is already below the bound, that 
value is used. Otherwise the time index
+     * names the last indexed timestamp below the bound and the records after 
that entry are scanned up to the bound.
+     */
+    public Optional<FileRecords.TimestampAndOffset> maxTimestampBelow(long 
exclusiveEndOffset, int maxRecordBodySize) throws IOException {
+        if (exclusiveEndOffset <= baseOffset || size() == 0)
+            return Optional.empty();
+
+        TimestampOffset segmentMax = readMaxTimestampAndOffsetSoFar();
+        if (segmentMax.offset() < exclusiveEndOffset)
+            return fetchMaxTimestampAndOffset(maxRecordBodySize);
+
+        TimestampOffset indexed = 
timeIndex().lastEntryWithOffsetBelow(exclusiveEndOffset);
+        int scanStart = offsetIndex().lookup(indexed.offset()).position();
+        int scanEnd = endPosition(exclusiveEndOffset);
+
+        RecordBatch bestBatch = null;
+        long bestTimestamp = RecordBatch.NO_TIMESTAMP;
+        for (RecordBatch batch : log.batchesFrom(scanStart, scanEnd)) {

Review Comment:
   Could we reuse FileRecords.largestTimestampAfter with an end-position 
overload?



##########
storage/src/main/java/org/apache/kafka/storage/internals/log/UnifiedLog.java:
##########
@@ -1738,6 +1748,11 @@ public OffsetResultHolder fetchOffsetByTimestamp(long 
targetTimestamp, Optional<
                     } else if (targetTimestamp == 
ListOffsetsRequest.EARLIEST_PENDING_UPLOAD_TIMESTAMP) {
                         return 
fetchEarliestPendingUploadOffset(remoteOffsetReader);
                     } else if (targetTimestamp == 
ListOffsetsRequest.MAX_TIMESTAMP) {
+                        // A bound below the log end means the segment with 
the global max timestamp may hold that
+                        // max past the high watermark. Search only the 
fetchable prefix. See KAFKA-21204.
+                        if (lastFetchableOffset < logEndOffset())

Review Comment:
   This check is not very reliable since immediately after this call, 
logEndOffset could advance.



##########
core/src/main/scala/kafka/cluster/Partition.scala:
##########
@@ -1464,7 +1463,7 @@ class Partition(val topicPartition: TopicPartition,
 
     def getOffsetByTimestamp: OffsetResultHolder = {
       logManager.getLog(topicPartition)
-        .map(log => log.fetchOffsetByTimestamp(timestamp, 
remoteLogManager.asInstanceOf[Option[AsyncOffsetReader]].toJava))
+        .map(log => log.fetchOffsetByTimestamp(timestamp, 
remoteLogManager.asInstanceOf[Option[AsyncOffsetReader]].toJava, 
lastFetchableOffset))

Review Comment:
   With this change, we can get rid of the bounding by lastFetchableOffset in 
ReplicaManager.fetchOffset().
   
   ```
                   if (resultHolder.lastFetchableOffset.isPresent &&
                     timestampAndOffsetOpt.offset >= 
resultHolder.lastFetchableOffset.get) {
                     resultHolder.maybeOffsetsError.map(e => throw e)
   ```



##########
storage/src/main/java/org/apache/kafka/storage/internals/log/LogSegment.java:
##########
@@ -750,12 +750,121 @@ public long getFirstBatchTimestamp() {
      * @return the timestamp and offset of the first message that meets the 
requirements. Empty will be returned if there is no such message.
      */
     public Optional<FileRecords.TimestampAndOffset> findOffsetByTimestamp(long 
timestampMs, long startingOffset, int maxRecordBodySize) throws IOException {
+        return findOffsetByTimestamp(timestampMs, startingOffset, 
Long.MAX_VALUE, maxRecordBodySize);
+    }
+
+    /**
+     * Search for the first message whose timestamp is at least {@code 
timestampMs} and whose offset is at least
+     * {@code startingOffset} and strictly less than {@code 
exclusiveEndOffset}.
+     * {@code exclusiveEndOffset} is a batch boundary, such as the high 
watermark or last stable offset.
+     * Pass {@link Long#MAX_VALUE} to search the whole segment.
+     */
+    public Optional<FileRecords.TimestampAndOffset> findOffsetByTimestamp(long 
timestampMs, long startingOffset,
+                                                                          long 
exclusiveEndOffset, int maxRecordBodySize) throws IOException {
+        if (exclusiveEndOffset <= baseOffset)
+            return Optional.empty();
+
         // Get the index entry with a timestamp less than or equal to the 
target timestamp
         TimestampOffset timestampOffset = timeIndex().lookup(timestampMs);
         int position = offsetIndex().lookup(Math.max(timestampOffset.offset(), 
startingOffset)).position();
+        int endPosition = endPosition(exclusiveEndOffset);
+        if (position >= endPosition)
+            return Optional.empty();
+
+        return Optional.ofNullable(log.searchForTimestamp(timestampMs, 
position, endPosition, startingOffset, maxRecordBodySize));
+    }
+
+    /**
+     * Largest timestamp in this segment, and the offset of the record that 
holds it.
+     * Empty when the segment has no batches or the batch does not expose a 
timestamp offset.
+     */
+    public Optional<FileRecords.TimestampAndOffset> 
fetchMaxTimestampAndOffset(int maxRecordBodySize) throws IOException {
+        if (size() == 0)
+            return Optional.empty();
+
+        TimestampOffset maxTimestampSoFar = readMaxTimestampAndOffsetSoFar();
+        int position = 
offsetIndex().lookup(maxTimestampSoFar.offset()).position();
+        for (RecordBatch batch : log.batchesFrom(position)) {
+            if (batch.maxTimestamp() == maxTimestampSoFar.timestamp())
+                return toTimestampAndOffset(batch, maxRecordBodySize);
+        }
+        return Optional.empty();
+    }
+
+    /**
+     * Largest timestamp among batches whose base offset is strictly less than 
{@code exclusiveEndOffset}.
+     * The high watermark and last stable offset are batch boundaries, so a 
batch is either entirely included or not.
+     * When this segment's own max timestamp is already below the bound, that 
value is used. Otherwise the time index
+     * names the last indexed timestamp below the bound and the records after 
that entry are scanned up to the bound.
+     */
+    public Optional<FileRecords.TimestampAndOffset> maxTimestampBelow(long 
exclusiveEndOffset, int maxRecordBodySize) throws IOException {
+        if (exclusiveEndOffset <= baseOffset || size() == 0)
+            return Optional.empty();
+
+        TimestampOffset segmentMax = readMaxTimestampAndOffsetSoFar();
+        if (segmentMax.offset() < exclusiveEndOffset)
+            return fetchMaxTimestampAndOffset(maxRecordBodySize);
+
+        TimestampOffset indexed = 
timeIndex().lastEntryWithOffsetBelow(exclusiveEndOffset);
+        int scanStart = offsetIndex().lookup(indexed.offset()).position();
+        int scanEnd = endPosition(exclusiveEndOffset);
+
+        RecordBatch bestBatch = null;
+        long bestTimestamp = RecordBatch.NO_TIMESTAMP;
+        for (RecordBatch batch : log.batchesFrom(scanStart, scanEnd)) {
+            if (batch.baseOffset() >= exclusiveEndOffset)
+                break;
+            long timestamp = batch.maxTimestamp();
+            if (bestBatch == null || timestamp > bestTimestamp) {
+                bestTimestamp = timestamp;
+                bestBatch = batch;
+            }
+        }
+
+        // The indexed timestamp can sit on a batch that the offset-index 
checkpoint starts after.
+        if (indexed.timestamp() > bestTimestamp) {
+            Optional<FileRecords.TimestampAndOffset> indexedResult =
+                    timestampAndOffsetAt(indexed.offset(), 
indexed.timestamp(), scanEnd, maxRecordBodySize);
+            if (indexedResult.isPresent())
+                return indexedResult;
+        }
+        if (bestBatch == null)
+            return Optional.empty();
+        return toTimestampAndOffset(bestBatch, maxRecordBodySize);
+    }
+
+    /**
+     * File position of the first batch at or after {@code 
exclusiveEndOffset}, or the segment size when every
+     * batch starts below it. {@link Long#MAX_VALUE} means the whole segment.
+     */
+    private int endPosition(long exclusiveEndOffset) throws IOException {
+        if (exclusiveEndOffset == Long.MAX_VALUE)
+            return log.sizeInBytes();
+        if (exclusiveEndOffset <= baseOffset)

Review Comment:
   This seems redundant since both callers check that already.



##########
storage/src/main/java/org/apache/kafka/storage/internals/log/UnifiedLog.java:
##########
@@ -1812,12 +1828,36 @@ public boolean isEmpty() {
         return logStartOffset == logEndOffset();
     }
 
-    private Optional<FileRecords.TimestampAndOffset> 
searchOffsetInLocalLog(long targetTimestamp, long startOffset) throws 
IOException {
+    /**
+     * Largest timestamp among records with offset strictly less than {@code 
exclusiveEndOffset}.
+     * Segments that end before the bound contribute their full max. The 
segment that contains the bound
+     * contributes only the prefix below it, so a later uncommitted timestamp 
cannot hide an earlier one.
+     */
+    private Optional<FileRecords.TimestampAndOffset> maxTimestampBelow(long 
exclusiveEndOffset) throws IOException {

Review Comment:
   This forces IO scan on every segment. Claude suggested the following 
optimization.
   
   ```
   // bound = lastFetchableOffset, or Long.MAX_VALUE for the unbounded case
   for (LogSegment segment : logSegments()) {
       if (segment.baseOffset() >= bound) break;
       TimestampOffset segMax = segment.readMaxTimestampAndOffsetSoFar();      
// cached, read once
       if (segMax.offset() < bound) {
           // segment's max is below the bound: compare cached values only, no 
I/O
           if (winnerMax == null || segMax.timestamp() > winnerMax.timestamp()) 
{ winner = segment; winnerMax = segMax; }
       } else {
           // only the segment containing the bound: scan its prefix below the 
bound
           boundCandidate = segment.maxTimestampBelow(bound, maxRecordBodySize);
       }
   }
   ```



##########
storage/src/main/java/org/apache/kafka/storage/internals/log/LogSegment.java:
##########
@@ -750,12 +750,121 @@ public long getFirstBatchTimestamp() {
      * @return the timestamp and offset of the first message that meets the 
requirements. Empty will be returned if there is no such message.
      */
     public Optional<FileRecords.TimestampAndOffset> findOffsetByTimestamp(long 
timestampMs, long startingOffset, int maxRecordBodySize) throws IOException {
+        return findOffsetByTimestamp(timestampMs, startingOffset, 
Long.MAX_VALUE, maxRecordBodySize);
+    }
+
+    /**
+     * Search for the first message whose timestamp is at least {@code 
timestampMs} and whose offset is at least
+     * {@code startingOffset} and strictly less than {@code 
exclusiveEndOffset}.
+     * {@code exclusiveEndOffset} is a batch boundary, such as the high 
watermark or last stable offset.
+     * Pass {@link Long#MAX_VALUE} to search the whole segment.
+     */
+    public Optional<FileRecords.TimestampAndOffset> findOffsetByTimestamp(long 
timestampMs, long startingOffset,
+                                                                          long 
exclusiveEndOffset, int maxRecordBodySize) throws IOException {
+        if (exclusiveEndOffset <= baseOffset)
+            return Optional.empty();
+
         // Get the index entry with a timestamp less than or equal to the 
target timestamp
         TimestampOffset timestampOffset = timeIndex().lookup(timestampMs);
         int position = offsetIndex().lookup(Math.max(timestampOffset.offset(), 
startingOffset)).position();
+        int endPosition = endPosition(exclusiveEndOffset);
+        if (position >= endPosition)
+            return Optional.empty();
+
+        return Optional.ofNullable(log.searchForTimestamp(timestampMs, 
position, endPosition, startingOffset, maxRecordBodySize));
+    }
+
+    /**
+     * Largest timestamp in this segment, and the offset of the record that 
holds it.
+     * Empty when the segment has no batches or the batch does not expose a 
timestamp offset.
+     */
+    public Optional<FileRecords.TimestampAndOffset> 
fetchMaxTimestampAndOffset(int maxRecordBodySize) throws IOException {
+        if (size() == 0)
+            return Optional.empty();
+
+        TimestampOffset maxTimestampSoFar = readMaxTimestampAndOffsetSoFar();
+        int position = 
offsetIndex().lookup(maxTimestampSoFar.offset()).position();
+        for (RecordBatch batch : log.batchesFrom(position)) {
+            if (batch.maxTimestamp() == maxTimestampSoFar.timestamp())
+                return toTimestampAndOffset(batch, maxRecordBodySize);
+        }
+        return Optional.empty();
+    }
+
+    /**
+     * Largest timestamp among batches whose base offset is strictly less than 
{@code exclusiveEndOffset}.
+     * The high watermark and last stable offset are batch boundaries, so a 
batch is either entirely included or not.
+     * When this segment's own max timestamp is already below the bound, that 
value is used. Otherwise the time index
+     * names the last indexed timestamp below the bound and the records after 
that entry are scanned up to the bound.
+     */
+    public Optional<FileRecords.TimestampAndOffset> maxTimestampBelow(long 
exclusiveEndOffset, int maxRecordBodySize) throws IOException {
+        if (exclusiveEndOffset <= baseOffset || size() == 0)
+            return Optional.empty();
+
+        TimestampOffset segmentMax = readMaxTimestampAndOffsetSoFar();
+        if (segmentMax.offset() < exclusiveEndOffset)
+            return fetchMaxTimestampAndOffset(maxRecordBodySize);
+
+        TimestampOffset indexed = 
timeIndex().lastEntryWithOffsetBelow(exclusiveEndOffset);
+        int scanStart = offsetIndex().lookup(indexed.offset()).position();
+        int scanEnd = endPosition(exclusiveEndOffset);
+
+        RecordBatch bestBatch = null;
+        long bestTimestamp = RecordBatch.NO_TIMESTAMP;
+        for (RecordBatch batch : log.batchesFrom(scanStart, scanEnd)) {
+            if (batch.baseOffset() >= exclusiveEndOffset)
+                break;
+            long timestamp = batch.maxTimestamp();
+            if (bestBatch == null || timestamp > bestTimestamp) {
+                bestTimestamp = timestamp;
+                bestBatch = batch;
+            }
+        }
+
+        // The indexed timestamp can sit on a batch that the offset-index 
checkpoint starts after.
+        if (indexed.timestamp() > bestTimestamp) {

Review Comment:
   It seems that this is always false. The previous scan includes the batch 
where indexed is in. So, bestTimestamp should always be larger or equal. 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to