All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.cassandra.db.commitlog.CommitLogArchiver Maven / Gradle / Ivy

Go to download

The Apache Cassandra Project develops a highly scalable second-generation distributed database, bringing together Dynamo's fully distributed design and Bigtable's ColumnFamily-based data model.

There is a newer version: 5.0.2
Show newest version
/*
 *
 * 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy