/**
* Copyright 2016 LinkedIn Corp. All rights reserved.
*
* Licensed 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.
*/
package com.github.ambry.store;
import com.github.ambry.utils.Pair;
import com.github.ambry.utils.Utils;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The Log is an abstraction over a group of files that are loaded as {@link LogSegment} instances. It provides a
* unified view of these segments and provides a way to interact and query different properties of this group of
* segments.
*/
class Log implements Write {
private final String dataDir;
private final long capacityInBytes;
private final boolean isLogSegmented;
private final StoreMetrics metrics;
private final Iterator<Pair<String, String>> segmentNameAndFileNameIterator;
private final ConcurrentSkipListMap<String, LogSegment> segmentsByName =
new ConcurrentSkipListMap<>(LogSegmentNameHelper.COMPARATOR);
private final Logger logger = LoggerFactory.getLogger(getClass());
private final AtomicLong remainingUnallocatedSegments = new AtomicLong(0);
private LogSegment activeSegment = null;
/**
* Create a Log instance
* @param dataDir the directory where the segments of the log need to be loaded from.
* @param totalCapacityInBytes the total capacity of this log.
* @param segmentCapacityInBytes the capacity of a single segment in the log.
* @param metrics the {@link StoreMetrics} instance to use.
* @throws IOException if there is any I/O error loading the segment files.
* @throws IllegalArgumentException if {@code totalCapacityInBytes} or {@code segmentCapacityInBytes} <= 0 or if
* {@code totalCapacityInBytes} > {@code segmentCapacityInBytes} and {@code totalCapacityInBytes} is not a perfect
* multiple of {@code segmentCapacityInBytes}.
*/
Log(String dataDir, long totalCapacityInBytes, long segmentCapacityInBytes, StoreMetrics metrics) throws IOException {
this.dataDir = dataDir;
this.capacityInBytes = totalCapacityInBytes;
this.isLogSegmented = totalCapacityInBytes > segmentCapacityInBytes;
this.metrics = metrics;
this.segmentNameAndFileNameIterator = Collections.EMPTY_LIST.iterator();
File dir = new File(dataDir);
File[] segmentFiles = dir.listFiles(LogSegmentNameHelper.LOG_FILE_FILTER);
if (segmentFiles == null) {
throw new IOException("Could not read from directory: " + dataDir);
} else {
initialize(getSegmentsToLoad(segmentFiles), segmentCapacityInBytes);
}
}
/**
* Create a Log instance
* @param dataDir the directory where the segments of the log need to be loaded from.
* @param totalCapacityInBytes the total capacity of this log.
* @param segmentCapacityInBytes the capacity of a single segment in the log.
* @param metrics the {@link StoreMetrics} instance to use.
* @param isLogSegmented {@code true} if this log is segmented or needs to be segmented.
* @param segmentsToLoad the list of pre-created {@link LogSegment} instances to load.
* @param segmentNameAndFileNameIterator an {@link Iterator} that provides the name and filename for newly allocated
* log segments. Once the iterator ends, the active segment name is used to
* generate the names of the subsequent segments.
* @throws IOException if there is any I/O error loading the segment files.
* @throws IllegalArgumentException if {@code totalCapacityInBytes} or {@code segmentCapacityInBytes} <= 0 or if
* {@code totalCapacityInBytes} > {@code segmentCapacityInBytes} and {@code totalCapacityInBytes} is not a perfect
* multiple of {@code segmentCapacityInBytes}.
*/
Log(String dataDir, long totalCapacityInBytes, long segmentCapacityInBytes, StoreMetrics metrics,
boolean isLogSegmented, List<LogSegment> segmentsToLoad,
Iterator<Pair<String, String>> segmentNameAndFileNameIterator) throws IOException {
this.dataDir = dataDir;
this.capacityInBytes = totalCapacityInBytes;
this.isLogSegmented = isLogSegmented;
this.metrics = metrics;
this.segmentNameAndFileNameIterator = segmentNameAndFileNameIterator;
initialize(segmentsToLoad, segmentCapacityInBytes);
}
/**
* {@inheritDoc}
* <p/>
* Appends the given {@code buffer} to the active log segment. The {@code buffer} will be written to a single log
* segment i.e. its data will not exist across segments.
* @param buffer The buffer from which data needs to be written from
* @return the number of bytes written.
* @throws IllegalArgumentException if the {@code buffer.remaining()} is greater than a single segment's size.
* @throws IllegalStateException if there no more capacity in the log.
* @throws IOException if there was an I/O error while writing.
*/
@Override
public int appendFrom(ByteBuffer buffer) throws IOException {
rollOverIfRequired(buffer.remaining());
return activeSegment.appendFrom(buffer);
}
/**
* {@inheritDoc}
* <p/>
* Appends the given data to the active log segment. The data will be written to a single log segment i.e. the data
* will not exist across segments.
* @param channel The channel from which data needs to be written from
* @param size The amount of data in bytes to be written from the channel
* @throws IllegalArgumentException if the {@code size} is greater than a single segment's size.
* @throws IllegalStateException if there no more capacity in the log.
* @throws IOException if there was an I/O error while writing.
*/
@Override
public void appendFrom(ReadableByteChannel channel, long size) throws IOException {
rollOverIfRequired(size);
activeSegment.appendFrom(channel, size);
}
/**
* Sets the active segment in the log.
* </p>
* Frees all segments that follow the active segment. Therefore, this should be
* used only after the active segment is conclusively determined.
* @param name the name of the log segment that is to be marked active.
* @throws IllegalArgumentException if there no segment with name {@code name}.
* @throws IOException if there is any I/O error freeing segments.
*/
void setActiveSegment(String name) throws IOException {
if (!segmentsByName.containsKey(name)) {
throw new IllegalArgumentException("There is no log segment with name: " + name);
}
ConcurrentNavigableMap<String, LogSegment> extraSegments = segmentsByName.tailMap(name, false);
Iterator<Map.Entry<String, LogSegment>> iterator = extraSegments.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, LogSegment> entry = iterator.next();
logger.info("Freeing extra segment with name [{}] ", entry.getValue().getName());
free(entry.getValue());
remainingUnallocatedSegments.getAndIncrement();
iterator.remove();
}
logger.info("Setting active segment to [{}]", name);
activeSegment = segmentsByName.get(name);
}
/**
* @return the capacity of a single segment.
*/
long getSegmentCapacity() {
// all segments same size
return getFirstSegment().getCapacityInBytes();
}
/**
* @return the total capacity, in bytes, of this log.
*/
long getCapacityInBytes() {
return capacityInBytes;
}
/**
* @return the first {@link LogSegment} instance in the log.
*/
LogSegment getFirstSegment() {
return segmentsByName.firstEntry().getValue();
}
/**
* Returns the {@link LogSegment} that is logically after the given {@code segment}.
* @param segment the {@link LogSegment} whose "next" segment is required.
* @return the {@link LogSegment} that is logically after the given {@code segment}.
*/
LogSegment getNextSegment(LogSegment segment) {
String name = segment.getName();
if (!segmentsByName.containsKey(name)) {
throw new IllegalArgumentException("Invalid log segment name: " + name);
}
Map.Entry<String, LogSegment> nextEntry = segmentsByName.higherEntry(name);
return nextEntry == null ? null : nextEntry.getValue();
}
/**
* Returns the {@link LogSegment} that is logically before the given {@code segment}.
* @param segment the {@link LogSegment} whose "previous" segment is required.
* @return the {@link LogSegment} that is logically before the given {@code segment}.
*/
LogSegment getPrevSegment(LogSegment segment) {
String name = segment.getName();
if (!segmentsByName.containsKey(name)) {
throw new IllegalArgumentException("Invalid log segment name: " + name);
}
Map.Entry<String, LogSegment> prevEntry = segmentsByName.lowerEntry(name);
return prevEntry == null ? null : prevEntry.getValue();
}
/**
* @param name the name of the segment required.
* @return a {@link LogSegment} with {@code name} if it exists.
*/
LogSegment getSegment(String name) {
return segmentsByName.get(name);
}
/**
* @return the end offset of the log abstraction.
*/
Offset getEndOffset() {
LogSegment segment = activeSegment;
return new Offset(segment.getName(), segment.getEndOffset());
}
/**
* Flushes the Log and all its segments.
* @throws IOException if the flush encountered an I/O error.
*/
void flush() throws IOException {
// TODO: try to flush only segments that require flushing.
logger.trace("Flushing log");
for (LogSegment segment : segmentsByName.values()) {
segment.flush();
}
}
/**
* Closes the Log and all its segments.
* @throws IOException if the flush encountered an I/O error.
*/
void close() throws IOException {
for (LogSegment segment : segmentsByName.values()) {
segment.close();
}
}
/**
* Checks the provided arguments for consistency and allocates the first segment file and creates the
* {@link LogSegment} instance for it.
* @param segmentCapacity the intended capacity of each segment of the log.
* @return the {@link LogSegment} instance that is created.
* @throws IOException if there is an I/O error creating the segment files or creating {@link LogSegment} instances.
*/
private LogSegment checkArgsAndGetFirstSegment(long segmentCapacity) throws IOException {
if (capacityInBytes <= 0 || segmentCapacity <= 0) {
throw new IllegalArgumentException(
"One of totalCapacityInBytes [" + capacityInBytes + "] or " + "segmentCapacityInBytes [" + segmentCapacity
+ "] is <=0");
}
segmentCapacity = Math.min(capacityInBytes, segmentCapacity);
// all segments should be the same size.
long numSegments = capacityInBytes / segmentCapacity;
if (capacityInBytes % segmentCapacity != 0) {
throw new IllegalArgumentException(
"Capacity of log [" + capacityInBytes + "] should be a multiple of segment capacity [" + segmentCapacity
+ "]");
}
Pair<String, String> segmentNameAndFilename = getNextSegmentNameAndFilename();
logger.info("Allocating first segment with name [{}], back by file {} and capacity {} bytes. Total number of "
+ "segments is {}", segmentNameAndFilename.getFirst(), segmentNameAndFilename.getSecond(), segmentCapacity,
numSegments);
File segmentFile = allocate(segmentNameAndFilename.getSecond(), segmentCapacity);
// to be backwards compatible, headers are not written for a log segment if it is the only log segment.
return new LogSegment(segmentNameAndFilename.getFirst(), segmentFile, segmentCapacity, metrics, isLogSegmented);
}
/**
* Creates {@link LogSegment} instances from {@code segmentFiles}.
* @param segmentFiles the files that form the segments of the log.
* @return {@code List} of {@link LogSegment} instances corresponding to {@code segmentFiles}.
* @throws IOException if there is an I/O error loading the segment files or creating {@link LogSegment} instances.
*/
private List<LogSegment> getSegmentsToLoad(File[] segmentFiles) throws IOException {
List<LogSegment> segments = new ArrayList<>(segmentFiles.length);
for (File segmentFile : segmentFiles) {
String name = LogSegmentNameHelper.nameFromFilename(segmentFile.getName());
logger.info("Loading segment with name [{}]", name);
LogSegment segment;
if (name.isEmpty()) {
// for backwards compatibility, a single segment log is loaded by providing capacity since the old logs have
// no headers
segment = new LogSegment(name, segmentFile, capacityInBytes, metrics, false);
} else {
segment = new LogSegment(name, segmentFile, metrics);
}
logger.info("Segment [{}] has capacity of {} bytes", name, segment.getCapacityInBytes());
segments.add(segment);
}
return segments;
}
/**
* Initializes the log.
* @param segmentsToLoad the {@link LogSegment} instances to include as a part of the log.
* @param segmentCapacityInBytes the capacity of a single {@link LogSegment}.
* @throws IOException if there is any I/O error during initialization.
*/
private void initialize(List<LogSegment> segmentsToLoad, long segmentCapacityInBytes) throws IOException {
if (segmentsToLoad.size() == 0) {
// bootstrapping log.
segmentsToLoad = Collections.singletonList(checkArgsAndGetFirstSegment(segmentCapacityInBytes));
}
LogSegment firstSegment = segmentsToLoad.get(0);
long totalSegments = firstSegment.getName().isEmpty() ? 1 : capacityInBytes / firstSegment.getCapacityInBytes();
for (LogSegment segment : segmentsToLoad) {
segmentsByName.put(segment.getName(), segment);
}
remainingUnallocatedSegments.set(totalSegments - segmentsByName.size());
activeSegment = segmentsByName.lastEntry().getValue();
}
/**
* Allocates a file named {@code filename} and of capacity {@code size}.
* @param filename the intended filename of the file.
* @param size the intended size of the file.
* @return a {@link File} instance that points to the created file named {@code filename} and capacity {@code size}.
* @throws IOException if the there is any I/O error in allocating the file.
*/
private File allocate(String filename, long size) throws IOException {
// TODO (DiskManager changes): This is intended to "request" the segment file from the DiskManager which will have
// TODO (DiskManager changes): a pool of segments.
File segmentFile = new File(dataDir, filename);
Utils.preAllocateFileIfNeeded(segmentFile, size);
return segmentFile;
}
/**
* Frees the given {@link LogSegment} and its backing segment file.
* @param logSegment the {@link LogSegment} instance whose backing file needs to be freed.
* @throws IOException if there is any I/O error freeing the log segment.
*/
private void free(LogSegment logSegment) throws IOException {
// TODO (DiskManager changes): This will actually return the segment to the DiskManager pool.
File segmentFile = logSegment.getView().getFirst();
logSegment.close();
if (!segmentFile.delete()) {
throw new IllegalStateException("Could not delete segment file: " + segmentFile.getAbsolutePath());
}
}
/**
* Rolls the active log segment over if required. If rollover is required, a new segment is allocated.
* @param writeSize the size of the incoming write.
* @throws IllegalArgumentException if the {@code writeSize} is greater than a single segment's size
* @throws IllegalStateException if there is no more capacity in the log.
* @throws IOException if any I/O error occurred as part of ensuring capacity.
*
*/
private void rollOverIfRequired(long writeSize) throws IOException {
if (activeSegment.getCapacityInBytes() - activeSegment.getEndOffset() < writeSize) {
ensureCapacity(writeSize);
// this cannot be null since capacity has either been ensured or has thrown.
LogSegment nextActiveSegment = segmentsByName.higherEntry(activeSegment.getName()).getValue();
logger.info("Rolling over writes to {} from {} on write of data of size {}. End offset was {} and capacity is {}",
nextActiveSegment.getName(), activeSegment.getName(), writeSize, activeSegment.getEndOffset(),
activeSegment.getCapacityInBytes());
activeSegment = nextActiveSegment;
}
}
/**
* Ensures that there is enough capacity for a write of size {@code writeSize} in the log. As a part of ensuring
* capacity, this function will also allocate more segments if required.
* @param writeSize the size of a subsequent write on the active log segment.
* @throws IllegalArgumentException if the {@code writeSize} is greater than a single segment's size
* @throws IllegalStateException if there no more capacity in the log.
* @throws IOException if any I/O error occurred as a part of ensuring capacity.
*/
private void ensureCapacity(long writeSize) throws IOException {
// all segments are (should be) the same size.
long segmentCapacity = activeSegment.getCapacityInBytes();
if (writeSize > segmentCapacity - LogSegment.HEADER_SIZE) {
metrics.overflowWriteError.inc();
throw new IllegalArgumentException("Write of size [" + writeSize + "] cannot be serviced because it is greater "
+ "than a single segment's capacity [" + (segmentCapacity - LogSegment.HEADER_SIZE) + "]");
}
if (remainingUnallocatedSegments.decrementAndGet() < 0) {
remainingUnallocatedSegments.incrementAndGet();
metrics.overflowWriteError.inc();
throw new IllegalStateException(
"There is no more capacity left in [" + dataDir + "]. Max capacity is [" + capacityInBytes + "]");
}
Pair<String, String> segmentNameAndFilename = getNextSegmentNameAndFilename();
logger.info("Allocating new segment with name: " + segmentNameAndFilename.getFirst());
File newSegmentFile = allocate(segmentNameAndFilename.getSecond(), segmentCapacity);
LogSegment newSegment =
new LogSegment(segmentNameAndFilename.getFirst(), newSegmentFile, segmentCapacity, metrics, true);
segmentsByName.put(segmentNameAndFilename.getFirst(), newSegment);
}
/**
* @return the name and filename of the segment that is to be created.
*/
private Pair<String, String> getNextSegmentNameAndFilename() {
Pair<String, String> nameAndFilename;
if (segmentNameAndFileNameIterator != null && segmentNameAndFileNameIterator.hasNext()) {
nameAndFilename = segmentNameAndFileNameIterator.next();
} else if (activeSegment == null) {
// this code path gets exercised only on first startup
String name = LogSegmentNameHelper.generateFirstSegmentName(isLogSegmented);
nameAndFilename = new Pair<>(name, LogSegmentNameHelper.nameToFilename(name));
} else {
String name = LogSegmentNameHelper.getNextPositionName(activeSegment.getName());
nameAndFilename = new Pair<>(name, LogSegmentNameHelper.nameToFilename(name));
}
return nameAndFilename;
}
/**
* Adds a {@link LogSegment} instance to the log.
* @param segment the {@link LogSegment} instance to add.
* @param increaseUsedSegmentCount {@code true} if the number of segments used has to be incremented, {@code false}
* otherwise.
* @throws IllegalArgumentException if the {@code segment} being added is past the active segment
*/
void addSegment(LogSegment segment, boolean increaseUsedSegmentCount) {
if (LogSegmentNameHelper.COMPARATOR.compare(segment.getName(), activeSegment.getName()) >= 0) {
throw new IllegalArgumentException(
"Cannot add segments past the current active segment. Active segment is [" + activeSegment.getName()
+ "]. Tried to add [" + segment.getName() + "]");
}
if (increaseUsedSegmentCount) {
remainingUnallocatedSegments.decrementAndGet();
}
segmentsByName.put(segment.getName(), segment);
}
/**
* Drops an existing {@link LogSegment} instance from the log.
* @param segmentName the {@link LogSegment} instance to drop.
* @param decreaseUsedSegmentCount {@code true} if the number of segments used has to be decremented, {@code false}
* otherwise.
* @throws IllegalArgumentException if {@code segmentName} is not a part of the log.
* @throws IOException if there is any I/O error cleaning up the log segment.
*/
void dropSegment(String segmentName, boolean decreaseUsedSegmentCount) throws IOException {
LogSegment segment = segmentsByName.get(segmentName);
if (segment == null || segment == activeSegment) {
throw new IllegalArgumentException("Segment does not exist or is the active segment: " + segmentName);
}
segmentsByName.remove(segmentName);
free(segment);
if (decreaseUsedSegmentCount) {
remainingUnallocatedSegments.incrementAndGet();
}
}
/**
* Gets the {@link FileSpan} for a message that is written starting at {@code endOffsetOfPrevMessage} and is of size
* {@code size}.
* @param endOffsetOfPrevMessage the end offset of the message that is before this one.
* @param size the size of the write.
* @return the {@link FileSpan} for a message that is written starting at {@code endOffsetOfPrevMessage} and is of
* size {@code size}.
*/
FileSpan getFileSpanForMessage(Offset endOffsetOfPrevMessage, long size) {
LogSegment segment = segmentsByName.get(endOffsetOfPrevMessage.getName());
long startOffset = endOffsetOfPrevMessage.getOffset();
if (startOffset > segment.getEndOffset()) {
throw new IllegalArgumentException("Start offset provided is greater than segment end offset");
} else if (startOffset == segment.getEndOffset()) {
// current segment has ended. Since a blob will be wholly contained within one segment, this blob is in the
// next segment
segment = getNextSegment(segment);
startOffset = segment.getStartOffset();
} else if (startOffset + size > segment.getEndOffset()) {
throw new IllegalStateException("Args indicate that blob is not wholly contained within a single segment");
}
return new FileSpan(new Offset(segment.getName(), startOffset), new Offset(segment.getName(), startOffset + size));
}
}