/**
* Copyright 2013 Benjamin Lerer
*
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.horizondb.db.commitlog;
import io.horizondb.db.AbstractComponent;
import io.horizondb.db.Configuration;
import io.horizondb.db.StorageEngine;
import io.horizondb.db.metrics.PrefixFilter;
import io.horizondb.db.metrics.ThreadPoolExecutorMetrics;
import io.horizondb.db.util.concurrent.ExecutorsUtils;
import io.horizondb.db.util.concurrent.NamedThreadFactory;
import io.horizondb.db.util.concurrent.SyncTask;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.annotation.concurrent.ThreadSafe;
import com.codahale.metrics.MetricRegistry;
import com.google.common.util.concurrent.ListenableFuture;
import static com.codahale.metrics.MetricRegistry.name;
/**
* Performs the preallocation and recycling of commit log segment files.
*
* @author Benjamin
*/
@ThreadSafe
final class CommitLogAllocator extends AbstractComponent {
/**
* The database configuration.
*/
private final Configuration configuration;
/**
* The database engine.
*/
private final StorageEngine databaseEngine;
/**
* Segments ready to be used
*/
private final BlockingQueue<CommitLogSegment> availableSegments = new LinkedBlockingQueue<>();
/**
* Active segments, containing non flushed data
*/
private final ConcurrentLinkedQueue<CommitLogSegment> activeSegments = new ConcurrentLinkedQueue<>();
/**
* The executor running the allocation tasks.
*/
private ExecutorService executor;
/**
* Creates a new <code>CommitLogAllocator</code> instance that will use the specified configuration.
*
* @param configuration the database configuration.
* @param databaseEngine the database engine.
*/
public CommitLogAllocator(Configuration configuration, StorageEngine databaseEngine) {
this.databaseEngine = databaseEngine;
this.configuration = configuration;
}
/**
* {@inheritDoc}
*/
@Override
public void register(MetricRegistry registry) {
registry.registerAll(new ThreadPoolExecutorMetrics(name(getName(), "executor"),
(ThreadPoolExecutor) this.executor));
}
/**
* {@inheritDoc}
*/
@Override
public void unregister(MetricRegistry registry) {
registry.removeMatching(new PrefixFilter(getName()));
}
/**
* Fetches the next writable segment file.
*
* @return the next writable segment
* @throws InterruptedException if the tread is interrupted while waiting for an available segment.
*/
public CommitLogSegment fetchSegment() throws InterruptedException {
checkRunning();
this.logger.debug("Retrieving next available segment.");
CommitLogSegment next = this.availableSegments.poll();
if (next == null) {
if (this.activeSegments.size() == this.configuration.getMaximumNumberOfCommitLogSegments() - 1) {
// The recycling is taking too much time or is blocked.
this.executor.execute(new AllocationTask(true));
}
next = this.availableSegments.poll(10, TimeUnit.SECONDS);
}
this.activeSegments.add(next);
this.logger.debug("CommitLogSegment {} has been added to the active segments.", Long.valueOf(next.getId()));
this.executor.execute(new AllocationTask());
return next;
}
/**
* {@inheritDoc}
*/
@Override
protected void doStart() throws IOException {
ThreadFactory threadFactory = new NamedThreadFactory(getName() + "-SegmentAllocator");
this.executor = Executors.newFixedThreadPool(1, threadFactory);
loadActiveSegmentsFromDisk();
replayActiveSegments();
this.executor.execute(new AllocationTask());
}
/**
* {@inheritDoc}
*/
@Override
protected void doShutdown() throws InterruptedException {
try {
ExecutorsUtils.shutdownAndAwaitForTermination(this.executor,
this.configuration.getShutdownWaitingTimeInSeconds());
} finally {
closeActiveSegments();
closeAvailableSegments();
}
}
/**
* Returns the active segments.
*
* @return the active segments.
*/
Iterable<CommitLogSegment> getActiveSegments() {
return this.activeSegments;
}
/**
* Blocks until all the allocation task previously submitted have been completed.
* <p>
* This method is implemented for testing purpose.
* </p>
*
* @throws Exception if a problem occurs while synchronizing.
*/
void sync() throws Exception {
this.executor.submit(new SyncTask()).get();
}
/**
* Allocates a new segment by either creating a new one or by recycling the oldest one.
*
* @throws InterruptedException if the current thread has been interrupted.
*/
private void allocateNextSegment() throws InterruptedException {
if (hasReachMaximumNumberOfSegments()) {
recycleSegment();
} else {
createNewSegment();
}
}
/**
* Checks if the maximum number of segments has been reached.
*
* @return <code>true</code> if the maximum number of segments has been reached, <code>false</code> otherwise.
*/
private boolean hasReachMaximumNumberOfSegments() {
return this.activeSegments.size() >= this.configuration.getMaximumNumberOfCommitLogSegments();
}
/**
* Creates a new commit log segment.
*
* @throws InterruptedException
*/
private void createNewSegment() throws InterruptedException {
try {
this.availableSegments.add(CommitLogSegment.freshSegment(this.configuration));
} catch (IOException e) {
this.logger.error("An error has occured while creating a new segment.", e);
}
}
/**
* Recycles the oldest active segment forcing the table manager to flush some data to the disk if some data of the
* segment have not been flushed to the disk yet.
*
* @throws InterruptedException if the current thread was interrupted while waiting for the table manager to flush
* some data.
*/
private void recycleSegment() throws InterruptedException {
final CommitLogSegment old = this.activeSegments.poll();
try {
old.flush();
ListenableFuture<?> future = this.databaseEngine.forceFlush(old.getId());
future.addListener(new Runnable() {
/**
* {@inheritDoc}
*/
@Override
public void run() {
try {
CommitLogAllocator.this.availableSegments.add(CommitLogSegment.recycleSegment(old));
} catch (IOException | InterruptedException e) {
CommitLogAllocator.this.logger.error("An error has occured while recycling the segment " + old.getPath() + " .", e);
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
}
}
}, this.executor);
} catch (IOException e) {
this.logger.error("An error has occured while recycling the segment " + old.getPath() + " .", e);
}
}
/**
* Loads the active segments from the disk.
*
* @throws IOException if a problem occurs while retrieving the segments from the disk.
*/
private void loadActiveSegmentsFromDisk() throws IOException {
this.activeSegments.addAll(loadSegmentsFromDisk());
}
/**
* Replays the active segments in order.
*
* @throws IOException if a problem occurs while replaying the segments.
*/
private void replayActiveSegments() throws IOException {
for (CommitLogSegment segment : this.activeSegments) {
this.logger.info("Replaying segment: " + segment.getPath().getFileName());
int count = segment.replay(this.databaseEngine);
this.logger.info("{} messages have been replayed from segment: {}",
Integer.valueOf(count),
segment.getPath().getFileName());
}
}
/**
* Loads the segments found in the commit log directory.
*
* @return the segments found in the commit log directory in order.
* @throws IOException if a problem occurs while loading the segments.
*/
private List<CommitLogSegment> loadSegmentsFromDisk() throws IOException {
Path logDirectory = this.configuration.getCommitLogDirectory();
if (!Files.exists(logDirectory)) {
this.logger.debug("Creating commitlog directory: {}.", logDirectory);
Files.createDirectory(logDirectory);
return Collections.emptyList();
}
this.logger.debug("Loading commitlog segments from the directory: {}", logDirectory);
final List<CommitLogSegment> segments = new ArrayList<>();
Files.walkFileTree(logDirectory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (CommitLogSegment.isCommitLogSegment(file)) {
try {
segments.add(CommitLogSegment.loadFromFile(CommitLogAllocator.this.configuration, file));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return FileVisitResult.CONTINUE;
}
});
Collections.sort(segments);
return segments;
}
/**
* Closes the available segments.
*/
private void closeAvailableSegments() {
closeSegments(this.availableSegments);
}
/**
* Closes the active segments.
*/
private void closeActiveSegments() {
closeSegments(this.activeSegments);
}
/**
* Closes the specified segments.
*
* @param segments the segments to close.
*/
private static void closeSegments(Iterable<CommitLogSegment> segments) {
for (CommitLogSegment segment : segments) {
segment.close();
}
}
/**
* <code>Runnable</code> that will try to allocate a new segment for future use.
*
*/
private class AllocationTask implements Runnable {
/**
* Force the creation of a new segment.
*/
private final boolean forceCreation;
/**
* Creates a new <code>AllocationTask</code>
*/
public AllocationTask() {
this(false);
}
/**
* Creates a new <code>AllocationTask</code>
* @param forceCreation force the creation of a new segment even if the limit has been reached.
*/
public AllocationTask(boolean forceCreation) {
this.forceCreation = forceCreation;
}
/**
* {@inheritDoc}
*/
@Override
public void run() {
try {
if (this.forceCreation) {
createNewSegment();
} else {
allocateNextSegment();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}