blob: 85f7ef67f1163a98f781c6fe6e489e39bc9e9d28 [file] [log] [blame]
/*
* 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.io.sstable;
import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Splitter;
import org.apache.cassandra.db.Directories;
import org.apache.cassandra.io.sstable.format.SSTableFormat;
import org.apache.cassandra.io.sstable.format.Version;
import org.apache.cassandra.io.sstable.metadata.IMetadataSerializer;
import org.apache.cassandra.io.sstable.metadata.MetadataSerializer;
import org.apache.cassandra.utils.Pair;
import org.apache.cassandra.utils.UUIDGen;
import static org.apache.cassandra.io.sstable.Component.separator;
/**
* A SSTable is described by the keyspace and column family it contains data
* for, a generation (where higher generations contain more recent data) and
* an alphabetic version string.
*
* A descriptor can be marked as temporary, which influences generated filenames.
*/
public class Descriptor
{
private final static String LEGACY_TMP_REGEX_STR = "^((.*)\\-(.*)\\-)?tmp(link)?\\-((?:l|k).)\\-(\\d)*\\-(.*)$";
private final static Pattern LEGACY_TMP_REGEX = Pattern.compile(LEGACY_TMP_REGEX_STR);
public static String TMP_EXT = ".tmp";
private static final Splitter filenameSplitter = Splitter.on('-');
/** canonicalized path to the directory where SSTable resides */
public final File directory;
/** version has the following format: <code>[a-z]+</code> */
public final Version version;
public final String ksname;
public final String cfname;
public final int generation;
public final SSTableFormat.Type formatType;
private final int hashCode;
/**
* A descriptor that assumes CURRENT_VERSION.
*/
@VisibleForTesting
public Descriptor(File directory, String ksname, String cfname, int generation)
{
this(SSTableFormat.Type.current().info.getLatestVersion(), directory, ksname, cfname, generation, SSTableFormat.Type.current());
}
/**
* Constructor for sstable writers only.
*/
public Descriptor(File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType)
{
this(formatType.info.getLatestVersion(), directory, ksname, cfname, generation, formatType);
}
@VisibleForTesting
public Descriptor(String version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType)
{
this(formatType.info.getVersion(version), directory, ksname, cfname, generation, formatType);
}
public Descriptor(Version version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType)
{
assert version != null && directory != null && ksname != null && cfname != null && formatType.info.getLatestVersion().getClass().equals(version.getClass());
this.version = version;
try
{
this.directory = directory.getCanonicalFile();
}
catch (IOException e)
{
throw new IOError(e);
}
this.ksname = ksname;
this.cfname = cfname;
this.generation = generation;
this.formatType = formatType;
hashCode = Objects.hashCode(version, this.directory, generation, ksname, cfname, formatType);
}
public Descriptor withGeneration(int newGeneration)
{
return new Descriptor(version, directory, ksname, cfname, newGeneration, formatType);
}
public Descriptor withFormatType(SSTableFormat.Type newType)
{
return new Descriptor(newType.info.getLatestVersion(), directory, ksname, cfname, generation, newType);
}
public String tmpFilenameFor(Component component)
{
return filenameFor(component) + TMP_EXT;
}
/**
* @return a unique temporary file name for given component during entire-sstable-streaming.
*/
public String tmpFilenameForStreaming(Component component)
{
// Use UUID to handle concurrent streamings on the same sstable.
// TMP_EXT allows temp file to be removed by {@link ColumnFamilyStore#scrubDataDirectories}
return String.format("%s.%s%s", filenameFor(component), UUIDGen.getTimeUUID(), TMP_EXT);
}
public String filenameFor(Component component)
{
return baseFilename() + separator + component.name();
}
public String baseFilename()
{
StringBuilder buff = new StringBuilder();
buff.append(directory).append(File.separatorChar);
appendFileName(buff);
return buff.toString();
}
private void appendFileName(StringBuilder buff)
{
buff.append(version).append(separator);
buff.append(generation);
buff.append(separator).append(formatType.name);
}
public String relativeFilenameFor(Component component)
{
final StringBuilder buff = new StringBuilder();
if (Directories.isSecondaryIndexFolder(directory))
{
buff.append(directory.getName()).append(File.separator);
}
appendFileName(buff);
buff.append(separator).append(component.name());
return buff.toString();
}
public SSTableFormat getFormat()
{
return formatType.info;
}
/** Return any temporary files found in the directory */
public List<File> getTemporaryFiles()
{
File[] tmpFiles = directory.listFiles((dir, name) ->
name.endsWith(Descriptor.TMP_EXT));
List<File> ret = new ArrayList<>(tmpFiles.length);
for (File tmpFile : tmpFiles)
ret.add(tmpFile);
return ret;
}
public static boolean isValidFile(File file)
{
String filename = file.getName();
return filename.endsWith(".db") && !LEGACY_TMP_REGEX.matcher(filename).matches();
}
/**
* Parse a sstable filename into a Descriptor.
* <p>
* This is a shortcut for {@code fromFilename(new File(filename))}.
*
* @param filename the filename to a sstable component.
* @return the descriptor for the parsed file.
*
* @throws IllegalArgumentException if the provided {@code file} does point to a valid sstable filename. This could
* mean either that the filename doesn't look like a sstable file, or that it is for an old and unsupported
* versions.
*/
public static Descriptor fromFilename(String filename)
{
return fromFilename(new File(filename));
}
/**
* Parse a sstable filename into a Descriptor.
* <p>
* SSTables files are all located within subdirectories of the form {@code <keyspace>/<table>/}. Normal sstables are
* are directly within that subdirectory structure while 2ndary index, backups and snapshot are each inside an
* additional subdirectory. The file themselves have the form:
* {@code <version>-<gen>-<format>-<component>}.
* <p>
* Note that this method will only sucessfully parse sstable files of supported versions.
*
* @param file the {@code File} object for the filename to parse.
* @return the descriptor for the parsed file.
*
* @throws IllegalArgumentException if the provided {@code file} does point to a valid sstable filename. This could
* mean either that the filename doesn't look like a sstable file, or that it is for an old and unsupported
* versions.
*/
public static Descriptor fromFilename(File file)
{
return fromFilenameWithComponent(file).left;
}
/**
* Parse a sstable filename, extracting both the {@code Descriptor} and {@code Component} part.
*
* @param file the {@code File} object for the filename to parse.
* @return a pair of the descriptor and component corresponding to the provided {@code file}.
*
* @throws IllegalArgumentException if the provided {@code file} does point to a valid sstable filename. This could
* mean either that the filename doesn't look like a sstable file, or that it is for an old and unsupported
* versions.
*/
public static Pair<Descriptor, Component> fromFilenameWithComponent(File file)
{
// We need to extract the keyspace and table names from the parent directories, so make sure we deal with the
// absolute path.
if (!file.isAbsolute())
file = file.getAbsoluteFile();
String name = file.getName();
List<String> tokens = filenameSplitter.splitToList(name);
int size = tokens.size();
if (size != 4)
{
// This is an invalid sstable file for this version. But to provide a more helpful error message, we detect
// old format sstable, which had the format:
// <keyspace>-<table>-(tmp-)?<version>-<gen>-<component>
// Note that we assume it's an old format sstable if it has the right number of tokens: this is not perfect
// but we're just trying to be helpful, not perfect.
if (size == 5 || size == 6)
throw new IllegalArgumentException(String.format("%s is of version %s which is now unsupported and cannot be read.",
name,
tokens.get(size - 3)));
throw new IllegalArgumentException(String.format("Invalid sstable file %s: the name doesn't look like a supported sstable file name", name));
}
String versionString = tokens.get(0);
if (!Version.validate(versionString))
throw invalidSSTable(name, "invalid version %s", versionString);
int generation;
try
{
generation = Integer.parseInt(tokens.get(1));
}
catch (NumberFormatException e)
{
throw invalidSSTable(name, "the 'generation' part of the name doesn't parse as a number");
}
String formatString = tokens.get(2);
SSTableFormat.Type format;
try
{
format = SSTableFormat.Type.validate(formatString);
}
catch (IllegalArgumentException e)
{
throw invalidSSTable(name, "unknown 'format' part (%s)", formatString);
}
Component component = Component.parse(tokens.get(3));
Version version = format.info.getVersion(versionString);
if (!version.isCompatible())
throw invalidSSTable(name, "incompatible sstable version (%s); you should have run upgradesstables before upgrading", versionString);
File directory = parentOf(name, file);
File tableDir = directory;
// Check if it's a 2ndary index directory (not that it doesn't exclude it to be also a backup or snapshot)
String indexName = "";
if (Directories.isSecondaryIndexFolder(tableDir))
{
indexName = tableDir.getName();
tableDir = parentOf(name, tableDir);
}
// Then it can be a backup or a snapshot
if (tableDir.getName().equals(Directories.BACKUPS_SUBDIR) && tableDir.getParentFile().getName().contains("-"))
tableDir = tableDir.getParentFile();
else
{
File keyspaceOrSnapshotDir = parentOf(name, tableDir);
if (keyspaceOrSnapshotDir.getName().equals(Directories.SNAPSHOT_SUBDIR)
&& parentOf(name, keyspaceOrSnapshotDir).getName().contains("-"))
tableDir = parentOf(name, keyspaceOrSnapshotDir);
}
String table = tableDir.getName().split("-")[0] + indexName;
String keyspace = parentOf(name, tableDir).getName();
return Pair.create(new Descriptor(version, directory, keyspace, table, generation, format), component);
}
private static File parentOf(String name, File file)
{
File parent = file.getParentFile();
if (parent == null)
throw invalidSSTable(name, "cannot extract keyspace and table name; make sure the sstable is in the proper sub-directories");
return parent;
}
private static IllegalArgumentException invalidSSTable(String name, String msgFormat, Object... parameters)
{
throw new IllegalArgumentException(String.format("Invalid sstable file " + name + ": " + msgFormat, parameters));
}
public IMetadataSerializer getMetadataSerializer()
{
return new MetadataSerializer();
}
/**
* @return true if the current Cassandra version can read the given sstable version
*/
public boolean isCompatible()
{
return version.isCompatible();
}
@Override
public String toString()
{
return baseFilename();
}
@Override
public boolean equals(Object o)
{
if (o == this)
return true;
if (!(o instanceof Descriptor))
return false;
Descriptor that = (Descriptor)o;
return that.directory.equals(this.directory)
&& that.generation == this.generation
&& that.ksname.equals(this.ksname)
&& that.cfname.equals(this.cfname)
&& that.version.equals(this.version)
&& that.formatType == this.formatType;
}
@Override
public int hashCode()
{
return hashCode;
}
}