org.apache.cassandra.db.commitlog.CommitLogArchiver Maven / Gradle / Ivy
Show all versions of cassandra-all Show documentation
/*
*
* 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.cassandra.db.commitlog;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.JMXEnabledThreadPoolExecutor;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.schema.CompressionParams;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.WrappedRunnable;
public class CommitLogArchiver
{
private static final Logger logger = LoggerFactory.getLogger(CommitLogArchiver.class);
public static final DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss[.[SSSSSS][SSS]]").withZone(ZoneId.of("GMT"));
private static final String COMMITLOG_ARCHIVNG_PROPERTIES_FILE_NAME = "commitlog_archiving.properties";
private static final String DELIMITER = ",";
private static final Pattern NAME = Pattern.compile("%name");
private static final Pattern PATH = Pattern.compile("%path");
private static final Pattern FROM = Pattern.compile("%from");
private static final Pattern TO = Pattern.compile("%to");
public final Map> archivePending = new ConcurrentHashMap>();
private final ExecutorService executor;
final String archiveCommand;
final String restoreCommand;
final String restoreDirectories;
TimeUnit precision;
long restorePointInTimeInMicroseconds;
public CommitLogArchiver(String archiveCommand,
String restoreCommand,
String restoreDirectories,
long restorePointInTimeInMicroseconds,
TimeUnit precision)
{
this.archiveCommand = archiveCommand;
this.restoreCommand = restoreCommand;
this.restoreDirectories = restoreDirectories;
this.restorePointInTimeInMicroseconds = restorePointInTimeInMicroseconds;
this.precision = precision;
executor = !Strings.isNullOrEmpty(archiveCommand) ? new JMXEnabledThreadPoolExecutor("CommitLogArchiver") : null;
}
public static CommitLogArchiver disabled()
{
return new CommitLogArchiver(null, null, null, Long.MAX_VALUE, TimeUnit.MICROSECONDS);
}
public static CommitLogArchiver construct()
{
Properties commitlogProperties = new Properties();
try (InputStream stream = CommitLogArchiver.class.getClassLoader().getResourceAsStream(COMMITLOG_ARCHIVNG_PROPERTIES_FILE_NAME))
{
if (stream == null)
{
logger.trace("No {} found; archiving and point-in-time-restoration will be disabled", COMMITLOG_ARCHIVNG_PROPERTIES_FILE_NAME);
return disabled();
}
else
{
commitlogProperties.load(stream);
return getArchiverFromProperties(commitlogProperties);
}
}
catch (IOException e)
{
throw new RuntimeException("Unable to load " + COMMITLOG_ARCHIVNG_PROPERTIES_FILE_NAME, e);
}
}
@VisibleForTesting
static CommitLogArchiver getArchiverFromProperties(Properties commitlogCommands)
{
assert !commitlogCommands.isEmpty();
String archiveCommand = commitlogCommands.getProperty("archive_command");
String restoreCommand = commitlogCommands.getProperty("restore_command");
String restoreDirectories = commitlogCommands.getProperty("restore_directories");
if (restoreDirectories != null && !restoreDirectories.isEmpty())
{
for (String dir : restoreDirectories.split(DELIMITER))
{
File directory = new File(dir);
if (!directory.exists())
{
if (!directory.mkdir())
{
throw new RuntimeException("Unable to create directory: " + dir);
}
}
}
}
String precisionPropertyValue = commitlogCommands.getProperty("precision", TimeUnit.MICROSECONDS.name());
TimeUnit precision;
try
{
precision = TimeUnit.valueOf(precisionPropertyValue);
}
catch (IllegalArgumentException ex)
{
throw new RuntimeException("Unable to parse precision of value " + precisionPropertyValue, ex);
}
if (precision == TimeUnit.NANOSECONDS)
throw new RuntimeException("NANOSECONDS level precision is not supported.");
String targetTime = commitlogCommands.getProperty("restore_point_in_time");
long restorePointInTime = Long.MAX_VALUE;
try
{
if (!Strings.isNullOrEmpty(targetTime))
{
// get restorePointInTime in microseconds level by default as cassandra use this level's timestamp
restorePointInTime = getRestorationPointInTimeInMicroseconds(targetTime);
}
}
catch (DateTimeParseException e)
{
throw new RuntimeException("Unable to parse restore target time", e);
}
return new CommitLogArchiver(archiveCommand, restoreCommand, restoreDirectories, restorePointInTime, precision);
}
public void maybeArchive(final CommitLogSegment segment)
{
if (Strings.isNullOrEmpty(archiveCommand))
return;
archivePending.put(segment.getName(), executor.submit(new WrappedRunnable()
{
protected void runMayThrow() throws IOException
{
segment.waitForFinalSync();
String command = NAME.matcher(archiveCommand).replaceAll(Matcher.quoteReplacement(segment.getName()));
command = PATH.matcher(command).replaceAll(Matcher.quoteReplacement(segment.getPath()));
exec(command);
}
}));
}
/**
* Differs from the above because it can be used on any file, rather than only
* managed commit log segments (and thus cannot call waitForFinalSync), and in
* the treatment of failures.
*
* Used to archive files present in the commit log directory at startup (CASSANDRA-6904).
* Since the files being already archived by normal operation could cause subsequent
* hard-linking or other operations to fail, we should not throw errors on failure
*/
public void maybeArchive(final String path, final String name)
{
if (Strings.isNullOrEmpty(archiveCommand))
return;
archivePending.put(name, executor.submit(() -> {
try
{
String command = NAME.matcher(archiveCommand).replaceAll(Matcher.quoteReplacement(name));
command = PATH.matcher(command).replaceAll(Matcher.quoteReplacement(path));
exec(command);
}
catch (IOException e)
{
logger.warn("Archiving file {} failed, file may have already been archived.", name, e);
}
}));
}
public boolean maybeWaitForArchiving(String name)
{
Future> f = archivePending.remove(name);
if (f == null)
return true; // archiving disabled
try
{
f.get();
}
catch (InterruptedException e)
{
throw new AssertionError(e);
}
catch (ExecutionException e)
{
if (e.getCause() instanceof RuntimeException)
{
if (e.getCause().getCause() instanceof IOException)
{
logger.error("Looks like the archiving of file {} failed earlier, Cassandra is going to ignore this segment for now.", name, e.getCause().getCause());
return false;
}
}
throw new RuntimeException(e);
}
return true;
}
public void maybeRestoreArchive()
{
if (Strings.isNullOrEmpty(restoreDirectories))
return;
for (String dir : restoreDirectories.split(DELIMITER))
{
File[] files = new File(dir).listFiles();
if (files == null)
{
throw new RuntimeException("Unable to list directory " + dir);
}
for (File fromFile : files)
{
CommitLogDescriptor fromHeader = CommitLogDescriptor.fromHeader(fromFile, DatabaseDescriptor.getEncryptionContext());
CommitLogDescriptor fromName = CommitLogDescriptor.isValid(fromFile.getName()) ? CommitLogDescriptor.fromFileName(fromFile.getName()) : null;
CommitLogDescriptor descriptor;
if (fromHeader == null && fromName == null)
throw new IllegalStateException("Cannot safely construct descriptor for segment, either from its name or its header: " + fromFile.getPath());
else if (fromHeader != null && fromName != null && !fromHeader.equalsIgnoringCompression(fromName))
throw new IllegalStateException(String.format("Cannot safely construct descriptor for segment, as name and header descriptors do not match (%s vs %s): %s", fromHeader, fromName, fromFile.getPath()));
else if (fromName != null && fromHeader == null)
throw new IllegalStateException("Cannot safely construct descriptor for segment, as name descriptor implies a version that should contain a header descriptor, but that descriptor could not be read: " + fromFile.getPath());
else if (fromHeader != null)
descriptor = fromHeader;
else descriptor = fromName;
if (descriptor.version > CommitLogDescriptor.current_version)
throw new IllegalStateException("Unsupported commit log version: " + descriptor.version);
if (descriptor.compression != null)
{
try
{
CompressionParams.createCompressor(descriptor.compression);
}
catch (ConfigurationException e)
{
throw new IllegalStateException("Unknown compression", e);
}
}
File toFile = new File(DatabaseDescriptor.getCommitLogLocation(), descriptor.fileName());
if (toFile.exists())
{
logger.trace("Skipping restore of archive {} as the segment already exists in the restore location {}",
fromFile.getPath(), toFile.getPath());
continue;
}
String command = FROM.matcher(restoreCommand).replaceAll(Matcher.quoteReplacement(fromFile.getPath()));
command = TO.matcher(command).replaceAll(Matcher.quoteReplacement(toFile.getPath()));
try
{
exec(command);
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
}
}
private void exec(String command) throws IOException
{
ProcessBuilder pb = new ProcessBuilder(command.split(" "));
pb.redirectErrorStream(true);
FBUtilities.exec(pb);
}
/**
* We change the restore_point_in_time from configuration file into microseconds level as Cassandra use microseconds
* as the timestamp.
*
* @param restorationPointInTime value of "restore_point_in_time" in properties file.
* @return microseconds value of restore_point_in_time
*/
@VisibleForTesting
public static long getRestorationPointInTimeInMicroseconds(String restorationPointInTime)
{
assert !Strings.isNullOrEmpty(restorationPointInTime) : "restore_point_in_time is null or empty!";
Instant instant = format.parse(restorationPointInTime, Instant::from);
return instant.getEpochSecond() * 1_000_000 + instant.getNano() / 1000;
}
public long getRestorePointInTimeInMicroseconds()
{
return this.restorePointInTimeInMicroseconds;
}
@VisibleForTesting
public void setRestorePointInTimeInMicroseconds(long restorePointInTimeInMicroseconds)
{
this.restorePointInTimeInMicroseconds = restorePointInTimeInMicroseconds;
}
@VisibleForTesting
public void setPrecision(TimeUnit timeUnit)
{
this.precision = timeUnit;
}
}