/** * 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.HorizonDBException; import io.horizondb.io.ReadableBuffer; import io.horizondb.model.ErrorCodes; import java.io.IOException; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import org.apache.commons.lang.Validate; import com.codahale.metrics.MetricRegistry; import com.google.common.util.concurrent.ListenableFuture; public final class CommitLog extends AbstractComponent { /** * The possible sync modes. */ public enum SyncMode { /** * Flushes the data to the disk after the specified interval of time. */ BATCH, /** * Flushes the data to the disk at specified interval of time. */ PERIODIC; } /** * The database configuration. */ private final Configuration configuration; /** * Performs the pre-allocation in the background of the segments. */ private final CommitLogAllocator allocator; /** * The executor used to performs the writes. */ private WriteExecutor executor; /** * The segment on which the writes must be performed. */ public CommitLogSegment activeSegment; public CommitLog(Configuration configuration, StorageEngine databaseEngine) { Validate.notNull(configuration, "the configuration parameter must be not null"); Validate.notNull(databaseEngine, "the databaseEngine parameter must be not null"); this.configuration = configuration; this.allocator = new CommitLogAllocator(configuration, databaseEngine); } /** * {@inheritDoc} */ @Override public void register(MetricRegistry registry) { this.allocator.register(registry); this.executor.register(registry); } /** * {@inheritDoc} */ @Override public void unregister(MetricRegistry registry) { this.executor.unregister(registry); this.allocator.unregister(registry); } /** * Writes the operation represented by the specified bytes to the commit log. * * @param bytes the bytes to add to the log * @return the future returning the <code>ReplayPosition</code> for the written bytes. */ public ListenableFuture<ReplayPosition> write(ReadableBuffer bytes) { checkRunning(); return this.executor.executeWrite(new WriteTask(bytes)); } /** * Waits for the commit log to flush the data to the disk if the sync mode is batch * * @param configuration the database configuration * @param future the commit log future * @throws HorizonDBException if a problem occurs while writing to the commit log */ public static void waitForCommitLogWriteIfNeeded(Configuration configuration, ListenableFuture<ReplayPosition> future) throws HorizonDBException { if (configuration.getCommitLogSyncMode() == CommitLog.SyncMode.BATCH) { waitForCommitLogWrite(future); } } /** * Wait for the specified future to complete. * * @param future the future * @throws HorizonDBException if an error occurs */ public static void waitForCommitLogWrite(ListenableFuture<ReplayPosition> future) throws HorizonDBException { try { future.get(); } catch (ExecutionException e) { throw new HorizonDBException(ErrorCodes.INTERNAL_ERROR, "an internal error has occured: ", e.getCause()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new HorizonDBException(ErrorCodes.INTERNAL_ERROR, "an internal error has occured: ", e.getCause()); } } /** * {@inheritDoc} */ @Override protected void doStart() throws IOException, InterruptedException { this.allocator.start(); if (this.configuration.getCommitLogSyncMode() == SyncMode.BATCH) { this.executor = new BatchWriteExecutor(this.configuration, new FlushTask()); } else { this.executor = new PeriodicWriteExecutor(this.configuration, new FlushTask()); } } /** * * {@inheritDoc} */ @Override protected void doShutdown() throws InterruptedException { try { this.executor.shutdown(); } finally { this.allocator.shutdown(); } } /** * Fetches a new segment file from the allocator and activates it. * * @return the newly activated segment * @throws InterruptedException */ private void activateNextSegment() throws InterruptedException { this.logger.debug("Activating new segment"); this.activeSegment = this.allocator.fetchSegment(); this.logger.debug("Active segment is now {}", this.activeSegment); } /** * Returns the active segment. * * @return the active segment. */ private CommitLogSegment getActiveSegment() { return this.activeSegment; } /** * Flushes the data of all the active commit log segments to disk. * * @throws IOException if a problem occurs while flushing the data. */ private void flush() throws IOException { for (CommitLogSegment segment : this.allocator.getActiveSegments()) { segment.flush(); } } /** * <code>Callable</code> in charge of performing the write to the commit log segment. * */ class WriteTask implements Callable<ReplayPosition> { /** * The bytes to write to the commit log. */ private final ReadableBuffer bytes; /** * Creates a <code>WriteTask</code> that will write the specified bytes into the actve segment. * * @param bytes the bytes to write. */ public WriteTask(ReadableBuffer bytes) { this.bytes = bytes; } /** * {@inheritDoc} */ @Override public ReplayPosition call() throws InterruptedException, IOException { if (getActiveSegment() == null || !getActiveSegment().hasCapacityFor(this.bytes.readableBytes())) { activateNextSegment(); } return getActiveSegment().write(this.bytes); } } /** * Flushes all the in memory data to the disk. * */ final class FlushTask implements Runnable { /** * {@inheritDoc} */ @Override public void run() { try { flush(); } catch (IOException e) { CommitLog.this.logger.error("", e); } } } }