Browse Source

KAFKA-14481: Move LogSegment/LogSegments to storage module (#14529)

A few notes:
* Delete a few methods from `UnifiedLog` that were simply invoking the related method in `LogFileUtils`
* Fix `CoreUtils.swallow` to use the passed in `logging`
* Fix `LogCleanerParameterizedIntegrationTest` to close `log` before reopening
* Minor tweaks in `LogSegment` for readability
 
For broader context on this change, please check:

* KAFKA-14470: Move log layer to storage module

Reviewers: Divij Vaidya <diviv@amazon.com>, Satish Duggana <satishd@apache.org>
pull/14101/merge
Ismael Juma 1 year ago committed by GitHub
parent
commit
1073d434ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      clients/src/main/java/org/apache/kafka/common/record/AbstractRecords.java
  2. 8
      clients/src/main/java/org/apache/kafka/common/record/Records.java
  3. 52
      clients/src/main/java/org/apache/kafka/common/utils/Utils.java
  4. 77
      clients/src/test/java/org/apache/kafka/common/utils/UtilsTest.java
  5. 8
      core/src/main/java/kafka/log/remote/RemoteLogManager.java
  6. 57
      core/src/main/scala/kafka/log/LocalLog.scala
  7. 9
      core/src/main/scala/kafka/log/LogCleaner.scala
  8. 14
      core/src/main/scala/kafka/log/LogCleanerManager.scala
  9. 64
      core/src/main/scala/kafka/log/LogLoader.scala
  10. 2
      core/src/main/scala/kafka/log/LogManager.scala
  11. 693
      core/src/main/scala/kafka/log/LogSegment.scala
  12. 268
      core/src/main/scala/kafka/log/LogSegments.scala
  13. 82
      core/src/main/scala/kafka/log/UnifiedLog.scala
  14. 35
      core/src/main/scala/kafka/utils/CoreUtils.scala
  15. 39
      core/src/test/java/kafka/log/remote/RemoteLogManagerTest.java
  16. 4
      core/src/test/scala/integration/kafka/api/GroupCoordinatorIntegrationTest.scala
  17. 2
      core/src/test/scala/integration/kafka/server/DynamicBrokerReconfigurationTest.scala
  18. 5
      core/src/test/scala/kafka/raft/KafkaMetadataLogTest.scala
  19. 5
      core/src/test/scala/unit/kafka/cluster/PartitionLockTest.scala
  20. 4
      core/src/test/scala/unit/kafka/cluster/PartitionTest.scala
  21. 5
      core/src/test/scala/unit/kafka/log/AbstractLogCleanerIntegrationTest.scala
  22. 40
      core/src/test/scala/unit/kafka/log/LocalLogTest.scala
  23. 8
      core/src/test/scala/unit/kafka/log/LogCleanerIntegrationTest.scala
  24. 2
      core/src/test/scala/unit/kafka/log/LogCleanerLagIntegrationTest.scala
  25. 8
      core/src/test/scala/unit/kafka/log/LogCleanerManagerTest.scala
  26. 15
      core/src/test/scala/unit/kafka/log/LogCleanerParameterizedIntegrationTest.scala
  27. 101
      core/src/test/scala/unit/kafka/log/LogCleanerTest.scala
  28. 2
      core/src/test/scala/unit/kafka/log/LogConcurrencyTest.scala
  29. 87
      core/src/test/scala/unit/kafka/log/LogLoaderTest.scala
  30. 16
      core/src/test/scala/unit/kafka/log/LogManagerTest.scala
  31. 121
      core/src/test/scala/unit/kafka/log/LogSegmentTest.scala
  32. 65
      core/src/test/scala/unit/kafka/log/LogSegmentsTest.scala
  33. 21
      core/src/test/scala/unit/kafka/log/LogTestUtils.scala
  34. 64
      core/src/test/scala/unit/kafka/log/UnifiedLogTest.scala
  35. 2
      core/src/test/scala/unit/kafka/server/DynamicConfigChangeTest.scala
  36. 12
      core/src/test/scala/unit/kafka/server/LogOffsetTest.scala
  37. 4
      core/src/test/scala/unit/kafka/server/ReplicaManagerTest.scala
  38. 5
      core/src/test/scala/unit/kafka/server/epoch/LeaderEpochIntegrationTest.scala
  39. 51
      core/src/test/scala/unit/kafka/utils/CoreUtilsTest.scala
  40. 8
      core/src/test/scala/unit/kafka/utils/SchedulerTest.scala
  41. 5
      gradle/spotbugs-exclude.xml
  42. 3
      storage/src/main/java/org/apache/kafka/storage/internals/log/LazyIndex.java
  43. 889
      storage/src/main/java/org/apache/kafka/storage/internals/log/LogSegment.java
  44. 22
      storage/src/main/java/org/apache/kafka/storage/internals/log/LogSegmentOffsetOverflowException.java
  45. 355
      storage/src/main/java/org/apache/kafka/storage/internals/log/LogSegments.java

12
clients/src/main/java/org/apache/kafka/common/record/AbstractRecords.java

@ -22,6 +22,7 @@ import org.apache.kafka.common.utils.Utils; @@ -22,6 +22,7 @@ import org.apache.kafka.common.utils.Utils;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.Optional;
public abstract class AbstractRecords implements Records {
@ -44,6 +45,17 @@ public abstract class AbstractRecords implements Records { @@ -44,6 +45,17 @@ public abstract class AbstractRecords implements Records {
return iterator.next();
}
@Override
public Optional<RecordBatch> lastBatch() {
Iterator<? extends RecordBatch> iterator = batches().iterator();
RecordBatch batch = null;
while (iterator.hasNext())
batch = iterator.next();
return Optional.ofNullable(batch);
}
/**
* Get an iterator over the deep records.
* @return An iterator over the records

8
clients/src/main/java/org/apache/kafka/common/record/Records.java

@ -20,6 +20,7 @@ import org.apache.kafka.common.utils.AbstractIterator; @@ -20,6 +20,7 @@ import org.apache.kafka.common.utils.AbstractIterator;
import org.apache.kafka.common.utils.Time;
import java.util.Iterator;
import java.util.Optional;
/**
@ -70,6 +71,13 @@ public interface Records extends TransferableRecords { @@ -70,6 +71,13 @@ public interface Records extends TransferableRecords {
*/
AbstractIterator<? extends RecordBatch> batchIterator();
/**
* Return the last record batch if non-empty or an empty `Optional` otherwise.
*
* Note that this requires iterating over all the record batches and hence it's expensive.
*/
Optional<RecordBatch> lastBatch();
/**
* Check whether all batches in this buffer have a certain magic value.
* @param magic The magic value to check

52
clients/src/main/java/org/apache/kafka/common/utils/Utils.java

@ -71,6 +71,7 @@ import java.util.Map; @@ -71,6 +71,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
@ -1042,14 +1043,20 @@ public final class Utils { @@ -1042,14 +1043,20 @@ public final class Utils {
if (exception != null)
throw exception;
}
public static void swallow(final Logger log, final Level level, final String what, final Runnable code) {
@FunctionalInterface
public interface SwallowAction {
void run() throws Throwable;
}
public static void swallow(final Logger log, final Level level, final String what, final SwallowAction code) {
swallow(log, level, what, code, null);
}
/**
* Run the supplied code. If an exception is thrown, it is swallowed and registered to the firstException parameter.
*/
public static void swallow(final Logger log, final Level level, final String what, final Runnable code,
public static void swallow(final Logger log, final Level level, final String what, final SwallowAction code,
final AtomicReference<Throwable> firstException) {
if (code != null) {
try {
@ -1102,11 +1109,26 @@ public final class Utils { @@ -1102,11 +1109,26 @@ public final class Utils {
* use a method reference from it.
*/
public static void closeQuietly(AutoCloseable closeable, String name) {
closeQuietly(closeable, name, log);
}
/**
* Closes {@code closeable} and if an exception is thrown, it is logged with the provided logger at the WARN level.
* <b>Be cautious when passing method references as an argument.</b> For example:
* <p>
* {@code closeQuietly(task::stop, "source task");}
* <p>
* Although this method gracefully handles null {@link AutoCloseable} objects, attempts to take a method
* reference from a null object will result in a {@link NullPointerException}. In the example code above,
* it would be the caller's responsibility to ensure that {@code task} was non-null before attempting to
* use a method reference from it.
*/
public static void closeQuietly(AutoCloseable closeable, String name, Logger logger) {
if (closeable != null) {
try {
closeable.close();
} catch (Throwable t) {
log.warn("Failed to close {} with type {}", name, closeable.getClass().getName(), t);
logger.warn("Failed to close {} with type {}", name, closeable.getClass().getName(), t);
}
}
}
@ -1143,6 +1165,30 @@ public final class Utils { @@ -1143,6 +1165,30 @@ public final class Utils {
for (AutoCloseable closeable : closeables) closeQuietly(closeable, name, firstException);
}
/**
* Invokes every function in `all` even if one or more functions throws an exception.
*
* If any of the functions throws an exception, the first one will be rethrown at the end with subsequent exceptions
* added as suppressed exceptions.
*/
// Note that this is a generalised version of `closeAll`. We could potentially make it more general by
// changing the signature to `public <R> List<R> tryAll(all: List[Callable<R>])`
public static void tryAll(List<Callable<Void>> all) throws Throwable {
Throwable exception = null;
for (Callable call : all) {
try {
call.call();
} catch (Throwable t) {
if (exception != null)
exception.addSuppressed(t);
else
exception = t;
}
}
if (exception != null)
throw exception;
}
/**
* A cheap way to deterministically convert a number to a positive value. When the input is
* positive, the original value is returned. When the input number is negative, the returned

77
clients/src/test/java/org/apache/kafka/common/utils/UtilsTest.java

@ -50,6 +50,7 @@ import java.util.Properties; @@ -50,6 +50,7 @@ import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@ -64,6 +65,7 @@ import static org.apache.kafka.common.utils.Utils.formatBytes; @@ -64,6 +65,7 @@ import static org.apache.kafka.common.utils.Utils.formatBytes;
import static org.apache.kafka.common.utils.Utils.getHost;
import static org.apache.kafka.common.utils.Utils.getPort;
import static org.apache.kafka.common.utils.Utils.intersection;
import static org.apache.kafka.common.utils.Utils.mkEntry;
import static org.apache.kafka.common.utils.Utils.mkSet;
import static org.apache.kafka.common.utils.Utils.murmur2;
import static org.apache.kafka.common.utils.Utils.union;
@ -952,4 +954,79 @@ public class UtilsTest { @@ -952,4 +954,79 @@ public class UtilsTest {
assertEquals(expected, actual);
}
@Test
public void testTryAll() throws Throwable {
Map<String, Object> recorded = new HashMap<>();
Utils.tryAll(asList(
recordingCallable(recorded, "valid-0", null),
recordingCallable(recorded, null, new TestException("exception-1")),
recordingCallable(recorded, "valid-2", null),
recordingCallable(recorded, null, new TestException("exception-3"))
));
Map<String, Object> expected = Utils.mkMap(
mkEntry("valid-0", "valid-0"),
mkEntry("exception-1", new TestException("exception-1")),
mkEntry("valid-2", "valid-2"),
mkEntry("exception-3", new TestException("exception-3"))
);
assertEquals(expected, recorded);
recorded.clear();
Utils.tryAll(asList(
recordingCallable(recorded, "valid-0", null),
recordingCallable(recorded, "valid-1", null)
));
expected = Utils.mkMap(
mkEntry("valid-0", "valid-0"),
mkEntry("valid-1", "valid-1")
);
assertEquals(expected, recorded);
recorded.clear();
Utils.tryAll(asList(
recordingCallable(recorded, null, new TestException("exception-0")),
recordingCallable(recorded, null, new TestException("exception-1")))
);
expected = Utils.mkMap(
mkEntry("exception-0", new TestException("exception-0")),
mkEntry("exception-1", new TestException("exception-1"))
);
assertEquals(expected, recorded);
}
private Callable<Void> recordingCallable(Map<String, Object> recordingMap, String success, TestException failure) {
return () -> {
if (success == null)
recordingMap.put(failure.key, failure);
else if (failure == null)
recordingMap.put(success, success);
else
throw new IllegalArgumentException("Either `success` or `failure` must be null, but both are non-null.");
return null;
};
}
private class TestException extends Exception {
final String key;
TestException(String key) {
this.key = key;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
TestException that = (TestException) o;
return key.equals(that.key);
}
@Override
public int hashCode() {
return key.hashCode();
}
}
}

8
core/src/main/java/kafka/log/remote/RemoteLogManager.java

@ -19,7 +19,6 @@ package kafka.log.remote; @@ -19,7 +19,6 @@ package kafka.log.remote;
import com.yammer.metrics.core.Gauge;
import kafka.cluster.EndPoint;
import kafka.cluster.Partition;
import kafka.log.LogSegment;
import kafka.log.UnifiedLog;
import kafka.server.BrokerTopicStats;
import kafka.server.KafkaConfig;
@ -62,6 +61,7 @@ import org.apache.kafka.storage.internals.log.EpochEntry; @@ -62,6 +61,7 @@ import org.apache.kafka.storage.internals.log.EpochEntry;
import org.apache.kafka.storage.internals.log.FetchDataInfo;
import org.apache.kafka.storage.internals.log.FetchIsolation;
import org.apache.kafka.storage.internals.log.LogOffsetMetadata;
import org.apache.kafka.storage.internals.log.LogSegment;
import org.apache.kafka.storage.internals.log.OffsetIndex;
import org.apache.kafka.storage.internals.log.OffsetPosition;
import org.apache.kafka.storage.internals.log.RemoteIndexCache;
@ -733,8 +733,8 @@ public class RemoteLogManager implements Closeable { @@ -733,8 +733,8 @@ public class RemoteLogManager implements Closeable {
remoteLogMetadataManager.addRemoteLogSegmentMetadata(copySegmentStartedRlsm).get();
ByteBuffer leaderEpochsIndex = getLeaderEpochCheckpoint(log, -1, nextSegmentBaseOffset).readAsByteBuffer();
LogSegmentData segmentData = new LogSegmentData(logFile.toPath(), toPathIfExists(segment.lazyOffsetIndex().get().file()),
toPathIfExists(segment.lazyTimeIndex().get().file()), Optional.ofNullable(toPathIfExists(segment.txnIndex().file())),
LogSegmentData segmentData = new LogSegmentData(logFile.toPath(), toPathIfExists(segment.offsetIndex().file()),
toPathIfExists(segment.timeIndex().file()), Optional.ofNullable(toPathIfExists(segment.txnIndex().file())),
producerStateSnapshotFile.toPath(), leaderEpochsIndex);
brokerTopicStats.topicStats(log.topicPartition().topic()).remoteCopyRequestRate().mark();
brokerTopicStats.allTopicsStats().remoteCopyRequestRate().mark();
@ -1374,7 +1374,7 @@ public class RemoteLogManager implements Closeable { @@ -1374,7 +1374,7 @@ public class RemoteLogManager implements Closeable {
}
// Search in local segments
collectAbortedTransactionInLocalSegments(startOffset, upperBoundOffset, accumulator, JavaConverters.asJavaIterator(log.logSegments().iterator()));
collectAbortedTransactionInLocalSegments(startOffset, upperBoundOffset, accumulator, log.logSegments().iterator());
}
private void collectAbortedTransactionInLocalSegments(long startOffset,

57
core/src/main/scala/kafka/log/LocalLog.scala

@ -24,15 +24,17 @@ import org.apache.kafka.common.record.MemoryRecords @@ -24,15 +24,17 @@ import org.apache.kafka.common.record.MemoryRecords
import org.apache.kafka.common.utils.{Time, Utils}
import org.apache.kafka.common.{KafkaException, TopicPartition}
import org.apache.kafka.server.util.Scheduler
import org.apache.kafka.storage.internals.log.{AbortedTxn, FetchDataInfo, LogConfig, LogDirFailureChannel, LogFileUtils, LogOffsetMetadata, OffsetPosition}
import org.apache.kafka.storage.internals.log.{AbortedTxn, FetchDataInfo, LogConfig, LogDirFailureChannel, LogFileUtils, LogOffsetMetadata, LogSegment, LogSegments, OffsetPosition}
import java.io.{File, IOException}
import java.nio.file.Files
import java.util
import java.util.concurrent.atomic.AtomicLong
import java.util.regex.Pattern
import java.util.{Collections, Optional}
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
import scala.collection.{Seq, immutable}
import scala.compat.java8.OptionConverters._
import scala.jdk.CollectionConverters._
/**
@ -171,9 +173,9 @@ class LocalLog(@volatile private var _dir: File, @@ -171,9 +173,9 @@ class LocalLog(@volatile private var _dir: File,
val currentRecoveryPoint = recoveryPoint
if (currentRecoveryPoint <= offset) {
val segmentsToFlush = segments.values(currentRecoveryPoint, offset)
segmentsToFlush.foreach(_.flush())
segmentsToFlush.forEach(_.flush())
// If there are any new segments, we need to flush the parent directory for crash consistency.
if (segmentsToFlush.exists(_.baseOffset >= currentRecoveryPoint)) {
if (segmentsToFlush.stream().anyMatch(_.baseOffset >= currentRecoveryPoint)) {
// The directory might be renamed concurrently for topic deletion, which may cause NoSuchFileException here.
// Since the directory is to be deleted anyways, we just swallow NoSuchFileException and let it go.
Utils.flushDirIfExists(dir.toPath)
@ -248,8 +250,8 @@ class LocalLog(@volatile private var _dir: File, @@ -248,8 +250,8 @@ class LocalLog(@volatile private var _dir: File,
*/
private[log] def deleteAllSegments(): Iterable[LogSegment] = {
maybeHandleIOException(s"Error while deleting all segments for $topicPartition in dir ${dir.getParent}") {
val deletableSegments = List[LogSegment]() ++ segments.values
removeAndDeleteSegments(segments.values, asyncDelete = false, LogDeletion(this))
val deletableSegments = new util.ArrayList(segments.values).asScala
removeAndDeleteSegments(segments.values.asScala, asyncDelete = false, LogDeletion(this))
isMemoryMappedBufferClosed = true
deletableSegments
}
@ -341,11 +343,11 @@ class LocalLog(@volatile private var _dir: File, @@ -341,11 +343,11 @@ class LocalLog(@volatile private var _dir: File,
segmentToDelete.changeFileSuffixes("", LogFileUtils.DELETED_FILE_SUFFIX)
val newSegment = LogSegment.open(dir,
baseOffset = newOffset,
newOffset,
config,
time = time,
initFileSize = config.initFileSize,
preallocate = config.preallocate)
time,
config.initFileSize,
config.preallocate)
segments.add(newSegment)
reason.logReason(List(segmentToDelete))
@ -394,7 +396,7 @@ class LocalLog(@volatile private var _dir: File, @@ -394,7 +396,7 @@ class LocalLog(@volatile private var _dir: File,
var segmentOpt = segments.floorSegment(startOffset)
// return error on attempt to read beyond the log end offset
if (startOffset > endOffset || segmentOpt.isEmpty)
if (startOffset > endOffset || !segmentOpt.isPresent)
throw new OffsetOutOfRangeException(s"Received request for offset $startOffset for partition $topicPartition, " +
s"but we only have log segments upto $endOffset.")
@ -407,7 +409,7 @@ class LocalLog(@volatile private var _dir: File, @@ -407,7 +409,7 @@ class LocalLog(@volatile private var _dir: File,
// but if that segment doesn't contain any messages with an offset greater than that
// continue to read from successive segments until we get some messages or we reach the end of the log
var fetchDataInfo: FetchDataInfo = null
while (fetchDataInfo == null && segmentOpt.isDefined) {
while (fetchDataInfo == null && segmentOpt.isPresent) {
val segment = segmentOpt.get
val baseOffset = segment.baseOffset
@ -435,8 +437,7 @@ class LocalLog(@volatile private var _dir: File, @@ -435,8 +437,7 @@ class LocalLog(@volatile private var _dir: File,
}
private[log] def append(lastOffset: Long, largestTimestamp: Long, shallowOffsetOfMaxTimestamp: Long, records: MemoryRecords): Unit = {
segments.activeSegment.append(largestOffset = lastOffset, largestTimestamp = largestTimestamp,
shallowOffsetOfMaxTimestamp = shallowOffsetOfMaxTimestamp, records = records)
segments.activeSegment.append(lastOffset, largestTimestamp, shallowOffsetOfMaxTimestamp, records)
updateLogEndOffset(lastOffset + 1)
}
@ -445,9 +446,8 @@ class LocalLog(@volatile private var _dir: File, @@ -445,9 +446,8 @@ class LocalLog(@volatile private var _dir: File,
val fetchSize = fetchInfo.records.sizeInBytes
val startOffsetPosition = new OffsetPosition(fetchInfo.fetchOffsetMetadata.messageOffset,
fetchInfo.fetchOffsetMetadata.relativePositionInSegment)
val upperBoundOffset = segment.fetchUpperBoundOffset(startOffsetPosition, fetchSize).orElse {
segments.higherSegment(segment.baseOffset).map(_.baseOffset).getOrElse(logEndOffset)
}
val upperBoundOffset = segment.fetchUpperBoundOffset(startOffsetPosition, fetchSize).orElse(
segments.higherSegment(segment.baseOffset).asScala.map(s => s.baseOffset).getOrElse(logEndOffset))
val abortedTransactions = ListBuffer.empty[FetchResponseData.AbortedTransaction]
def accumulator(abortedTxns: Seq[AbortedTxn]): Unit = abortedTransactions ++= abortedTxns.map(_.asAbortedTransaction)
@ -478,7 +478,7 @@ class LocalLog(@volatile private var _dir: File, @@ -478,7 +478,7 @@ class LocalLog(@volatile private var _dir: File,
val segmentEntry = segments.floorSegment(baseOffset)
val allAbortedTxns = ListBuffer.empty[AbortedTxn]
def accumulator(abortedTxns: Seq[AbortedTxn]): Unit = allAbortedTxns ++= abortedTxns
segmentEntry.foreach(segment => collectAbortedTransactions(logStartOffset, upperBoundOffset, segment, accumulator))
segmentEntry.ifPresent(segment => collectAbortedTransactions(logStartOffset, upperBoundOffset, segment, accumulator))
allAbortedTxns.toList
}
@ -529,15 +529,15 @@ class LocalLog(@volatile private var _dir: File, @@ -529,15 +529,15 @@ class LocalLog(@volatile private var _dir: File,
Files.delete(file.toPath)
}
segments.lastSegment.foreach(_.onBecomeInactiveSegment())
segments.lastSegment.ifPresent(_.onBecomeInactiveSegment())
}
val newSegment = LogSegment.open(dir,
baseOffset = newOffset,
newOffset,
config,
time = time,
initFileSize = config.initFileSize,
preallocate = config.preallocate)
time,
config.initFileSize,
config.preallocate)
segments.add(newSegment)
// We need to update the segment base offset and append position data of the metadata when log rolls.
@ -560,7 +560,7 @@ class LocalLog(@volatile private var _dir: File, @@ -560,7 +560,7 @@ class LocalLog(@volatile private var _dir: File,
maybeHandleIOException(s"Error while truncating the entire log for $topicPartition in dir ${dir.getParent}") {
debug(s"Truncate and start at offset $newOffset")
checkIfMemoryMappedBufferClosed()
val segmentsToDelete = List[LogSegment]() ++ segments.values
val segmentsToDelete = new util.ArrayList(segments.values).asScala
if (segmentsToDelete.nonEmpty) {
removeAndDeleteSegments(segmentsToDelete.dropRight(1), asyncDelete = true, LogTruncation(this))
@ -582,7 +582,7 @@ class LocalLog(@volatile private var _dir: File, @@ -582,7 +582,7 @@ class LocalLog(@volatile private var _dir: File,
* @return the list of segments that were scheduled for deletion
*/
private[log] def truncateTo(targetOffset: Long): Iterable[LogSegment] = {
val deletableSegments = List[LogSegment]() ++ segments.filter(segment => segment.baseOffset > targetOffset)
val deletableSegments = segments.filter(segment => segment.baseOffset > targetOffset).asScala
removeAndDeleteSegments(deletableSegments, asyncDelete = true, LogTruncation(this))
segments.activeSegment.truncateTo(targetOffset)
updateLogEndOffset(targetOffset)
@ -777,7 +777,7 @@ object LocalLog extends Logging { @@ -777,7 +777,7 @@ object LocalLog extends Logging {
newSegments.foreach { splitSegment =>
splitSegment.onBecomeInactiveSegment()
splitSegment.flush()
splitSegment.lastModified = segment.lastModified
splitSegment.setLastModified(segment.lastModified)
totalSizeOfNewSegments += splitSegment.log.sizeInBytes
}
// size of all the new segments combined must equal size of the original segment
@ -945,9 +945,8 @@ object LocalLog extends Logging { @@ -945,9 +945,8 @@ object LocalLog extends Logging {
}
private[log] def createNewCleanedSegment(dir: File, logConfig: LogConfig, baseOffset: Long): LogSegment = {
LogSegment.deleteIfExists(dir, baseOffset, fileSuffix = CleanedFileSuffix)
LogSegment.open(dir, baseOffset, logConfig, Time.SYSTEM,
fileSuffix = CleanedFileSuffix, initFileSize = logConfig.initFileSize, preallocate = logConfig.preallocate)
LogSegment.deleteIfExists(dir, baseOffset, CleanedFileSuffix)
LogSegment.open(dir, baseOffset, logConfig, Time.SYSTEM, false, logConfig.initFileSize, logConfig.preallocate, CleanedFileSuffix)
}
/**
@ -958,7 +957,7 @@ object LocalLog extends Logging { @@ -958,7 +957,7 @@ object LocalLog extends Logging {
* @tparam T the type of object held within the iterator
* @return Some(iterator.next) if a next element exists, None otherwise.
*/
private def nextOption[T](iterator: Iterator[T]): Option[T] = {
private def nextOption[T](iterator: util.Iterator[T]): Option[T] = {
if (iterator.hasNext)
Some(iterator.next())
else

9
core/src/main/scala/kafka/log/LogCleaner.scala

@ -34,7 +34,7 @@ import org.apache.kafka.common.record._ @@ -34,7 +34,7 @@ import org.apache.kafka.common.record._
import org.apache.kafka.common.utils.{BufferSupplier, Time}
import org.apache.kafka.server.metrics.KafkaMetricsGroup
import org.apache.kafka.server.util.ShutdownableThread
import org.apache.kafka.storage.internals.log.{AbortedTxn, CleanerConfig, LastRecord, LogDirFailureChannel, OffsetMap, SkimpyOffsetMap, TransactionIndex}
import org.apache.kafka.storage.internals.log.{AbortedTxn, CleanerConfig, LastRecord, LogDirFailureChannel, LogSegment, LogSegmentOffsetOverflowException, OffsetMap, SkimpyOffsetMap, TransactionIndex}
import scala.jdk.CollectionConverters._
import scala.collection.mutable.ListBuffer
@ -696,7 +696,7 @@ private[log] class Cleaner(val id: Int, @@ -696,7 +696,7 @@ private[log] class Cleaner(val id: Int,
// update the modification date to retain the last modified date of the original files
val modified = segments.last.lastModified
cleaned.lastModified = modified
cleaned.setLastModified(modified)
// swap in new segment
info(s"Swapping in cleaned segment $cleaned for segment(s) $segments in log $log")
@ -812,10 +812,7 @@ private[log] class Cleaner(val id: Int, @@ -812,10 +812,7 @@ private[log] class Cleaner(val id: Int,
val retained = MemoryRecords.readableRecords(outputBuffer)
// it's OK not to hold the Log's lock in this case, because this segment is only accessed by other threads
// after `Log.replaceSegments` (which acquires the lock) is called
dest.append(largestOffset = result.maxOffset,
largestTimestamp = result.maxTimestamp,
shallowOffsetOfMaxTimestamp = result.shallowOffsetOfMaxTimestamp,
records = retained)
dest.append(result.maxOffset, result.maxTimestamp, result.shallowOffsetOfMaxTimestamp, retained)
throttler.maybeThrottle(outputBuffer.limit())
}

14
core/src/main/scala/kafka/log/LogCleanerManager.scala

@ -20,7 +20,6 @@ package kafka.log @@ -20,7 +20,6 @@ package kafka.log
import java.io.File
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kafka.common.LogCleaningAbortedException
import kafka.server.checkpoints.OffsetCheckpointFile
import kafka.utils.CoreUtils._
@ -31,6 +30,7 @@ import org.apache.kafka.common.utils.Time @@ -31,6 +30,7 @@ import org.apache.kafka.common.utils.Time
import org.apache.kafka.storage.internals.log.LogDirFailureChannel
import org.apache.kafka.server.metrics.KafkaMetricsGroup
import java.util.Comparator
import scala.collection.{Iterable, Seq, mutable}
import scala.jdk.CollectionConverters._
@ -595,13 +595,9 @@ private[log] object LogCleanerManager extends Logging { @@ -595,13 +595,9 @@ private[log] object LogCleanerManager extends Logging {
*/
def maxCompactionDelay(log: UnifiedLog, firstDirtyOffset: Long, now: Long) : Long = {
val dirtyNonActiveSegments = log.nonActiveLogSegmentsFrom(firstDirtyOffset)
val firstBatchTimestamps = log.getFirstBatchTimestampForSegments(dirtyNonActiveSegments).filter(_ > 0)
val firstBatchTimestamps = log.getFirstBatchTimestampForSegments(dirtyNonActiveSegments).stream.filter(_ > 0)
val earliestDirtySegmentTimestamp = {
if (firstBatchTimestamps.nonEmpty)
firstBatchTimestamps.min
else Long.MaxValue
}
val earliestDirtySegmentTimestamp = firstBatchTimestamps.min(Comparator.naturalOrder()).orElse(Long.MaxValue);
val maxCompactionLagMs = math.max(log.config.maxCompactionLagMs, 0L)
val cleanUntilTime = now - maxCompactionLagMs
@ -662,7 +658,7 @@ private[log] object LogCleanerManager extends Logging { @@ -662,7 +658,7 @@ private[log] object LogCleanerManager extends Logging {
if (minCompactionLagMs > 0) {
// dirty log segments
val dirtyNonActiveSegments = log.nonActiveLogSegmentsFrom(firstDirtyOffset)
dirtyNonActiveSegments.find { s =>
dirtyNonActiveSegments.asScala.find { s =>
val isUncleanable = s.largestTimestamp > now - minCompactionLagMs
debug(s"Checking if log segment may be cleaned: log='${log.name}' segment.baseOffset=${s.baseOffset} " +
s"segment.largestTimestamp=${s.largestTimestamp}; now - compactionLag=${now - minCompactionLagMs}; " +
@ -684,7 +680,7 @@ private[log] object LogCleanerManager extends Logging { @@ -684,7 +680,7 @@ private[log] object LogCleanerManager extends Logging {
* @return the biggest uncleanable offset and the total amount of cleanable bytes
*/
def calculateCleanableBytes(log: UnifiedLog, firstDirtyOffset: Long, uncleanableOffset: Long): (Long, Long) = {
val firstUncleanableSegment = log.nonActiveLogSegmentsFrom(uncleanableOffset).headOption.getOrElse(log.activeSegment)
val firstUncleanableSegment = log.nonActiveLogSegmentsFrom(uncleanableOffset).asScala.headOption.getOrElse(log.activeSegment)
val firstUncleanableOffset = firstUncleanableSegment.baseOffset
val cleanableBytes = log.logSegments(math.min(firstDirtyOffset, firstUncleanableOffset), firstUncleanableOffset).map(_.size.toLong).sum

64
core/src/main/scala/kafka/log/LogLoader.scala

@ -19,7 +19,6 @@ package kafka.log @@ -19,7 +19,6 @@ package kafka.log
import java.io.{File, IOException}
import java.nio.file.{Files, NoSuchFileException}
import kafka.common.LogSegmentOffsetOverflowException
import kafka.log.UnifiedLog.{CleanedFileSuffix, SwapFileSuffix, isIndexFile, isLogFile, offsetFromFile}
import kafka.utils.Logging
import org.apache.kafka.common.TopicPartition
@ -28,9 +27,11 @@ import org.apache.kafka.common.utils.{Time, Utils} @@ -28,9 +27,11 @@ import org.apache.kafka.common.utils.{Time, Utils}
import org.apache.kafka.snapshot.Snapshots
import org.apache.kafka.server.util.Scheduler
import org.apache.kafka.storage.internals.epoch.LeaderEpochFileCache
import org.apache.kafka.storage.internals.log.{CorruptIndexException, LoadedLogOffsets, LogConfig, LogDirFailureChannel, LogFileUtils, LogOffsetMetadata, ProducerStateManager}
import org.apache.kafka.storage.internals.log.{CorruptIndexException, LoadedLogOffsets, LogConfig, LogDirFailureChannel, LogFileUtils, LogOffsetMetadata, LogSegment, LogSegmentOffsetOverflowException, LogSegments, ProducerStateManager}
import java.util.Optional
import java.util.concurrent.{ConcurrentHashMap, ConcurrentMap}
import scala.collection.mutable.ArrayBuffer
import scala.collection.{Set, mutable}
import scala.jdk.CollectionConverters._
@ -76,7 +77,7 @@ class LogLoader( @@ -76,7 +77,7 @@ class LogLoader(
segments: LogSegments,
logStartOffsetCheckpoint: Long,
recoveryPointCheckpoint: Long,
leaderEpochCache: Option[LeaderEpochFileCache],
leaderEpochCache: Optional[LeaderEpochFileCache],
producerStateManager: ProducerStateManager,
numRemainingSegments: ConcurrentMap[String, Int] = new ConcurrentHashMap[String, Int],
isRemoteLogEnabled: Boolean = false,
@ -111,10 +112,13 @@ class LogLoader( @@ -111,10 +112,13 @@ class LogLoader(
swapFiles.filter(f => UnifiedLog.isLogFile(new File(Utils.replaceSuffix(f.getPath, SwapFileSuffix, "")))).foreach { f =>
val baseOffset = offsetFromFile(f)
val segment = LogSegment.open(f.getParentFile,
baseOffset = baseOffset,
baseOffset,
config,
time = time,
fileSuffix = UnifiedLog.SwapFileSuffix)
time,
false,
0,
false,
UnifiedLog.SwapFileSuffix)
info(s"Found log file ${f.getPath} from interrupted swap operation, which is recoverable from ${UnifiedLog.SwapFileSuffix} files by renaming.")
minSwapFileOffset = Math.min(segment.baseOffset, minSwapFileOffset)
maxSwapFileOffset = Math.max(segment.readNextOffset, maxSwapFileOffset)
@ -170,24 +174,25 @@ class LogLoader( @@ -170,24 +174,25 @@ class LogLoader(
if (segments.isEmpty) {
segments.add(
LogSegment.open(
dir = dir,
baseOffset = 0,
dir,
0,
config,
time = time,
initFileSize = config.initFileSize))
time,
config.initFileSize,
false))
}
(0L, 0L)
}
}
leaderEpochCache.foreach(_.truncateFromEnd(nextOffset))
leaderEpochCache.ifPresent(_.truncateFromEnd(nextOffset))
val newLogStartOffset = if (isRemoteLogEnabled) {
logStartOffsetCheckpoint
} else {
math.max(logStartOffsetCheckpoint, segments.firstSegment.get.baseOffset)
}
// The earliest leader epoch may not be flushed during a hard failure. Recover it here.
leaderEpochCache.foreach(_.truncateFromStart(logStartOffsetCheckpoint))
leaderEpochCache.ifPresent(_.truncateFromStart(logStartOffsetCheckpoint))
// Any segment loading or recovery code must not use producerStateManager, so that we can build the full state here
// from scratch.
@ -197,7 +202,7 @@ class LogLoader( @@ -197,7 +202,7 @@ class LogLoader(
// Reload all snapshots into the ProducerStateManager cache, the intermediate ProducerStateManager used
// during log recovery may have deleted some files without the LogLoader.producerStateManager instance witnessing the
// deletion.
producerStateManager.removeStraySnapshots(segments.baseOffsets.map(x => Long.box(x)).asJavaCollection)
producerStateManager.removeStraySnapshots(segments.baseOffsets)
UnifiedLog.rebuildProducerState(
producerStateManager,
segments,
@ -313,7 +318,7 @@ class LogLoader( @@ -313,7 +318,7 @@ class LogLoader(
if (isIndexFile(file)) {
// if it is an index file, make sure it has a corresponding .log file
val offset = offsetFromFile(file)
val logFile = UnifiedLog.logFile(dir, offset)
val logFile = LogFileUtils.logFile(dir, offset)
if (!logFile.exists) {
warn(s"Found an orphaned index file ${file.getAbsolutePath}, with no corresponding log file.")
Files.deleteIfExists(file.toPath)
@ -321,13 +326,16 @@ class LogLoader( @@ -321,13 +326,16 @@ class LogLoader(
} else if (isLogFile(file)) {
// if it's a log file, load the corresponding log segment
val baseOffset = offsetFromFile(file)
val timeIndexFileNewlyCreated = !UnifiedLog.timeIndexFile(dir, baseOffset).exists()
val timeIndexFileNewlyCreated = !LogFileUtils.timeIndexFile(dir, baseOffset).exists()
val segment = LogSegment.open(
dir = dir,
baseOffset = baseOffset,
dir,
baseOffset,
config,
time = time,
fileAlreadyExists = true)
time,
true,
0,
false,
"")
try segment.sanityCheck(timeIndexFileNewlyCreated)
catch {
@ -403,8 +411,8 @@ class LogLoader( @@ -403,8 +411,8 @@ class LogLoader(
warn(s"Deleting all segments because logEndOffset ($logEndOffset) " +
s"is smaller than logStartOffset $logStartOffsetCheckpoint. " +
"This could happen if segment files were deleted from the file system.")
removeAndDeleteSegmentsAsync(segments.values)
leaderEpochCache.foreach(_.clearAndFlush())
removeAndDeleteSegmentsAsync(segments.values.asScala)
leaderEpochCache.ifPresent(_.clearAndFlush())
producerStateManager.truncateFullyAndStartAt(logStartOffsetCheckpoint)
None
}
@ -439,7 +447,9 @@ class LogLoader( @@ -439,7 +447,9 @@ class LogLoader(
// we had an invalid message, delete all remaining log
warn(s"Corruption found in segment ${segment.baseOffset}," +
s" truncating to offset ${segment.readNextOffset}")
removeAndDeleteSegmentsAsync(unflushedIter.toList)
val unflushedRemaining = new ArrayBuffer[LogSegment]
unflushedIter.forEachRemaining(s => unflushedRemaining += s)
removeAndDeleteSegmentsAsync(unflushedRemaining)
truncated = true
// segment is truncated, so set remaining segments to 0
numRemainingSegments.put(threadName, 0)
@ -456,12 +466,12 @@ class LogLoader( @@ -456,12 +466,12 @@ class LogLoader(
// no existing segments, create a new mutable segment beginning at logStartOffset
segments.add(
LogSegment.open(
dir = dir,
baseOffset = logStartOffsetCheckpoint,
dir,
logStartOffsetCheckpoint,
config,
time = time,
initFileSize = config.initFileSize,
preallocate = config.preallocate))
time,
config.initFileSize,
config.preallocate))
}
// Update the recovery point if there was a clean shutdown and did not perform any changes to

2
core/src/main/scala/kafka/log/LogManager.scala

@ -837,7 +837,7 @@ class LogManager(logDirs: Seq[File], @@ -837,7 +837,7 @@ class LogManager(logDirs: Seq[File],
try {
logStartOffsetCheckpoints.get(logDir).foreach { checkpoint =>
val logStartOffsets = logsToCheckpoint.collect {
case (tp, log) if log.logStartOffset > log.logSegments.head.baseOffset => tp -> log.logStartOffset
case (tp, log) if log.logStartOffset > log.logSegments.asScala.head.baseOffset => tp -> log.logStartOffset
}
checkpoint.write(logStartOffsets)
}

693
core/src/main/scala/kafka/log/LogSegment.scala

@ -1,693 +0,0 @@ @@ -1,693 +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.log
import com.yammer.metrics.core.Timer
import kafka.common.LogSegmentOffsetOverflowException
import kafka.utils._
import org.apache.kafka.common.InvalidRecordException
import org.apache.kafka.common.errors.CorruptRecordException
import org.apache.kafka.common.record.FileRecords.{LogOffsetPosition, TimestampAndOffset}
import org.apache.kafka.common.record._
import org.apache.kafka.common.utils.{BufferSupplier, Time, Utils}
import org.apache.kafka.server.metrics.KafkaMetricsGroup
import org.apache.kafka.storage.internals.epoch.LeaderEpochFileCache
import org.apache.kafka.storage.internals.log.{AbortedTxn, AppendOrigin, CompletedTxn, FetchDataInfo, LazyIndex, LogConfig, LogOffsetMetadata, OffsetIndex, OffsetPosition, ProducerStateManager, RollParams, TimeIndex, TimestampOffset, TransactionIndex, TxnIndexSearchResult}
import java.io.{File, IOException}
import java.nio.file.attribute.FileTime
import java.nio.file.{Files, NoSuchFileException}
import java.util.Optional
import java.util.concurrent.TimeUnit
import scala.compat.java8.OptionConverters._
import scala.jdk.CollectionConverters._
import scala.math._
/**
* A segment of the log. Each segment has two components: a log and an index. The log is a FileRecords containing
* the actual messages. The index is an OffsetIndex that maps from logical offsets to physical file positions. Each
* segment has a base offset which is an offset <= the least offset of any message in this segment and > any offset in
* any previous segment.
*
* A segment with a base offset of [base_offset] would be stored in two files, a [base_offset].index and a [base_offset].log file.
*
* @param log The file records containing log entries
* @param lazyOffsetIndex The offset index
* @param lazyTimeIndex The timestamp index
* @param txnIndex The transaction index
* @param baseOffset A lower bound on the offsets in this segment
* @param indexIntervalBytes The approximate number of bytes between entries in the index
* @param rollJitterMs The maximum random jitter subtracted from the scheduled segment roll time
* @param time The time instance
*/
@nonthreadsafe
class LogSegment private[log] (val log: FileRecords,
val lazyOffsetIndex: LazyIndex[OffsetIndex],
val lazyTimeIndex: LazyIndex[TimeIndex],
val txnIndex: TransactionIndex,
val baseOffset: Long,
val indexIntervalBytes: Int,
val rollJitterMs: Long,
val time: Time) extends Logging {
def offsetIndex: OffsetIndex = lazyOffsetIndex.get
def timeIndex: TimeIndex = lazyTimeIndex.get
def shouldRoll(rollParams: RollParams): Boolean = {
val reachedRollMs = timeWaitedForRoll(rollParams.now, rollParams.maxTimestampInMessages) > rollParams.maxSegmentMs - rollJitterMs
size > rollParams.maxSegmentBytes - rollParams.messagesSize ||
(size > 0 && reachedRollMs) ||
offsetIndex.isFull || timeIndex.isFull || !canConvertToRelativeOffset(rollParams.maxOffsetInMessages)
}
def resizeIndexes(size: Int): Unit = {
offsetIndex.resize(size)
timeIndex.resize(size)
}
def sanityCheck(timeIndexFileNewlyCreated: Boolean): Unit = {
if (lazyOffsetIndex.file.exists) {
// Resize the time index file to 0 if it is newly created.
if (timeIndexFileNewlyCreated)
timeIndex.resize(0)
// Sanity checks for time index and offset index are skipped because
// we will recover the segments above the recovery point in recoverLog()
// in any case so sanity checking them here is redundant.
txnIndex.sanityCheck()
}
else throw new NoSuchFileException(s"Offset index file ${lazyOffsetIndex.file.getAbsolutePath} does not exist")
}
private var created = time.milliseconds
/* the number of bytes since we last added an entry in the offset index */
private var bytesSinceLastIndexEntry = 0
// The timestamp we used for time based log rolling and for ensuring max compaction delay
// volatile for LogCleaner to see the update
@volatile private var rollingBasedTimestamp: Option[Long] = None
/* The maximum timestamp and offset we see so far */
@volatile private var _maxTimestampAndOffsetSoFar: TimestampOffset = TimestampOffset.UNKNOWN
def maxTimestampAndOffsetSoFar_= (timestampOffset: TimestampOffset): Unit = _maxTimestampAndOffsetSoFar = timestampOffset
def maxTimestampAndOffsetSoFar: TimestampOffset = {
if (_maxTimestampAndOffsetSoFar == TimestampOffset.UNKNOWN)
_maxTimestampAndOffsetSoFar = timeIndex.lastEntry
_maxTimestampAndOffsetSoFar
}
/* The maximum timestamp we see so far */
def maxTimestampSoFar: Long = {
maxTimestampAndOffsetSoFar.timestamp
}
def offsetOfMaxTimestampSoFar: Long = {
maxTimestampAndOffsetSoFar.offset
}
/* Return the size in bytes of this log segment */
def size: Int = log.sizeInBytes()
/**
* checks that the argument offset can be represented as an integer offset relative to the baseOffset.
*/
def canConvertToRelativeOffset(offset: Long): Boolean = {
offsetIndex.canAppendOffset(offset)
}
/**
* Append the given messages starting with the given offset. Add
* an entry to the index if needed.
*
* It is assumed this method is being called from within a lock.
*
* @param largestOffset The last offset in the message set
* @param largestTimestamp The largest timestamp in the message set.
* @param shallowOffsetOfMaxTimestamp The offset of the message that has the largest timestamp in the messages to append.
* @param records The log entries to append.
* @throws LogSegmentOffsetOverflowException if the largest offset causes index offset overflow
*/
@nonthreadsafe
def append(largestOffset: Long,
largestTimestamp: Long,
shallowOffsetOfMaxTimestamp: Long,
records: MemoryRecords): Unit = {
if (records.sizeInBytes > 0) {
trace(s"Inserting ${records.sizeInBytes} bytes at end offset $largestOffset at position ${log.sizeInBytes} " +
s"with largest timestamp $largestTimestamp at shallow offset $shallowOffsetOfMaxTimestamp")
val physicalPosition = log.sizeInBytes()
if (physicalPosition == 0)
rollingBasedTimestamp = Some(largestTimestamp)
ensureOffsetInRange(largestOffset)
// append the messages
val appendedBytes = log.append(records)
trace(s"Appended $appendedBytes to ${log.file} at end offset $largestOffset")
// Update the in memory max timestamp and corresponding offset.
if (largestTimestamp > maxTimestampSoFar) {
maxTimestampAndOffsetSoFar = new TimestampOffset(largestTimestamp, shallowOffsetOfMaxTimestamp)
}
// append an entry to the index (if needed)
if (bytesSinceLastIndexEntry > indexIntervalBytes) {
offsetIndex.append(largestOffset, physicalPosition)
timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestampSoFar)
bytesSinceLastIndexEntry = 0
}
bytesSinceLastIndexEntry += records.sizeInBytes
}
}
private def ensureOffsetInRange(offset: Long): Unit = {
if (!canConvertToRelativeOffset(offset))
throw new LogSegmentOffsetOverflowException(this, offset)
}
private def appendChunkFromFile(records: FileRecords, position: Int, bufferSupplier: BufferSupplier): Int = {
var bytesToAppend = 0
var maxTimestamp = Long.MinValue
var offsetOfMaxTimestamp = Long.MinValue
var maxOffset = Long.MinValue
var readBuffer = bufferSupplier.get(1024 * 1024)
def canAppend(batch: RecordBatch) =
canConvertToRelativeOffset(batch.lastOffset) &&
(bytesToAppend == 0 || bytesToAppend + batch.sizeInBytes < readBuffer.capacity)
// find all batches that are valid to be appended to the current log segment and
// determine the maximum offset and timestamp
val nextBatches = records.batchesFrom(position).asScala.iterator
for (batch <- nextBatches.takeWhile(canAppend)) {
if (batch.maxTimestamp > maxTimestamp) {
maxTimestamp = batch.maxTimestamp
offsetOfMaxTimestamp = batch.lastOffset
}
maxOffset = batch.lastOffset
bytesToAppend += batch.sizeInBytes
}
if (bytesToAppend > 0) {
// Grow buffer if needed to ensure we copy at least one batch
if (readBuffer.capacity < bytesToAppend)
readBuffer = bufferSupplier.get(bytesToAppend)
readBuffer.limit(bytesToAppend)
records.readInto(readBuffer, position)
append(maxOffset, maxTimestamp, offsetOfMaxTimestamp, MemoryRecords.readableRecords(readBuffer))
}
bufferSupplier.release(readBuffer)
bytesToAppend
}
/**
* Append records from a file beginning at the given position until either the end of the file
* is reached or an offset is found which is too large to convert to a relative offset for the indexes.
*
* @return the number of bytes appended to the log (may be less than the size of the input if an
* offset is encountered which would overflow this segment)
*/
def appendFromFile(records: FileRecords, start: Int): Int = {
var position = start
val bufferSupplier: BufferSupplier = new BufferSupplier.GrowableBufferSupplier
while (position < start + records.sizeInBytes) {
val bytesAppended = appendChunkFromFile(records, position, bufferSupplier)
if (bytesAppended == 0)
return position - start
position += bytesAppended
}
position - start
}
@nonthreadsafe
def updateTxnIndex(completedTxn: CompletedTxn, lastStableOffset: Long): Unit = {
if (completedTxn.isAborted) {
trace(s"Writing aborted transaction $completedTxn to transaction index, last stable offset is $lastStableOffset")
txnIndex.append(new AbortedTxn(completedTxn, lastStableOffset))
}
}
private def updateProducerState(producerStateManager: ProducerStateManager, batch: RecordBatch): Unit = {
if (batch.hasProducerId) {
val producerId = batch.producerId
val appendInfo = producerStateManager.prepareUpdate(producerId, AppendOrigin.REPLICATION)
val maybeCompletedTxn = appendInfo.append(batch, Optional.empty())
producerStateManager.update(appendInfo)
maybeCompletedTxn.ifPresent(completedTxn => {
val lastStableOffset = producerStateManager.lastStableOffset(completedTxn)
updateTxnIndex(completedTxn, lastStableOffset)
producerStateManager.completeTxn(completedTxn)
})
}
producerStateManager.updateMapEndOffset(batch.lastOffset + 1)
}
/**
* Find the physical file position for the first message with offset >= the requested offset.
*
* The startingFilePosition argument is an optimization that can be used if we already know a valid starting position
* in the file higher than the greatest-lower-bound from the index.
*
* @param offset The offset we want to translate
* @param startingFilePosition A lower bound on the file position from which to begin the search. This is purely an optimization and
* when omitted, the search will begin at the position in the offset index.
* @return The position in the log storing the message with the least offset >= the requested offset and the size of the
* message or null if no message meets this criteria.
*/
@threadsafe
private[log] def translateOffset(offset: Long, startingFilePosition: Int = 0): LogOffsetPosition = {
val mapping = offsetIndex.lookup(offset)
log.searchForOffsetWithSize(offset, max(mapping.position, startingFilePosition))
}
/**
* Read a message set from this segment beginning with the first offset >= startOffset. The message set will include
* no more than maxSize bytes and will end before maxOffset if a maxOffset is specified.
*
* @param startOffset A lower bound on the first offset to include in the message set we read
* @param maxSize The maximum number of bytes to include in the message set we read
* @param maxPosition The maximum position in the log segment that should be exposed for read
* @param minOneMessage If this is true, the first message will be returned even if it exceeds `maxSize` (if one exists)
*
* @return The fetched data and the offset metadata of the first message whose offset is >= startOffset,
* or null if the startOffset is larger than the largest offset in this log
*/
@threadsafe
def read(startOffset: Long,
maxSize: Int,
maxPosition: Long = size,
minOneMessage: Boolean = false): FetchDataInfo = {
if (maxSize < 0)
throw new IllegalArgumentException(s"Invalid max size $maxSize for log read from segment $log")
val startOffsetAndSize = translateOffset(startOffset)
// if the start position is already off the end of the log, return null
if (startOffsetAndSize == null)
return null
val startPosition = startOffsetAndSize.position
val offsetMetadata = new LogOffsetMetadata(startOffset, this.baseOffset, startPosition)
val adjustedMaxSize =
if (minOneMessage) math.max(maxSize, startOffsetAndSize.size)
else maxSize
// return a log segment but with zero size in the case below
if (adjustedMaxSize == 0)
return new FetchDataInfo(offsetMetadata, MemoryRecords.EMPTY)
// calculate the length of the message set to read based on whether or not they gave us a maxOffset
val fetchSize: Int = min((maxPosition - startPosition).toInt, adjustedMaxSize)
new FetchDataInfo(offsetMetadata, log.slice(startPosition, fetchSize),
adjustedMaxSize < startOffsetAndSize.size, Optional.empty())
}
def fetchUpperBoundOffset(startOffsetPosition: OffsetPosition, fetchSize: Int): Optional[Long] =
offsetIndex.fetchUpperBoundOffset(startOffsetPosition, fetchSize).map(_.offset)
/**
* Run recovery on the given segment. This will rebuild the index from the log file and lop off any invalid bytes
* from the end of the log and index.
*
* @param producerStateManager Producer state corresponding to the segment's base offset. This is needed to recover
* the transaction index.
* @param leaderEpochCache Optionally a cache for updating the leader epoch during recovery.
* @return The number of bytes truncated from the log
* @throws LogSegmentOffsetOverflowException if the log segment contains an offset that causes the index offset to overflow
*/
@nonthreadsafe
def recover(producerStateManager: ProducerStateManager, leaderEpochCache: Option[LeaderEpochFileCache] = None): Int = {
offsetIndex.reset()
timeIndex.reset()
txnIndex.reset()
var validBytes = 0
var lastIndexEntry = 0
maxTimestampAndOffsetSoFar = TimestampOffset.UNKNOWN
try {
for (batch <- log.batches.asScala) {
batch.ensureValid()
ensureOffsetInRange(batch.lastOffset)
// The max timestamp is exposed at the batch level, so no need to iterate the records
if (batch.maxTimestamp > maxTimestampSoFar) {
maxTimestampAndOffsetSoFar = new TimestampOffset(batch.maxTimestamp, batch.lastOffset)
}
// Build offset index
if (validBytes - lastIndexEntry > indexIntervalBytes) {
offsetIndex.append(batch.lastOffset, validBytes)
timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestampSoFar)
lastIndexEntry = validBytes
}
validBytes += batch.sizeInBytes()
if (batch.magic >= RecordBatch.MAGIC_VALUE_V2) {
leaderEpochCache.foreach { cache =>
if (batch.partitionLeaderEpoch >= 0 && cache.latestEpoch.asScala.forall(batch.partitionLeaderEpoch > _))
cache.assign(batch.partitionLeaderEpoch, batch.baseOffset)
}
updateProducerState(producerStateManager, batch)
}
}
} catch {
case e@ (_: CorruptRecordException | _: InvalidRecordException) =>
warn("Found invalid messages in log segment %s at byte offset %d: %s. %s"
.format(log.file.getAbsolutePath, validBytes, e.getMessage, e.getCause))
}
val truncated = log.sizeInBytes - validBytes
if (truncated > 0)
debug(s"Truncated $truncated invalid bytes at the end of segment ${log.file.getAbsoluteFile} during recovery")
log.truncateTo(validBytes)
offsetIndex.trimToValidSize()
// A normally closed segment always appends the biggest timestamp ever seen into log segment, we do this as well.
timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestampSoFar, true)
timeIndex.trimToValidSize()
truncated
}
private def loadLargestTimestamp(): Unit = {
// Get the last time index entry. If the time index is empty, it will return (-1, baseOffset)
val lastTimeIndexEntry = timeIndex.lastEntry
maxTimestampAndOffsetSoFar = lastTimeIndexEntry
val offsetPosition = offsetIndex.lookup(lastTimeIndexEntry.offset)
// Scan the rest of the messages to see if there is a larger timestamp after the last time index entry.
val maxTimestampOffsetAfterLastEntry = log.largestTimestampAfter(offsetPosition.position)
if (maxTimestampOffsetAfterLastEntry.timestamp > lastTimeIndexEntry.timestamp) {
maxTimestampAndOffsetSoFar = new TimestampOffset(maxTimestampOffsetAfterLastEntry.timestamp, maxTimestampOffsetAfterLastEntry.offset)
}
}
/**
* Check whether the last offset of the last batch in this segment overflows the indexes.
*/
def hasOverflow: Boolean = {
val nextOffset = readNextOffset
nextOffset > baseOffset && !canConvertToRelativeOffset(nextOffset - 1)
}
def collectAbortedTxns(fetchOffset: Long, upperBoundOffset: Long): TxnIndexSearchResult =
txnIndex.collectAbortedTxns(fetchOffset, upperBoundOffset)
override def toString: String = "LogSegment(baseOffset=" + baseOffset +
", size=" + size +
", lastModifiedTime=" + lastModified +
", largestRecordTimestamp=" + largestRecordTimestamp +
")"
/**
* Truncate off all index and log entries with offsets >= the given offset.
* If the given offset is larger than the largest message in this segment, do nothing.
*
* @param offset The offset to truncate to
* @return The number of log bytes truncated
*/
@nonthreadsafe
def truncateTo(offset: Long): Int = {
// Do offset translation before truncating the index to avoid needless scanning
// in case we truncate the full index
val mapping = translateOffset(offset)
offsetIndex.truncateTo(offset)
timeIndex.truncateTo(offset)
txnIndex.truncateTo(offset)
// After truncation, reset and allocate more space for the (new currently active) index
offsetIndex.resize(offsetIndex.maxIndexSize)
timeIndex.resize(timeIndex.maxIndexSize)
val bytesTruncated = if (mapping == null) 0 else log.truncateTo(mapping.position)
if (log.sizeInBytes == 0) {
created = time.milliseconds
rollingBasedTimestamp = None
}
bytesSinceLastIndexEntry = 0
if (maxTimestampSoFar >= 0)
loadLargestTimestamp()
bytesTruncated
}
/**
* Calculate the offset that would be used for the next message to be append to this segment.
* Note that this is expensive.
*/
@threadsafe
def readNextOffset: Long = {
val fetchData = read(offsetIndex.lastOffset, log.sizeInBytes)
if (fetchData == null)
baseOffset
else
fetchData.records.batches.asScala.lastOption
.map(_.nextOffset)
.getOrElse(baseOffset)
}
/**
* Flush this log segment to disk
*/
@threadsafe
def flush(): Unit = {
LogFlushStats.logFlushTimer.time { () =>
log.flush()
offsetIndex.flush()
timeIndex.flush()
txnIndex.flush()
}
}
/**
* Update the directory reference for the log and indices in this segment. This would typically be called after a
* directory is renamed.
*/
def updateParentDir(dir: File): Unit = {
log.updateParentDir(dir)
lazyOffsetIndex.updateParentDir(dir)
lazyTimeIndex.updateParentDir(dir)
txnIndex.updateParentDir(dir)
}
/**
* Change the suffix for the index and log files for this log segment
* IOException from this method should be handled by the caller
*/
def changeFileSuffixes(oldSuffix: String, newSuffix: String): Unit = {
log.renameTo(new File(Utils.replaceSuffix(log.file.getPath, oldSuffix, newSuffix)))
lazyOffsetIndex.renameTo(new File(Utils.replaceSuffix(lazyOffsetIndex.file.getPath, oldSuffix, newSuffix)))
lazyTimeIndex.renameTo(new File(Utils.replaceSuffix(lazyTimeIndex.file.getPath, oldSuffix, newSuffix)))
txnIndex.renameTo(new File(Utils.replaceSuffix(txnIndex.file.getPath, oldSuffix, newSuffix)))
}
def hasSuffix(suffix: String): Boolean = {
log.file.getName.endsWith(suffix) &&
lazyOffsetIndex.file.getName.endsWith(suffix) &&
lazyTimeIndex.file.getName.endsWith(suffix) &&
txnIndex.file.getName.endsWith(suffix)
}
/**
* Append the largest time index entry to the time index and trim the log and indexes.
*
* The time index entry appended will be used to decide when to delete the segment.
*/
def onBecomeInactiveSegment(): Unit = {
timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestampSoFar, true)
offsetIndex.trimToValidSize()
timeIndex.trimToValidSize()
log.trim()
}
/**
* If not previously loaded,
* load the timestamp of the first message into memory.
*/
private def loadFirstBatchTimestamp(): Unit = {
if (rollingBasedTimestamp.isEmpty) {
val iter = log.batches.iterator()
if (iter.hasNext)
rollingBasedTimestamp = Some(iter.next().maxTimestamp)
}
}
/**
* The time this segment has waited to be rolled.
* If the first message batch has a timestamp we use its timestamp to determine when to roll a segment. A segment
* is rolled if the difference between the new batch's timestamp and the first batch's timestamp exceeds the
* segment rolling time.
* If the first batch does not have a timestamp, we use the wall clock time to determine when to roll a segment. A
* segment is rolled if the difference between the current wall clock time and the segment create time exceeds the
* segment rolling time.
*/
def timeWaitedForRoll(now: Long, messageTimestamp: Long): Long = {
// Load the timestamp of the first message into memory
loadFirstBatchTimestamp()
rollingBasedTimestamp match {
case Some(t) if t >= 0 => messageTimestamp - t
case _ => now - created
}
}
/**
* @return the first batch timestamp if the timestamp is available. Otherwise return Long.MaxValue
*/
def getFirstBatchTimestamp(): Long = {
loadFirstBatchTimestamp()
rollingBasedTimestamp match {
case Some(t) if t >= 0 => t
case _ => Long.MaxValue
}
}
/**
* Search the message offset based on timestamp and offset.
*
* This method returns an option of TimestampOffset. The returned value is determined using the following ordered list of rules:
*
* - If all the messages in the segment have smaller offsets, return None
* - If all the messages in the segment have smaller timestamps, return None
* - If all the messages in the segment have larger timestamps, or no message in the segment has a timestamp
* the returned the offset will be max(the base offset of the segment, startingOffset) and the timestamp will be Message.NoTimestamp.
* - Otherwise, return an option of TimestampOffset. The offset is the offset of the first message whose timestamp
* is greater than or equals to the target timestamp and whose offset is greater than or equals to the startingOffset.
*
* This methods only returns None when 1) all messages' offset < startOffing or 2) the log is not empty but we did not
* see any message when scanning the log from the indexed position. The latter could happen if the log is truncated
* after we get the indexed position but before we scan the log from there. In this case we simply return None and the
* caller will need to check on the truncated log and maybe retry or even do the search on another log segment.
*
* @param timestamp The timestamp to search for.
* @param startingOffset The starting offset to search.
* @return the timestamp and offset of the first message that meets the requirements. None will be returned if there is no such message.
*/
def findOffsetByTimestamp(timestamp: Long, startingOffset: Long = baseOffset): Option[TimestampAndOffset] = {
// Get the index entry with a timestamp less than or equal to the target timestamp
val timestampOffset = timeIndex.lookup(timestamp)
val position = offsetIndex.lookup(math.max(timestampOffset.offset, startingOffset)).position
// Search the timestamp
Option(log.searchForTimestamp(timestamp, position, startingOffset))
}
/**
* Close this log segment
*/
def close(): Unit = {
if (_maxTimestampAndOffsetSoFar != TimestampOffset.UNKNOWN)
CoreUtils.swallow(timeIndex.maybeAppend(maxTimestampSoFar, offsetOfMaxTimestampSoFar, true), this)
CoreUtils.swallow(lazyOffsetIndex.close(), this)
CoreUtils.swallow(lazyTimeIndex.close(), this)
CoreUtils.swallow(log.close(), this)
CoreUtils.swallow(txnIndex.close(), this)
}
/**
* Close file handlers used by the log segment but don't write to disk. This is used when the disk may have failed
*/
def closeHandlers(): Unit = {
CoreUtils.swallow(lazyOffsetIndex.closeHandler(), this)
CoreUtils.swallow(lazyTimeIndex.closeHandler(), this)
CoreUtils.swallow(log.closeHandlers(), this)
CoreUtils.swallow(txnIndex.close(), this)
}
/**
* Delete this log segment from the filesystem.
*/
def deleteIfExists(): Unit = {
def delete(delete: () => Boolean, fileType: String, file: File, logIfMissing: Boolean): Unit = {
try {
if (delete())
info(s"Deleted $fileType ${file.getAbsolutePath}.")
else if (logIfMissing)
info(s"Failed to delete $fileType ${file.getAbsolutePath} because it does not exist.")
}
catch {
case e: IOException => throw new IOException(s"Delete of $fileType ${file.getAbsolutePath} failed.", e)
}
}
CoreUtils.tryAll(Seq(
() => delete(log.deleteIfExists _, "log", log.file, logIfMissing = true),
() => delete(lazyOffsetIndex.deleteIfExists _, "offset index", lazyOffsetIndex.file, logIfMissing = true),
() => delete(lazyTimeIndex.deleteIfExists _, "time index", lazyTimeIndex.file, logIfMissing = true),
() => delete(txnIndex.deleteIfExists _, "transaction index", txnIndex.file, logIfMissing = false)
))
}
def deleted(): Boolean = {
!log.file.exists() && !lazyOffsetIndex.file.exists() && !lazyTimeIndex.file.exists() && !txnIndex.file.exists()
}
/**
* The last modified time of this log segment as a unix time stamp
*/
def lastModified = log.file.lastModified
/**
* The largest timestamp this segment contains, if maxTimestampSoFar >= 0, otherwise None.
*/
def largestRecordTimestamp: Option[Long] = if (maxTimestampSoFar >= 0) Some(maxTimestampSoFar) else None
/**
* The largest timestamp this segment contains.
*/
def largestTimestamp = if (maxTimestampSoFar >= 0) maxTimestampSoFar else lastModified
/**
* Change the last modified time for this log segment
*/
def lastModified_=(ms: Long) = {
val fileTime = FileTime.fromMillis(ms)
Files.setLastModifiedTime(log.file.toPath, fileTime)
Files.setLastModifiedTime(lazyOffsetIndex.file.toPath, fileTime)
Files.setLastModifiedTime(lazyTimeIndex.file.toPath, fileTime)
}
}
object LogSegment {
def open(dir: File, baseOffset: Long, config: LogConfig, time: Time, fileAlreadyExists: Boolean = false,
initFileSize: Int = 0, preallocate: Boolean = false, fileSuffix: String = ""): LogSegment = {
val maxIndexSize = config.maxIndexSize
new LogSegment(
FileRecords.open(UnifiedLog.logFile(dir, baseOffset, fileSuffix), fileAlreadyExists, initFileSize, preallocate),
LazyIndex.forOffset(UnifiedLog.offsetIndexFile(dir, baseOffset, fileSuffix), baseOffset, maxIndexSize),
LazyIndex.forTime(UnifiedLog.timeIndexFile(dir, baseOffset, fileSuffix), baseOffset, maxIndexSize),
new TransactionIndex(baseOffset, UnifiedLog.transactionIndexFile(dir, baseOffset, fileSuffix)),
baseOffset,
indexIntervalBytes = config.indexInterval,
rollJitterMs = config.randomSegmentJitter,
time)
}
def deleteIfExists(dir: File, baseOffset: Long, fileSuffix: String = ""): Unit = {
UnifiedLog.deleteFileIfExists(UnifiedLog.offsetIndexFile(dir, baseOffset, fileSuffix))
UnifiedLog.deleteFileIfExists(UnifiedLog.timeIndexFile(dir, baseOffset, fileSuffix))
UnifiedLog.deleteFileIfExists(UnifiedLog.transactionIndexFile(dir, baseOffset, fileSuffix))
UnifiedLog.deleteFileIfExists(UnifiedLog.logFile(dir, baseOffset, fileSuffix))
}
}
object LogFlushStats {
private val metricsGroup = new KafkaMetricsGroup(LogFlushStats.getClass)
val logFlushTimer: Timer = metricsGroup.newTimer("LogFlushRateAndTimeMs", TimeUnit.MILLISECONDS, TimeUnit.SECONDS)
}

268
core/src/main/scala/kafka/log/LogSegments.scala

@ -1,268 +0,0 @@ @@ -1,268 +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.log
import java.io.File
import java.util.Map
import java.util.concurrent.{ConcurrentNavigableMap, ConcurrentSkipListMap}
import kafka.utils.threadsafe
import org.apache.kafka.common.TopicPartition
import scala.jdk.CollectionConverters._
/**
* This class encapsulates a thread-safe navigable map of LogSegment instances and provides the
* required read and write behavior on the map.
*
* @param topicPartition the TopicPartition associated with the segments
* (useful for logging purposes)
*/
class LogSegments(topicPartition: TopicPartition) {
/* the segments of the log with key being LogSegment base offset and value being a LogSegment */
private val segments: ConcurrentNavigableMap[Long, LogSegment] = new ConcurrentSkipListMap[Long, LogSegment]
/**
* @return true if the segments are empty, false otherwise.
*/
@threadsafe
def isEmpty: Boolean = segments.isEmpty
/**
* @return true if the segments are non-empty, false otherwise.
*/
@threadsafe
def nonEmpty: Boolean = !isEmpty
/**
* Add the given segment, or replace an existing entry.
*
* @param segment the segment to add
*/
@threadsafe
def add(segment: LogSegment): LogSegment = this.segments.put(segment.baseOffset, segment)
/**
* Remove the segment at the provided offset.
*
* @param offset the offset to be removed
*/
@threadsafe
def remove(offset: Long): Unit = segments.remove(offset)
/**
* Clears all entries.
*/
@threadsafe
def clear(): Unit = segments.clear()
/**
* Close all segments.
*/
def close(): Unit = values.foreach(_.close())
/**
* Close the handlers for all segments.
*/
def closeHandlers(): Unit = values.foreach(_.closeHandlers())
/**
* Update the directory reference for the log and indices of all segments.
*
* @param dir the renamed directory
*/
def updateParentDir(dir: File): Unit = values.foreach(_.updateParentDir(dir))
/**
* Take care! this is an O(n) operation, where n is the number of segments.
*
* @return The number of segments.
*
*/
@threadsafe
def numberOfSegments: Int = segments.size
/**
* @return the base offsets of all segments
*/
def baseOffsets: Iterable[Long] = segments.values().asScala.map(_.baseOffset)
/**
* @param offset the segment to be checked
* @return true if a segment exists at the provided offset, false otherwise.
*/
@threadsafe
def contains(offset: Long): Boolean = segments.containsKey(offset)
/**
* Retrieves a segment at the specified offset.
*
* @param offset the segment to be retrieved
*
* @return the segment if it exists, otherwise None.
*/
@threadsafe
def get(offset: Long): Option[LogSegment] = Option(segments.get(offset))
/**
* @return an iterator to the log segments ordered from oldest to newest.
*/
def values: Iterable[LogSegment] = segments.values.asScala
/**
* @return An iterator to all segments beginning with the segment that includes "from" and ending
* with the segment that includes up to "to-1" or the end of the log (if to > end of log).
*/
def values(from: Long, to: Long): Iterable[LogSegment] = {
if (from == to) {
// Handle non-segment-aligned empty sets
List.empty[LogSegment]
} else if (to < from) {
throw new IllegalArgumentException(s"Invalid log segment range: requested segments in $topicPartition " +
s"from offset $from which is greater than limit offset $to")
} else {
val view = Option(segments.floorKey(from)).map { floor =>
segments.subMap(floor, to)
}.getOrElse(segments.headMap(to))
view.values.asScala
}
}
def nonActiveLogSegmentsFrom(from: Long): Iterable[LogSegment] = {
val activeSegment = lastSegment.get
if (from > activeSegment.baseOffset)
Seq.empty
else
values(from, activeSegment.baseOffset)
}
/**
* @return the entry associated with the greatest offset less than or equal to the given offset,
* if it exists.
*/
@threadsafe
private def floorEntry(offset: Long): Option[Map.Entry[Long, LogSegment]] = Option(segments.floorEntry(offset))
/**
* @return the log segment with the greatest offset less than or equal to the given offset,
* if it exists.
*/
@threadsafe
def floorSegment(offset: Long): Option[LogSegment] = floorEntry(offset).map(_.getValue)
/**
* @return the entry associated with the greatest offset strictly less than the given offset,
* if it exists.
*/
@threadsafe
private def lowerEntry(offset: Long): Option[Map.Entry[Long, LogSegment]] = Option(segments.lowerEntry(offset))
/**
* @return the log segment with the greatest offset strictly less than the given offset,
* if it exists.
*/
@threadsafe
def lowerSegment(offset: Long): Option[LogSegment] = lowerEntry(offset).map(_.getValue)
/**
* @return the entry associated with the smallest offset strictly greater than the given offset,
* if it exists.
*/
@threadsafe
def higherEntry(offset: Long): Option[Map.Entry[Long, LogSegment]] = Option(segments.higherEntry(offset))
/**
* @return the log segment with the smallest offset strictly greater than the given offset,
* if it exists.
*/
@threadsafe
def higherSegment(offset: Long): Option[LogSegment] = higherEntry(offset).map(_.getValue)
/**
* @return the entry associated with the smallest offset, if it exists.
*/
@threadsafe
def firstEntry: Option[Map.Entry[Long, LogSegment]] = Option(segments.firstEntry)
/**
* @return the log segment associated with the smallest offset, if it exists.
*/
@threadsafe
def firstSegment: Option[LogSegment] = firstEntry.map(_.getValue)
/**
* @return the base offset of the log segment associated with the smallest offset, if it exists
*/
private[log] def firstSegmentBaseOffset: Option[Long] = firstSegment.map(_.baseOffset)
/**
* @return the entry associated with the greatest offset, if it exists.
*/
@threadsafe
def lastEntry: Option[Map.Entry[Long, LogSegment]] = Option(segments.lastEntry)
/**
* @return the log segment with the greatest offset, if it exists.
*/
@threadsafe
def lastSegment: Option[LogSegment] = lastEntry.map(_.getValue)
/**
* @return an iterable with log segments ordered from lowest base offset to highest,
* each segment returned has a base offset strictly greater than the provided baseOffset.
*/
def higherSegments(baseOffset: Long): Iterable[LogSegment] = {
val view =
Option(segments.higherKey(baseOffset)).map {
higherOffset => segments.tailMap(higherOffset, true)
}.getOrElse(collection.immutable.Map[Long, LogSegment]().asJava)
view.values.asScala
}
/**
* The active segment that is currently taking appends
*/
def activeSegment = lastSegment.get
def sizeInBytes: Long = LogSegments.sizeInBytes(values)
/**
* Returns an Iterable containing segments matching the provided predicate.
*
* @param predicate the predicate to be used for filtering segments.
*/
def filter(predicate: LogSegment => Boolean): Iterable[LogSegment] = values.filter(predicate)
}
object LogSegments {
/**
* Calculate a log's size (in bytes) from the provided log segments.
*
* @param segments The log segments to calculate the size of
* @return Sum of the log segments' sizes (in bytes)
*/
def sizeInBytes(segments: Iterable[LogSegment]): Long =
segments.map(_.size.toLong).sum
def getFirstBatchTimestampForSegments(segments: Iterable[LogSegment]): Iterable[Long] = {
segments.map {
segment =>
segment.getFirstBatchTimestamp()
}
}
}

82
core/src/main/scala/kafka/log/UnifiedLog.scala

@ -40,12 +40,13 @@ import org.apache.kafka.server.record.BrokerCompressionType @@ -40,12 +40,13 @@ import org.apache.kafka.server.record.BrokerCompressionType
import org.apache.kafka.server.util.Scheduler
import org.apache.kafka.storage.internals.checkpoint.LeaderEpochCheckpointFile
import org.apache.kafka.storage.internals.epoch.LeaderEpochFileCache
import org.apache.kafka.storage.internals.log.{AbortedTxn, AppendOrigin, BatchMetadata, CompletedTxn, EpochEntry, FetchDataInfo, FetchIsolation, LastRecord, LeaderHwChange, LogAppendInfo, LogConfig, LogDirFailureChannel, LogFileUtils, LogOffsetMetadata, LogOffsetSnapshot, LogOffsetsListener, LogStartOffsetIncrementReason, LogValidator, ProducerAppendInfo, ProducerStateManager, ProducerStateManagerConfig, RollParams}
import org.apache.kafka.storage.internals.log.{AbortedTxn, AppendOrigin, BatchMetadata, CompletedTxn, EpochEntry, FetchDataInfo, FetchIsolation, LastRecord, LeaderHwChange, LogAppendInfo, LogConfig, LogDirFailureChannel, LogFileUtils, LogOffsetMetadata, LogOffsetSnapshot, LogOffsetsListener, LogSegment, LogSegments, LogStartOffsetIncrementReason, LogValidator, ProducerAppendInfo, ProducerStateManager, ProducerStateManagerConfig, RollParams}
import java.io.{File, IOException}
import java.nio.file.Files
import java.util
import java.util.concurrent.{ConcurrentHashMap, ConcurrentMap}
import java.util.stream.Collectors
import java.util.{Collections, Optional, OptionalInt, OptionalLong}
import scala.annotation.nowarn
import scala.collection.mutable.ListBuffer
@ -165,7 +166,7 @@ class UnifiedLog(@volatile var logStartOffset: Long, @@ -165,7 +166,7 @@ class UnifiedLog(@volatile var logStartOffset: Long,
initializePartitionMetadata()
updateLogStartOffset(logStartOffset)
updateLocalLogStartOffset(math.max(logStartOffset, localLog.segments.firstSegmentBaseOffset.getOrElse(0L)))
updateLocalLogStartOffset(math.max(logStartOffset, localLog.segments.firstSegmentBaseOffset.orElse(0L)))
if (!remoteLogEnabled())
logStartOffset = localLogStartOffset()
maybeIncrementFirstUnstableOffset()
@ -1313,9 +1314,9 @@ class UnifiedLog(@volatile var logStartOffset: Long, @@ -1313,9 +1314,9 @@ class UnifiedLog(@volatile var logStartOffset: Long,
} else if (targetTimestamp == ListOffsetsRequest.MAX_TIMESTAMP) {
// Cache to avoid race conditions. `toBuffer` is faster than most alternatives and provides
// constant time access while being safe to use with concurrent collections unlike `toArray`.
val segmentsCopy = logSegments.toBuffer
val segmentsCopy = logSegments.asScala.toBuffer
val latestTimestampSegment = segmentsCopy.maxBy(_.maxTimestampSoFar)
val latestTimestampAndOffset = latestTimestampSegment.maxTimestampAndOffsetSoFar
val latestTimestampAndOffset = latestTimestampSegment.readMaxTimestampAndOffsetSoFar
Some(new TimestampAndOffset(latestTimestampAndOffset.timestamp,
latestTimestampAndOffset.offset,
@ -1347,15 +1348,15 @@ class UnifiedLog(@volatile var logStartOffset: Long, @@ -1347,15 +1348,15 @@ class UnifiedLog(@volatile var logStartOffset: Long,
private def searchOffsetInLocalLog(targetTimestamp: Long, startOffset: Long): Option[TimestampAndOffset] = {
// Cache to avoid race conditions. `toBuffer` is faster than most alternatives and provides
// constant time access while being safe to use with concurrent collections unlike `toArray`.
val segmentsCopy = logSegments.toBuffer
val segmentsCopy = logSegments.asScala.toBuffer
val targetSeg = segmentsCopy.find(_.largestTimestamp >= targetTimestamp)
targetSeg.flatMap(_.findOffsetByTimestamp(targetTimestamp, startOffset))
targetSeg.flatMap(_.findOffsetByTimestamp(targetTimestamp, startOffset).asScala)
}
def legacyFetchOffsetsBefore(timestamp: Long, maxNumOffsets: Int): Seq[Long] = {
// Cache to avoid race conditions. `toBuffer` is faster than most alternatives and provides
// constant time access while being safe to use with concurrent collections unlike `toArray`.
val allSegments = logSegments.toBuffer
val allSegments = logSegments.asScala.toBuffer
val lastSegmentHasSize = allSegments.last.size > 0
val offsetTimeArray =
@ -1463,7 +1464,7 @@ class UnifiedLog(@volatile var logStartOffset: Long, @@ -1463,7 +1464,7 @@ class UnifiedLog(@volatile var logStartOffset: Long,
// remove the segments for lookups
localLog.removeAndDeleteSegments(segmentsToDelete, asyncDelete = true, reason)
deleteProducerSnapshots(deletable, asyncDelete = true)
incrementStartOffset(localLog.segments.firstSegmentBaseOffset.get, LogStartOffsetIncrementReason.SegmentDeletion)
incrementStartOffset(localLog.segments.firstSegmentBaseOffset.getAsLong, LogStartOffsetIncrementReason.SegmentDeletion)
}
numToDelete
}
@ -1531,7 +1532,8 @@ class UnifiedLog(@volatile var logStartOffset: Long, @@ -1531,7 +1532,8 @@ class UnifiedLog(@volatile var logStartOffset: Long,
/**
* The log size in bytes for all segments that are only in local log but not yet in remote log.
*/
def onlyLocalLogSegmentsSize: Long = UnifiedLog.sizeInBytes(logSegments.filter(_.baseOffset >= highestOffsetInRemoteStorage))
def onlyLocalLogSegmentsSize: Long =
UnifiedLog.sizeInBytes(logSegments.stream.filter(_.baseOffset >= highestOffsetInRemoteStorage).collect(Collectors.toList[LogSegment]))
/**
* The offset of the next message that will be appended to the log
@ -1719,7 +1721,7 @@ class UnifiedLog(@volatile var logStartOffset: Long, @@ -1719,7 +1721,7 @@ class UnifiedLog(@volatile var logStartOffset: Long,
info(s"Truncating to offset $targetOffset")
lock synchronized {
localLog.checkIfMemoryMappedBufferClosed()
if (localLog.segments.firstSegmentBaseOffset.get > targetOffset) {
if (localLog.segments.firstSegmentBaseOffset.getAsLong > targetOffset) {
truncateFullyAndStartAt(targetOffset)
} else {
val deletedSegments = localLog.truncateTo(targetOffset)
@ -1770,17 +1772,17 @@ class UnifiedLog(@volatile var logStartOffset: Long, @@ -1770,17 +1772,17 @@ class UnifiedLog(@volatile var logStartOffset: Long,
/**
* All the log segments in this log ordered from oldest to newest
*/
def logSegments: Iterable[LogSegment] = localLog.segments.values
def logSegments: util.Collection[LogSegment] = localLog.segments.values
/**
* Get all segments beginning with the segment that includes "from" and ending with the segment
* that includes up to "to-1" or the end of the log (if to > logEndOffset).
*/
def logSegments(from: Long, to: Long): Iterable[LogSegment] = lock synchronized {
localLog.segments.values(from, to)
localLog.segments.values(from, to).asScala
}
def nonActiveLogSegmentsFrom(from: Long): Iterable[LogSegment] = lock synchronized {
def nonActiveLogSegmentsFrom(from: Long): util.Collection[LogSegment] = lock synchronized {
localLog.segments.nonActiveLogSegmentsFrom(from)
}
@ -1814,7 +1816,7 @@ class UnifiedLog(@volatile var logStartOffset: Long, @@ -1814,7 +1816,7 @@ class UnifiedLog(@volatile var logStartOffset: Long,
* Currently, it is used by LogCleaner threads on log compact non-active segments only with LogCleanerManager's lock
* to ensure no other logcleaner threads and retention thread can work on the same segment.
*/
private[log] def getFirstBatchTimestampForSegments(segments: Iterable[LogSegment]): Iterable[Long] = {
private[log] def getFirstBatchTimestampForSegments(segments: util.Collection[LogSegment]): util.Collection[java.lang.Long] = {
LogSegments.getFirstBatchTimestampForSegments(segments)
}
@ -1926,7 +1928,7 @@ object UnifiedLog extends Logging { @@ -1926,7 +1928,7 @@ object UnifiedLog extends Logging {
segments,
logStartOffset,
recoveryPoint,
leaderEpochCache,
leaderEpochCache.asJava,
producerStateManager,
numRemainingSegments,
isRemoteLogEnabled,
@ -1945,26 +1947,17 @@ object UnifiedLog extends Logging { @@ -1945,26 +1947,17 @@ object UnifiedLog extends Logging {
logOffsetsListener)
}
def logFile(dir: File, offset: Long, suffix: String = ""): File = LogFileUtils.logFile(dir, offset, suffix)
def logDeleteDirName(topicPartition: TopicPartition): String = LocalLog.logDeleteDirName(topicPartition)
def logFutureDirName(topicPartition: TopicPartition): String = LocalLog.logFutureDirName(topicPartition)
def logDirName(topicPartition: TopicPartition): String = LocalLog.logDirName(topicPartition)
def offsetIndexFile(dir: File, offset: Long, suffix: String = ""): File = LogFileUtils.offsetIndexFile(dir, offset, suffix)
def timeIndexFile(dir: File, offset: Long, suffix: String = ""): File = LogFileUtils.timeIndexFile(dir, offset, suffix)
def deleteFileIfExists(file: File, suffix: String = ""): Unit =
Files.deleteIfExists(new File(file.getPath + suffix).toPath)
def transactionIndexFile(dir: File, offset: Long, suffix: String = ""): File = LogFileUtils.transactionIndexFile(dir, offset, suffix)
def offsetFromFile(file: File): Long = LogFileUtils.offsetFromFile(file)
def sizeInBytes(segments: Iterable[LogSegment]): Long = LogSegments.sizeInBytes(segments)
def sizeInBytes(segments: util.Collection[LogSegment]): Long = LogSegments.sizeInBytes(segments)
def parseTopicPartitionName(dir: File): TopicPartition = LocalLog.parseTopicPartitionName(dir)
@ -2103,7 +2096,7 @@ object UnifiedLog extends Logging { @@ -2103,7 +2096,7 @@ object UnifiedLog extends Logging {
val offsetsToSnapshot =
if (segments.nonEmpty) {
val lastSegmentBaseOffset = segments.lastSegment.get.baseOffset
val nextLatestSegmentBaseOffset = segments.lowerSegment(lastSegmentBaseOffset).map(_.baseOffset)
val nextLatestSegmentBaseOffset = segments.lowerSegment(lastSegmentBaseOffset).asScala.map(_.baseOffset)
Seq(nextLatestSegmentBaseOffset, Some(lastSegmentBaseOffset), Some(lastOffset))
} else {
Seq(Some(lastOffset))
@ -2147,14 +2140,14 @@ object UnifiedLog extends Logging { @@ -2147,14 +2140,14 @@ object UnifiedLog extends Logging {
if (lastOffset > producerStateManager.mapEndOffset && !isEmptyBeforeTruncation) {
val segmentOfLastOffset = segments.floorSegment(lastOffset)
segments.values(producerStateManager.mapEndOffset, lastOffset).foreach { segment =>
segments.values(producerStateManager.mapEndOffset, lastOffset).forEach { segment =>
val startOffset = Utils.max(segment.baseOffset, producerStateManager.mapEndOffset, logStartOffset)
producerStateManager.updateMapEndOffset(startOffset)
if (offsetsToSnapshot.contains(Some(segment.baseOffset)))
producerStateManager.takeSnapshot()
val maxPosition = if (segmentOfLastOffset.contains(segment)) {
val maxPosition = if (segmentOfLastOffset.isPresent && segmentOfLastOffset.get == segment) {
Option(segment.translateOffset(lastOffset))
.map(_.position)
.getOrElse(segment.size)
@ -2162,9 +2155,7 @@ object UnifiedLog extends Logging { @@ -2162,9 +2155,7 @@ object UnifiedLog extends Logging {
segment.size
}
val fetchDataInfo = segment.read(startOffset,
maxSize = Int.MaxValue,
maxPosition = maxPosition)
val fetchDataInfo = segment.read(startOffset, Int.MaxValue, maxPosition)
if (fetchDataInfo != null)
loadProducersFromRecords(producerStateManager, fetchDataInfo.records)
}
@ -2264,21 +2255,20 @@ case class RetentionMsBreach(log: UnifiedLog, remoteLogEnabled: Boolean) extends @@ -2264,21 +2255,20 @@ case class RetentionMsBreach(log: UnifiedLog, remoteLogEnabled: Boolean) extends
override def logReason(toDelete: List[LogSegment]): Unit = {
val retentionMs = UnifiedLog.localRetentionMs(log.config, remoteLogEnabled)
toDelete.foreach { segment =>
segment.largestRecordTimestamp match {
case Some(_) =>
if (remoteLogEnabled)
log.info(s"Deleting segment $segment due to local log retention time ${retentionMs}ms breach based on the largest " +
s"record timestamp in the segment")
else
log.info(s"Deleting segment $segment due to log retention time ${retentionMs}ms breach based on the largest " +
s"record timestamp in the segment")
case None =>
if (remoteLogEnabled)
log.info(s"Deleting segment $segment due to local log retention time ${retentionMs}ms breach based on the " +
s"last modified time of the segment")
else
log.info(s"Deleting segment $segment due to log retention time ${retentionMs}ms breach based on the " +
s"last modified time of the segment")
if (segment.largestRecordTimestamp.isPresent)
if (remoteLogEnabled)
log.info(s"Deleting segment $segment due to local log retention time ${retentionMs}ms breach based on the largest " +
s"record timestamp in the segment")
else
log.info(s"Deleting segment $segment due to log retention time ${retentionMs}ms breach based on the largest " +
s"record timestamp in the segment")
else {
if (remoteLogEnabled)
log.info(s"Deleting segment $segment due to local log retention time ${retentionMs}ms breach based on the " +
s"last modified time of the segment")
else
log.info(s"Deleting segment $segment due to log retention time ${retentionMs}ms breach based on the " +
s"last modified time of the segment")
}
}
}

35
core/src/main/scala/kafka/utils/CoreUtils.scala

@ -61,16 +61,17 @@ object CoreUtils { @@ -61,16 +61,17 @@ object CoreUtils {
* @param logging The logging instance to use for logging the thrown exception.
* @param logLevel The log level to use for logging.
*/
@noinline // inlining this method is not typically useful and it triggers spurious spotbugs warnings
def swallow(action: => Unit, logging: Logging, logLevel: Level = Level.WARN): Unit = {
try {
action
} catch {
case e: Throwable => logLevel match {
case Level.ERROR => logger.error(e.getMessage, e)
case Level.WARN => logger.warn(e.getMessage, e)
case Level.INFO => logger.info(e.getMessage, e)
case Level.DEBUG => logger.debug(e.getMessage, e)
case Level.TRACE => logger.trace(e.getMessage, e)
case Level.ERROR => logging.error(e.getMessage, e)
case Level.WARN => logging.warn(e.getMessage, e)
case Level.INFO => logging.info(e.getMessage, e)
case Level.DEBUG => logging.debug(e.getMessage, e)
case Level.TRACE => logging.trace(e.getMessage, e)
}
}
}
@ -81,30 +82,6 @@ object CoreUtils { @@ -81,30 +82,6 @@ object CoreUtils {
*/
def delete(files: Seq[String]): Unit = files.foreach(f => Utils.delete(new File(f)))
/**
* Invokes every function in `all` even if one or more functions throws an exception.
*
* If any of the functions throws an exception, the first one will be rethrown at the end with subsequent exceptions
* added as suppressed exceptions.
*/
// Note that this is a generalised version of `Utils.closeAll`. We could potentially make it more general by
// changing the signature to `def tryAll[R](all: Seq[() => R]): Seq[R]`
def tryAll(all: Seq[() => Unit]): Unit = {
var exception: Throwable = null
all.foreach { element =>
try element.apply()
catch {
case e: Throwable =>
if (exception != null)
exception.addSuppressed(e)
else
exception = e
}
}
if (exception != null)
throw exception
}
/**
* Register the given mbean with the platform mbean server,
* unregistering any mbean that was there before. Note,

39
core/src/test/java/kafka/log/remote/RemoteLogManagerTest.java

@ -19,7 +19,6 @@ package kafka.log.remote; @@ -19,7 +19,6 @@ package kafka.log.remote;
import com.yammer.metrics.core.Gauge;
import kafka.cluster.EndPoint;
import kafka.cluster.Partition;
import kafka.log.LogSegment;
import kafka.log.UnifiedLog;
import kafka.server.BrokerTopicStats;
import kafka.server.KafkaConfig;
@ -65,6 +64,8 @@ import org.apache.kafka.storage.internals.log.FetchDataInfo; @@ -65,6 +64,8 @@ import org.apache.kafka.storage.internals.log.FetchDataInfo;
import org.apache.kafka.storage.internals.log.FetchIsolation;
import org.apache.kafka.storage.internals.log.LazyIndex;
import org.apache.kafka.storage.internals.log.LogConfig;
import org.apache.kafka.storage.internals.log.LogFileUtils;
import org.apache.kafka.storage.internals.log.LogSegment;
import org.apache.kafka.storage.internals.log.OffsetIndex;
import org.apache.kafka.storage.internals.log.ProducerStateManager;
import org.apache.kafka.storage.internals.log.RemoteStorageFetchInfo;
@ -428,13 +429,13 @@ public class RemoteLogManagerTest { @@ -428,13 +429,13 @@ public class RemoteLogManagerTest {
when(mockLog.lastStableOffset()).thenReturn(lastStableOffset);
when(mockLog.logEndOffset()).thenReturn(logEndOffset);
LazyIndex idx = LazyIndex.forOffset(UnifiedLog.offsetIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1000);
LazyIndex timeIdx = LazyIndex.forTime(UnifiedLog.timeIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1500);
OffsetIndex idx = LazyIndex.forOffset(LogFileUtils.offsetIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1000).get();
TimeIndex timeIdx = LazyIndex.forTime(LogFileUtils.timeIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1500).get();
File txnFile = UnifiedLog.transactionIndexFile(tempDir, oldSegmentStartOffset, "");
txnFile.createNewFile();
TransactionIndex txnIndex = new TransactionIndex(oldSegmentStartOffset, txnFile);
when(oldSegment.lazyTimeIndex()).thenReturn(timeIdx);
when(oldSegment.lazyOffsetIndex()).thenReturn(idx);
when(oldSegment.timeIndex()).thenReturn(timeIdx);
when(oldSegment.offsetIndex()).thenReturn(idx);
when(oldSegment.txnIndex()).thenReturn(txnIndex);
CompletableFuture<Void> dummyFuture = new CompletableFuture<>();
@ -542,13 +543,13 @@ public class RemoteLogManagerTest { @@ -542,13 +543,13 @@ public class RemoteLogManagerTest {
when(mockLog.lastStableOffset()).thenReturn(lastStableOffset);
when(mockLog.logEndOffset()).thenReturn(logEndOffset);
LazyIndex idx = LazyIndex.forOffset(UnifiedLog.offsetIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1000);
LazyIndex timeIdx = LazyIndex.forTime(UnifiedLog.timeIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1500);
OffsetIndex idx = LazyIndex.forOffset(LogFileUtils.offsetIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1000).get();
TimeIndex timeIdx = LazyIndex.forTime(LogFileUtils.timeIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1500).get();
File txnFile = UnifiedLog.transactionIndexFile(tempDir, oldSegmentStartOffset, "");
txnFile.createNewFile();
TransactionIndex txnIndex = new TransactionIndex(oldSegmentStartOffset, txnFile);
when(oldSegment.lazyTimeIndex()).thenReturn(timeIdx);
when(oldSegment.lazyOffsetIndex()).thenReturn(idx);
when(oldSegment.timeIndex()).thenReturn(timeIdx);
when(oldSegment.offsetIndex()).thenReturn(idx);
when(oldSegment.txnIndex()).thenReturn(txnIndex);
int customMetadataSizeLimit = 128;
@ -628,13 +629,13 @@ public class RemoteLogManagerTest { @@ -628,13 +629,13 @@ public class RemoteLogManagerTest {
when(mockStateManager.fetchSnapshot(anyLong())).thenReturn(Optional.of(mockProducerSnapshotIndex));
when(mockLog.lastStableOffset()).thenReturn(250L);
LazyIndex idx = LazyIndex.forOffset(UnifiedLog.offsetIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1000);
LazyIndex timeIdx = LazyIndex.forTime(UnifiedLog.timeIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1500);
OffsetIndex idx = LazyIndex.forOffset(LogFileUtils.offsetIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1000).get();
TimeIndex timeIdx = LazyIndex.forTime(LogFileUtils.timeIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1500).get();
File txnFile = UnifiedLog.transactionIndexFile(tempDir, oldSegmentStartOffset, "");
txnFile.createNewFile();
TransactionIndex txnIndex = new TransactionIndex(oldSegmentStartOffset, txnFile);
when(oldSegment.lazyTimeIndex()).thenReturn(timeIdx);
when(oldSegment.lazyOffsetIndex()).thenReturn(idx);
when(oldSegment.timeIndex()).thenReturn(timeIdx);
when(oldSegment.offsetIndex()).thenReturn(idx);
when(oldSegment.txnIndex()).thenReturn(txnIndex);
CompletableFuture<Void> dummyFuture = new CompletableFuture<>();
@ -706,13 +707,13 @@ public class RemoteLogManagerTest { @@ -706,13 +707,13 @@ public class RemoteLogManagerTest {
when(mockStateManager.fetchSnapshot(anyLong())).thenReturn(Optional.of(mockProducerSnapshotIndex));
when(mockLog.lastStableOffset()).thenReturn(250L);
LazyIndex idx = LazyIndex.forOffset(UnifiedLog.offsetIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1000);
LazyIndex timeIdx = LazyIndex.forTime(UnifiedLog.timeIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1500);
OffsetIndex idx = LazyIndex.forOffset(LogFileUtils.offsetIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1000).get();
TimeIndex timeIdx = LazyIndex.forTime(LogFileUtils.timeIndexFile(tempDir, oldSegmentStartOffset, ""), oldSegmentStartOffset, 1500).get();
File txnFile = UnifiedLog.transactionIndexFile(tempDir, oldSegmentStartOffset, "");
txnFile.createNewFile();
TransactionIndex txnIndex = new TransactionIndex(oldSegmentStartOffset, txnFile);
when(oldSegment.lazyTimeIndex()).thenReturn(timeIdx);
when(oldSegment.lazyOffsetIndex()).thenReturn(idx);
when(oldSegment.timeIndex()).thenReturn(timeIdx);
when(oldSegment.offsetIndex()).thenReturn(idx);
when(oldSegment.txnIndex()).thenReturn(txnIndex);
CompletableFuture<Void> dummyFuture = new CompletableFuture<>();
@ -856,8 +857,8 @@ public class RemoteLogManagerTest { @@ -856,8 +857,8 @@ public class RemoteLogManagerTest {
}
private void verifyLogSegmentData(LogSegmentData logSegmentData,
LazyIndex idx,
LazyIndex timeIdx,
OffsetIndex idx,
TimeIndex timeIdx,
TransactionIndex txnIndex,
File tempFile,
File mockProducerSnapshotIndex,

4
core/src/test/scala/integration/kafka/api/GroupCoordinatorIntegrationTest.scala

@ -48,10 +48,10 @@ class GroupCoordinatorIntegrationTest extends KafkaServerTestHarness { @@ -48,10 +48,10 @@ class GroupCoordinatorIntegrationTest extends KafkaServerTestHarness {
def getGroupMetadataLogOpt: Option[UnifiedLog] =
logManager.getLog(new TopicPartition(Topic.GROUP_METADATA_TOPIC_NAME, 0))
TestUtils.waitUntilTrue(() => getGroupMetadataLogOpt.exists(_.logSegments.exists(_.log.batches.asScala.nonEmpty)),
TestUtils.waitUntilTrue(() => getGroupMetadataLogOpt.exists(_.logSegments.asScala.exists(_.log.batches.asScala.nonEmpty)),
"Commit message not appended in time")
val logSegments = getGroupMetadataLogOpt.get.logSegments
val logSegments = getGroupMetadataLogOpt.get.logSegments.asScala
val incorrectCompressionCodecs = logSegments
.flatMap(_.log.batches.asScala.map(_.compressionType))
.filter(_ != offsetsTopicCompressionCodec)

2
core/src/test/scala/integration/kafka/server/DynamicBrokerReconfigurationTest.scala

@ -672,7 +672,7 @@ class DynamicBrokerReconfigurationTest extends QuorumTestHarness with SaslSetup @@ -672,7 +672,7 @@ class DynamicBrokerReconfigurationTest extends QuorumTestHarness with SaslSetup
consumerThread.waitForMatchingRecords(record => record.timestampType == TimestampType.LOG_APPEND_TIME)
// Verify that the new config is actually used for new segments of existing logs
TestUtils.waitUntilTrue(() => log.logSegments.exists(_.size > 3000), "Log segment size increase not applied")
TestUtils.waitUntilTrue(() => log.logSegments.asScala.exists(_.size > 3000), "Log segment size increase not applied")
// Verify that overridden topic configs are not updated when broker default is updated
val log2 = servers.head.logManager.getLog(new TopicPartition(Topic.GROUP_METADATA_TOPIC_NAME, 0))

5
core/src/test/scala/kafka/raft/KafkaMetadataLogTest.scala

@ -40,6 +40,7 @@ import java.nio.ByteBuffer @@ -40,6 +40,7 @@ import java.nio.ByteBuffer
import java.nio.file.{Files, Path}
import java.util
import java.util.{Collections, Optional, Properties}
import scala.jdk.CollectionConverters._
final class KafkaMetadataLogTest {
import KafkaMetadataLogTest._
@ -974,7 +975,7 @@ final class KafkaMetadataLogTest { @@ -974,7 +975,7 @@ final class KafkaMetadataLogTest {
// The clean up code requires that there are at least two snapshots
// Generate first snapshots that includes the first segment by using the base offset of the second segment
val snapshotId1 = new OffsetAndEpoch(
log.log.logSegments.drop(1).head.baseOffset,
log.log.logSegments.asScala.drop(1).head.baseOffset,
1
)
TestUtils.resource(log.storeSnapshot(snapshotId1).get()) { snapshot =>
@ -982,7 +983,7 @@ final class KafkaMetadataLogTest { @@ -982,7 +983,7 @@ final class KafkaMetadataLogTest {
}
// Generate second snapshots that includes the second segment by using the base offset of the third segment
val snapshotId2 = new OffsetAndEpoch(
log.log.logSegments.drop(2).head.baseOffset,
log.log.logSegments.asScala.drop(2).head.baseOffset,
1
)
TestUtils.resource(log.storeSnapshot(snapshotId2).get()) { snapshot =>

5
core/src/test/scala/unit/kafka/cluster/PartitionLockTest.scala

@ -37,12 +37,13 @@ import org.apache.kafka.common.{TopicPartition, Uuid} @@ -37,12 +37,13 @@ import org.apache.kafka.common.{TopicPartition, Uuid}
import org.apache.kafka.server.common.MetadataVersion
import org.apache.kafka.server.util.MockTime
import org.apache.kafka.storage.internals.epoch.LeaderEpochFileCache
import org.apache.kafka.storage.internals.log.{AppendOrigin, CleanerConfig, FetchIsolation, FetchParams, LogAppendInfo, LogConfig, LogDirFailureChannel, ProducerStateManager, ProducerStateManagerConfig}
import org.apache.kafka.storage.internals.log.{AppendOrigin, CleanerConfig, FetchIsolation, FetchParams, LogAppendInfo, LogConfig, LogDirFailureChannel, LogSegments, ProducerStateManager, ProducerStateManagerConfig}
import org.junit.jupiter.api.Assertions.{assertEquals, assertFalse, assertTrue}
import org.junit.jupiter.api.{AfterEach, BeforeEach, Test}
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.{mock, when}
import scala.compat.java8.OptionConverters._
import scala.concurrent.duration._
import scala.jdk.CollectionConverters._
@ -317,7 +318,7 @@ class PartitionLockTest extends Logging { @@ -317,7 +318,7 @@ class PartitionLockTest extends Logging {
segments,
0L,
0L,
leaderEpochCache,
leaderEpochCache.asJava,
producerStateManager
).load()
val localLog = new LocalLog(log.dir, log.config, segments, offsets.recoveryPoint,

4
core/src/test/scala/unit/kafka/cluster/PartitionTest.scala

@ -56,7 +56,7 @@ import org.apache.kafka.server.common.MetadataVersion.IBP_2_6_IV0 @@ -56,7 +56,7 @@ import org.apache.kafka.server.common.MetadataVersion.IBP_2_6_IV0
import org.apache.kafka.server.metrics.KafkaYammerMetrics
import org.apache.kafka.server.util.{KafkaScheduler, MockTime}
import org.apache.kafka.storage.internals.epoch.LeaderEpochFileCache
import org.apache.kafka.storage.internals.log.{AppendOrigin, CleanerConfig, EpochEntry, FetchIsolation, FetchParams, LogAppendInfo, LogDirFailureChannel, LogOffsetMetadata, LogReadInfo, LogStartOffsetIncrementReason, ProducerStateManager, ProducerStateManagerConfig}
import org.apache.kafka.storage.internals.log.{AppendOrigin, CleanerConfig, EpochEntry, FetchIsolation, FetchParams, LogAppendInfo, LogDirFailureChannel, LogOffsetMetadata, LogReadInfo, LogSegments, LogStartOffsetIncrementReason, ProducerStateManager, ProducerStateManagerConfig}
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
@ -451,7 +451,7 @@ class PartitionTest extends AbstractPartitionTest { @@ -451,7 +451,7 @@ class PartitionTest extends AbstractPartitionTest {
segments = segments,
logStartOffsetCheckpoint = 0L,
recoveryPointCheckpoint = 0L,
leaderEpochCache,
leaderEpochCache.asJava,
producerStateManager
).load()
val localLog = new LocalLog(log.dir, log.config, segments, offsets.recoveryPoint,

5
core/src/test/scala/unit/kafka/log/AbstractLogCleanerIntegrationTest.scala

@ -164,4 +164,9 @@ abstract class AbstractLogCleanerIntegrationTest { @@ -164,4 +164,9 @@ abstract class AbstractLogCleanerIntegrationTest {
magicValue = messageFormatVersion)
(value, messageSet)
}
def closeLog(log: UnifiedLog): Unit = {
log.close()
logs -= log
}
}

40
core/src/test/scala/unit/kafka/log/LocalLogTest.scala

@ -29,7 +29,7 @@ import org.apache.kafka.common.errors.KafkaStorageException @@ -29,7 +29,7 @@ import org.apache.kafka.common.errors.KafkaStorageException
import org.apache.kafka.common.record.{CompressionType, MemoryRecords, Record, SimpleRecord}
import org.apache.kafka.common.utils.{Time, Utils}
import org.apache.kafka.server.util.{MockTime, Scheduler}
import org.apache.kafka.storage.internals.log.{FetchDataInfo, LogConfig, LogDirFailureChannel, LogFileUtils, LogOffsetMetadata}
import org.apache.kafka.storage.internals.log.{FetchDataInfo, LogConfig, LogDirFailureChannel, LogFileUtils, LogOffsetMetadata, LogSegment, LogSegments}
import org.junit.jupiter.api.Assertions._
import org.junit.jupiter.api.function.Executable
import org.junit.jupiter.api.{AfterEach, BeforeEach, Test}
@ -124,7 +124,7 @@ class LocalLogTest { @@ -124,7 +124,7 @@ class LocalLogTest {
log.roll()
assertEquals(2, log.segments.numberOfSegments)
assertFalse(logDir.listFiles.isEmpty)
val segmentsBeforeDelete = List[LogSegment]() ++ log.segments.values
val segmentsBeforeDelete = log.segments.values.asScala.toVector
val deletedSegments = log.deleteAllSegments()
assertTrue(log.segments.isEmpty)
assertEquals(segmentsBeforeDelete, deletedSegments)
@ -181,7 +181,7 @@ class LocalLogTest { @@ -181,7 +181,7 @@ class LocalLogTest {
assertEquals(newLogDir, log.dir)
assertEquals(newLogDir.getParent, log.parentDir)
assertEquals(newLogDir.getParent, log.dir.getParent)
log.segments.values.foreach(segment => assertEquals(newLogDir.getPath, segment.log.file().getParentFile.getPath))
log.segments.values.forEach(segment => assertEquals(newLogDir.getPath, segment.log.file().getParentFile.getPath))
assertEquals(2, log.segments.numberOfSegments)
}
@ -278,7 +278,7 @@ class LocalLogTest { @@ -278,7 +278,7 @@ class LocalLogTest {
def deletedSegments: Iterable[LogSegment] = _deletedSegments
}
val reason = new TestDeletionReason()
val toDelete = List[LogSegment]() ++ log.segments.values
val toDelete = log.segments.values.asScala.toVector
log.removeAndDeleteSegments(toDelete, asyncDelete = asyncDelete, reason)
if (asyncDelete) {
mockTime.sleep(log.config.fileDeleteDelayMs + 1)
@ -307,7 +307,7 @@ class LocalLogTest { @@ -307,7 +307,7 @@ class LocalLogTest {
assertEquals(10L, log.segments.numberOfSegments)
val toDelete = List[LogSegment]() ++ log.segments.values
val toDelete = log.segments.values.asScala.toVector
LocalLog.deleteSegmentFiles(toDelete, asyncDelete = asyncDelete, log.dir, log.topicPartition, log.config, log.scheduler, log.logDirFailureChannel, "")
if (asyncDelete) {
toDelete.foreach {
@ -343,14 +343,14 @@ class LocalLogTest { @@ -343,14 +343,14 @@ class LocalLogTest {
{
val deletable = log.deletableSegments(
(segment: LogSegment, _: Option[LogSegment]) => segment.baseOffset <= 5)
val expected = log.segments.nonActiveLogSegmentsFrom(0L).filter(segment => segment.baseOffset <= 5).toList
val expected = log.segments.nonActiveLogSegmentsFrom(0L).asScala.filter(segment => segment.baseOffset <= 5).toList
assertEquals(6, expected.length)
assertEquals(expected, deletable.toList)
}
{
val deletable = log.deletableSegments((_: LogSegment, _: Option[LogSegment]) => true)
val expected = log.segments.nonActiveLogSegmentsFrom(0L).toList
val expected = log.segments.nonActiveLogSegmentsFrom(0L).asScala.toList
assertEquals(9, expected.length)
assertEquals(expected, deletable.toList)
}
@ -359,7 +359,7 @@ class LocalLogTest { @@ -359,7 +359,7 @@ class LocalLogTest {
val record = new SimpleRecord(mockTime.milliseconds, "a".getBytes)
appendRecords(List(record), initialOffset = 9L)
val deletable = log.deletableSegments((_: LogSegment, _: Option[LogSegment]) => true)
val expected = log.segments.values.toList
val expected = log.segments.values.asScala.toList
assertEquals(10, expected.length)
assertEquals(expected, deletable.toList)
}
@ -380,14 +380,14 @@ class LocalLogTest { @@ -380,14 +380,14 @@ class LocalLogTest {
(segment: LogSegment, nextSegmentOpt: Option[LogSegment]) => {
assertEquals(offset, segment.baseOffset)
val floorSegmentOpt = log.segments.floorSegment(offset)
assertTrue(floorSegmentOpt.isDefined)
assertTrue(floorSegmentOpt.isPresent)
assertEquals(floorSegmentOpt.get, segment)
if (offset == log.logEndOffset) {
assertFalse(nextSegmentOpt.isDefined)
} else {
assertTrue(nextSegmentOpt.isDefined)
val higherSegmentOpt = log.segments.higherSegment(segment.baseOffset)
assertTrue(higherSegmentOpt.isDefined)
assertTrue(higherSegmentOpt.isPresent)
assertEquals(segment.baseOffset + 1, higherSegmentOpt.get.baseOffset)
assertEquals(higherSegmentOpt.get, nextSegmentOpt.get)
}
@ -395,7 +395,7 @@ class LocalLogTest { @@ -395,7 +395,7 @@ class LocalLogTest {
true
})
assertEquals(10L, log.segments.numberOfSegments)
assertEquals(log.segments.nonActiveLogSegmentsFrom(0L).toSeq, deletableSegments.toSeq)
assertEquals(log.segments.nonActiveLogSegmentsFrom(0L).asScala.toSeq, deletableSegments.toSeq)
}
@Test
@ -430,7 +430,7 @@ class LocalLogTest { @@ -430,7 +430,7 @@ class LocalLogTest {
}
assertEquals(5, log.segments.numberOfSegments)
assertNotEquals(10L, log.segments.activeSegment.baseOffset)
val expected = List[LogSegment]() ++ log.segments.values
val expected = log.segments.values.asScala.toVector
val deleted = log.truncateFullyAndStartAt(10L)
assertEquals(expected, deleted)
assertEquals(1, log.segments.numberOfSegments)
@ -452,10 +452,10 @@ class LocalLogTest { @@ -452,10 +452,10 @@ class LocalLogTest {
assertEquals(5, log.segments.numberOfSegments)
assertEquals(12L, log.logEndOffset)
val expected = List[LogSegment]() ++ log.segments.values(9L, log.logEndOffset + 1)
val expected = log.segments.values(9L, log.logEndOffset + 1).asScala.toVector
// Truncate to an offset before the base offset of the active segment
val deleted = log.truncateTo(7L)
assertEquals(expected, deleted)
assertEquals(expected, deleted.toVector)
assertEquals(3, log.segments.numberOfSegments)
assertEquals(6L, log.segments.activeSegment.baseOffset)
assertEquals(0L, log.recoveryPoint)
@ -479,7 +479,7 @@ class LocalLogTest { @@ -479,7 +479,7 @@ class LocalLogTest {
}
def nonActiveBaseOffsetsFrom(startOffset: Long): Seq[Long] = {
log.segments.nonActiveLogSegmentsFrom(startOffset).map(_.baseOffset).toSeq
log.segments.nonActiveLogSegmentsFrom(startOffset).asScala.map(_.baseOffset).toSeq
}
assertEquals(5L, log.segments.activeSegment.baseOffset)
@ -726,12 +726,12 @@ class LocalLogTest { @@ -726,12 +726,12 @@ class LocalLogTest {
time: Time = mockTime,
topicPartition: TopicPartition = topicPartition,
logDirFailureChannel: LogDirFailureChannel = logDirFailureChannel): LocalLog = {
segments.add(LogSegment.open(dir = dir,
baseOffset = 0L,
segments.add(LogSegment.open(dir,
0L,
config,
time = time,
initFileSize = config.initFileSize,
preallocate = config.preallocate))
time,
config.initFileSize,
config.preallocate))
new LocalLog(_dir = dir,
config = config,
segments = segments,

8
core/src/test/scala/unit/kafka/log/LogCleanerIntegrationTest.scala

@ -56,7 +56,7 @@ class LogCleanerIntegrationTest extends AbstractLogCleanerIntegrationTest { @@ -56,7 +56,7 @@ class LogCleanerIntegrationTest extends AbstractLogCleanerIntegrationTest {
val log = cleaner.logs.get(tp)
writeDups(numKeys = 20, numDups = 3, log = log, codec = codec)
val partitionFile = log.logSegments.last.log.file()
val partitionFile = log.logSegments.asScala.last.log.file()
val writer = new PrintWriter(partitionFile)
writer.write("jogeajgoea")
writer.close()
@ -76,8 +76,8 @@ class LogCleanerIntegrationTest extends AbstractLogCleanerIntegrationTest { @@ -76,8 +76,8 @@ class LogCleanerIntegrationTest extends AbstractLogCleanerIntegrationTest {
val uncleanableBytesGauge = getGauge[Long]("uncleanable-bytes", uncleanableDirectory)
TestUtils.waitUntilTrue(() => uncleanablePartitionsCountGauge.value() == 2, "There should be 2 uncleanable partitions", 2000L)
val expectedTotalUncleanableBytes = LogCleanerManager.calculateCleanableBytes(log, 0, log.logSegments.last.baseOffset)._2 +
LogCleanerManager.calculateCleanableBytes(log2, 0, log2.logSegments.last.baseOffset)._2
val expectedTotalUncleanableBytes = LogCleanerManager.calculateCleanableBytes(log, 0, log.logSegments.asScala.last.baseOffset)._2 +
LogCleanerManager.calculateCleanableBytes(log2, 0, log2.logSegments.asScala.last.baseOffset)._2
TestUtils.waitUntilTrue(() => uncleanableBytesGauge.value() == expectedTotalUncleanableBytes,
s"There should be $expectedTotalUncleanableBytes uncleanable bytes", 1000L)
@ -192,7 +192,7 @@ class LogCleanerIntegrationTest extends AbstractLogCleanerIntegrationTest { @@ -192,7 +192,7 @@ class LogCleanerIntegrationTest extends AbstractLogCleanerIntegrationTest {
}
private def readFromLog(log: UnifiedLog): Iterable[(Int, Int)] = {
for (segment <- log.logSegments; record <- segment.log.records.asScala) yield {
for (segment <- log.logSegments.asScala; record <- segment.log.records.asScala) yield {
val key = TestUtils.readString(record.key).toInt
val value = TestUtils.readString(record.value).toInt
key -> value

2
core/src/test/scala/unit/kafka/log/LogCleanerLagIntegrationTest.scala

@ -95,7 +95,7 @@ class LogCleanerLagIntegrationTest extends AbstractLogCleanerIntegrationTest wit @@ -95,7 +95,7 @@ class LogCleanerLagIntegrationTest extends AbstractLogCleanerIntegrationTest wit
}
private def readFromLog(log: UnifiedLog): Iterable[(Int, Int)] = {
for (segment <- log.logSegments; record <- segment.log.records.asScala) yield {
for (segment <- log.logSegments.asScala; record <- segment.log.records.asScala) yield {
val key = TestUtils.readString(record.key).toInt
val value = TestUtils.readString(record.value).toInt
key -> value

8
core/src/test/scala/unit/kafka/log/LogCleanerManagerTest.scala

@ -27,11 +27,13 @@ import org.apache.kafka.common.config.TopicConfig @@ -27,11 +27,13 @@ import org.apache.kafka.common.config.TopicConfig
import org.apache.kafka.common.record._
import org.apache.kafka.common.utils.Utils
import org.apache.kafka.server.util.MockTime
import org.apache.kafka.storage.internals.log.{AppendOrigin, LogConfig, LogDirFailureChannel, LogStartOffsetIncrementReason, ProducerStateManager, ProducerStateManagerConfig}
import org.apache.kafka.storage.internals.log.{AppendOrigin, LogConfig, LogDirFailureChannel, LogSegment, LogSegments, LogStartOffsetIncrementReason, ProducerStateManager, ProducerStateManagerConfig}
import org.junit.jupiter.api.Assertions._
import org.junit.jupiter.api.{AfterEach, Test}
import java.util
import scala.collection.mutable
import scala.compat.java8.OptionConverters._
/**
* Unit tests for the log cleaning logic
@ -117,7 +119,7 @@ class LogCleanerManagerTest extends Logging { @@ -117,7 +119,7 @@ class LogCleanerManagerTest extends Logging {
segments,
0L,
0L,
leaderEpochCache,
leaderEpochCache.asJava,
producerStateManager
).load()
val localLog = new LocalLog(tpDir, config, segments, offsets.recoveryPoint,
@ -127,7 +129,7 @@ class LogCleanerManagerTest extends Logging { @@ -127,7 +129,7 @@ class LogCleanerManagerTest extends Logging {
producerIdExpirationCheckIntervalMs, leaderEpochCache,
producerStateManager, _topicId = None, keepPartitionMetadataFile = true) {
// Throw an error in getFirstBatchTimestampForSegments since it is called in grabFilthiestLog()
override def getFirstBatchTimestampForSegments(segments: Iterable[LogSegment]): Iterable[Long] =
override def getFirstBatchTimestampForSegments(segments: util.Collection[LogSegment]): util.Collection[java.lang.Long] =
throw new IllegalStateException("Error!")
}

15
core/src/test/scala/unit/kafka/log/LogCleanerParameterizedIntegrationTest.scala

@ -62,7 +62,7 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati @@ -62,7 +62,7 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati
val firstDirty = log.activeSegment.baseOffset
checkLastCleaned("log", 0, firstDirty)
val compactedSize = log.logSegments.map(_.size).sum
val compactedSize = log.logSegments.asScala.map(_.size).sum
assertTrue(startSize > compactedSize, s"log should have been compacted: startSize=$startSize compactedSize=$compactedSize")
checkLogAfterAppendingDups(log, startSize, appends)
@ -111,7 +111,7 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati @@ -111,7 +111,7 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati
// should compact the log
checkLastCleaned("log", 0, firstDirty)
val compactedSize = log.logSegments.map(_.size).sum
val compactedSize = log.logSegments.asScala.map(_.size).sum
assertTrue(startSize > compactedSize, s"log should have been compacted: startSize=$startSize compactedSize=$compactedSize")
(log, messages)
}
@ -120,12 +120,13 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati @@ -120,12 +120,13 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati
// Set the last modified time to an old value to force deletion of old segments
val endOffset = log.logEndOffset
log.logSegments.foreach(_.lastModified = time.milliseconds - (2 * retentionMs))
log.logSegments.forEach(_.setLastModified(time.milliseconds - (2 * retentionMs)))
TestUtils.waitUntilTrue(() => log.logStartOffset == endOffset,
"Timed out waiting for deletion of old segments")
assertEquals(1, log.numberOfSegments)
cleaner.shutdown()
closeLog(log)
// run the cleaner again to make sure if there are no issues post deletion
val (log2, messages) = runCleanerAndCheckCompacted(20)
@ -162,7 +163,7 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati @@ -162,7 +163,7 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati
val firstDirty = log.activeSegment.baseOffset
checkLastCleaned("log", 0, firstDirty)
val compactedSize = log.logSegments.map(_.size).sum
val compactedSize = log.logSegments.asScala.map(_.size).sum
assertTrue(startSize > compactedSize, s"log should have been compacted: startSize=$startSize compactedSize=$compactedSize")
checkLogAfterAppendingDups(log, startSize, appends)
@ -220,7 +221,7 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati @@ -220,7 +221,7 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati
assertTrue(firstDirty > appendsV0.size) // ensure we clean data from V0 and V1
checkLastCleaned("log", 0, firstDirty)
val compactedSize = log.logSegments.map(_.size).sum
val compactedSize = log.logSegments.asScala.map(_.size).sum
assertTrue(startSize > compactedSize, s"log should have been compacted: startSize=$startSize compactedSize=$compactedSize")
checkLogAfterAppendingDups(log, startSize, appends)
@ -274,7 +275,7 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati @@ -274,7 +275,7 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati
assertEquals(2, cleaner.cleanerCount)
checkLastCleaned("log", 0, firstDirty)
val compactedSize = log.logSegments.map(_.size).sum
val compactedSize = log.logSegments.asScala.map(_.size).sum
assertTrue(startSize > compactedSize, s"log should have been compacted: startSize=$startSize compactedSize=$compactedSize")
}
@ -298,7 +299,7 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati @@ -298,7 +299,7 @@ class LogCleanerParameterizedIntegrationTest extends AbstractLogCleanerIntegrati
}
private def readFromLog(log: UnifiedLog): Iterable[(Int, String, Long)] = {
for (segment <- log.logSegments; deepLogEntry <- segment.log.records.asScala) yield {
for (segment <- log.logSegments.asScala; deepLogEntry <- segment.log.records.asScala) yield {
val key = TestUtils.readString(deepLogEntry.key).toInt
val value = TestUtils.readString(deepLogEntry.value)
(key, value, deepLogEntry.offset)

101
core/src/test/scala/unit/kafka/log/LogCleanerTest.scala

@ -27,7 +27,7 @@ import org.apache.kafka.common.record._ @@ -27,7 +27,7 @@ import org.apache.kafka.common.record._
import org.apache.kafka.common.utils.Utils
import org.apache.kafka.server.metrics.KafkaMetricsGroup
import org.apache.kafka.server.util.MockTime
import org.apache.kafka.storage.internals.log.{AbortedTxn, AppendOrigin, CleanerConfig, LogAppendInfo, LogConfig, LogDirFailureChannel, LogFileUtils, LogStartOffsetIncrementReason, OffsetMap, ProducerStateManager, ProducerStateManagerConfig}
import org.apache.kafka.storage.internals.log.{AbortedTxn, AppendOrigin, CleanerConfig, LogAppendInfo, LogConfig, LogDirFailureChannel, LogFileUtils, LogSegment, LogSegments, LogStartOffsetIncrementReason, OffsetMap, ProducerStateManager, ProducerStateManagerConfig}
import org.junit.jupiter.api.Assertions._
import org.junit.jupiter.api.{AfterEach, Test}
import org.mockito.ArgumentMatchers
@ -41,6 +41,7 @@ import java.nio.file.Paths @@ -41,6 +41,7 @@ import java.nio.file.Paths
import java.util.Properties
import java.util.concurrent.{CountDownLatch, TimeUnit}
import scala.collection._
import scala.compat.java8.OptionConverters._
import scala.jdk.CollectionConverters._
/**
@ -138,7 +139,7 @@ class LogCleanerTest { @@ -138,7 +139,7 @@ class LogCleanerTest {
keys.foreach(k => map.put(key(k), Long.MaxValue))
// clean the log
val segments = log.logSegments.take(3).toSeq
val segments = log.logSegments.asScala.take(3).toSeq
val stats = new CleanerStats()
val expectedBytesRead = segments.map(_.size).sum
val shouldRemain = LogTestUtils.keysInLog(log).filterNot(keys.contains)
@ -177,7 +178,7 @@ class LogCleanerTest { @@ -177,7 +178,7 @@ class LogCleanerTest {
logSegments,
0L,
0L,
leaderEpochCache,
leaderEpochCache.asJava,
producerStateManager
).load()
val localLog = new LocalLog(dir, config, logSegments, offsets.recoveryPoint,
@ -220,7 +221,7 @@ class LogCleanerTest { @@ -220,7 +221,7 @@ class LogCleanerTest {
assertEquals(3, log.numberOfSegments)
// Remember reference to the first log and determine its file name expected for async deletion
val firstLogFile = log.logSegments.head.log
val firstLogFile = log.logSegments.asScala.head.log
val expectedFileName = Utils.replaceSuffix(firstLogFile.file.getPath, "", LogFileUtils.DELETED_FILE_SUFFIX)
// Clean the log. This should trigger replaceSegments() and deleteOldSegments();
@ -898,7 +899,7 @@ class LogCleanerTest { @@ -898,7 +899,7 @@ class LogCleanerTest {
// clean the log
val stats = new CleanerStats()
cleaner.cleanSegments(log, Seq(log.logSegments.head), map, 0L, stats, new CleanedTransactionMetadata, -1)
cleaner.cleanSegments(log, Seq(log.logSegments.asScala.head), map, 0L, stats, new CleanedTransactionMetadata, -1)
val shouldRemain = LogTestUtils.keysInLog(log).filterNot(keys.contains)
assertEquals(shouldRemain, LogTestUtils.keysInLog(log))
}
@ -911,7 +912,7 @@ class LogCleanerTest { @@ -911,7 +912,7 @@ class LogCleanerTest {
val (log, offsetMap) = createLogWithMessagesLargerThanMaxSize(largeMessageSize = 1024 * 1024)
val cleaner = makeCleaner(Int.MaxValue, maxMessageSize=1024)
cleaner.cleanSegments(log, Seq(log.logSegments.head), offsetMap, 0L, new CleanerStats, new CleanedTransactionMetadata, -1)
cleaner.cleanSegments(log, Seq(log.logSegments.asScala.head), offsetMap, 0L, new CleanerStats, new CleanedTransactionMetadata, -1)
val shouldRemain = LogTestUtils.keysInLog(log).filter(k => !offsetMap.map.containsKey(k.toString))
assertEquals(shouldRemain, LogTestUtils.keysInLog(log))
}
@ -923,14 +924,14 @@ class LogCleanerTest { @@ -923,14 +924,14 @@ class LogCleanerTest {
@Test
def testMessageLargerThanMaxMessageSizeWithCorruptHeader(): Unit = {
val (log, offsetMap) = createLogWithMessagesLargerThanMaxSize(largeMessageSize = 1024 * 1024)
val file = new RandomAccessFile(log.logSegments.head.log.file, "rw")
val file = new RandomAccessFile(log.logSegments.asScala.head.log.file, "rw")
file.seek(Records.MAGIC_OFFSET)
file.write(0xff)
file.close()
val cleaner = makeCleaner(Int.MaxValue, maxMessageSize=1024)
assertThrows(classOf[CorruptRecordException], () =>
cleaner.cleanSegments(log, Seq(log.logSegments.head), offsetMap, 0L, new CleanerStats, new CleanedTransactionMetadata, -1)
cleaner.cleanSegments(log, Seq(log.logSegments.asScala.head), offsetMap, 0L, new CleanerStats, new CleanedTransactionMetadata, -1)
)
}
@ -941,13 +942,13 @@ class LogCleanerTest { @@ -941,13 +942,13 @@ class LogCleanerTest {
@Test
def testCorruptMessageSizeLargerThanBytesAvailable(): Unit = {
val (log, offsetMap) = createLogWithMessagesLargerThanMaxSize(largeMessageSize = 1024 * 1024)
val file = new RandomAccessFile(log.logSegments.head.log.file, "rw")
val file = new RandomAccessFile(log.logSegments.asScala.head.log.file, "rw")
file.setLength(1024)
file.close()
val cleaner = makeCleaner(Int.MaxValue, maxMessageSize=1024)
assertThrows(classOf[CorruptRecordException], () =>
cleaner.cleanSegments(log, Seq(log.logSegments.head), offsetMap, 0L, new CleanerStats, new CleanedTransactionMetadata, -1)
cleaner.cleanSegments(log, Seq(log.logSegments.asScala.head), offsetMap, 0L, new CleanerStats, new CleanedTransactionMetadata, -1)
)
}
@ -1188,7 +1189,7 @@ class LogCleanerTest { @@ -1188,7 +1189,7 @@ class LogCleanerTest {
// the last (active) segment has just one message
def distinctValuesBySegment = log.logSegments.map(s => s.log.records.asScala.map(record => TestUtils.readString(record.value)).toSet.size).toSeq
def distinctValuesBySegment = log.logSegments.asScala.map(s => s.log.records.asScala.map(record => TestUtils.readString(record.value)).toSet.size).toSeq
val disctinctValuesBySegmentBeforeClean = distinctValuesBySegment
assertTrue(distinctValuesBySegment.reverse.tail.forall(_ > N),
@ -1236,7 +1237,7 @@ class LogCleanerTest { @@ -1236,7 +1237,7 @@ class LogCleanerTest {
log.appendAsLeader(createRecords, leaderEpoch = 0)
// segments [0,1] are clean; segments [2, 3] are cleanable; segments [4,5] are uncleanable
val segs = log.logSegments.toSeq
val segs = log.logSegments.asScala.toSeq
val logToClean = LogToClean(new TopicPartition("test", 0), log, segs(2).baseOffset, segs(4).baseOffset)
val expectedCleanSize = segs.take(2).map(_.size).sum
@ -1285,27 +1286,27 @@ class LogCleanerTest { @@ -1285,27 +1286,27 @@ class LogCleanerTest {
}
private def batchBaseOffsetsInLog(log: UnifiedLog): Iterable[Long] = {
for (segment <- log.logSegments; batch <- segment.log.batches.asScala)
for (segment <- log.logSegments.asScala; batch <- segment.log.batches.asScala)
yield batch.baseOffset
}
def lastOffsetsPerBatchInLog(log: UnifiedLog): Iterable[Long] = {
for (segment <- log.logSegments; batch <- segment.log.batches.asScala)
for (segment <- log.logSegments.asScala; batch <- segment.log.batches.asScala)
yield batch.lastOffset
}
def lastSequencesInLog(log: UnifiedLog): Map[Long, Int] = {
(for (segment <- log.logSegments;
(for (segment <- log.logSegments.asScala;
batch <- segment.log.batches.asScala if !batch.isControlBatch && batch.hasProducerId)
yield batch.producerId -> batch.lastSequence).toMap
}
/* extract all the offsets from a log */
def offsetsInLog(log: UnifiedLog): Iterable[Long] =
log.logSegments.flatMap(s => s.log.records.asScala.filter(_.hasValue).filter(_.hasKey).map(m => m.offset))
log.logSegments.asScala.flatMap(s => s.log.records.asScala.filter(_.hasValue).filter(_.hasKey).map(m => m.offset))
def unkeyedMessageCountInLog(log: UnifiedLog) =
log.logSegments.map(s => s.log.records.asScala.filter(_.hasValue).count(m => !m.hasKey)).sum
log.logSegments.asScala.map(s => s.log.records.asScala.filter(_.hasValue).count(m => !m.hasKey)).sum
def abortCheckDone(topicPartition: TopicPartition): Unit = {
throw new LogCleaningAbortedException()
@ -1330,7 +1331,7 @@ class LogCleanerTest { @@ -1330,7 +1331,7 @@ class LogCleanerTest {
val map = new FakeOffsetMap(Int.MaxValue)
keys.foreach(k => map.put(key(k), Long.MaxValue))
assertThrows(classOf[LogCleaningAbortedException], () =>
cleaner.cleanSegments(log, log.logSegments.take(3).toSeq, map, 0L, new CleanerStats(),
cleaner.cleanSegments(log, log.logSegments.asScala.take(3).toSeq, map, 0L, new CleanerStats(),
new CleanedTransactionMetadata, -1)
)
}
@ -1355,17 +1356,17 @@ class LogCleanerTest { @@ -1355,17 +1356,17 @@ class LogCleanerTest {
}
// grouping by very large values should result in a single group with all the segments in it
var groups = cleaner.groupSegmentsBySize(log.logSegments, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, log.logEndOffset)
var groups = cleaner.groupSegmentsBySize(log.logSegments.asScala, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, log.logEndOffset)
assertEquals(1, groups.size)
assertEquals(log.numberOfSegments, groups.head.size)
checkSegmentOrder(groups)
// grouping by very small values should result in all groups having one entry
groups = cleaner.groupSegmentsBySize(log.logSegments, maxSize = 1, maxIndexSize = Int.MaxValue, log.logEndOffset)
groups = cleaner.groupSegmentsBySize(log.logSegments.asScala, maxSize = 1, maxIndexSize = Int.MaxValue, log.logEndOffset)
assertEquals(log.numberOfSegments, groups.size)
assertTrue(groups.forall(_.size == 1), "All groups should be singletons.")
checkSegmentOrder(groups)
groups = cleaner.groupSegmentsBySize(log.logSegments, maxSize = Int.MaxValue, maxIndexSize = 1, log.logEndOffset)
groups = cleaner.groupSegmentsBySize(log.logSegments.asScala, maxSize = Int.MaxValue, maxIndexSize = 1, log.logEndOffset)
assertEquals(log.numberOfSegments, groups.size)
assertTrue(groups.forall(_.size == 1), "All groups should be singletons.")
checkSegmentOrder(groups)
@ -1373,14 +1374,14 @@ class LogCleanerTest { @@ -1373,14 +1374,14 @@ class LogCleanerTest {
val groupSize = 3
// check grouping by log size
val logSize = log.logSegments.take(groupSize).map(_.size).sum.toInt + 1
groups = cleaner.groupSegmentsBySize(log.logSegments, maxSize = logSize, maxIndexSize = Int.MaxValue, log.logEndOffset)
val logSize = log.logSegments.asScala.take(groupSize).map(_.size).sum.toInt + 1
groups = cleaner.groupSegmentsBySize(log.logSegments.asScala, maxSize = logSize, maxIndexSize = Int.MaxValue, log.logEndOffset)
checkSegmentOrder(groups)
assertTrue(groups.dropRight(1).forall(_.size == groupSize), "All but the last group should be the target size.")
// check grouping by index size
val indexSize = log.logSegments.take(groupSize).map(_.offsetIndex.sizeInBytes).sum + 1
groups = cleaner.groupSegmentsBySize(log.logSegments, maxSize = Int.MaxValue, maxIndexSize = indexSize, log.logEndOffset)
val indexSize = log.logSegments.asScala.take(groupSize).map(_.offsetIndex.sizeInBytes).sum + 1
groups = cleaner.groupSegmentsBySize(log.logSegments.asScala, maxSize = Int.MaxValue, maxIndexSize = indexSize, log.logEndOffset)
checkSegmentOrder(groups)
assertTrue(groups.dropRight(1).forall(_.size == groupSize),
"All but the last group should be the target size.")
@ -1413,17 +1414,17 @@ class LogCleanerTest { @@ -1413,17 +1414,17 @@ class LogCleanerTest {
val notCleanableSegments = 1
assertEquals(totalSegments, log.numberOfSegments)
var groups = cleaner.groupSegmentsBySize(log.logSegments, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, firstUncleanableOffset)
var groups = cleaner.groupSegmentsBySize(log.logSegments.asScala, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, firstUncleanableOffset)
//because index file uses 4 byte relative index offset and current segments all none empty,
//segments will not group even their size is very small.
assertEquals(totalSegments - notCleanableSegments, groups.size)
//do clean to clean first 2 segments to empty
cleaner.clean(LogToClean(log.topicPartition, log, 0, firstUncleanableOffset))
assertEquals(totalSegments, log.numberOfSegments)
assertEquals(0, log.logSegments.head.size)
assertEquals(0, log.logSegments.asScala.head.size)
//after clean we got 2 empty segment, they will group together this time
groups = cleaner.groupSegmentsBySize(log.logSegments, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, firstUncleanableOffset)
groups = cleaner.groupSegmentsBySize(log.logSegments.asScala, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, firstUncleanableOffset)
val noneEmptySegment = 1
assertEquals(noneEmptySegment + 1, groups.size)
@ -1459,14 +1460,14 @@ class LogCleanerTest { @@ -1459,14 +1460,14 @@ class LogCleanerTest {
assertEquals(Int.MaxValue, log.activeSegment.offsetIndex.lastOffset)
// grouping should result in a single group with maximum relative offset of Int.MaxValue
var groups = cleaner.groupSegmentsBySize(log.logSegments, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, log.logEndOffset)
var groups = cleaner.groupSegmentsBySize(log.logSegments.asScala, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, log.logEndOffset)
assertEquals(1, groups.size)
// append another message, making last offset of second segment > Int.MaxValue
log.appendAsLeader(TestUtils.singletonRecords(value = "hello".getBytes, key = "hello".getBytes), leaderEpoch = 0)
// grouping should not group the two segments to ensure that maximum relative offset in each group <= Int.MaxValue
groups = cleaner.groupSegmentsBySize(log.logSegments, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, log.logEndOffset)
groups = cleaner.groupSegmentsBySize(log.logSegments.asScala, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, log.logEndOffset)
assertEquals(2, groups.size)
checkSegmentOrder(groups)
@ -1474,7 +1475,7 @@ class LogCleanerTest { @@ -1474,7 +1475,7 @@ class LogCleanerTest {
while (log.numberOfSegments < 4)
log.appendAsLeader(TestUtils.singletonRecords(value = "hello".getBytes, key = "hello".getBytes), leaderEpoch = 0)
groups = cleaner.groupSegmentsBySize(log.logSegments, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, log.logEndOffset)
groups = cleaner.groupSegmentsBySize(log.logSegments.asScala, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, log.logEndOffset)
assertEquals(log.numberOfSegments - 1, groups.size)
for (group <- groups)
assertTrue(group.last.offsetIndex.lastOffset - group.head.offsetIndex.baseOffset <= Int.MaxValue,
@ -1513,11 +1514,11 @@ class LogCleanerTest { @@ -1513,11 +1514,11 @@ class LogCleanerTest {
log.appendAsFollower(record4)
assertTrue(log.logEndOffset - 1 - log.logStartOffset > Int.MaxValue, "Actual offset range should be > Int.MaxValue")
assertTrue(log.logSegments.last.offsetIndex.lastOffset - log.logStartOffset <= Int.MaxValue,
assertTrue(log.logSegments.asScala.last.offsetIndex.lastOffset - log.logStartOffset <= Int.MaxValue,
"index.lastOffset is reporting the wrong last offset")
// grouping should result in two groups because the second segment takes the offset range > MaxInt
val groups = cleaner.groupSegmentsBySize(log.logSegments, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, log.logEndOffset)
val groups = cleaner.groupSegmentsBySize(log.logSegments.asScala, maxSize = Int.MaxValue, maxIndexSize = Int.MaxValue, log.logEndOffset)
assertEquals(2, groups.size)
for (group <- groups)
@ -1556,7 +1557,7 @@ class LogCleanerTest { @@ -1556,7 +1557,7 @@ class LogCleanerTest {
assertEquals(end - start, stats.mapMessagesRead)
}
val segments = log.logSegments.toSeq
val segments = log.logSegments.asScala.toSeq
checkRange(map, 0, segments(1).baseOffset.toInt)
checkRange(map, segments(1).baseOffset.toInt, segments(3).baseOffset.toInt)
checkRange(map, segments(3).baseOffset.toInt, log.logEndOffset.toInt)
@ -1598,7 +1599,7 @@ class LogCleanerTest { @@ -1598,7 +1599,7 @@ class LogCleanerTest {
assertFalse(LogTestUtils.hasOffsetOverflow(log))
// Clean each segment now that split is complete.
for (segmentToClean <- log.logSegments)
for (segmentToClean <- log.logSegments.asScala)
cleaner.cleanSegments(log, List(segmentToClean), offsetMap, 0L, new CleanerStats(),
new CleanedTransactionMetadata, -1)
assertEquals(expectedKeysAfterCleaning, LogTestUtils.keysInLog(log))
@ -1640,7 +1641,7 @@ class LogCleanerTest { @@ -1640,7 +1641,7 @@ class LogCleanerTest {
offsetMap.put(key(k), Long.MaxValue)
// clean the log
cleaner.cleanSegments(log, log.logSegments.take(9).toSeq, offsetMap, 0L, new CleanerStats(),
cleaner.cleanSegments(log, log.logSegments.asScala.take(9).toSeq, offsetMap, 0L, new CleanerStats(),
new CleanedTransactionMetadata, -1)
// clear scheduler so that async deletes don't run
time.scheduler.clear()
@ -1649,14 +1650,14 @@ class LogCleanerTest { @@ -1649,14 +1650,14 @@ class LogCleanerTest {
// 1) Simulate recovery just after .cleaned file is created, before rename to .swap
// On recovery, clean operation is aborted. All messages should be present in the log
log.logSegments.head.changeFileSuffixes("", UnifiedLog.CleanedFileSuffix)
log.logSegments.asScala.head.changeFileSuffixes("", UnifiedLog.CleanedFileSuffix)
for (file <- dir.listFiles if file.getName.endsWith(LogFileUtils.DELETED_FILE_SUFFIX)) {
Utils.atomicMoveWithFallback(file.toPath, Paths.get(Utils.replaceSuffix(file.getPath, LogFileUtils.DELETED_FILE_SUFFIX, "")), false)
}
log = recoverAndCheck(config, allKeys)
// clean again
cleaner.cleanSegments(log, log.logSegments.take(9).toSeq, offsetMap, 0L, new CleanerStats(),
cleaner.cleanSegments(log, log.logSegments.asScala.take(9).toSeq, offsetMap, 0L, new CleanerStats(),
new CleanedTransactionMetadata, -1)
// clear scheduler so that async deletes don't run
time.scheduler.clear()
@ -1665,15 +1666,15 @@ class LogCleanerTest { @@ -1665,15 +1666,15 @@ class LogCleanerTest {
// 2) Simulate recovery just after .cleaned file is created, and a subset of them are renamed to .swap
// On recovery, clean operation is aborted. All messages should be present in the log
log.logSegments.head.changeFileSuffixes("", UnifiedLog.CleanedFileSuffix)
log.logSegments.head.log.renameTo(new File(Utils.replaceSuffix(log.logSegments.head.log.file.getPath, UnifiedLog.CleanedFileSuffix, UnifiedLog.SwapFileSuffix)))
log.logSegments.asScala.head.changeFileSuffixes("", UnifiedLog.CleanedFileSuffix)
log.logSegments.asScala.head.log.renameTo(new File(Utils.replaceSuffix(log.logSegments.asScala.head.log.file.getPath, UnifiedLog.CleanedFileSuffix, UnifiedLog.SwapFileSuffix)))
for (file <- dir.listFiles if file.getName.endsWith(LogFileUtils.DELETED_FILE_SUFFIX)) {
Utils.atomicMoveWithFallback(file.toPath, Paths.get(Utils.replaceSuffix(file.getPath, LogFileUtils.DELETED_FILE_SUFFIX, "")), false)
}
log = recoverAndCheck(config, allKeys)
// clean again
cleaner.cleanSegments(log, log.logSegments.take(9).toSeq, offsetMap, 0L, new CleanerStats(),
cleaner.cleanSegments(log, log.logSegments.asScala.take(9).toSeq, offsetMap, 0L, new CleanerStats(),
new CleanedTransactionMetadata, -1)
// clear scheduler so that async deletes don't run
time.scheduler.clear()
@ -1682,7 +1683,7 @@ class LogCleanerTest { @@ -1682,7 +1683,7 @@ class LogCleanerTest {
// 3) Simulate recovery just after swap file is created, before old segment files are
// renamed to .deleted. Clean operation is resumed during recovery.
log.logSegments.head.changeFileSuffixes("", UnifiedLog.SwapFileSuffix)
log.logSegments.asScala.head.changeFileSuffixes("", UnifiedLog.SwapFileSuffix)
for (file <- dir.listFiles if file.getName.endsWith(LogFileUtils.DELETED_FILE_SUFFIX)) {
Utils.atomicMoveWithFallback(file.toPath, Paths.get(Utils.replaceSuffix(file.getPath, LogFileUtils.DELETED_FILE_SUFFIX, "")), false)
}
@ -1695,7 +1696,7 @@ class LogCleanerTest { @@ -1695,7 +1696,7 @@ class LogCleanerTest {
}
for (k <- 1 until messageCount by 2)
offsetMap.put(key(k), Long.MaxValue)
cleaner.cleanSegments(log, log.logSegments.take(9).toSeq, offsetMap, 0L, new CleanerStats(),
cleaner.cleanSegments(log, log.logSegments.asScala.take(9).toSeq, offsetMap, 0L, new CleanerStats(),
new CleanedTransactionMetadata, -1)
// clear scheduler so that async deletes don't run
time.scheduler.clear()
@ -1703,7 +1704,7 @@ class LogCleanerTest { @@ -1703,7 +1704,7 @@ class LogCleanerTest {
// 4) Simulate recovery after swap file is created and old segments files are renamed
// to .deleted. Clean operation is resumed during recovery.
log.logSegments.head.changeFileSuffixes("", UnifiedLog.SwapFileSuffix)
log.logSegments.asScala.head.changeFileSuffixes("", UnifiedLog.SwapFileSuffix)
log = recoverAndCheck(config, cleanedKeys)
// add some more messages and clean the log again
@ -1713,7 +1714,7 @@ class LogCleanerTest { @@ -1713,7 +1714,7 @@ class LogCleanerTest {
}
for (k <- 1 until messageCount by 2)
offsetMap.put(key(k), Long.MaxValue)
cleaner.cleanSegments(log, log.logSegments.take(9).toSeq, offsetMap, 0L, new CleanerStats(),
cleaner.cleanSegments(log, log.logSegments.asScala.take(9).toSeq, offsetMap, 0L, new CleanerStats(),
new CleanedTransactionMetadata, -1)
// clear scheduler so that async deletes don't run
time.scheduler.clear()
@ -1721,7 +1722,7 @@ class LogCleanerTest { @@ -1721,7 +1722,7 @@ class LogCleanerTest {
// 5) Simulate recovery after a subset of swap files are renamed to regular files and old segments files are renamed
// to .deleted. Clean operation is resumed during recovery.
log.logSegments.head.timeIndex.file.renameTo(new File(Utils.replaceSuffix(log.logSegments.head.timeIndex.file.getPath, "", UnifiedLog.SwapFileSuffix)))
log.logSegments.asScala.head.timeIndex.file.renameTo(new File(Utils.replaceSuffix(log.logSegments.asScala.head.timeIndex.file.getPath, "", UnifiedLog.SwapFileSuffix)))
log = recoverAndCheck(config, cleanedKeys)
// add some more messages and clean the log again
@ -1731,7 +1732,7 @@ class LogCleanerTest { @@ -1731,7 +1732,7 @@ class LogCleanerTest {
}
for (k <- 1 until messageCount by 2)
offsetMap.put(key(k), Long.MaxValue)
cleaner.cleanSegments(log, log.logSegments.take(9).toSeq, offsetMap, 0L, new CleanerStats(),
cleaner.cleanSegments(log, log.logSegments.asScala.take(9).toSeq, offsetMap, 0L, new CleanerStats(),
new CleanedTransactionMetadata, -1)
// clear scheduler so that async deletes don't run
time.scheduler.clear()
@ -1828,7 +1829,7 @@ class LogCleanerTest { @@ -1828,7 +1829,7 @@ class LogCleanerTest {
cleaner.clean(LogToClean(new TopicPartition("test", 0), log, 0, log.activeSegment.baseOffset))
for (segment <- log.logSegments; batch <- segment.log.batches.asScala; record <- batch.asScala) {
for (segment <- log.logSegments.asScala; batch <- segment.log.batches.asScala; record <- batch.asScala) {
assertTrue(record.hasMagic(batch.magic))
val value = TestUtils.readString(record.value).toLong
assertEquals(record.offset, value)
@ -1875,7 +1876,7 @@ class LogCleanerTest { @@ -1875,7 +1876,7 @@ class LogCleanerTest {
log.roll()
cleaner.clean(LogToClean(new TopicPartition("test", 0), log, 1, log.activeSegment.baseOffset))
assertEquals(1, log.logSegments.head.log.batches.iterator.next().lastOffset,
assertEquals(1, log.logSegments.asScala.head.log.batches.iterator.next().lastOffset,
"The tombstone should be retained.")
// Append a message and roll out another log segment.
log.appendAsLeader(TestUtils.singletonRecords(value = "1".getBytes,
@ -1883,7 +1884,7 @@ class LogCleanerTest { @@ -1883,7 +1884,7 @@ class LogCleanerTest {
timestamp = time.milliseconds()), leaderEpoch = 0)
log.roll()
cleaner.clean(LogToClean(new TopicPartition("test", 0), log, 2, log.activeSegment.baseOffset))
assertEquals(1, log.logSegments.head.log.batches.iterator.next().lastOffset,
assertEquals(1, log.logSegments.asScala.head.log.batches.iterator.next().lastOffset,
"The tombstone should be retained.")
}

2
core/src/test/scala/unit/kafka/log/LogConcurrencyTest.scala

@ -161,7 +161,7 @@ class LogConcurrencyTest { @@ -161,7 +161,7 @@ class LogConcurrencyTest {
private def validateConsumedData(log: UnifiedLog, consumedBatches: Iterable[FetchedBatch]): Unit = {
val iter = consumedBatches.iterator
log.logSegments.foreach { segment =>
log.logSegments.forEach { segment =>
segment.log.batches.forEach { batch =>
if (iter.hasNext) {
val consumedBatch = iter.next()

87
core/src/test/scala/unit/kafka/log/LogLoaderTest.scala

@ -33,7 +33,7 @@ import org.apache.kafka.server.common.MetadataVersion @@ -33,7 +33,7 @@ import org.apache.kafka.server.common.MetadataVersion
import org.apache.kafka.server.common.MetadataVersion.IBP_0_11_0_IV0
import org.apache.kafka.server.util.{MockTime, Scheduler}
import org.apache.kafka.storage.internals.epoch.LeaderEpochFileCache
import org.apache.kafka.storage.internals.log.{AbortedTxn, CleanerConfig, EpochEntry, FetchDataInfo, LogConfig, LogDirFailureChannel, LogFileUtils, LogOffsetMetadata, LogStartOffsetIncrementReason, OffsetIndex, ProducerStateManager, ProducerStateManagerConfig, SnapshotFile}
import org.apache.kafka.storage.internals.log.{AbortedTxn, CleanerConfig, EpochEntry, FetchDataInfo, LogConfig, LogDirFailureChannel, LogFileUtils, LogOffsetMetadata, LogSegment, LogSegments, LogStartOffsetIncrementReason, OffsetIndex, ProducerStateManager, ProducerStateManagerConfig, SnapshotFile}
import org.junit.jupiter.api.Assertions.{assertDoesNotThrow, assertEquals, assertFalse, assertNotEquals, assertThrows, assertTrue}
import org.junit.jupiter.api.function.Executable
import org.junit.jupiter.api.{AfterEach, BeforeEach, Test}
@ -154,7 +154,7 @@ class LogLoaderTest { @@ -154,7 +154,7 @@ class LogLoaderTest {
this.maxTransactionTimeoutMs, this.producerStateManagerConfig, time)
val logLoader = new LogLoader(logDir, topicPartition, config, time.scheduler, time,
logDirFailureChannel, hadCleanShutdown, segments, logStartOffset, logRecoveryPoint,
leaderEpochCache, producerStateManager)
leaderEpochCache.asJava, producerStateManager)
val offsets = logLoader.load()
val localLog = new LocalLog(logDir, logConfig, segments, offsets.recoveryPoint,
offsets.nextOffsetMetadata, mockTime.scheduler, mockTime, topicPartition,
@ -317,7 +317,7 @@ class LogLoaderTest { @@ -317,7 +317,7 @@ class LogLoaderTest {
}
assertTrue(log.logSegments.size >= 5)
val segmentOffsets = log.logSegments.toVector.map(_.baseOffset)
val segmentOffsets = log.logSegments.asScala.toVector.map(_.baseOffset)
val activeSegmentOffset = segmentOffsets.last
// We want the recovery point to be past the segment offset and before the last 2 segments including a gap of
@ -336,10 +336,10 @@ class LogLoaderTest { @@ -336,10 +336,10 @@ class LogLoaderTest {
if (logConfig.messageFormatVersion.isLessThan(IBP_0_11_0_IV0)) {
expectedSegmentsWithReads += activeSegmentOffset
expectedSnapshotOffsets ++= log.logSegments.map(_.baseOffset).toVector.takeRight(2) :+ log.logEndOffset
expectedSnapshotOffsets ++= log.logSegments.asScala.map(_.baseOffset).toVector.takeRight(2) :+ log.logEndOffset
} else {
expectedSegmentsWithReads ++= segOffsetsBeforeRecovery ++ Set(activeSegmentOffset)
expectedSnapshotOffsets ++= log.logSegments.map(_.baseOffset).toVector.takeRight(4) :+ log.logEndOffset
expectedSnapshotOffsets ++= log.logSegments.asScala.map(_.baseOffset).toVector.takeRight(4) :+ log.logEndOffset
}
def createLogWithInterceptedReads(recoveryPoint: Long): UnifiedLog = {
@ -350,8 +350,7 @@ class LogLoaderTest { @@ -350,8 +350,7 @@ class LogLoaderTest {
// Intercept all segment read calls
val interceptedLogSegments = new LogSegments(topicPartition) {
override def add(segment: LogSegment): LogSegment = {
val wrapper = new LogSegment(segment.log, segment.lazyOffsetIndex, segment.lazyTimeIndex, segment.txnIndex, segment.baseOffset,
segment.indexIntervalBytes, segment.rollJitterMs, mockTime) {
val wrapper = new LogSegment(segment) {
override def read(startOffset: Long, maxSize: Int, maxPosition: Long, minOneMessage: Boolean): FetchDataInfo = {
segmentsWithReads += this
@ -359,7 +358,7 @@ class LogLoaderTest { @@ -359,7 +358,7 @@ class LogLoaderTest {
}
override def recover(producerStateManager: ProducerStateManager,
leaderEpochCache: Option[LeaderEpochFileCache]): Int = {
leaderEpochCache: Optional[LeaderEpochFileCache]): Int = {
recoveredSegments += this
super.recover(producerStateManager, leaderEpochCache)
}
@ -381,7 +380,7 @@ class LogLoaderTest { @@ -381,7 +380,7 @@ class LogLoaderTest {
interceptedLogSegments,
0L,
recoveryPoint,
leaderEpochCache,
leaderEpochCache.asJava,
producerStateManager)
val offsets = logLoader.load()
val localLog = new LocalLog(logDir, logConfig, interceptedLogSegments, offsets.recoveryPoint,
@ -443,7 +442,7 @@ class LogLoaderTest { @@ -443,7 +442,7 @@ class LogLoaderTest {
segments,
0L,
0L,
leaderEpochCache,
leaderEpochCache.asJava,
stateManager
).load()
val localLog = new LocalLog(logDir, config, segments, offsets.recoveryPoint,
@ -552,7 +551,7 @@ class LogLoaderTest { @@ -552,7 +551,7 @@ class LogLoaderTest {
segments,
0L,
0L,
leaderEpochCache,
leaderEpochCache.asJava,
stateManager
).load()
val localLog = new LocalLog(logDir, config, segments, offsets.recoveryPoint,
@ -606,7 +605,7 @@ class LogLoaderTest { @@ -606,7 +605,7 @@ class LogLoaderTest {
segments,
0L,
0L,
leaderEpochCache,
leaderEpochCache.asJava,
stateManager
).load()
val localLog = new LocalLog(logDir, config, segments, offsets.recoveryPoint,
@ -659,7 +658,7 @@ class LogLoaderTest { @@ -659,7 +658,7 @@ class LogLoaderTest {
segments,
0L,
0L,
leaderEpochCache,
leaderEpochCache.asJava,
stateManager
).load()
val localLog = new LocalLog(logDir, config, segments, offsets.recoveryPoint,
@ -843,8 +842,8 @@ class LogLoaderTest { @@ -843,8 +842,8 @@ class LogLoaderTest {
var log = createLog(logDir, logConfig)
for(i <- 0 until numMessages)
log.appendAsLeader(TestUtils.singletonRecords(value = TestUtils.randomBytes(10), timestamp = mockTime.milliseconds + i * 10), leaderEpoch = 0)
val indexFiles = log.logSegments.map(_.lazyOffsetIndex.file)
val timeIndexFiles = log.logSegments.map(_.lazyTimeIndex.file)
val indexFiles = log.logSegments.asScala.map(_.offsetIndexFile)
val timeIndexFiles = log.logSegments.asScala.map(_.timeIndexFile)
log.close()
// delete all the index files
@ -854,12 +853,12 @@ class LogLoaderTest { @@ -854,12 +853,12 @@ class LogLoaderTest {
// reopen the log
log = createLog(logDir, logConfig, lastShutdownClean = false)
assertEquals(numMessages, log.logEndOffset, "Should have %d messages when log is reopened".format(numMessages))
assertTrue(log.logSegments.head.offsetIndex.entries > 0, "The index should have been rebuilt")
assertTrue(log.logSegments.head.timeIndex.entries > 0, "The time index should have been rebuilt")
assertTrue(log.logSegments.asScala.head.offsetIndex.entries > 0, "The index should have been rebuilt")
assertTrue(log.logSegments.asScala.head.timeIndex.entries > 0, "The time index should have been rebuilt")
for(i <- 0 until numMessages) {
assertEquals(i, LogTestUtils.readLog(log, i, 100).records.batches.iterator.next().lastOffset)
if (i == 0)
assertEquals(log.logSegments.head.baseOffset, log.fetchOffsetByTimestamp(mockTime.milliseconds + i * 10).get.offset)
assertEquals(log.logSegments.asScala.head.baseOffset, log.fetchOffsetByTimestamp(mockTime.milliseconds + i * 10).get.offset)
else
assertEquals(i, log.fetchOffsetByTimestamp(mockTime.milliseconds + i * 10).get.offset)
}
@ -883,7 +882,7 @@ class LogLoaderTest { @@ -883,7 +882,7 @@ class LogLoaderTest {
for (i <- 0 until numMessages)
log.appendAsLeader(TestUtils.singletonRecords(value = TestUtils.randomBytes(10),
timestamp = mockTime.milliseconds + i * 10, magicValue = RecordBatch.MAGIC_VALUE_V1), leaderEpoch = 0)
val timeIndexFiles = log.logSegments.map(_.lazyTimeIndex.file)
val timeIndexFiles = log.logSegments.asScala.map(_.timeIndexFile())
log.close()
// Delete the time index.
@ -891,9 +890,9 @@ class LogLoaderTest { @@ -891,9 +890,9 @@ class LogLoaderTest {
// The rebuilt time index should be empty
log = createLog(logDir, logConfig, recoveryPoint = numMessages + 1, lastShutdownClean = false)
for (segment <- log.logSegments.init) {
for (segment <- log.logSegments.asScala.init) {
assertEquals(0, segment.timeIndex.entries, "The time index should be empty")
assertEquals(0, segment.lazyTimeIndex.file.length, "The time index file size should be 0")
assertEquals(0, segment.timeIndexFile().length, "The time index file size should be 0")
}
}
@ -909,8 +908,8 @@ class LogLoaderTest { @@ -909,8 +908,8 @@ class LogLoaderTest {
var log = createLog(logDir, logConfig)
for(i <- 0 until numMessages)
log.appendAsLeader(TestUtils.singletonRecords(value = TestUtils.randomBytes(10), timestamp = mockTime.milliseconds + i * 10), leaderEpoch = 0)
val indexFiles = log.logSegments.map(_.lazyOffsetIndex.file)
val timeIndexFiles = log.logSegments.map(_.lazyTimeIndex.file)
val indexFiles = log.logSegments.asScala.map(_.offsetIndexFile())
val timeIndexFiles = log.logSegments.asScala.map(_.timeIndexFile())
log.close()
// corrupt all the index files
@ -933,7 +932,7 @@ class LogLoaderTest { @@ -933,7 +932,7 @@ class LogLoaderTest {
for(i <- 0 until numMessages) {
assertEquals(i, LogTestUtils.readLog(log, i, 100).records.batches.iterator.next().lastOffset)
if (i == 0)
assertEquals(log.logSegments.head.baseOffset, log.fetchOffsetByTimestamp(mockTime.milliseconds + i * 10).get.offset)
assertEquals(log.logSegments.asScala.head.baseOffset, log.fetchOffsetByTimestamp(mockTime.milliseconds + i * 10).get.offset)
else
assertEquals(i, log.fetchOffsetByTimestamp(mockTime.milliseconds + i * 10).get.offset)
}
@ -945,10 +944,10 @@ class LogLoaderTest { @@ -945,10 +944,10 @@ class LogLoaderTest {
*/
@Test
def testBogusIndexSegmentsAreRemoved(): Unit = {
val bogusIndex1 = UnifiedLog.offsetIndexFile(logDir, 0)
val bogusTimeIndex1 = UnifiedLog.timeIndexFile(logDir, 0)
val bogusIndex2 = UnifiedLog.offsetIndexFile(logDir, 5)
val bogusTimeIndex2 = UnifiedLog.timeIndexFile(logDir, 5)
val bogusIndex1 = LogFileUtils.offsetIndexFile(logDir, 0)
val bogusTimeIndex1 = LogFileUtils.timeIndexFile(logDir, 0)
val bogusIndex2 = LogFileUtils.offsetIndexFile(logDir, 5)
val bogusTimeIndex2 = LogFileUtils.timeIndexFile(logDir, 5)
// The files remain absent until we first access it because we are doing lazy loading for time index and offset index
// files but in this test case we need to create these files in order to test we will remove them.
@ -960,8 +959,8 @@ class LogLoaderTest { @@ -960,8 +959,8 @@ class LogLoaderTest {
val log = createLog(logDir, logConfig)
// Force the segment to access the index files because we are doing index lazy loading.
log.logSegments.toSeq.head.offsetIndex
log.logSegments.toSeq.head.timeIndex
log.logSegments.asScala.head.offsetIndex
log.logSegments.asScala.head.timeIndex
assertTrue(bogusIndex1.length > 0,
"The first index file should have been replaced with a larger file")
@ -1033,18 +1032,18 @@ class LogLoaderTest { @@ -1033,18 +1032,18 @@ class LogLoaderTest {
val numMessages = 50 + TestUtils.random.nextInt(50)
for (_ <- 0 until numMessages)
log.appendAsLeader(createRecords, leaderEpoch = 0)
val records = log.logSegments.flatMap(_.log.records.asScala.toList).toList
val records = log.logSegments.asScala.flatMap(_.log.records.asScala.toList).toList
log.close()
// corrupt index and log by appending random bytes
TestUtils.appendNonsenseToFile(log.activeSegment.lazyOffsetIndex.file, TestUtils.random.nextInt(1024) + 1)
TestUtils.appendNonsenseToFile(log.activeSegment.offsetIndexFile, TestUtils.random.nextInt(1024) + 1)
TestUtils.appendNonsenseToFile(log.activeSegment.log.file, TestUtils.random.nextInt(1024) + 1)
// attempt recovery
log = createLog(logDir, logConfig, brokerTopicStats, 0L, recoveryPoint, lastShutdownClean = false)
assertEquals(numMessages, log.logEndOffset)
val recovered = log.logSegments.flatMap(_.log.records.asScala.toList).toList
val recovered = log.logSegments.asScala.flatMap(_.log.records.asScala.toList).toList
assertEquals(records.size, recovered.size)
for (i <- records.indices) {
@ -1209,7 +1208,7 @@ class LogLoaderTest { @@ -1209,7 +1208,7 @@ class LogLoaderTest {
// Running split again would throw an error
for (segment <- recoveredLog.logSegments) {
for (segment <- recoveredLog.logSegments.asScala) {
assertThrows(classOf[IllegalArgumentException], () => log.splitOverflowedSegment(segment))
}
}
@ -1438,7 +1437,7 @@ class LogLoaderTest { @@ -1438,7 +1437,7 @@ class LogLoaderTest {
LogTestUtils.appendEndTxnMarkerAsLeader(log, pid4, epoch, ControlRecordType.COMMIT, mockTime.milliseconds()) // 90
// delete all the offset and transaction index files to force recovery
log.logSegments.foreach { segment =>
log.logSegments.forEach { segment =>
segment.offsetIndex.deleteIfExists()
segment.txnIndex.deleteIfExists()
}
@ -1489,7 +1488,7 @@ class LogLoaderTest { @@ -1489,7 +1488,7 @@ class LogLoaderTest {
LogTestUtils.appendEndTxnMarkerAsLeader(log, pid4, epoch, ControlRecordType.COMMIT, mockTime.milliseconds()) // 90
// delete the last offset and transaction index files to force recovery
val lastSegment = log.logSegments.last
val lastSegment = log.logSegments.asScala.last
val recoveryPoint = lastSegment.baseOffset
lastSegment.offsetIndex.deleteIfExists()
lastSegment.txnIndex.deleteIfExists()
@ -1543,7 +1542,7 @@ class LogLoaderTest { @@ -1543,7 +1542,7 @@ class LogLoaderTest {
// delete the last offset and transaction index files to force recovery. this should force us to rebuild
// the producer state from the start of the log
val lastSegment = log.logSegments.last
val lastSegment = log.logSegments.asScala.last
val recoveryPoint = lastSegment.baseOffset
lastSegment.offsetIndex.deleteIfExists()
lastSegment.txnIndex.deleteIfExists()
@ -1574,7 +1573,7 @@ class LogLoaderTest { @@ -1574,7 +1573,7 @@ class LogLoaderTest {
assertTrue(log.logEndOffset > log.logStartOffset)
// Append garbage to a segment below the current log start offset
val segmentToForceTruncation = log.logSegments.take(2).last
val segmentToForceTruncation = log.logSegments.asScala.take(2).last
val bw = new BufferedWriter(new FileWriter(segmentToForceTruncation.log.file))
bw.write("corruptRecord")
bw.close()
@ -1590,11 +1589,11 @@ class LogLoaderTest { @@ -1590,11 +1589,11 @@ class LogLoaderTest {
assertEquals(startOffset, log.logStartOffset)
assertEquals(startOffset, log.logEndOffset)
// Validate that the remaining segment matches our expectations
val onlySegment = log.logSegments.head
val onlySegment = log.logSegments.asScala.head
assertEquals(startOffset, onlySegment.baseOffset)
assertTrue(onlySegment.log.file().exists())
assertTrue(onlySegment.lazyOffsetIndex.file.exists())
assertTrue(onlySegment.lazyTimeIndex.file.exists())
assertTrue(onlySegment.offsetIndexFile().exists())
assertTrue(onlySegment.timeIndexFile().exists())
}
@Test
@ -1639,7 +1638,7 @@ class LogLoaderTest { @@ -1639,7 +1638,7 @@ class LogLoaderTest {
// | |---> logEndOffset
// corrupt record inserted
//
val segmentToForceTruncation = log.logSegments.take(2).last
val segmentToForceTruncation = log.logSegments.asScala.take(2).last
assertEquals(1, segmentToForceTruncation.baseOffset)
val bw = new BufferedWriter(new FileWriter(segmentToForceTruncation.log.file))
bw.write("corruptRecord")
@ -1786,7 +1785,7 @@ class LogLoaderTest { @@ -1786,7 +1785,7 @@ class LogLoaderTest {
log.deleteOldSegments()
val segments = new LogSegments(topicPartition)
log.logSegments.foreach(segment => segments.add(segment))
log.logSegments.forEach(segment => segments.add(segment))
assertEquals(5, segments.firstSegment.get.baseOffset)
val leaderEpochCache = UnifiedLog.maybeCreateLeaderEpochCache(logDir, topicPartition, logDirFailureChannel, logConfig.recordVersion, "")
@ -1801,7 +1800,7 @@ class LogLoaderTest { @@ -1801,7 +1800,7 @@ class LogLoaderTest {
segments,
0L,
0L,
leaderEpochCache,
leaderEpochCache.asJava,
stateManager,
isRemoteLogEnabled = isRemoteLogEnabled
).load()

16
core/src/test/scala/unit/kafka/log/LogManagerTest.scala

@ -275,15 +275,15 @@ class LogManagerTest { @@ -275,15 +275,15 @@ class LogManagerTest {
assertTrue(log.numberOfSegments > 1, "There should be more than one segment now.")
log.updateHighWatermark(log.logEndOffset)
log.logSegments.foreach(_.log.file.setLastModified(time.milliseconds))
log.logSegments.forEach(_.log.file.setLastModified(time.milliseconds))
time.sleep(maxLogAgeMs + 1)
assertEquals(1, log.numberOfSegments, "Now there should only be only one segment in the index.")
time.sleep(log.config.fileDeleteDelayMs + 1)
log.logSegments.foreach(s => {
s.lazyOffsetIndex.get
s.lazyTimeIndex.get
log.logSegments.forEach(s => {
s.offsetIndex()
s.timeIndex()
})
// there should be a log file, two indexes, one producer snapshot, and the leader epoch checkpoint
@ -374,7 +374,7 @@ class LogManagerTest { @@ -374,7 +374,7 @@ class LogManagerTest {
val numSegments = log.numberOfSegments
assertTrue(log.numberOfSegments > 1, "There should be more than one segment now.")
log.logSegments.foreach(_.log.file.setLastModified(time.milliseconds))
log.logSegments.forEach(_.log.file.setLastModified(time.milliseconds))
time.sleep(maxLogAgeMs + 1)
assertEquals(numSegments, log.numberOfSegments, "number of segments shouldn't have changed")
@ -511,12 +511,12 @@ class LogManagerTest { @@ -511,12 +511,12 @@ class LogManagerTest {
val removedLog = logManager.asyncDelete(new TopicPartition(name, 0)).get
val removedSegment = removedLog.activeSegment
val indexFilesAfterDelete = Seq(removedSegment.lazyOffsetIndex.file, removedSegment.lazyTimeIndex.file,
val indexFilesAfterDelete = Seq(removedSegment.offsetIndexFile, removedSegment.timeIndexFile,
removedSegment.txnIndex.file)
assertEquals(new File(removedLog.dir, logName), removedSegment.log.file)
assertEquals(new File(removedLog.dir, indexName), removedSegment.lazyOffsetIndex.file)
assertEquals(new File(removedLog.dir, timeIndexName), removedSegment.lazyTimeIndex.file)
assertEquals(new File(removedLog.dir, indexName), removedSegment.offsetIndexFile)
assertEquals(new File(removedLog.dir, timeIndexName), removedSegment.timeIndexFile)
assertEquals(new File(removedLog.dir, txnIndexName), removedSegment.txnIndex.file)
// Try to detect the case where a new index type was added and we forgot to update the pointer

121
core/src/test/scala/unit/kafka/log/LogSegmentTest.scala

@ -16,7 +16,6 @@ @@ -16,7 +16,6 @@
*/
package kafka.log
import kafka.common.LogSegmentOffsetOverflowException
import kafka.utils.TestUtils
import kafka.utils.TestUtils.checkEquals
import org.apache.kafka.common.TopicPartition
@ -25,7 +24,7 @@ import org.apache.kafka.common.record._ @@ -25,7 +24,7 @@ import org.apache.kafka.common.record._
import org.apache.kafka.common.utils.{MockTime, Time, Utils}
import org.apache.kafka.storage.internals.checkpoint.LeaderEpochCheckpoint
import org.apache.kafka.storage.internals.epoch.LeaderEpochFileCache
import org.apache.kafka.storage.internals.log.{BatchMetadata, EpochEntry, LogConfig, ProducerStateEntry, ProducerStateManager, ProducerStateManagerConfig, RollParams}
import org.apache.kafka.storage.internals.log.{BatchMetadata, EpochEntry, LogConfig, LogFileUtils, LogSegment, LogSegmentOffsetOverflowException, ProducerStateEntry, ProducerStateManager, ProducerStateManagerConfig, RollParams}
import org.junit.jupiter.api.Assertions._
import org.junit.jupiter.api.{AfterEach, BeforeEach, Test}
import org.junit.jupiter.params.ParameterizedTest
@ -33,7 +32,7 @@ import org.junit.jupiter.params.provider.CsvSource @@ -33,7 +32,7 @@ import org.junit.jupiter.params.provider.CsvSource
import java.io.File
import java.util
import java.util.OptionalLong
import java.util.{Optional, OptionalLong}
import scala.collection._
import scala.jdk.CollectionConverters._
@ -99,7 +98,7 @@ class LogSegmentTest { @@ -99,7 +98,7 @@ class LogSegmentTest {
@Test
def testReadOnEmptySegment(): Unit = {
val seg = createSegment(40)
val read = seg.read(startOffset = 40, maxSize = 300)
val read = seg.read(40, 300)
assertNull(read, "Read beyond the last offset in the segment should be null")
}
@ -112,7 +111,7 @@ class LogSegmentTest { @@ -112,7 +111,7 @@ class LogSegmentTest {
val seg = createSegment(40)
val ms = records(50, "hello", "there", "little", "bee")
seg.append(53, RecordBatch.NO_TIMESTAMP, -1L, ms)
val read = seg.read(startOffset = 41, maxSize = 300).records
val read = seg.read(41, 300).records
checkEquals(ms.records.iterator, read.records.iterator)
}
@ -124,7 +123,7 @@ class LogSegmentTest { @@ -124,7 +123,7 @@ class LogSegmentTest {
val seg = createSegment(40)
val ms = records(50, "hello", "there")
seg.append(51, RecordBatch.NO_TIMESTAMP, -1L, ms)
val read = seg.read(startOffset = 52, maxSize = 200)
val read = seg.read(52, 200)
assertNull(read, "Read beyond the last offset in the segment should give null")
}
@ -139,7 +138,7 @@ class LogSegmentTest { @@ -139,7 +138,7 @@ class LogSegmentTest {
seg.append(51, RecordBatch.NO_TIMESTAMP, -1L, ms)
val ms2 = records(60, "alpha", "beta")
seg.append(61, RecordBatch.NO_TIMESTAMP, -1L, ms2)
val read = seg.read(startOffset = 55, maxSize = 200)
val read = seg.read(55, 200)
checkEquals(ms2.records.iterator, read.records.records.iterator)
}
@ -264,17 +263,17 @@ class LogSegmentTest { @@ -264,17 +263,17 @@ class LogSegmentTest {
assertEquals(490, seg.largestTimestamp)
// Search for an indexed timestamp
assertEquals(42, seg.findOffsetByTimestamp(420).get.offset)
assertEquals(43, seg.findOffsetByTimestamp(421).get.offset)
assertEquals(42, seg.findOffsetByTimestamp(420, 0L).get.offset)
assertEquals(43, seg.findOffsetByTimestamp(421, 0L).get.offset)
// Search for an un-indexed timestamp
assertEquals(43, seg.findOffsetByTimestamp(430).get.offset)
assertEquals(44, seg.findOffsetByTimestamp(431).get.offset)
assertEquals(43, seg.findOffsetByTimestamp(430, 0L).get.offset)
assertEquals(44, seg.findOffsetByTimestamp(431, 0L).get.offset)
// Search beyond the last timestamp
assertEquals(None, seg.findOffsetByTimestamp(491))
assertEquals(Optional.empty(), seg.findOffsetByTimestamp(491, 0L))
// Search before the first indexed timestamp
assertEquals(41, seg.findOffsetByTimestamp(401).get.offset)
assertEquals(41, seg.findOffsetByTimestamp(401, 0L).get.offset)
// Search before the first timestamp
assertEquals(40, seg.findOffsetByTimestamp(399).get.offset)
assertEquals(40, seg.findOffsetByTimestamp(399, 0L).get.offset)
}
/**
@ -295,26 +294,26 @@ class LogSegmentTest { @@ -295,26 +294,26 @@ class LogSegmentTest {
def testChangeFileSuffixes(): Unit = {
val seg = createSegment(40)
val logFile = seg.log.file
val indexFile = seg.lazyOffsetIndex.file
val timeIndexFile = seg.lazyTimeIndex.file
val indexFile = seg.offsetIndexFile
val timeIndexFile = seg.timeIndexFile
// Ensure that files for offset and time indices have not been created eagerly.
assertFalse(seg.lazyOffsetIndex.file.exists)
assertFalse(seg.lazyTimeIndex.file.exists)
assertFalse(seg.offsetIndexFile.exists)
assertFalse(seg.timeIndexFile.exists)
seg.changeFileSuffixes("", ".deleted")
// Ensure that attempt to change suffixes for non-existing offset and time indices does not create new files.
assertFalse(seg.lazyOffsetIndex.file.exists)
assertFalse(seg.lazyTimeIndex.file.exists)
assertFalse(seg.offsetIndexFile.exists)
assertFalse(seg.timeIndexFile.exists)
// Ensure that file names are updated accordingly.
assertEquals(logFile.getAbsolutePath + ".deleted", seg.log.file.getAbsolutePath)
assertEquals(indexFile.getAbsolutePath + ".deleted", seg.lazyOffsetIndex.file.getAbsolutePath)
assertEquals(timeIndexFile.getAbsolutePath + ".deleted", seg.lazyTimeIndex.file.getAbsolutePath)
assertEquals(indexFile.getAbsolutePath + ".deleted", seg.offsetIndexFile.getAbsolutePath)
assertEquals(timeIndexFile.getAbsolutePath + ".deleted", seg.timeIndexFile.getAbsolutePath)
assertTrue(seg.log.file.exists)
// Ensure lazy creation of offset index file upon accessing it.
seg.lazyOffsetIndex.get
assertTrue(seg.lazyOffsetIndex.file.exists)
seg.offsetIndex()
assertTrue(seg.offsetIndexFile.exists)
// Ensure lazy creation of time index file upon accessing it.
seg.lazyTimeIndex.get
assertTrue(seg.lazyTimeIndex.file.exists)
seg.timeIndex()
assertTrue(seg.timeIndexFile.exists)
}
/**
@ -326,11 +325,11 @@ class LogSegmentTest { @@ -326,11 +325,11 @@ class LogSegmentTest {
val seg = createSegment(0)
for(i <- 0 until 100)
seg.append(i, RecordBatch.NO_TIMESTAMP, -1L, records(i, i.toString))
val indexFile = seg.lazyOffsetIndex.file
val indexFile = seg.offsetIndexFile
TestUtils.writeNonsenseToFile(indexFile, 5, indexFile.length.toInt)
seg.recover(newProducerStateManager())
seg.recover(newProducerStateManager(), Optional.empty())
for(i <- 0 until 100) {
val records = seg.read(i, 1, minOneMessage = true).records.records
val records = seg.read(i, 1, seg.size(), true).records.records
assertEquals(i, records.iterator.next().offset)
}
}
@ -346,30 +345,28 @@ class LogSegmentTest { @@ -346,30 +345,28 @@ class LogSegmentTest {
val pid2 = 10L
// append transactional records from pid1
segment.append(largestOffset = 101L, largestTimestamp = RecordBatch.NO_TIMESTAMP,
shallowOffsetOfMaxTimestamp = 100L, records = MemoryRecords.withTransactionalRecords(100L, CompressionType.NONE,
segment.append(101L, RecordBatch.NO_TIMESTAMP,
100L, MemoryRecords.withTransactionalRecords(100L, CompressionType.NONE,
pid1, producerEpoch, sequence, partitionLeaderEpoch, new SimpleRecord("a".getBytes), new SimpleRecord("b".getBytes)))
// append transactional records from pid2
segment.append(largestOffset = 103L, largestTimestamp = RecordBatch.NO_TIMESTAMP,
shallowOffsetOfMaxTimestamp = 102L, records = MemoryRecords.withTransactionalRecords(102L, CompressionType.NONE,
segment.append(103L, RecordBatch.NO_TIMESTAMP, 102L, MemoryRecords.withTransactionalRecords(102L, CompressionType.NONE,
pid2, producerEpoch, sequence, partitionLeaderEpoch, new SimpleRecord("a".getBytes), new SimpleRecord("b".getBytes)))
// append non-transactional records
segment.append(largestOffset = 105L, largestTimestamp = RecordBatch.NO_TIMESTAMP,
shallowOffsetOfMaxTimestamp = 104L, records = MemoryRecords.withRecords(104L, CompressionType.NONE,
segment.append(105L, RecordBatch.NO_TIMESTAMP, 104L, MemoryRecords.withRecords(104L, CompressionType.NONE,
partitionLeaderEpoch, new SimpleRecord("a".getBytes), new SimpleRecord("b".getBytes)))
// abort the transaction from pid2 (note LSO should be 100L since the txn from pid1 has not completed)
segment.append(largestOffset = 106L, largestTimestamp = RecordBatch.NO_TIMESTAMP,
shallowOffsetOfMaxTimestamp = 106L, records = endTxnRecords(ControlRecordType.ABORT, pid2, producerEpoch, offset = 106L))
segment.append(106L, RecordBatch.NO_TIMESTAMP, 106L,
endTxnRecords(ControlRecordType.ABORT, pid2, producerEpoch, offset = 106L))
// commit the transaction from pid1
segment.append(largestOffset = 107L, largestTimestamp = RecordBatch.NO_TIMESTAMP,
shallowOffsetOfMaxTimestamp = 107L, records = endTxnRecords(ControlRecordType.COMMIT, pid1, producerEpoch, offset = 107L))
segment.append(107L, RecordBatch.NO_TIMESTAMP, 107L,
endTxnRecords(ControlRecordType.COMMIT, pid1, producerEpoch, offset = 107L))
var stateManager = newProducerStateManager()
segment.recover(stateManager)
segment.recover(stateManager, Optional.empty())
assertEquals(108L, stateManager.mapEndOffset)
@ -384,7 +381,7 @@ class LogSegmentTest { @@ -384,7 +381,7 @@ class LogSegmentTest {
// recover again, but this time assuming the transaction from pid2 began on a previous segment
stateManager = newProducerStateManager()
stateManager.loadProducerEntry(new ProducerStateEntry(pid2, producerEpoch, 0, RecordBatch.NO_TIMESTAMP, OptionalLong.of(75L), java.util.Optional.of(new BatchMetadata(10, 10L, 5, RecordBatch.NO_TIMESTAMP))))
segment.recover(stateManager)
segment.recover(stateManager, Optional.empty())
assertEquals(108L, stateManager.mapEndOffset)
abortedTxns = segment.txnIndex.allAbortedTxns
@ -415,23 +412,19 @@ class LogSegmentTest { @@ -415,23 +412,19 @@ class LogSegmentTest {
}
val cache = new LeaderEpochFileCache(topicPartition, checkpoint)
seg.append(largestOffset = 105L, largestTimestamp = RecordBatch.NO_TIMESTAMP,
shallowOffsetOfMaxTimestamp = 104L, records = MemoryRecords.withRecords(104L, CompressionType.NONE, 0,
seg.append(105L, RecordBatch.NO_TIMESTAMP, 104L, MemoryRecords.withRecords(104L, CompressionType.NONE, 0,
new SimpleRecord("a".getBytes), new SimpleRecord("b".getBytes)))
seg.append(largestOffset = 107L, largestTimestamp = RecordBatch.NO_TIMESTAMP,
shallowOffsetOfMaxTimestamp = 106L, records = MemoryRecords.withRecords(106L, CompressionType.NONE, 1,
seg.append(107L, RecordBatch.NO_TIMESTAMP, 106L, MemoryRecords.withRecords(106L, CompressionType.NONE, 1,
new SimpleRecord("a".getBytes), new SimpleRecord("b".getBytes)))
seg.append(largestOffset = 109L, largestTimestamp = RecordBatch.NO_TIMESTAMP,
shallowOffsetOfMaxTimestamp = 108L, records = MemoryRecords.withRecords(108L, CompressionType.NONE, 1,
seg.append(109L, RecordBatch.NO_TIMESTAMP, 108L, MemoryRecords.withRecords(108L, CompressionType.NONE, 1,
new SimpleRecord("a".getBytes), new SimpleRecord("b".getBytes)))
seg.append(largestOffset = 111L, largestTimestamp = RecordBatch.NO_TIMESTAMP,
shallowOffsetOfMaxTimestamp = 110, records = MemoryRecords.withRecords(110L, CompressionType.NONE, 2,
seg.append(111L, RecordBatch.NO_TIMESTAMP, 110, MemoryRecords.withRecords(110L, CompressionType.NONE, 2,
new SimpleRecord("a".getBytes), new SimpleRecord("b".getBytes)))
seg.recover(newProducerStateManager(), Some(cache))
seg.recover(newProducerStateManager(), Optional.of(cache))
assertEquals(java.util.Arrays.asList(new EpochEntry(0, 104L),
new EpochEntry(1, 106),
new EpochEntry(2, 110)),
@ -458,13 +451,13 @@ class LogSegmentTest { @@ -458,13 +451,13 @@ class LogSegmentTest {
val seg = createSegment(0)
for(i <- 0 until 100)
seg.append(i, i * 10, i, records(i, i.toString))
val timeIndexFile = seg.lazyTimeIndex.file
val timeIndexFile = seg.timeIndexFile
TestUtils.writeNonsenseToFile(timeIndexFile, 5, timeIndexFile.length.toInt)
seg.recover(newProducerStateManager())
seg.recover(newProducerStateManager(), Optional.empty())
for(i <- 0 until 100) {
assertEquals(i, seg.findOffsetByTimestamp(i * 10).get.offset)
assertEquals(i, seg.findOffsetByTimestamp(i * 10, 0L).get.offset)
if (i < 99)
assertEquals(i + 1, seg.findOffsetByTimestamp(i * 10 + 1).get.offset)
assertEquals(i + 1, seg.findOffsetByTimestamp(i * 10 + 1, 0L).get.offset)
}
}
@ -484,7 +477,7 @@ class LogSegmentTest { @@ -484,7 +477,7 @@ class LogSegmentTest {
val recordPosition = seg.log.searchForOffsetWithSize(offsetToBeginCorruption, 0)
val position = recordPosition.position + TestUtils.random.nextInt(15)
TestUtils.writeNonsenseToFile(seg.log.file, position, (seg.log.file.length - position).toInt)
seg.recover(newProducerStateManager())
seg.recover(newProducerStateManager(), Optional.empty())
assertEquals((0 until offsetToBeginCorruption).toList, seg.log.batches.asScala.map(_.lastOffset).toList,
"Should have truncated off bad messages.")
seg.deleteIfExists()
@ -498,8 +491,7 @@ class LogSegmentTest { @@ -498,8 +491,7 @@ class LogSegmentTest {
TopicConfig.SEGMENT_INDEX_BYTES_CONFIG -> 1000,
TopicConfig.SEGMENT_JITTER_MS_CONFIG -> 0
).asJava)
val seg = LogSegment.open(tempDir, baseOffset, logConfig, Time.SYSTEM, fileAlreadyExists = fileAlreadyExists,
initFileSize = initFileSize, preallocate = preallocate)
val seg = LogSegment.open(tempDir, baseOffset, logConfig, Time.SYSTEM, fileAlreadyExists, initFileSize, preallocate, "")
segments += seg
seg
}
@ -512,7 +504,7 @@ class LogSegmentTest { @@ -512,7 +504,7 @@ class LogSegmentTest {
seg.append(51, RecordBatch.NO_TIMESTAMP, -1L, ms)
val ms2 = records(60, "alpha", "beta")
seg.append(61, RecordBatch.NO_TIMESTAMP, -1L, ms2)
val read = seg.read(startOffset = 55, maxSize = 200)
val read = seg.read(55, 200)
checkEquals(ms2.records.iterator, read.records.records.iterator)
}
@ -526,14 +518,14 @@ class LogSegmentTest { @@ -526,14 +518,14 @@ class LogSegmentTest {
TopicConfig.SEGMENT_JITTER_MS_CONFIG -> 0
).asJava)
val seg = LogSegment.open(tempDir, baseOffset = 40, logConfig, Time.SYSTEM,
initFileSize = 512 * 1024 * 1024, preallocate = true)
val seg = LogSegment.open(tempDir, 40, logConfig, Time.SYSTEM,
512 * 1024 * 1024, true)
val ms = records(50, "hello", "there")
seg.append(51, RecordBatch.NO_TIMESTAMP, -1L, ms)
val ms2 = records(60, "alpha", "beta")
seg.append(61, RecordBatch.NO_TIMESTAMP, -1L, ms2)
val read = seg.read(startOffset = 55, maxSize = 200)
val read = seg.read(55, 200)
checkEquals(ms2.records.iterator, read.records.records.iterator)
val oldSize = seg.log.sizeInBytes()
val oldPosition = seg.log.channel.position
@ -543,11 +535,10 @@ class LogSegmentTest { @@ -543,11 +535,10 @@ class LogSegmentTest {
//After close, file should be trimmed
assertEquals(oldSize, seg.log.file.length)
val segReopen = LogSegment.open(tempDir, baseOffset = 40, logConfig, Time.SYSTEM, fileAlreadyExists = true,
initFileSize = 512 * 1024 * 1024, preallocate = true)
val segReopen = LogSegment.open(tempDir, 40, logConfig, Time.SYSTEM, true, 512 * 1024 * 1024, true, "")
segments += segReopen
val readAgain = segReopen.read(startOffset = 55, maxSize = 200)
val readAgain = segReopen.read(55, 200)
checkEquals(ms2.records.iterator, readAgain.records.records.iterator)
val size = segReopen.log.sizeInBytes()
val position = segReopen.log.channel.position
@ -589,7 +580,7 @@ class LogSegmentTest { @@ -589,7 +580,7 @@ class LogSegmentTest {
// create a log file in a separate directory to avoid conflicting with created segments
val tempDir = TestUtils.tempDir()
val fileRecords = FileRecords.open(UnifiedLog.logFile(tempDir, 0))
val fileRecords = FileRecords.open(LogFileUtils.logFile(tempDir, 0))
// Simulate a scenario where we have a single log with an offset range exceeding Int.MaxValue
fileRecords.append(records(0, 1024))

65
core/src/test/scala/unit/kafka/log/LogSegmentsTest.scala

@ -17,14 +17,18 @@ @@ -17,14 +17,18 @@
package kafka.log
import java.io.File
import kafka.utils.TestUtils
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.utils.{Time, Utils}
import org.apache.kafka.storage.internals.log.{LogSegment, LogSegments}
import org.junit.jupiter.api.Assertions._
import org.junit.jupiter.api.{AfterEach, BeforeEach, Test}
import org.mockito.Mockito.{mock, when}
import java.util.Arrays.asList
import java.util.Optional
import scala.jdk.CollectionConverters._
class LogSegmentsTest {
val topicPartition = new TopicPartition("topic", 0)
@ -47,7 +51,7 @@ class LogSegmentsTest { @@ -47,7 +51,7 @@ class LogSegmentsTest {
Utils.delete(logDir)
}
private def assertEntry(segment: LogSegment, tested: java.util.Map.Entry[Long, LogSegment]): Unit = {
private def assertEntry(segment: LogSegment, tested: java.util.Map.Entry[java.lang.Long, LogSegment]): Unit = {
assertEquals(segment.baseOffset, tested.getKey())
assertEquals(segment, tested.getValue())
}
@ -70,7 +74,7 @@ class LogSegmentsTest { @@ -70,7 +74,7 @@ class LogSegmentsTest {
assertTrue(segments.nonEmpty)
assertEquals(1, segments.numberOfSegments)
assertTrue(segments.contains(offset1))
assertEquals(Some(seg1), segments.get(offset1))
assertEquals(Optional.of(seg1), segments.get(offset1))
// Add seg2
segments.add(seg2)
@ -78,7 +82,7 @@ class LogSegmentsTest { @@ -78,7 +82,7 @@ class LogSegmentsTest {
assertTrue(segments.nonEmpty)
assertEquals(2, segments.numberOfSegments)
assertTrue(segments.contains(offset2))
assertEquals(Some(seg2), segments.get(offset2))
assertEquals(Optional.of(seg2), segments.get(offset2))
// Replace seg1 with seg3
segments.add(seg3)
@ -86,7 +90,7 @@ class LogSegmentsTest { @@ -86,7 +90,7 @@ class LogSegmentsTest {
assertTrue(segments.nonEmpty)
assertEquals(2, segments.numberOfSegments)
assertTrue(segments.contains(offset1))
assertEquals(Some(seg3), segments.get(offset1))
assertEquals(Optional.of(seg3), segments.get(offset1))
// Remove seg2
segments.remove(offset2)
@ -119,31 +123,30 @@ class LogSegmentsTest { @@ -119,31 +123,30 @@ class LogSegmentsTest {
val seg4 = createSegment(offset4)
// Test firstEntry, lastEntry
List(seg1, seg2, seg3, seg4).foreach {
seg =>
segments.add(seg)
assertEntry(seg1, segments.firstEntry.get)
assertEquals(Some(seg1), segments.firstSegment)
assertEntry(seg, segments.lastEntry.get)
assertEquals(Some(seg), segments.lastSegment)
List(seg1, seg2, seg3, seg4).foreach { seg =>
segments.add(seg)
assertEntry(seg1, segments.firstEntry.get)
assertEquals(Optional.of(seg1), segments.firstSegment)
assertEntry(seg, segments.lastEntry.get)
assertEquals(Optional.of(seg), segments.lastSegment)
}
// Test baseOffsets
assertEquals(Seq(offset1, offset2, offset3, offset4), segments.baseOffsets)
assertEquals(Seq(offset1, offset2, offset3, offset4), segments.baseOffsets.asScala.toSeq)
// Test values
assertEquals(Seq(seg1, seg2, seg3, seg4), segments.values.toSeq)
assertEquals(Seq(seg1, seg2, seg3, seg4), segments.values.asScala.toSeq)
// Test values(to, from)
assertThrows(classOf[IllegalArgumentException], () => segments.values(2, 1))
assertEquals(Seq(), segments.values(1, 1).toSeq)
assertEquals(Seq(seg1), segments.values(1, 2).toSeq)
assertEquals(Seq(seg1, seg2), segments.values(1, 3).toSeq)
assertEquals(Seq(seg1, seg2, seg3), segments.values(1, 4).toSeq)
assertEquals(Seq(seg2, seg3), segments.values(2, 4).toSeq)
assertEquals(Seq(seg3), segments.values(3, 4).toSeq)
assertEquals(Seq(), segments.values(4, 4).toSeq)
assertEquals(Seq(seg4), segments.values(4, 5).toSeq)
assertEquals(Seq(), segments.values(1, 1).asScala.toSeq)
assertEquals(Seq(seg1), segments.values(1, 2).asScala.toSeq)
assertEquals(Seq(seg1, seg2), segments.values(1, 3).asScala.toSeq)
assertEquals(Seq(seg1, seg2, seg3), segments.values(1, 4).asScala.toSeq)
assertEquals(Seq(seg2, seg3), segments.values(2, 4).asScala.toSeq)
assertEquals(Seq(seg3), segments.values(3, 4).asScala.toSeq)
assertEquals(Seq(), segments.values(4, 4).asScala.toSeq)
assertEquals(Seq(seg4), segments.values(4, 5).asScala.toSeq)
segments.close()
}
@ -160,17 +163,17 @@ class LogSegmentsTest { @@ -160,17 +163,17 @@ class LogSegmentsTest {
List(seg1, seg2, seg3, seg4).foreach(segments.add)
// Test floorSegment
assertEquals(Some(seg1), segments.floorSegment(2))
assertEquals(Some(seg2), segments.floorSegment(3))
assertEquals(Optional.of(seg1), segments.floorSegment(2))
assertEquals(Optional.of(seg2), segments.floorSegment(3))
// Test lowerSegment
assertEquals(Some(seg1), segments.lowerSegment(3))
assertEquals(Some(seg2), segments.lowerSegment(4))
assertEquals(Optional.of(seg1), segments.lowerSegment(3))
assertEquals(Optional.of(seg2), segments.lowerSegment(4))
// Test higherSegment, higherEntry
assertEquals(Some(seg3), segments.higherSegment(4))
assertEquals(Optional.of(seg3), segments.higherSegment(4))
assertEntry(seg3, segments.higherEntry(4).get)
assertEquals(Some(seg4), segments.higherSegment(5))
assertEquals(Optional.of(seg4), segments.higherSegment(5))
assertEntry(seg4, segments.higherEntry(5).get)
segments.close()
@ -232,8 +235,8 @@ class LogSegmentsTest { @@ -232,8 +235,8 @@ class LogSegmentsTest {
when(logSegment.size).thenReturn(Int.MaxValue)
assertEquals(Int.MaxValue, LogSegments.sizeInBytes(Seq(logSegment)))
assertEquals(largeSize, LogSegments.sizeInBytes(Seq(logSegment, logSegment)))
assertTrue(UnifiedLog.sizeInBytes(Seq(logSegment, logSegment)) > Int.MaxValue)
assertEquals(Int.MaxValue, LogSegments.sizeInBytes(asList(logSegment)))
assertEquals(largeSize, LogSegments.sizeInBytes(asList(logSegment, logSegment)))
assertTrue(UnifiedLog.sizeInBytes(asList(logSegment, logSegment)) > Int.MaxValue)
}
}

21
core/src/test/scala/unit/kafka/log/LogTestUtils.scala

@ -33,7 +33,7 @@ import java.util.concurrent.{ConcurrentHashMap, ConcurrentMap} @@ -33,7 +33,7 @@ import java.util.concurrent.{ConcurrentHashMap, ConcurrentMap}
import org.apache.kafka.common.config.TopicConfig
import org.apache.kafka.server.util.Scheduler
import org.apache.kafka.storage.internals.checkpoint.LeaderEpochCheckpointFile
import org.apache.kafka.storage.internals.log.{AbortedTxn, AppendOrigin, FetchDataInfo, FetchIsolation, LazyIndex, LogAppendInfo, LogConfig, LogDirFailureChannel, LogFileUtils, LogOffsetsListener, ProducerStateManager, ProducerStateManagerConfig, TransactionIndex}
import org.apache.kafka.storage.internals.log.{AbortedTxn, AppendOrigin, FetchDataInfo, FetchIsolation, LazyIndex, LogAppendInfo, LogConfig, LogDirFailureChannel, LogFileUtils, LogOffsetsListener, LogSegment, ProducerStateManager, ProducerStateManagerConfig, TransactionIndex}
import scala.jdk.CollectionConverters._
@ -45,9 +45,9 @@ object LogTestUtils { @@ -45,9 +45,9 @@ object LogTestUtils {
logDir: File,
indexIntervalBytes: Int = 10,
time: Time = Time.SYSTEM): LogSegment = {
val ms = FileRecords.open(UnifiedLog.logFile(logDir, offset))
val idx = LazyIndex.forOffset(UnifiedLog.offsetIndexFile(logDir, offset), offset, 1000)
val timeIdx = LazyIndex.forTime(UnifiedLog.timeIndexFile(logDir, offset), offset, 1500)
val ms = FileRecords.open(LogFileUtils.logFile(logDir, offset))
val idx = LazyIndex.forOffset(LogFileUtils.offsetIndexFile(logDir, offset), offset, 1000)
val timeIdx = LazyIndex.forTime(LogFileUtils.timeIndexFile(logDir, offset), offset, 1500)
val txnIndex = new TransactionIndex(offset, UnifiedLog.transactionIndexFile(logDir, offset))
new LogSegment(ms, idx, timeIdx, txnIndex, offset, indexIntervalBytes, 0, time)
@ -128,7 +128,7 @@ object LogTestUtils { @@ -128,7 +128,7 @@ object LogTestUtils {
def hasOverflow(baseOffset: Long, batch: RecordBatch): Boolean =
batch.lastOffset > baseOffset + Int.MaxValue || batch.baseOffset < baseOffset
for (segment <- log.logSegments) {
for (segment <- log.logSegments.asScala) {
val overflowBatch = segment.log.batches.asScala.find(batch => hasOverflow(segment.baseOffset, batch))
if (overflowBatch.isDefined)
return Some(segment)
@ -137,7 +137,7 @@ object LogTestUtils { @@ -137,7 +137,7 @@ object LogTestUtils {
}
def rawSegment(logDir: File, baseOffset: Long): FileRecords =
FileRecords.open(UnifiedLog.logFile(logDir, baseOffset))
FileRecords.open(LogFileUtils.logFile(logDir, baseOffset))
/**
* Initialize the given log directory with a set of segments, one of which will have an
@ -158,8 +158,8 @@ object LogTestUtils { @@ -158,8 +158,8 @@ object LogTestUtils {
segment.append(MemoryRecords.withRecords(baseOffset + Int.MaxValue - 1, CompressionType.NONE, 0,
record(baseOffset + Int.MaxValue - 1)))
// Need to create the offset files explicitly to avoid triggering segment recovery to truncate segment.
Files.createFile(UnifiedLog.offsetIndexFile(logDir, baseOffset).toPath)
Files.createFile(UnifiedLog.timeIndexFile(logDir, baseOffset).toPath)
Files.createFile(LogFileUtils.offsetIndexFile(logDir, baseOffset).toPath)
Files.createFile(LogFileUtils.timeIndexFile(logDir, baseOffset).toPath)
baseOffset + Int.MaxValue
}
@ -186,7 +186,7 @@ object LogTestUtils { @@ -186,7 +186,7 @@ object LogTestUtils {
/* extract all the keys from a log */
def keysInLog(log: UnifiedLog): Iterable[Long] = {
for (logSegment <- log.logSegments;
for (logSegment <- log.logSegments.asScala;
batch <- logSegment.log.batches.asScala if !batch.isControlBatch;
record <- batch.asScala if record.hasValue && record.hasKey)
yield TestUtils.readString(record.key).toLong
@ -238,7 +238,8 @@ object LogTestUtils { @@ -238,7 +238,8 @@ object LogTestUtils {
log.read(startOffset, maxLength, isolation, minOneMessage)
}
def allAbortedTransactions(log: UnifiedLog): Iterable[AbortedTxn] = log.logSegments.flatMap(_.txnIndex.allAbortedTxns.asScala)
def allAbortedTransactions(log: UnifiedLog): Iterable[AbortedTxn] =
log.logSegments.asScala.flatMap(_.txnIndex.allAbortedTxns.asScala)
def deleteProducerSnapshotFiles(logDir: File): Unit = {
val files = logDir.listFiles.filter(f => f.isFile && f.getName.endsWith(LogFileUtils.PRODUCER_SNAPSHOT_FILE_SUFFIX))

64
core/src/test/scala/unit/kafka/log/UnifiedLogTest.scala

@ -36,7 +36,7 @@ import org.apache.kafka.server.metrics.KafkaYammerMetrics @@ -36,7 +36,7 @@ import org.apache.kafka.server.metrics.KafkaYammerMetrics
import org.apache.kafka.server.util.{KafkaScheduler, MockTime, Scheduler}
import org.apache.kafka.storage.internals.checkpoint.LeaderEpochCheckpointFile
import org.apache.kafka.storage.internals.epoch.LeaderEpochFileCache
import org.apache.kafka.storage.internals.log.{AbortedTxn, AppendOrigin, EpochEntry, FetchIsolation, LogConfig, LogFileUtils, LogOffsetMetadata, LogOffsetSnapshot, LogOffsetsListener, LogStartOffsetIncrementReason, ProducerStateManager, ProducerStateManagerConfig, RecordValidationException}
import org.apache.kafka.storage.internals.log.{AbortedTxn, AppendOrigin, EpochEntry, FetchIsolation, LogConfig, LogFileUtils, LogOffsetMetadata, LogOffsetSnapshot, LogOffsetsListener, LogSegment, LogStartOffsetIncrementReason, ProducerStateManager, ProducerStateManagerConfig, RecordValidationException}
import org.junit.jupiter.api.Assertions._
import org.junit.jupiter.api.{AfterEach, BeforeEach, Test}
import org.junit.jupiter.params.ParameterizedTest
@ -80,8 +80,8 @@ class UnifiedLogTest { @@ -80,8 +80,8 @@ class UnifiedLogTest {
def createEmptyLogs(dir: File, offsets: Int*): Unit = {
for(offset <- offsets) {
Files.createFile(UnifiedLog.logFile(dir, offset).toPath)
Files.createFile(UnifiedLog.offsetIndexFile(dir, offset).toPath)
Files.createFile(LogFileUtils.logFile(dir, offset).toPath)
Files.createFile(LogFileUtils.offsetIndexFile(dir, offset).toPath)
}
}
@ -719,7 +719,7 @@ class UnifiedLogTest { @@ -719,7 +719,7 @@ class UnifiedLogTest {
// Reload after clean shutdown
log = createLog(logDir, logConfig, recoveryPoint = logEndOffset)
var expectedSnapshotOffsets = log.logSegments.map(_.baseOffset).takeRight(2).toVector :+ log.logEndOffset
var expectedSnapshotOffsets = log.logSegments.asScala.map(_.baseOffset).takeRight(2).toVector :+ log.logEndOffset
assertEquals(expectedSnapshotOffsets, LogTestUtils.listProducerSnapshotOffsets(logDir))
log.close()
@ -735,7 +735,7 @@ class UnifiedLogTest { @@ -735,7 +735,7 @@ class UnifiedLogTest {
// Reload after unclean shutdown with recoveryPoint set to 0
log = createLog(logDir, logConfig, recoveryPoint = 0L, lastShutdownClean = false)
// We progressively create a snapshot for each segment after the recovery point
expectedSnapshotOffsets = log.logSegments.map(_.baseOffset).tail.toVector :+ log.logEndOffset
expectedSnapshotOffsets = log.logSegments.asScala.map(_.baseOffset).tail.toVector :+ log.logEndOffset
assertEquals(expectedSnapshotOffsets, LogTestUtils.listProducerSnapshotOffsets(logDir))
log.close()
}
@ -1051,7 +1051,7 @@ class UnifiedLogTest { @@ -1051,7 +1051,7 @@ class UnifiedLogTest {
log.appendAsLeader(TestUtils.records(List(new SimpleRecord("a".getBytes, "c".getBytes())), producerId = pid1,
producerEpoch = epoch, sequence = 2), leaderEpoch = 0)
log.updateHighWatermark(log.logEndOffset)
assertEquals(log.logSegments.map(_.baseOffset).toSeq.sorted.drop(1), ProducerStateManager.listSnapshotFiles(logDir).asScala.map(_.offset).sorted,
assertEquals(log.logSegments.asScala.map(_.baseOffset).toSeq.sorted.drop(1), ProducerStateManager.listSnapshotFiles(logDir).asScala.map(_.offset).sorted,
"expected a snapshot file per segment base offset, except the first segment")
assertEquals(2, ProducerStateManager.listSnapshotFiles(logDir).size)
@ -1061,7 +1061,7 @@ class UnifiedLogTest { @@ -1061,7 +1061,7 @@ class UnifiedLogTest {
log.deleteOldSegments()
// Sleep to breach the file delete delay and run scheduled file deletion tasks
mockTime.sleep(1)
assertEquals(log.logSegments.map(_.baseOffset).toSeq.sorted.drop(1), ProducerStateManager.listSnapshotFiles(logDir).asScala.map(_.offset).sorted,
assertEquals(log.logSegments.asScala.map(_.baseOffset).toSeq.sorted.drop(1), ProducerStateManager.listSnapshotFiles(logDir).asScala.map(_.offset).sorted,
"expected a snapshot file per segment base offset, excluding the first")
}
@ -1596,7 +1596,7 @@ class UnifiedLogTest { @@ -1596,7 +1596,7 @@ class UnifiedLogTest {
log.appendAsLeader(TestUtils.singletonRecords(value = "42".getBytes), leaderEpoch = 0)
// now manually truncate off all but one message from the first segment to create a gap in the messages
log.logSegments.head.truncateTo(1)
log.logSegments.asScala.head.truncateTo(1)
assertEquals(log.logEndOffset - 1, LogTestUtils.readLog(log, 1, 200).records.batches.iterator.next().lastOffset,
"A read should now return the last message in the log")
@ -1996,7 +1996,7 @@ class UnifiedLogTest { @@ -1996,7 +1996,7 @@ class UnifiedLogTest {
MemoryRecords.withRecords(100 + i, CompressionType.NONE, 0, new SimpleRecord(mockTime.milliseconds + i, i.toString.getBytes()))
}
messages.foreach(log.appendAsFollower)
val timeIndexEntries = log.logSegments.foldLeft(0) { (entries, segment) => entries + segment.timeIndex.entries }
val timeIndexEntries = log.logSegments.asScala.foldLeft(0) { (entries, segment) => entries + segment.timeIndex.entries }
assertEquals(numMessages - 1, timeIndexEntries, s"There should be ${numMessages - 1} time index entries")
assertEquals(mockTime.milliseconds + numMessages - 1, log.activeSegment.timeIndex.lastEntry.timestamp,
s"The last time index entry should have timestamp ${mockTime.milliseconds + numMessages - 1}")
@ -2203,16 +2203,16 @@ class UnifiedLogTest { @@ -2203,16 +2203,16 @@ class UnifiedLogTest {
assertEquals(2, log.numberOfSegments, "There should be exactly 2 segment.")
val expectedEntries = msgPerSeg - 1
assertEquals(expectedEntries, log.logSegments.toList.head.offsetIndex.maxEntries,
assertEquals(expectedEntries, log.logSegments.asScala.toList.head.offsetIndex.maxEntries,
s"The index of the first segment should have $expectedEntries entries")
assertEquals(expectedEntries, log.logSegments.toList.head.timeIndex.maxEntries,
assertEquals(expectedEntries, log.logSegments.asScala.toList.head.timeIndex.maxEntries,
s"The time index of the first segment should have $expectedEntries entries")
log.truncateTo(0)
assertEquals(1, log.numberOfSegments, "There should be exactly 1 segment.")
assertEquals(log.config.maxIndexSize/8, log.logSegments.toList.head.offsetIndex.maxEntries,
assertEquals(log.config.maxIndexSize/8, log.logSegments.asScala.toList.head.offsetIndex.maxEntries,
"The index of segment 1 should be resized to maxIndexSize")
assertEquals(log.config.maxIndexSize/12, log.logSegments.toList.head.timeIndex.maxEntries,
assertEquals(log.config.maxIndexSize/12, log.logSegments.asScala.toList.head.timeIndex.maxEntries,
"The time index of segment 1 should be resized to maxIndexSize")
mockTime.sleep(msgPerSeg)
@ -2238,22 +2238,22 @@ class UnifiedLogTest { @@ -2238,22 +2238,22 @@ class UnifiedLogTest {
log.appendAsLeader(createRecords, leaderEpoch = 0)
// files should be renamed
val segments = log.logSegments.toArray
val oldFiles = segments.map(_.log.file) ++ segments.map(_.lazyOffsetIndex.file)
val segments = log.logSegments.asScala.toArray
val oldFiles = segments.map(_.log.file) ++ segments.map(_.offsetIndexFile)
log.updateHighWatermark(log.logEndOffset)
log.deleteOldSegments()
assertEquals(1, log.numberOfSegments, "Only one segment should remain.")
assertTrue(segments.forall(_.log.file.getName.endsWith(LogFileUtils.DELETED_FILE_SUFFIX)) &&
segments.forall(_.lazyOffsetIndex.file.getName.endsWith(LogFileUtils.DELETED_FILE_SUFFIX)),
segments.forall(_.offsetIndexFile.getName.endsWith(LogFileUtils.DELETED_FILE_SUFFIX)),
"All log and index files should end in .deleted")
assertTrue(segments.forall(_.log.file.exists) && segments.forall(_.lazyOffsetIndex.file.exists),
assertTrue(segments.forall(_.log.file.exists) && segments.forall(_.offsetIndexFile.exists),
"The .deleted files should still be there.")
assertTrue(oldFiles.forall(!_.exists), "The original file should be gone.")
// when enough time passes the files should be deleted
val deletedFiles = segments.map(_.log.file) ++ segments.map(_.lazyOffsetIndex.file)
val deletedFiles = segments.map(_.log.file) ++ segments.map(_.offsetIndexFile)
mockTime.sleep(asyncDeleteMs + 1)
assertTrue(deletedFiles.forall(!_.exists), "Files should all be gone.")
}
@ -2520,8 +2520,8 @@ class UnifiedLogTest { @@ -2520,8 +2520,8 @@ class UnifiedLogTest {
private def testDegenerateSplitSegmentWithOverflow(segmentBaseOffset: Long, records: List[MemoryRecords]): Unit = {
val segment = LogTestUtils.rawSegment(logDir, segmentBaseOffset)
// Need to create the offset files explicitly to avoid triggering segment recovery to truncate segment.
Files.createFile(UnifiedLog.offsetIndexFile(logDir, segmentBaseOffset).toPath)
Files.createFile(UnifiedLog.timeIndexFile(logDir, segmentBaseOffset).toPath)
Files.createFile(LogFileUtils.offsetIndexFile(logDir, segmentBaseOffset).toPath)
Files.createFile(LogFileUtils.timeIndexFile(logDir, segmentBaseOffset).toPath)
records.foreach(segment.append _)
segment.close()
@ -2568,8 +2568,8 @@ class UnifiedLogTest { @@ -2568,8 +2568,8 @@ class UnifiedLogTest {
log.updateHighWatermark(hw)
log.deleteOldSegments()
assertTrue(log.logStartOffset <= hw)
log.logSegments.foreach { segment =>
val segmentFetchInfo = segment.read(startOffset = segment.baseOffset, maxSize = Int.MaxValue)
log.logSegments.forEach { segment =>
val segmentFetchInfo = segment.read(segment.baseOffset, Int.MaxValue)
val segmentLastOffsetOpt = segmentFetchInfo.records.records.asScala.lastOption.map(_.offset)
segmentLastOffsetOpt.foreach { lastOffset =>
assertTrue(lastOffset >= hw)
@ -2715,7 +2715,7 @@ class UnifiedLogTest { @@ -2715,7 +2715,7 @@ class UnifiedLogTest {
log.appendAsLeader(createRecords, leaderEpoch = 0)
// mark oldest segment as older the retention.ms
log.logSegments.head.lastModified = mockTime.milliseconds - 20000
log.logSegments.asScala.head.setLastModified(mockTime.milliseconds - 20000)
val segments = log.numberOfSegments
log.updateHighWatermark(log.logEndOffset)
@ -2750,7 +2750,7 @@ class UnifiedLogTest { @@ -2750,7 +2750,7 @@ class UnifiedLogTest {
log.appendAsLeader(createRecords, leaderEpoch = 0)
// Three segments should be created
assertEquals(3, log.logSegments.count(_ => true))
assertEquals(3, log.logSegments.asScala.count(_ => true))
log.updateHighWatermark(log.logEndOffset)
log.maybeIncrementLogStartOffset(recordsPerSegment, LogStartOffsetIncrementReason.ClientRecordDeletion)
@ -2760,8 +2760,8 @@ class UnifiedLogTest { @@ -2760,8 +2760,8 @@ class UnifiedLogTest {
log.updateHighWatermark(log.logEndOffset)
log.deleteOldSegments()
assertEquals(2, log.numberOfSegments, "There should be 2 segments remaining")
assertTrue(log.logSegments.head.baseOffset <= log.logStartOffset)
assertTrue(log.logSegments.tail.forall(s => s.baseOffset > log.logStartOffset))
assertTrue(log.logSegments.asScala.head.baseOffset <= log.logStartOffset)
assertTrue(log.logSegments.asScala.tail.forall(s => s.baseOffset > log.logStartOffset))
}
@Test
@ -3149,9 +3149,9 @@ class UnifiedLogTest { @@ -3149,9 +3149,9 @@ class UnifiedLogTest {
assertTrue(offsetMetadata.relativePositionInSegment <= segment.size)
val readInfo = segment.read(offsetMetadata.messageOffset,
maxSize = 2048,
maxPosition = segment.size,
minOneMessage = false)
2048,
segment.size,
false)
if (offsetMetadata.relativePositionInSegment < segment.size)
assertEquals(offsetMetadata, readInfo.fetchOffsetMetadata)
@ -3483,11 +3483,11 @@ class UnifiedLogTest { @@ -3483,11 +3483,11 @@ class UnifiedLogTest {
assertTrue(log.logStartOffset <= hw)
// verify that all segments up to the high watermark have been deleted
log.logSegments.headOption.foreach { segment =>
log.logSegments.asScala.headOption.foreach { segment =>
assertTrue(segment.baseOffset <= hw)
assertTrue(segment.baseOffset >= log.logStartOffset)
}
log.logSegments.tail.foreach { segment =>
log.logSegments.asScala.tail.foreach { segment =>
assertTrue(segment.baseOffset > hw)
assertTrue(segment.baseOffset >= log.logStartOffset)
}
@ -3989,7 +3989,7 @@ class UnifiedLogTest { @@ -3989,7 +3989,7 @@ class UnifiedLogTest {
object UnifiedLogTest {
def allRecords(log: UnifiedLog): List[Record] = {
val recordsFound = ListBuffer[Record]()
for (logSegment <- log.logSegments) {
for (logSegment <- log.logSegments.asScala) {
for (batch <- logSegment.log.batches.asScala) {
recordsFound ++= batch.iterator().asScala
}

2
core/src/test/scala/unit/kafka/server/DynamicConfigChangeTest.scala

@ -143,7 +143,7 @@ class DynamicConfigChangeTest extends KafkaServerTestHarness { @@ -143,7 +143,7 @@ class DynamicConfigChangeTest extends KafkaServerTestHarness {
(1 to 50).foreach(i => TestUtils.produceMessage(brokers, tp.topic, i.toString))
// Verify that the new config is used for all segments
assertTrue(log.logSegments.forall(_.size > 1000), "Log segment size change not applied")
assertTrue(log.logSegments.stream.allMatch(_.size > 1000), "Log segment size change not applied")
}
@nowarn("cat=deprecation")

12
core/src/test/scala/unit/kafka/server/LogOffsetTest.scala

@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
package kafka.server
import kafka.log.{LogSegment, UnifiedLog}
import kafka.log.UnifiedLog
import kafka.utils.TestUtils
import org.apache.kafka.common.message.ListOffsetsRequestData.{ListOffsetsPartition, ListOffsetsTopic}
import org.apache.kafka.common.message.ListOffsetsResponseData.{ListOffsetsPartitionResponse, ListOffsetsTopicResponse}
@ -25,7 +25,7 @@ import org.apache.kafka.common.protocol.{ApiKeys, Errors} @@ -25,7 +25,7 @@ import org.apache.kafka.common.protocol.{ApiKeys, Errors}
import org.apache.kafka.common.requests.{FetchRequest, FetchResponse, ListOffsetsRequest, ListOffsetsResponse}
import org.apache.kafka.common.utils.Time
import org.apache.kafka.common.{IsolationLevel, TopicPartition}
import org.apache.kafka.storage.internals.log.LogStartOffsetIncrementReason
import org.apache.kafka.storage.internals.log.{LogSegment, LogStartOffsetIncrementReason}
import org.junit.jupiter.api.Assertions._
import org.junit.jupiter.api.Timeout
import org.junit.jupiter.params.ParameterizedTest
@ -35,6 +35,8 @@ import org.mockito.invocation.InvocationOnMock @@ -35,6 +35,8 @@ import org.mockito.invocation.InvocationOnMock
import org.mockito.stubbing.Answer
import java.io.File
import java.util
import java.util.Arrays.asList
import java.util.concurrent.atomic.AtomicInteger
import java.util.{Optional, Properties, Random}
import scala.collection.mutable.Buffer
@ -276,7 +278,7 @@ class LogOffsetTest extends BaseRequestTest { @@ -276,7 +278,7 @@ class LogOffsetTest extends BaseRequestTest {
private[this] val value = new AtomicInteger(0)
override def answer(invocation: InvocationOnMock): Int = value.getAndIncrement()
})
val logSegments = Seq(logSegment)
val logSegments = Seq(logSegment).asJava
when(log.logSegments).thenReturn(logSegments)
log.legacyFetchOffsetsBefore(System.currentTimeMillis, 100)
}
@ -289,9 +291,9 @@ class LogOffsetTest extends BaseRequestTest { @@ -289,9 +291,9 @@ class LogOffsetTest extends BaseRequestTest {
val log: UnifiedLog = mock(classOf[UnifiedLog])
val logSegment: LogSegment = mock(classOf[LogSegment])
when(log.logSegments).thenReturn(
new Iterable[LogSegment] {
new util.AbstractCollection[LogSegment] {
override def size = 2
override def iterator = Seq(logSegment).iterator
override def iterator = asList(logSegment).iterator
}
)
log.legacyFetchOffsetsBefore(System.currentTimeMillis, 100)

4
core/src/test/scala/unit/kafka/server/ReplicaManagerTest.scala

@ -60,7 +60,7 @@ import org.apache.kafka.server.common.OffsetAndEpoch @@ -60,7 +60,7 @@ import org.apache.kafka.server.common.OffsetAndEpoch
import org.apache.kafka.server.common.MetadataVersion.IBP_2_6_IV0
import org.apache.kafka.server.metrics.{KafkaMetricsGroup, KafkaYammerMetrics}
import org.apache.kafka.server.util.{MockScheduler, MockTime}
import org.apache.kafka.storage.internals.log.{AppendOrigin, FetchDataInfo, FetchIsolation, FetchParams, FetchPartitionData, LogConfig, LogDirFailureChannel, LogOffsetMetadata, LogStartOffsetIncrementReason, ProducerStateManager, ProducerStateManagerConfig, RemoteStorageFetchInfo}
import org.apache.kafka.storage.internals.log.{AppendOrigin, FetchDataInfo, FetchIsolation, FetchParams, FetchPartitionData, LogConfig, LogDirFailureChannel, LogOffsetMetadata, LogSegments, LogStartOffsetIncrementReason, ProducerStateManager, ProducerStateManagerConfig, RemoteStorageFetchInfo}
import org.junit.jupiter.api.Assertions._
import org.junit.jupiter.api.{AfterEach, BeforeEach, Test}
import org.junit.jupiter.params.ParameterizedTest
@ -2617,7 +2617,7 @@ class ReplicaManagerTest { @@ -2617,7 +2617,7 @@ class ReplicaManagerTest {
segments,
0L,
0L,
leaderEpochCache,
leaderEpochCache.asJava,
producerStateManager
).load()
val localLog = new LocalLog(logDir, logConfig, segments, offsets.recoveryPoint,

5
core/src/test/scala/unit/kafka/server/epoch/LeaderEpochIntegrationTest.scala

@ -246,12 +246,11 @@ class LeaderEpochIntegrationTest extends QuorumTestHarness with Logging { @@ -246,12 +246,11 @@ class LeaderEpochIntegrationTest extends QuorumTestHarness with Logging {
val tp = new TopicPartition(topic, 0)
val leo = broker.getLogManager.getLog(tp).get.logEndOffset
result = result && leo > 0 && brokers.forall { broker =>
broker.getLogManager.getLog(tp).get.logSegments.iterator.forall { segment =>
broker.getLogManager.getLog(tp).get.logSegments.stream.allMatch { segment =>
if (segment.read(minOffset, Integer.MAX_VALUE) == null) {
false
} else {
segment.read(minOffset, Integer.MAX_VALUE)
.records.batches().iterator().asScala.forall(
segment.read(minOffset, Integer.MAX_VALUE).records.batches().iterator().asScala.forall(
expectedLeaderEpoch == _.partitionLeaderEpoch()
)
}

51
core/src/test/scala/unit/kafka/utils/CoreUtilsTest.scala

@ -31,7 +31,6 @@ import org.apache.kafka.common.utils.Utils @@ -31,7 +31,6 @@ import org.apache.kafka.common.utils.Utils
import org.slf4j.event.Level
import scala.jdk.CollectionConverters._
import scala.collection.mutable
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, ExecutionContext, ExecutionContextExecutorService, Future}
@ -44,56 +43,6 @@ class CoreUtilsTest extends Logging { @@ -44,56 +43,6 @@ class CoreUtilsTest extends Logging {
CoreUtils.swallow(throw new KafkaException("test"), this, Level.INFO)
}
@Test
def testTryAll(): Unit = {
case class TestException(key: String) extends Exception
val recorded = mutable.Map.empty[String, Either[TestException, String]]
def recordingFunction(v: Either[TestException, String]): Unit = {
val key = v match {
case Right(key) => key
case Left(e) => e.key
}
recorded(key) = v
}
CoreUtils.tryAll(Seq(
() => recordingFunction(Right("valid-0")),
() => recordingFunction(Left(TestException("exception-1"))),
() => recordingFunction(Right("valid-2")),
() => recordingFunction(Left(TestException("exception-3")))
))
var expected = Map(
"valid-0" -> Right("valid-0"),
"exception-1" -> Left(TestException("exception-1")),
"valid-2" -> Right("valid-2"),
"exception-3" -> Left(TestException("exception-3"))
)
assertEquals(expected, recorded)
recorded.clear()
CoreUtils.tryAll(Seq(
() => recordingFunction(Right("valid-0")),
() => recordingFunction(Right("valid-1"))
))
expected = Map(
"valid-0" -> Right("valid-0"),
"valid-1" -> Right("valid-1")
)
assertEquals(expected, recorded)
recorded.clear()
CoreUtils.tryAll(Seq(
() => recordingFunction(Left(TestException("exception-0"))),
() => recordingFunction(Left(TestException("exception-1")))
))
expected = Map(
"exception-0" -> Left(TestException("exception-0")),
"exception-1" -> Left(TestException("exception-1"))
)
assertEquals(expected, recorded)
}
@Test
def testReadBytes(): Unit = {
for(testCase <- List("", "a", "abcd")) {

8
core/src/test/scala/unit/kafka/utils/SchedulerTest.scala

@ -19,14 +19,16 @@ package kafka.utils @@ -19,14 +19,16 @@ package kafka.utils
import java.util.Properties
import java.util.concurrent.atomic._
import java.util.concurrent.{CountDownLatch, Executors, TimeUnit}
import kafka.log.{LocalLog, LogLoader, LogSegments, UnifiedLog}
import kafka.log.{LocalLog, LogLoader, UnifiedLog}
import kafka.server.BrokerTopicStats
import kafka.utils.TestUtils.retry
import org.apache.kafka.server.util.{KafkaScheduler, MockTime}
import org.apache.kafka.storage.internals.log.{LogConfig, LogDirFailureChannel, ProducerStateManager, ProducerStateManagerConfig}
import org.apache.kafka.storage.internals.log.{LogConfig, LogDirFailureChannel, LogSegments, ProducerStateManager, ProducerStateManagerConfig}
import org.junit.jupiter.api.Assertions._
import org.junit.jupiter.api.{AfterEach, BeforeEach, Test, Timeout}
import scala.compat.java8.OptionConverters._
class SchedulerTest {
val scheduler = new KafkaScheduler(1)
@ -150,7 +152,7 @@ class SchedulerTest { @@ -150,7 +152,7 @@ class SchedulerTest {
segments,
0L,
0L,
leaderEpochCache,
leaderEpochCache.asJava,
producerStateManager
).load()
val localLog = new LocalLog(logDir, logConfig, segments, offsets.recoveryPoint,

5
gradle/spotbugs-exclude.xml

@ -161,7 +161,10 @@ For a detailed description of spotbugs bug categories, see https://spotbugs.read @@ -161,7 +161,10 @@ For a detailed description of spotbugs bug categories, see https://spotbugs.read
<!-- Scala doesn't have checked exceptions so one cannot catch RuntimeException and rely
on the compiler to fail if the code is changed to call a method that throws Exception.
Given that, this bug pattern doesn't make sense for Scala code. -->
<Class name="kafka.log.Log"/>
<Or>
<Class name="kafka.log.Log"/>
<Class name="kafka.log.LocalLog$"/>
</Or>
<Bug pattern="REC_CATCH_EXCEPTION"/>
</Match>

3
storage/src/main/java/org/apache/kafka/storage/internals/log/LazyIndex.java

@ -41,7 +41,7 @@ import org.apache.kafka.common.utils.Utils; @@ -41,7 +41,7 @@ import org.apache.kafka.common.utils.Utils;
* Methods of this class are thread safe. Make sure to check `AbstractIndex` subclasses
* documentation to establish their thread safety.
*/
public class LazyIndex<T extends AbstractIndex> {
public class LazyIndex<T extends AbstractIndex> implements Closeable {
private enum IndexType {
OFFSET, TIME
@ -214,6 +214,7 @@ public class LazyIndex<T extends AbstractIndex> { @@ -214,6 +214,7 @@ public class LazyIndex<T extends AbstractIndex> {
}
}
@Override
public void close() throws IOException {
lock.lock();
try {

889
storage/src/main/java/org/apache/kafka/storage/internals/log/LogSegment.java

@ -0,0 +1,889 @@ @@ -0,0 +1,889 @@
/*
* 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.storage.internals.log;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.attribute.FileTime;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import com.yammer.metrics.core.MetricName;
import com.yammer.metrics.core.Timer;
import org.apache.kafka.common.InvalidRecordException;
import org.apache.kafka.common.errors.CorruptRecordException;
import org.apache.kafka.common.record.FileLogInputStream.FileChannelRecordBatch;
import org.apache.kafka.common.record.FileRecords;
import org.apache.kafka.common.record.FileRecords.LogOffsetPosition;
import org.apache.kafka.common.record.MemoryRecords;
import org.apache.kafka.common.record.RecordBatch;
import org.apache.kafka.common.utils.BufferSupplier;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.common.utils.Utils;
import org.apache.kafka.server.metrics.KafkaMetricsGroup;
import org.apache.kafka.storage.internals.epoch.LeaderEpochFileCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import static java.util.Arrays.asList;
/**
* A segment of the log. Each segment has two components: a log and an index. The log is a FileRecords containing
* the actual messages. The index is an OffsetIndex that maps from logical offsets to physical file positions. Each
* segment has a base offset which is an offset <= the least offset of any message in this segment and > any offset in
* any previous segment.
*
* A segment with a base offset of [base_offset] would be stored in two files, a [base_offset].index and a [base_offset].log file.
*
* This class is not thread-safe.
*/
public class LogSegment implements Closeable {
private static final Logger LOGGER = LoggerFactory.getLogger(LogSegment.class);
private static final Timer LOG_FLUSH_TIMER;
static {
KafkaMetricsGroup logFlushStatsMetricsGroup = new KafkaMetricsGroup(LogSegment.class) {
@Override
public MetricName metricName(String name, Map<String, String> tags) {
// Override the group and type names for compatibility - this metrics group was previously defined within
// a Scala object named `kafka.log.LogFlushStats`
return KafkaMetricsGroup.explicitMetricName("kafka.log", "LogFlushStats", name, tags);
}
};
LOG_FLUSH_TIMER = logFlushStatsMetricsGroup.newTimer("LogFlushRateAndTimeMs", TimeUnit.MILLISECONDS, TimeUnit.SECONDS);
}
private final FileRecords log;
private final LazyIndex<OffsetIndex> lazyOffsetIndex;
private final LazyIndex<TimeIndex> lazyTimeIndex;
private final TransactionIndex txnIndex;
private final long baseOffset;
private final int indexIntervalBytes;
private final long rollJitterMs;
private final Time time;
// The timestamp we used for time based log rolling and for ensuring max compaction delay
// volatile for LogCleaner to see the update
private volatile OptionalLong rollingBasedTimestamp = OptionalLong.empty();
/* The maximum timestamp and offset we see so far */
private volatile TimestampOffset maxTimestampAndOffsetSoFar = TimestampOffset.UNKNOWN;
private long created;
/* the number of bytes since we last added an entry in the offset index */
private int bytesSinceLastIndexEntry = 0;
/**
* Create a LogSegment with the provided parameters.
*
* @param log The file records containing log entries
* @param lazyOffsetIndex The offset index
* @param lazyTimeIndex The timestamp index
* @param txnIndex The transaction index
* @param baseOffset A lower bound on the offsets in this segment
* @param indexIntervalBytes The approximate number of bytes between entries in the index
* @param rollJitterMs The maximum random jitter subtracted from the scheduled segment roll time
* @param time The time instance
*/
public LogSegment(FileRecords log,
LazyIndex<OffsetIndex> lazyOffsetIndex,
LazyIndex<TimeIndex> lazyTimeIndex,
TransactionIndex txnIndex,
long baseOffset,
int indexIntervalBytes,
long rollJitterMs,
Time time) {
this.log = log;
this.lazyOffsetIndex = lazyOffsetIndex;
this.lazyTimeIndex = lazyTimeIndex;
this.txnIndex = txnIndex;
this.baseOffset = baseOffset;
this.indexIntervalBytes = indexIntervalBytes;
this.rollJitterMs = rollJitterMs;
this.time = time;
this.created = time.milliseconds();
}
// Visible for testing
public LogSegment(LogSegment segment) {
this(segment.log, segment.lazyOffsetIndex, segment.lazyTimeIndex, segment.txnIndex, segment.baseOffset,
segment.indexIntervalBytes, segment.rollJitterMs, segment.time);
}
public OffsetIndex offsetIndex() throws IOException {
return lazyOffsetIndex.get();
}
public File offsetIndexFile() {
return lazyOffsetIndex.file();
}
public TimeIndex timeIndex() throws IOException {
return lazyTimeIndex.get();
}
public File timeIndexFile() {
return lazyTimeIndex.file();
}
public long baseOffset() {
return baseOffset;
}
public FileRecords log() {
return log;
}
public long rollJitterMs() {
return rollJitterMs;
}
public TransactionIndex txnIndex() {
return txnIndex;
}
public boolean shouldRoll(RollParams rollParams) throws IOException {
boolean reachedRollMs = timeWaitedForRoll(rollParams.now, rollParams.maxTimestampInMessages) > rollParams.maxSegmentMs - rollJitterMs;
int size = size();
return size > rollParams.maxSegmentBytes - rollParams.messagesSize ||
(size > 0 && reachedRollMs) ||
offsetIndex().isFull() || timeIndex().isFull() || !canConvertToRelativeOffset(rollParams.maxOffsetInMessages);
}
public void resizeIndexes(int size) throws IOException {
offsetIndex().resize(size);
timeIndex().resize(size);
}
public void sanityCheck(boolean timeIndexFileNewlyCreated) throws IOException {
if (offsetIndexFile().exists()) {
// Resize the time index file to 0 if it is newly created.
if (timeIndexFileNewlyCreated)
timeIndex().resize(0);
// Sanity checks for time index and offset index are skipped because
// we will recover the segments above the recovery point in recoverLog()
// in any case so sanity checking them here is redundant.
txnIndex.sanityCheck();
} else
throw new NoSuchFileException("Offset index file " + offsetIndexFile().getAbsolutePath() + " does not exist");
}
/**
* The first time this is invoked, it will result in a time index lookup (including potential materialization of
* the time index).
*/
public TimestampOffset readMaxTimestampAndOffsetSoFar() throws IOException {
if (maxTimestampAndOffsetSoFar == TimestampOffset.UNKNOWN)
maxTimestampAndOffsetSoFar = timeIndex().lastEntry();
return maxTimestampAndOffsetSoFar;
}
/**
* The maximum timestamp we see so far.
*
* Note that this may result in time index materialization.
*/
public long maxTimestampSoFar() throws IOException {
return readMaxTimestampAndOffsetSoFar().timestamp;
}
/**
* Note that this may result in time index materialization.
*/
private long offsetOfMaxTimestampSoFar() throws IOException {
return readMaxTimestampAndOffsetSoFar().offset;
}
/* Return the size in bytes of this log segment */
public int size() {
return log.sizeInBytes();
}
/**
* checks that the argument offset can be represented as an integer offset relative to the baseOffset.
*/
private boolean canConvertToRelativeOffset(long offset) throws IOException {
return offsetIndex().canAppendOffset(offset);
}
/**
* Append the given messages starting with the given offset. Add
* an entry to the index if needed.
*
* It is assumed this method is being called from within a lock, it is not thread-safe otherwise.
*
* @param largestOffset The last offset in the message set
* @param largestTimestampMs The largest timestamp in the message set.
* @param shallowOffsetOfMaxTimestamp The offset of the message that has the largest timestamp in the messages to append.
* @param records The log entries to append.
* @throws LogSegmentOffsetOverflowException if the largest offset causes index offset overflow
*/
public void append(long largestOffset,
long largestTimestampMs,
long shallowOffsetOfMaxTimestamp,
MemoryRecords records) throws IOException {
if (records.sizeInBytes() > 0) {
LOGGER.trace("Inserting {} bytes at end offset {} at position {} with largest timestamp {} at shallow offset {}",
records.sizeInBytes(), largestOffset, log.sizeInBytes(), largestTimestampMs, shallowOffsetOfMaxTimestamp);
int physicalPosition = log.sizeInBytes();
if (physicalPosition == 0)
rollingBasedTimestamp = OptionalLong.of(largestTimestampMs);
ensureOffsetInRange(largestOffset);
// append the messages
long appendedBytes = log.append(records);
LOGGER.trace("Appended {} to {} at end offset {}", appendedBytes, log.file(), largestOffset);
// Update the in memory max timestamp and corresponding offset.
if (largestTimestampMs > maxTimestampSoFar()) {
maxTimestampAndOffsetSoFar = new TimestampOffset(largestTimestampMs, shallowOffsetOfMaxTimestamp);
}
// append an entry to the index (if needed)
if (bytesSinceLastIndexEntry > indexIntervalBytes) {
offsetIndex().append(largestOffset, physicalPosition);
timeIndex().maybeAppend(maxTimestampSoFar(), offsetOfMaxTimestampSoFar());
bytesSinceLastIndexEntry = 0;
}
bytesSinceLastIndexEntry += records.sizeInBytes();
}
}
private void ensureOffsetInRange(long offset) throws IOException {
if (!canConvertToRelativeOffset(offset))
throw new LogSegmentOffsetOverflowException(this, offset);
}
private int appendChunkFromFile(FileRecords records, int position, BufferSupplier bufferSupplier) throws IOException {
int bytesToAppend = 0;
long maxTimestamp = Long.MIN_VALUE;
long offsetOfMaxTimestamp = Long.MIN_VALUE;
long maxOffset = Long.MIN_VALUE;
ByteBuffer readBuffer = bufferSupplier.get(1024 * 1024);
// find all batches that are valid to be appended to the current log segment and
// determine the maximum offset and timestamp
Iterator<FileChannelRecordBatch> nextBatches = records.batchesFrom(position).iterator();
FileChannelRecordBatch batch;
while ((batch = nextAppendableBatch(nextBatches, readBuffer, bytesToAppend)) != null) {
if (batch.maxTimestamp() > maxTimestamp) {
maxTimestamp = batch.maxTimestamp();
offsetOfMaxTimestamp = batch.lastOffset();
}
maxOffset = batch.lastOffset();
bytesToAppend += batch.sizeInBytes();
}
if (bytesToAppend > 0) {
// Grow buffer if needed to ensure we copy at least one batch
if (readBuffer.capacity() < bytesToAppend)
readBuffer = bufferSupplier.get(bytesToAppend);
readBuffer.limit(bytesToAppend);
records.readInto(readBuffer, position);
append(maxOffset, maxTimestamp, offsetOfMaxTimestamp, MemoryRecords.readableRecords(readBuffer));
}
bufferSupplier.release(readBuffer);
return bytesToAppend;
}
private FileChannelRecordBatch nextAppendableBatch(Iterator<FileChannelRecordBatch> recordBatches,
ByteBuffer readBuffer,
int bytesToAppend) throws IOException {
if (recordBatches.hasNext()) {
FileChannelRecordBatch batch = recordBatches.next();
if (canConvertToRelativeOffset(batch.lastOffset()) &&
(bytesToAppend == 0 || bytesToAppend + batch.sizeInBytes() < readBuffer.capacity()))
return batch;
}
return null;
}
/**
* Append records from a file beginning at the given position until either the end of the file
* is reached or an offset is found which is too large to convert to a relative offset for the indexes.
*
* @return the number of bytes appended to the log (may be less than the size of the input if an
* offset is encountered which would overflow this segment)
*/
public int appendFromFile(FileRecords records, int start) throws IOException {
int position = start;
BufferSupplier bufferSupplier = new BufferSupplier.GrowableBufferSupplier();
while (position < start + records.sizeInBytes()) {
int bytesAppended = appendChunkFromFile(records, position, bufferSupplier);
if (bytesAppended == 0)
return position - start;
position += bytesAppended;
}
return position - start;
}
/* not thread safe */
public void updateTxnIndex(CompletedTxn completedTxn, long lastStableOffset) throws IOException {
if (completedTxn.isAborted) {
LOGGER.trace("Writing aborted transaction {} to transaction index, last stable offset is {}", completedTxn, lastStableOffset);
txnIndex.append(new AbortedTxn(completedTxn, lastStableOffset));
}
}
private void updateProducerState(ProducerStateManager producerStateManager, RecordBatch batch) throws IOException {
if (batch.hasProducerId()) {
long producerId = batch.producerId();
ProducerAppendInfo appendInfo = producerStateManager.prepareUpdate(producerId, AppendOrigin.REPLICATION);
Optional<CompletedTxn> maybeCompletedTxn = appendInfo.append(batch, Optional.empty());
producerStateManager.update(appendInfo);
if (maybeCompletedTxn.isPresent()) {
CompletedTxn completedTxn = maybeCompletedTxn.get();
long lastStableOffset = producerStateManager.lastStableOffset(completedTxn);
updateTxnIndex(completedTxn, lastStableOffset);
producerStateManager.completeTxn(completedTxn);
}
}
producerStateManager.updateMapEndOffset(batch.lastOffset() + 1);
}
/**
* Equivalent to {@code translateOffset(offset, 0)}.
*
* See {@link #translateOffset(long, int)} for details.
*/
public LogOffsetPosition translateOffset(long offset) throws IOException {
return translateOffset(offset, 0);
}
/**
* Find the physical file position for the first message with offset >= the requested offset.
*
* The startingFilePosition argument is an optimization that can be used if we already know a valid starting position
* in the file higher than the greatest-lower-bound from the index.
*
* This method is thread-safe.
*
* @param offset The offset we want to translate
* @param startingFilePosition A lower bound on the file position from which to begin the search. This is purely an optimization and
* when omitted, the search will begin at the position in the offset index.
* @return The position in the log storing the message with the least offset >= the requested offset and the size of the
* message or null if no message meets this criteria.
*/
LogOffsetPosition translateOffset(long offset, int startingFilePosition) throws IOException {
OffsetPosition mapping = offsetIndex().lookup(offset);
return log.searchForOffsetWithSize(offset, Math.max(mapping.position, startingFilePosition));
}
/**
* Equivalent to {@code read(startOffset, maxSize, size())}.
*
* See {@link #read(long, int, long, boolean)} for details.
*/
public FetchDataInfo read(long startOffset, int maxSize) throws IOException {
return read(startOffset, maxSize, size());
}
/**
* Equivalent to {@code read(startOffset, maxSize, maxPosition, false)}.
*
* See {@link #read(long, int, long, boolean)} for details.
*/
public FetchDataInfo read(long startOffset, int maxSize, long maxPosition) throws IOException {
return read(startOffset, maxSize, maxPosition, false);
}
/**
* Read a message set from this segment beginning with the first offset >= startOffset. The message set will include
* no more than maxSize bytes and will end before maxOffset if a maxOffset is specified.
*
* This method is thread-safe.
*
* @param startOffset A lower bound on the first offset to include in the message set we read
* @param maxSize The maximum number of bytes to include in the message set we read
* @param maxPosition The maximum position in the log segment that should be exposed for read
* @param minOneMessage If this is true, the first message will be returned even if it exceeds `maxSize` (if one exists)
*
* @return The fetched data and the offset metadata of the first message whose offset is >= startOffset,
* or null if the startOffset is larger than the largest offset in this log
*/
public FetchDataInfo read(long startOffset, int maxSize, long maxPosition, boolean minOneMessage) throws IOException {
if (maxSize < 0)
throw new IllegalArgumentException("Invalid max size " + maxSize + " for log read from segment " + log);
LogOffsetPosition startOffsetAndSize = translateOffset(startOffset);
// if the start position is already off the end of the log, return null
if (startOffsetAndSize == null)
return null;
int startPosition = startOffsetAndSize.position;
LogOffsetMetadata offsetMetadata = new LogOffsetMetadata(startOffset, this.baseOffset, startPosition);
int adjustedMaxSize = maxSize;
if (minOneMessage)
adjustedMaxSize = Math.max(maxSize, startOffsetAndSize.size);
// return a log segment but with zero size in the case below
if (adjustedMaxSize == 0)
return new FetchDataInfo(offsetMetadata, MemoryRecords.EMPTY);
// calculate the length of the message set to read based on whether or not they gave us a maxOffset
int fetchSize = Math.min((int) (maxPosition - startPosition), adjustedMaxSize);
return new FetchDataInfo(offsetMetadata, log.slice(startPosition, fetchSize),
adjustedMaxSize < startOffsetAndSize.size, Optional.empty());
}
public OptionalLong fetchUpperBoundOffset(OffsetPosition startOffsetPosition, int fetchSize) throws IOException {
Optional<OffsetPosition> position = offsetIndex().fetchUpperBoundOffset(startOffsetPosition, fetchSize);
if (position.isPresent())
return OptionalLong.of(position.get().offset);
return OptionalLong.empty();
}
/**
* Run recovery on the given segment. This will rebuild the index from the log file and lop off any invalid bytes
* from the end of the log and index.
*
* This method is not thread-safe.
*
* @param producerStateManager Producer state corresponding to the segment's base offset. This is needed to recover
* the transaction index.
* @param leaderEpochCache Optionally a cache for updating the leader epoch during recovery.
* @return The number of bytes truncated from the log
* @throws LogSegmentOffsetOverflowException if the log segment contains an offset that causes the index offset to overflow
*/
public int recover(ProducerStateManager producerStateManager, Optional<LeaderEpochFileCache> leaderEpochCache) throws IOException {
offsetIndex().reset();
timeIndex().reset();
txnIndex.reset();
int validBytes = 0;
int lastIndexEntry = 0;
maxTimestampAndOffsetSoFar = TimestampOffset.UNKNOWN;
try {
for (RecordBatch batch : log.batches()) {
batch.ensureValid();
ensureOffsetInRange(batch.lastOffset());
// The max timestamp is exposed at the batch level, so no need to iterate the records
if (batch.maxTimestamp() > maxTimestampSoFar()) {
maxTimestampAndOffsetSoFar = new TimestampOffset(batch.maxTimestamp(), batch.lastOffset());
}
// Build offset index
if (validBytes - lastIndexEntry > indexIntervalBytes) {
offsetIndex().append(batch.lastOffset(), validBytes);
timeIndex().maybeAppend(maxTimestampSoFar(), offsetOfMaxTimestampSoFar());
lastIndexEntry = validBytes;
}
validBytes += batch.sizeInBytes();
if (batch.magic() >= RecordBatch.MAGIC_VALUE_V2) {
leaderEpochCache.ifPresent(cache -> {
if (batch.partitionLeaderEpoch() >= 0 &&
(!cache.latestEpoch().isPresent() || batch.partitionLeaderEpoch() > cache.latestEpoch().getAsInt()))
cache.assign(batch.partitionLeaderEpoch(), batch.baseOffset());
});
updateProducerState(producerStateManager, batch);
}
}
} catch (CorruptRecordException | InvalidRecordException e) {
LOGGER.warn("Found invalid messages in log segment {} at byte offset {}: {}. {}", log.file().getAbsolutePath(),
validBytes, e.getMessage(), e.getCause());
}
int truncated = log.sizeInBytes() - validBytes;
if (truncated > 0)
LOGGER.debug("Truncated {} invalid bytes at the end of segment {} during recovery", truncated, log.file().getAbsolutePath());
log.truncateTo(validBytes);
offsetIndex().trimToValidSize();
// A normally closed segment always appends the biggest timestamp ever seen into log segment, we do this as well.
timeIndex().maybeAppend(maxTimestampSoFar(), offsetOfMaxTimestampSoFar(), true);
timeIndex().trimToValidSize();
return truncated;
}
/**
* Check whether the last offset of the last batch in this segment overflows the indexes.
*/
public boolean hasOverflow() throws IOException {
long nextOffset = readNextOffset();
return nextOffset > baseOffset && !canConvertToRelativeOffset(nextOffset - 1);
}
public TxnIndexSearchResult collectAbortedTxns(long fetchOffset, long upperBoundOffset) {
return txnIndex.collectAbortedTxns(fetchOffset, upperBoundOffset);
}
@Override
public String toString() {
// We don't call `largestRecordTimestamp` below to avoid materializing the time index when `toString` is invoked
return "LogSegment(baseOffset=" + baseOffset +
", size=" + size() +
", lastModifiedTime=" + lastModified() +
", largestRecordTimestamp=" + maxTimestampAndOffsetSoFar.timestamp +
")";
}
/**
* Truncate off all index and log entries with offsets >= the given offset.
* If the given offset is larger than the largest message in this segment, do nothing.
*
* This method is not thread-safe.
*
* @param offset The offset to truncate to
* @return The number of log bytes truncated
*/
public int truncateTo(long offset) throws IOException {
// Do offset translation before truncating the index to avoid needless scanning
// in case we truncate the full index
LogOffsetPosition mapping = translateOffset(offset);
OffsetIndex offsetIndex = offsetIndex();
TimeIndex timeIndex = timeIndex();
offsetIndex.truncateTo(offset);
timeIndex.truncateTo(offset);
txnIndex.truncateTo(offset);
// After truncation, reset and allocate more space for the (new currently active) index
offsetIndex.resize(offsetIndex.maxIndexSize());
timeIndex.resize(timeIndex.maxIndexSize());
int bytesTruncated;
if (mapping == null)
bytesTruncated = 0;
else
bytesTruncated = log.truncateTo(mapping.position);
if (log.sizeInBytes() == 0) {
created = time.milliseconds();
rollingBasedTimestamp = OptionalLong.empty();
}
bytesSinceLastIndexEntry = 0;
if (maxTimestampSoFar() >= 0)
maxTimestampAndOffsetSoFar = readLargestTimestamp();
return bytesTruncated;
}
private TimestampOffset readLargestTimestamp() throws IOException {
// Get the last time index entry. If the time index is empty, it will return (-1, baseOffset)
TimestampOffset lastTimeIndexEntry = timeIndex().lastEntry();
OffsetPosition offsetPosition = offsetIndex().lookup(lastTimeIndexEntry.offset);
// Scan the rest of the messages to see if there is a larger timestamp after the last time index entry.
FileRecords.TimestampAndOffset maxTimestampOffsetAfterLastEntry = log.largestTimestampAfter(offsetPosition.position);
if (maxTimestampOffsetAfterLastEntry.timestamp > lastTimeIndexEntry.timestamp)
return new TimestampOffset(maxTimestampOffsetAfterLastEntry.timestamp, maxTimestampOffsetAfterLastEntry.offset);
return lastTimeIndexEntry;
}
/**
* Calculate the offset that would be used for the next message to be append to this segment.
* Note that this is expensive.
*
* This method is thread-safe.
*/
public long readNextOffset() throws IOException {
FetchDataInfo fetchData = read(offsetIndex().lastOffset(), log.sizeInBytes());
if (fetchData == null)
return baseOffset;
else
return fetchData.records.lastBatch()
.map(batch -> batch.nextOffset())
.orElse(baseOffset);
}
/**
* Flush this log segment to disk.
*
* This method is thread-safe.
*/
public void flush() throws IOException {
try {
LOG_FLUSH_TIMER.time(new Callable<Void>() {
// lambdas cannot declare a more specific exception type, so we use an anonymous inner class
@Override
public Void call() throws IOException {
log.flush();
offsetIndex().flush();
timeIndex().flush();
txnIndex.flush();
return null;
}
});
} catch (Exception e) {
if (e instanceof IOException)
throw (IOException) e;
else if (e instanceof RuntimeException)
throw (RuntimeException) e;
else
throw new IllegalStateException("Unexpected exception thrown: " + e, e);
}
}
/**
* Update the directory reference for the log and indices in this segment. This would typically be called after a
* directory is renamed.
*/
void updateParentDir(File dir) {
log.updateParentDir(dir);
lazyOffsetIndex.updateParentDir(dir);
lazyTimeIndex.updateParentDir(dir);
txnIndex.updateParentDir(dir);
}
/**
* Change the suffix for the index and log files for this log segment
* IOException from this method should be handled by the caller
*/
public void changeFileSuffixes(String oldSuffix, String newSuffix) throws IOException {
log.renameTo(new File(Utils.replaceSuffix(log.file().getPath(), oldSuffix, newSuffix)));
lazyOffsetIndex.renameTo(new File(Utils.replaceSuffix(offsetIndexFile().getPath(), oldSuffix, newSuffix)));
lazyTimeIndex.renameTo(new File(Utils.replaceSuffix(timeIndexFile().getPath(), oldSuffix, newSuffix)));
txnIndex.renameTo(new File(Utils.replaceSuffix(txnIndex.file().getPath(), oldSuffix, newSuffix)));
}
public boolean hasSuffix(String suffix) {
return log.file().getName().endsWith(suffix) &&
offsetIndexFile().getName().endsWith(suffix) &&
timeIndexFile().getName().endsWith(suffix) &&
txnIndex.file().getName().endsWith(suffix);
}
/**
* Append the largest time index entry to the time index and trim the log and indexes.
*
* The time index entry appended will be used to decide when to delete the segment.
*/
public void onBecomeInactiveSegment() throws IOException {
timeIndex().maybeAppend(maxTimestampSoFar(), offsetOfMaxTimestampSoFar(), true);
offsetIndex().trimToValidSize();
timeIndex().trimToValidSize();
log.trim();
}
/**
* If not previously loaded,
* load the timestamp of the first message into memory.
*/
private void loadFirstBatchTimestamp() {
if (!rollingBasedTimestamp.isPresent()) {
Iterator<FileChannelRecordBatch> iter = log.batches().iterator();
if (iter.hasNext())
rollingBasedTimestamp = OptionalLong.of(iter.next().maxTimestamp());
}
}
/**
* The time this segment has waited to be rolled.
* If the first message batch has a timestamp we use its timestamp to determine when to roll a segment. A segment
* is rolled if the difference between the new batch's timestamp and the first batch's timestamp exceeds the
* segment rolling time.
* If the first batch does not have a timestamp, we use the wall clock time to determine when to roll a segment. A
* segment is rolled if the difference between the current wall clock time and the segment create time exceeds the
* segment rolling time.
*/
public long timeWaitedForRoll(long now, long messageTimestamp) {
// Load the timestamp of the first message into memory
loadFirstBatchTimestamp();
long ts = rollingBasedTimestamp.orElse(-1L);
if (ts >= 0)
return messageTimestamp - ts;
return now - created;
}
/**
* @return the first batch timestamp if the timestamp is available. Otherwise return Long.MaxValue
*/
long getFirstBatchTimestamp() {
loadFirstBatchTimestamp();
OptionalLong timestamp = rollingBasedTimestamp;
if (timestamp.isPresent() && timestamp.getAsLong() >= 0)
return timestamp.getAsLong();
return Long.MAX_VALUE;
}
/**
* Search the message offset based on timestamp and offset.
*
* This method returns an option of TimestampOffset. The returned value is determined using the following ordered list of rules:
*
* - If all the messages in the segment have smaller offsets, return None
* - If all the messages in the segment have smaller timestamps, return None
* - If all the messages in the segment have larger timestamps, or no message in the segment has a timestamp
* the returned the offset will be max(the base offset of the segment, startingOffset) and the timestamp will be Message.NoTimestamp.
* - Otherwise, return an option of TimestampOffset. The offset is the offset of the first message whose timestamp
* is greater than or equals to the target timestamp and whose offset is greater than or equals to the startingOffset.
*
* This method only returns None when 1) all messages' offset < startOffing or 2) the log is not empty but we did not
* see any message when scanning the log from the indexed position. The latter could happen if the log is truncated
* after we get the indexed position but before we scan the log from there. In this case we simply return None and the
* caller will need to check on the truncated log and maybe retry or even do the search on another log segment.
*
* @param timestampMs The timestamp to search for.
* @param startingOffset The starting offset to search.
* @return the timestamp and offset of the first message that meets the requirements. None will be returned if there is no such message.
*/
public Optional<FileRecords.TimestampAndOffset> findOffsetByTimestamp(long timestampMs, long startingOffset) throws IOException {
// 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;
// Search the timestamp
return Optional.ofNullable(log.searchForTimestamp(timestampMs, position, startingOffset));
}
/**
* Close this log segment
*/
@Override
public void close() throws IOException {
if (maxTimestampAndOffsetSoFar != TimestampOffset.UNKNOWN)
Utils.swallow(LOGGER, Level.WARN, "maybeAppend", () -> timeIndex().maybeAppend(maxTimestampSoFar(), offsetOfMaxTimestampSoFar(), true));
Utils.closeQuietly(lazyOffsetIndex, "offsetIndex", LOGGER);
Utils.closeQuietly(lazyTimeIndex, "timeIndex", LOGGER);
Utils.closeQuietly(log, "log", LOGGER);
Utils.closeQuietly(txnIndex, "txnIndex", LOGGER);
}
/**
* Close file handlers used by the log segment but don't write to disk. This is used when the disk may have failed
*/
void closeHandlers() {
Utils.swallow(LOGGER, Level.WARN, "offsetIndex", () -> lazyOffsetIndex.closeHandler());
Utils.swallow(LOGGER, Level.WARN, "timeIndex", () -> lazyTimeIndex.closeHandler());
Utils.swallow(LOGGER, Level.WARN, "log", () -> log.closeHandlers());
Utils.closeQuietly(txnIndex, "txnIndex", LOGGER);
}
/**
* Delete this log segment from the filesystem.
*/
public void deleteIfExists() throws IOException {
try {
Utils.tryAll(asList(
() -> deleteTypeIfExists(() -> log.deleteIfExists(), "log", log.file(), true),
() -> deleteTypeIfExists(() -> lazyOffsetIndex.deleteIfExists(), "offset index", offsetIndexFile(), true),
() -> deleteTypeIfExists(() -> lazyTimeIndex.deleteIfExists(), "time index", timeIndexFile(), true),
() -> deleteTypeIfExists(() -> txnIndex.deleteIfExists(), "transaction index", txnIndex.file(), false)));
} catch (Throwable t) {
if (t instanceof IOException)
throw (IOException) t;
if (t instanceof Error)
throw (Error) t;
if (t instanceof RuntimeException)
throw (RuntimeException) t;
throw new IllegalStateException("Unexpected exception: " + t.getMessage(), t);
}
}
// Helper method for `deleteIfExists()`
private Void deleteTypeIfExists(StorageAction<Boolean, IOException> delete, String fileType, File file, boolean logIfMissing) throws IOException {
try {
if (delete.execute())
LOGGER.info("Deleted {} {}.", fileType, file.getAbsolutePath());
else if (logIfMissing)
LOGGER.info("Failed to delete {} {} because it does not exist.", fileType, file.getAbsolutePath());
return null;
} catch (IOException e) {
throw new IOException("Delete of " + fileType + " " + file.getAbsolutePath() + " failed.", e);
}
}
// Visible for testing
public boolean deleted() {
return !log.file().exists() && !offsetIndexFile().exists() && !timeIndexFile().exists() && !txnIndex.file().exists();
}
/**
* The last modified time of this log segment as a unix time stamp
*/
public long lastModified() {
return log.file().lastModified();
}
/**
* The largest timestamp this segment contains, if maxTimestampSoFar >= 0, otherwise None.
*/
public OptionalLong largestRecordTimestamp() throws IOException {
long maxTimestampSoFar = maxTimestampSoFar();
if (maxTimestampSoFar >= 0)
return OptionalLong.of(maxTimestampSoFar);
return OptionalLong.empty();
}
/**
* The largest timestamp this segment contains.
*/
public long largestTimestamp() throws IOException {
long maxTimestampSoFar = maxTimestampSoFar();
if (maxTimestampSoFar >= 0)
return maxTimestampSoFar;
return lastModified();
}
/**
* Change the last modified time for this log segment
*/
public void setLastModified(long ms) throws IOException {
FileTime fileTime = FileTime.fromMillis(ms);
Files.setLastModifiedTime(log.file().toPath(), fileTime);
Files.setLastModifiedTime(offsetIndexFile().toPath(), fileTime);
Files.setLastModifiedTime(timeIndexFile().toPath(), fileTime);
}
public static LogSegment open(File dir, long baseOffset, LogConfig config, Time time, int initFileSize, boolean preallocate) throws IOException {
return open(dir, baseOffset, config, time, false, initFileSize, preallocate, "");
}
public static LogSegment open(File dir, long baseOffset, LogConfig config, Time time, boolean fileAlreadyExists,
int initFileSize, boolean preallocate, String fileSuffix) throws IOException {
int maxIndexSize = config.maxIndexSize;
return new LogSegment(
FileRecords.open(LogFileUtils.logFile(dir, baseOffset, fileSuffix), fileAlreadyExists, initFileSize, preallocate),
LazyIndex.forOffset(LogFileUtils.offsetIndexFile(dir, baseOffset, fileSuffix), baseOffset, maxIndexSize),
LazyIndex.forTime(LogFileUtils.timeIndexFile(dir, baseOffset, fileSuffix), baseOffset, maxIndexSize),
new TransactionIndex(baseOffset, LogFileUtils.transactionIndexFile(dir, baseOffset, fileSuffix)),
baseOffset,
config.indexInterval,
config.randomSegmentJitter(),
time);
}
public static void deleteIfExists(File dir, long baseOffset, String fileSuffix) throws IOException {
deleteFileIfExists(LogFileUtils.offsetIndexFile(dir, baseOffset, fileSuffix));
deleteFileIfExists(LogFileUtils.timeIndexFile(dir, baseOffset, fileSuffix));
deleteFileIfExists(LogFileUtils.transactionIndexFile(dir, baseOffset, fileSuffix));
deleteFileIfExists(LogFileUtils.logFile(dir, baseOffset, fileSuffix));
}
private static boolean deleteFileIfExists(File file) throws IOException {
return Files.deleteIfExists(file.toPath());
}
}

22
core/src/main/scala/kafka/common/LogSegmentOffsetOverflowException.scala → storage/src/main/java/org/apache/kafka/storage/internals/log/LogSegmentOffsetOverflowException.java

@ -1,10 +1,10 @@ @@ -1,10 +1,10 @@
/**
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* 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
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
@ -14,10 +14,9 @@ @@ -14,10 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.kafka.storage.internals.log;
package kafka.common
import kafka.log.LogSegment
import org.apache.kafka.common.KafkaException;
/**
* Indicates that the log segment contains one or more messages that overflow the offset (and / or time) index. This is
@ -25,6 +24,13 @@ import kafka.log.LogSegment @@ -25,6 +24,13 @@ import kafka.log.LogSegment
* KAFKA-5413. With KAFKA-6264, we have the ability to split such log segments into multiple log segments such that we
* do not have any segments with offset overflow.
*/
class LogSegmentOffsetOverflowException(val segment: LogSegment, val offset: Long)
extends org.apache.kafka.common.KafkaException(s"Detected offset overflow at offset $offset in segment $segment") {
public class LogSegmentOffsetOverflowException extends KafkaException {
public final LogSegment segment;
public final long offset;
public LogSegmentOffsetOverflowException(LogSegment segment, long offset) {
super("Detected offset overflow at offset " + offset + " in segment " + segment);
this.segment = segment;
this.offset = offset;
}
}

355
storage/src/main/java/org/apache/kafka/storage/internals/log/LogSegments.java

@ -0,0 +1,355 @@ @@ -0,0 +1,355 @@
/*
* 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.storage.internals.log;
import org.apache.kafka.common.TopicPartition;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* This class encapsulates a thread-safe navigable map of LogSegment instances and provides the
* required read and write behavior on the map.
*/
public class LogSegments {
private final TopicPartition topicPartition;
/* the segments of the log with key being LogSegment base offset and value being a LogSegment */
private final ConcurrentNavigableMap<Long, LogSegment> segments = new ConcurrentSkipListMap<>();
/**
* Create new instance.
*
* @param topicPartition the TopicPartition associated with the segments
* (useful for logging purposes)
*/
public LogSegments(TopicPartition topicPartition) {
this.topicPartition = topicPartition;
}
/**
* Return true if the segments are empty, false otherwise.
*
* This method is thread-safe.
*/
public boolean isEmpty() {
return segments.isEmpty();
}
/**
* Return true if the segments are non-empty, false otherwise.
*
* This method is thread-safe.
*/
public boolean nonEmpty() {
return !isEmpty();
}
/**
* Add the given segment, or replace an existing entry.
*
* This method is thread-safe.
*
* @param segment the segment to add
*/
public LogSegment add(LogSegment segment) {
return this.segments.put(segment.baseOffset(), segment);
}
/**
* Remove the segment at the provided offset.
*
* This method is thread-safe.
*
* @param offset the offset to be removed
*/
public void remove(long offset) {
segments.remove(offset);
}
/**
* Clears all entries.
*
* This method is thread-safe.
*/
public void clear() {
segments.clear();
}
/**
* Close all segments.
*/
public void close() throws IOException {
for (LogSegment s : values())
s.close();
}
/**
* Close the handlers for all segments.
*/
public void closeHandlers() {
for (LogSegment s : values())
s.closeHandlers();
}
/**
* Update the directory reference for the log and indices of all segments.
*
* @param dir the renamed directory
*/
public void updateParentDir(File dir) {
for (LogSegment s : values())
s.updateParentDir(dir);
}
/**
* Take care! this is an O(n) operation, where n is the number of segments.
*
* This method is thread-safe.
*
* @return The number of segments.
*
*/
public int numberOfSegments() {
return segments.size();
}
/**
* @return the base offsets of all segments
*/
public Collection<Long> baseOffsets() {
return values().stream().map(s -> s.baseOffset()).collect(Collectors.toList());
}
/**
* Return true if a segment exists at the provided offset, false otherwise.
*
* This method is thread-safe.
*
* @param offset the segment to be checked
*/
public boolean contains(long offset) {
return segments.containsKey(offset);
}
/**
* Retrieves a segment at the specified offset.
*
* This method is thread-safe.
*
* @param offset the segment to be retrieved
*
* @return the segment if it exists, otherwise None.
*/
public Optional<LogSegment> get(long offset) {
return Optional.ofNullable(segments.get(offset));
}
/**
* @return an iterator to the log segments ordered from oldest to newest.
*/
public Collection<LogSegment> values() {
return segments.values();
}
/**
* @return An iterator to all segments beginning with the segment that includes "from" and ending
* with the segment that includes up to "to-1" or the end of the log (if to > end of log).
*/
public Collection<LogSegment> values(long from, long to) {
if (from == to) {
// Handle non-segment-aligned empty sets
return Collections.emptyList();
} else if (to < from) {
throw new IllegalArgumentException("Invalid log segment range: requested segments in " + topicPartition +
" from offset " + from + " which is greater than limit offset " + to);
} else {
Long floor = segments.floorKey(from);
if (floor != null)
return segments.subMap(floor, to).values();
return segments.headMap(to).values();
}
}
public Collection<LogSegment> nonActiveLogSegmentsFrom(long from) {
LogSegment activeSegment = lastSegment().get();
if (from > activeSegment.baseOffset())
return Collections.emptyList();
else
return values(from, activeSegment.baseOffset());
}
/**
* Return the entry associated with the greatest offset less than or equal to the given offset,
* if it exists.
*
* This method is thread-safe.
*/
private Optional<Map.Entry<Long, LogSegment>> floorEntry(long offset) {
return Optional.ofNullable(segments.floorEntry(offset));
}
/**
* Return the log segment with the greatest offset less than or equal to the given offset,
* if it exists.
*
* This method is thread-safe.
*/
public Optional<LogSegment> floorSegment(long offset) {
return floorEntry(offset).map(e -> e.getValue());
}
/**
* Return the entry associated with the greatest offset strictly less than the given offset,
* if it exists.
*
* This method is thread-safe.
*/
private Optional<Map.Entry<Long, LogSegment>> lowerEntry(long offset) {
return Optional.ofNullable(segments.lowerEntry(offset));
}
/**
* Return the log segment with the greatest offset strictly less than the given offset,
* if it exists.
*
* This method is thread-safe.
*/
public Optional<LogSegment> lowerSegment(long offset) {
return lowerEntry(offset).map(e -> e.getValue());
}
/**
* Return the entry associated with the smallest offset strictly greater than the given offset,
* if it exists.
*
* This method is thread-safe.
*/
public Optional<Map.Entry<Long, LogSegment>> higherEntry(long offset) {
return Optional.ofNullable(segments.higherEntry(offset));
}
/**
* Return the log segment with the smallest offset strictly greater than the given offset,
* if it exists.
*
* This method is thread-safe.
*/
public Optional<LogSegment> higherSegment(long offset) {
return higherEntry(offset).map(e -> e.getValue());
}
/**
* Return the entry associated with the smallest offset, if it exists.
*
* This method is thread-safe.
*/
public Optional<Map.Entry<Long, LogSegment>> firstEntry() {
return Optional.ofNullable(segments.firstEntry());
}
/**
* Return the log segment associated with the smallest offset, if it exists.
*
* This method is thread-safe.
*/
public Optional<LogSegment> firstSegment() {
return firstEntry().map(s -> s.getValue());
}
/**
* @return the base offset of the log segment associated with the smallest offset, if it exists
*/
public OptionalLong firstSegmentBaseOffset() {
Optional<LogSegment> first = firstSegment();
if (first.isPresent())
return OptionalLong.of(first.get().baseOffset());
return OptionalLong.empty();
}
/**
* Return the entry associated with the greatest offset, if it exists.
*
* This method is thread-safe.
*/
public Optional<Map.Entry<Long, LogSegment>> lastEntry() {
return Optional.ofNullable(segments.lastEntry());
}
/**
* Return the log segment with the greatest offset, if it exists.
*
* This method is thread-safe.
*/
public Optional<LogSegment> lastSegment() {
return lastEntry().map(e -> e.getValue());
}
/**
* @return an iterable with log segments ordered from lowest base offset to highest,
* each segment returned has a base offset strictly greater than the provided baseOffset.
*/
public Collection<LogSegment> higherSegments(long baseOffset) {
Long higherOffset = segments.higherKey(baseOffset);
if (higherOffset != null)
return segments.tailMap(higherOffset, true).values();
return Collections.emptyList();
}
/**
* The active segment that is currently taking appends
*/
public LogSegment activeSegment() {
return lastSegment().get();
}
public long sizeInBytes() {
return LogSegments.sizeInBytes(values());
}
/**
* Returns an Iterable containing segments matching the provided predicate.
*
* @param predicate the predicate to be used for filtering segments.
*/
public Collection<LogSegment> filter(Predicate<LogSegment> predicate) {
return values().stream().filter(predicate).collect(Collectors.toList());
}
/**
* Calculate a log's size (in bytes) from the provided log segments.
*
* @param segments The log segments to calculate the size of
* @return Sum of the log segments' sizes (in bytes)
*/
public static long sizeInBytes(Collection<LogSegment> segments) {
return segments.stream().mapToLong(s -> s.size()).sum();
}
public static Collection<Long> getFirstBatchTimestampForSegments(Collection<LogSegment> segments) {
return segments.stream().map(s -> s.getFirstBatchTimestamp()).collect(Collectors.toList());
}
}
Loading…
Cancel
Save