blob: d10092a41af8892778ad4469c343f6fb52321351 [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.service.snapshot;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.db.Directories;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.utils.DirectorySizeCalculator;
public class TableSnapshot
{
private static final Logger logger = LoggerFactory.getLogger(TableSnapshot.class);
private final String keyspaceName;
private final String tableName;
private final UUID tableId;
private final String tag;
private final boolean ephemeral;
private final Instant createdAt;
private final Instant expiresAt;
private final Set<File> snapshotDirs;
public TableSnapshot(String keyspaceName, String tableName, UUID tableId,
String tag, Instant createdAt, Instant expiresAt,
Set<File> snapshotDirs, boolean ephemeral)
{
this.keyspaceName = keyspaceName;
this.tableName = tableName;
this.tableId = tableId;
this.tag = tag;
this.createdAt = createdAt;
this.expiresAt = expiresAt;
this.snapshotDirs = snapshotDirs;
this.ephemeral = ephemeral;
}
/**
* Unique identifier of a snapshot. Used
* only to deduplicate snapshots internally,
* not exposed externally.
*
* Format: "$ks:$table_name:$table_id:$tag"
*/
public String getId()
{
return buildSnapshotId(keyspaceName, tableName, tableId, tag);
}
public String getKeyspaceName()
{
return keyspaceName;
}
public String getTableName()
{
return tableName;
}
public String getTag()
{
return tag;
}
public Instant getCreatedAt()
{
if (createdAt == null)
{
long minCreation = snapshotDirs.stream().mapToLong(File::lastModified).min().orElse(0);
if (minCreation != 0)
{
return Instant.ofEpochMilli(minCreation);
}
}
return createdAt;
}
public Instant getExpiresAt()
{
return expiresAt;
}
public boolean isExpired(Instant now)
{
if (createdAt == null || expiresAt == null)
{
return false;
}
return expiresAt.compareTo(now) < 0;
}
public boolean exists()
{
return snapshotDirs.stream().anyMatch(File::exists);
}
public boolean isEphemeral()
{
return ephemeral;
}
public boolean isExpiring()
{
return expiresAt != null;
}
public long computeSizeOnDiskBytes()
{
return snapshotDirs.stream().mapToLong(FileUtils::folderSize).sum();
}
public long computeTrueSizeBytes()
{
DirectorySizeCalculator visitor = new SnapshotTrueSizeCalculator();
for (File snapshotDir : snapshotDirs)
{
try
{
Files.walkFileTree(snapshotDir.toPath(), visitor);
}
catch (IOException e)
{
logger.error("Could not calculate the size of {}.", snapshotDir, e);
}
}
return visitor.getAllocatedSize();
}
public Collection<File> getDirectories()
{
return snapshotDirs;
}
public Optional<File> getManifestFile()
{
for (File snapshotDir : snapshotDirs)
{
File manifestFile = Directories.getSnapshotManifestFile(snapshotDir);
if (manifestFile.exists())
{
return Optional.of(manifestFile);
}
}
return Optional.empty();
}
public Optional<File> getSchemaFile()
{
for (File snapshotDir : snapshotDirs)
{
File schemaFile = Directories.getSnapshotSchemaFile(snapshotDir);
if (schemaFile.exists())
{
return Optional.of(schemaFile);
}
}
return Optional.empty();
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TableSnapshot snapshot = (TableSnapshot) o;
return Objects.equals(keyspaceName, snapshot.keyspaceName) && Objects.equals(tableName, snapshot.tableName) &&
Objects.equals(tableId, snapshot.tableId) && Objects.equals(tag, snapshot.tag) &&
Objects.equals(createdAt, snapshot.createdAt) && Objects.equals(expiresAt, snapshot.expiresAt) &&
Objects.equals(snapshotDirs, snapshot.snapshotDirs) && Objects.equals(ephemeral, snapshot.ephemeral);
}
@Override
public int hashCode()
{
return Objects.hash(keyspaceName, tableName, tableId, tag, createdAt, expiresAt, snapshotDirs, ephemeral);
}
@Override
public String toString()
{
return "TableSnapshot{" +
"keyspaceName='" + keyspaceName + '\'' +
", tableName='" + tableName + '\'' +
", tableId=" + tableId +
", tag='" + tag + '\'' +
", createdAt=" + createdAt +
", expiresAt=" + expiresAt +
", snapshotDirs=" + snapshotDirs +
", ephemeral=" + ephemeral +
'}';
}
static class Builder {
private final String keyspaceName;
private final String tableName;
private final UUID tableId;
private final String tag;
private Instant createdAt = null;
private Instant expiresAt = null;
private boolean ephemeral;
private final Set<File> snapshotDirs = new HashSet<>();
Builder(String keyspaceName, String tableName, UUID tableId, String tag)
{
this.keyspaceName = keyspaceName;
this.tableName = tableName;
this.tag = tag;
this.tableId = tableId;
}
void addSnapshotDir(File snapshotDir)
{
snapshotDirs.add(snapshotDir);
File manifestFile = new File(snapshotDir, "manifest.json");
if (manifestFile.exists() && createdAt == null && expiresAt == null)
loadMetadataFromManifest(manifestFile);
// check if an ephemeral marker file exists only in case it is not already ephemeral
// by reading it from manifest
// TODO remove this on Cassandra 4.3 release, see CASSANDRA-16911
if (!ephemeral && new File(snapshotDir, "ephemeral.snapshot").exists())
ephemeral = true;
}
private void loadMetadataFromManifest(File manifestFile)
{
try
{
logger.trace("Loading snapshot manifest from {}", manifestFile);
SnapshotManifest manifest = SnapshotManifest.deserializeFromJsonFile(manifestFile);
createdAt = manifest.createdAt;
expiresAt = manifest.expiresAt;
// a snapshot may be ephemeral when it has a marker file (old way) or flag in manifest (new way)
if (!ephemeral)
ephemeral = manifest.ephemeral;
}
catch (IOException e)
{
logger.warn("Cannot read manifest file {} of snapshot {}.", manifestFile, tag, e);
}
}
TableSnapshot build()
{
return new TableSnapshot(keyspaceName, tableName, tableId, tag, createdAt, expiresAt, snapshotDirs, ephemeral);
}
}
protected static String buildSnapshotId(String keyspaceName, String tableName, UUID tableId, String tag)
{
return String.format("%s:%s:%s:%s", keyspaceName, tableName, tableId, tag);
}
public static class SnapshotTrueSizeCalculator extends DirectorySizeCalculator
{
/**
* Snapshots are composed of hard-linked sstables. The true snapshot size should only include
* snapshot files which do not contain a corresponding "live" sstable file.
*/
@Override
public boolean isAcceptable(Path snapshotFilePath)
{
return !getLiveFileFromSnapshotFile(snapshotFilePath).exists();
}
}
/**
* Returns the corresponding live file for a given snapshot file.
*
* Example:
* - Base table:
* - Snapshot file: ~/.ccm/test/node1/data0/test_ks/tbl-e03faca0813211eca100c705ea09b5ef/snapshots/1643481737850/me-1-big-Data.db
* - Live file: ~/.ccm/test/node1/data0/test_ks/tbl-e03faca0813211eca100c705ea09b5ef/me-1-big-Data.db
* - Secondary index:
* - Snapshot file: ~/.ccm/test/node1/data0/test_ks/tbl-e03faca0813211eca100c705ea09b5ef/snapshots/1643481737850/.tbl_val_idx/me-1-big-Summary.db
* - Live file: ~/.ccm/test/node1/data0/test_ks/tbl-e03faca0813211eca100c705ea09b5ef/.tbl_val_idx/me-1-big-Summary.db
*
*/
static File getLiveFileFromSnapshotFile(Path snapshotFilePath)
{
// Snapshot directory structure format is {data_dir}/snapshots/{snapshot_name}/{snapshot_file}
Path liveDir = snapshotFilePath.getParent().getParent().getParent();
if (Directories.isSecondaryIndexFolder(snapshotFilePath.getParent()))
{
// Snapshot file structure format is {data_dir}/snapshots/{snapshot_name}/.{index}/{sstable-component}.db
liveDir = File.getPath(liveDir.getParent().toString(), snapshotFilePath.getParent().getFileName().toString());
}
return new File(liveDir.toString(), snapshotFilePath.getFileName().toString());
}
public static Predicate<TableSnapshot> shouldClearSnapshot(String tag, long olderThanTimestamp)
{
return ts ->
{
// When no tag is supplied, all snapshots must be cleared
boolean clearAll = tag == null || tag.isEmpty();
if (!clearAll && ts.isEphemeral())
logger.info("Skipping deletion of ephemeral snapshot '{}' in keyspace {}. " +
"Ephemeral snapshots are not removable by a user.",
tag, ts.keyspaceName);
boolean notEphemeral = !ts.isEphemeral();
boolean shouldClearTag = clearAll || ts.tag.equals(tag);
boolean byTimestamp = true;
if (olderThanTimestamp > 0L)
{
Instant createdAt = ts.getCreatedAt();
if (createdAt != null)
byTimestamp = createdAt.isBefore(Instant.ofEpochMilli(olderThanTimestamp));
}
return notEphemeral && shouldClearTag && byTimestamp;
};
}
}