This is an automated email from the ASF dual-hosted git repository.

paulo pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 31aa17a2a3 List snapshots of dropped tables
31aa17a2a3 is described below

commit 31aa17a2a3b18bdda723123cad811f075287807d
Author: Paulo Motta <[email protected]>
AuthorDate: Thu Apr 28 19:03:03 2022 -0300

    List snapshots of dropped tables
    
    Patch by Paulo Motta; Reviewed by Stefan Miklosovic and Brandon Williams or 
CASSANDRA-16843
---
 CHANGES.txt                                        |   1 +
 .../org/apache/cassandra/db/ColumnFamilyStore.java |   4 +-
 src/java/org/apache/cassandra/db/Directories.java  |  18 +-
 .../cassandra/db/SnapshotDetailsTabularData.java   |   2 +-
 .../db/commitlog/CommitLogSegmentManagerCDC.java   |   4 +-
 .../apache/cassandra/service/StorageService.java   |  29 +--
 .../cassandra/service/snapshot/SnapshotLoader.java | 137 ++++++++++++
 .../cassandra/service/snapshot/TableSnapshot.java  | 223 +++++++++++++++----
 .../cassandra/utils/DirectorySizeCalculator.java   |   8 +-
 .../cassandra/distributed/test/SnapshotsTest.java  |  40 ++++
 .../test/microbench/DirectorySizerBench.java       |   2 +-
 .../apache/cassandra/db/ColumnFamilyStoreTest.java |  23 +-
 .../org/apache/cassandra/db/DirectoriesTest.java   |   9 +-
 .../apache/cassandra/index/sasi/SASIIndexTest.java |   4 +-
 .../service/snapshot/SnapshotLoaderTest.java       | 236 +++++++++++++++++++++
 .../service/snapshot/SnapshotManagerTest.java      |  15 +-
 .../service/snapshot/TableSnapshotTest.java        |  72 ++++---
 17 files changed, 707 insertions(+), 120 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 8ad715c1b9..726cfad750 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 4.1
+ * List snapshots of dropped tables (CASSANDRA-16843)
  * Add information whether sstables are dropped to SchemaChangeListener 
(CASSANDRA-17582)
  * Add a pluggable memtable API (CEP-11 / CASSANDRA-17034)
  * Save sstable id as string in activity table (CASSANDRA-17585)
diff --git a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java 
b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java
index 2bdac2b0e4..d46b20421a 100644
--- a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java
+++ b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java
@@ -2060,8 +2060,8 @@ public class ColumnFamilyStore implements 
ColumnFamilyStoreMBean, Memtable.Owner
             snapshotDirs.add(ephemeralSnapshotMarker.parent().toAbsolute()); 
// marker may create empty snapshot dir
         }
 
-        TableSnapshot snapshot = new TableSnapshot(metadata.keyspace, 
metadata.name, tag, manifest.createdAt,
-                                                   manifest.expiresAt, 
snapshotDirs, directories::getTrueAllocatedSizeIn);
+        TableSnapshot snapshot = new TableSnapshot(metadata.keyspace, 
metadata.name, metadata.id.asUUID(), tag,
+                                                   manifest.createdAt, 
manifest.expiresAt, snapshotDirs);
 
         StorageService.instance.addSnapshot(snapshot);
         return snapshot;
diff --git a/src/java/org/apache/cassandra/db/Directories.java 
b/src/java/org/apache/cassandra/db/Directories.java
index 972ba6d3cd..8672377b75 100644
--- a/src/java/org/apache/cassandra/db/Directories.java
+++ b/src/java/org/apache/cassandra/db/Directories.java
@@ -556,7 +556,7 @@ public class Directories
         return getSnapshotManifestFile(snapshotDir);
     }
 
-    protected static File getSnapshotManifestFile(File snapshotDir)
+    public static File getSnapshotManifestFile(File snapshotDir)
     {
         return new File(snapshotDir, "manifest.json");
     }
@@ -564,6 +564,11 @@ public class Directories
     public File getSnapshotSchemaFile(String snapshotName)
     {
         File snapshotDir = getSnapshotDirectory(getDirectoryForNewSSTables(), 
snapshotName);
+        return getSnapshotSchemaFile(snapshotDir);
+    }
+
+    public static File getSnapshotSchemaFile(File snapshotDir)
+    {
         return new File(snapshotDir, "schema.cql");
     }
 
@@ -981,8 +986,8 @@ public class Directories
     protected TableSnapshot buildSnapshot(String tag, SnapshotManifest 
manifest, Set<File> snapshotDirs) {
         Instant createdAt = manifest == null ? null : manifest.createdAt;
         Instant expiresAt = manifest == null ? null : manifest.expiresAt;
-        return new TableSnapshot(metadata.keyspace, metadata.name, tag, 
createdAt, expiresAt, snapshotDirs,
-                                 this::getTrueAllocatedSizeIn);
+        return new TableSnapshot(metadata.keyspace, metadata.name, 
metadata.id.asUUID(), tag, createdAt, expiresAt,
+                                 snapshotDirs);
     }
 
     @VisibleForTesting
@@ -1155,7 +1160,7 @@ public class Directories
         if (!snapshotDir.isDirectory())
             return 0;
 
-        SSTableSizeSummer visitor = new SSTableSizeSummer(snapshotDir, 
sstableLister(OnTxnErr.THROW).listFiles());
+        SSTableSizeSummer visitor = new 
SSTableSizeSummer(sstableLister(OnTxnErr.THROW).listFiles());
         try
         {
             Files.walkFileTree(snapshotDir.toPath(), visitor);
@@ -1244,10 +1249,9 @@ public class Directories
     private class SSTableSizeSummer extends DirectorySizeCalculator
     {
         private final Set<String> toSkip;
-        SSTableSizeSummer(File path, List<File> files)
+        SSTableSizeSummer(List<File> files)
         {
-            super(path);
-            toSkip = files.stream().map(f -> 
f.name()).collect(Collectors.toSet());
+            toSkip = 
files.stream().map(File::name).collect(Collectors.toSet());
         }
 
         @Override
diff --git a/src/java/org/apache/cassandra/db/SnapshotDetailsTabularData.java 
b/src/java/org/apache/cassandra/db/SnapshotDetailsTabularData.java
index 73a4876df8..4e6ab116c4 100644
--- a/src/java/org/apache/cassandra/db/SnapshotDetailsTabularData.java
+++ b/src/java/org/apache/cassandra/db/SnapshotDetailsTabularData.java
@@ -78,7 +78,7 @@ public class SnapshotDetailsTabularData
             String createdAt = safeToString(details.getCreatedAt());
             String expiresAt = safeToString(details.getExpiresAt());
             result.put(new CompositeDataSupport(COMPOSITE_TYPE, ITEM_NAMES,
-                    new Object[]{ details.getTag(), details.getKeyspace(), 
details.getTable(), liveSize, totalSize, createdAt, expiresAt }));
+                    new Object[]{ details.getTag(), details.getKeyspaceName(), 
details.getTableName(), liveSize, totalSize, createdAt, expiresAt }));
         }
         catch (OpenDataException e)
         {
diff --git 
a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDC.java 
b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDC.java
index f00bbfc613..c75c3a9313 100644
--- a/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDC.java
+++ b/src/java/org/apache/cassandra/db/commitlog/CommitLogSegmentManagerCDC.java
@@ -274,9 +274,11 @@ public class CommitLogSegmentManagerCDC extends 
AbstractCommitLogSegmentManager
         // track the total size between two dictionary size calculations
         private final AtomicLong sizeInProgress;
 
+        private final File path;
+
         CDCSizeTracker(CommitLogSegmentManagerCDC segmentManager, File path)
         {
-            super(path);
+            this.path = path;
             this.segmentManager = segmentManager;
             this.sizeInProgress = new AtomicLong(0);
         }
diff --git a/src/java/org/apache/cassandra/service/StorageService.java 
b/src/java/org/apache/cassandra/service/StorageService.java
index 97d5204722..dd35c70fdd 100644
--- a/src/java/org/apache/cassandra/service/StorageService.java
+++ b/src/java/org/apache/cassandra/service/StorageService.java
@@ -73,6 +73,7 @@ import org.apache.cassandra.io.util.File;
 import org.apache.cassandra.locator.ReplicaCollection.Builder.Conflict;
 import org.apache.cassandra.schema.Keyspaces;
 import org.apache.cassandra.service.disk.usage.DiskUsageBroadcaster;
+import org.apache.cassandra.service.snapshot.SnapshotLoader;
 import org.apache.cassandra.utils.concurrent.Future;
 import org.apache.cassandra.schema.TableId;
 import org.apache.cassandra.utils.concurrent.FutureCombiner;
@@ -4135,24 +4136,26 @@ public class StorageService extends 
NotificationBroadcasterSupport implements IE
 
     public Map<String, TabularData> getSnapshotDetails(Map<String, String> 
options)
     {
+        boolean skipExpiring = options != null && 
Boolean.parseBoolean(options.getOrDefault("no_ttl", "false"));
+
+        SnapshotLoader loader = new SnapshotLoader();
         Map<String, TabularData> snapshotMap = new HashMap<>();
-        for (Keyspace keyspace : Keyspace.all())
+
+        for (TableSnapshot snapshot : loader.loadSnapshots())
         {
-            for (ColumnFamilyStore cfStore : keyspace.getColumnFamilyStores())
-            {
-                for (Map.Entry<String, TableSnapshot> snapshotDetail : 
TableSnapshot.filter(cfStore.listSnapshots(), options).entrySet())
-                {
-                    TabularDataSupport data = (TabularDataSupport) 
snapshotMap.get(snapshotDetail.getKey());
-                    if (data == null)
-                    {
-                        data = new 
TabularDataSupport(SnapshotDetailsTabularData.TABULAR_TYPE);
-                        snapshotMap.put(snapshotDetail.getKey(), data);
-                    }
+            if (skipExpiring && snapshot.isExpiring())
+                continue;
 
-                    SnapshotDetailsTabularData.from(snapshotDetail.getValue(), 
data);
-                }
+            TabularDataSupport data = (TabularDataSupport) 
snapshotMap.get(snapshot.getTag());
+            if (data == null)
+            {
+                data = new 
TabularDataSupport(SnapshotDetailsTabularData.TABULAR_TYPE);
+                snapshotMap.put(snapshot.getTag(), data);
             }
+
+            SnapshotDetailsTabularData.from(snapshot, data);
         }
+
         return snapshotMap;
     }
 
diff --git a/src/java/org/apache/cassandra/service/snapshot/SnapshotLoader.java 
b/src/java/org/apache/cassandra/service/snapshot/SnapshotLoader.java
new file mode 100644
index 0000000000..75ae9ee5b5
--- /dev/null
+++ b/src/java/org/apache/cassandra/service/snapshot/SnapshotLoader.java
@@ -0,0 +1,137 @@
+/*
+ * 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.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.Directories;
+import org.apache.cassandra.io.util.File;
+
+import static org.apache.cassandra.db.Directories.SNAPSHOT_SUBDIR;
+import static 
org.apache.cassandra.service.snapshot.TableSnapshot.buildSnapshotId;
+
+/**
+ * Loads snapshot metadata from data directories
+ */
+public class SnapshotLoader extends SimpleFileVisitor<Path>
+{
+    private static final Logger logger = 
LoggerFactory.getLogger(SnapshotLoader.class);
+
+    static final Pattern SNAPSHOT_DIR_PATTERN = 
Pattern.compile("(?<keyspace>\\w+)/(?<tableName>\\w+)\\-(?<tableId>[0-9a-f]{32})/snapshots/(?<tag>[\\w-]+)$");
+
+    private final Collection<Path> dataDirectories;
+    private final Map<String, TableSnapshot.Builder> snapshots = new 
HashMap<>();
+
+    public SnapshotLoader()
+    {
+        this(DatabaseDescriptor.getAllDataFileLocations());
+    }
+
+    public SnapshotLoader(String[] dataDirectories)
+    {
+        this.dataDirectories = 
Arrays.stream(dataDirectories).map(Paths::get).collect(Collectors.toList());
+    }
+
+    public SnapshotLoader(Collection<Path> dataDirs)
+    {
+        this.dataDirectories = dataDirs;
+    }
+
+    public Set<TableSnapshot> loadSnapshots()
+    {
+        for (Path dataDir : dataDirectories)
+        {
+            try
+            {
+                Files.walkFileTree(dataDir, Collections.EMPTY_SET, 5, this);
+            }
+            catch (IOException e)
+            {
+                throw new RuntimeException(String.format("Error while loading 
snapshots from %s", dataDir));
+            }
+        }
+        return 
snapshots.values().stream().map(TableSnapshot.Builder::build).collect(Collectors.toSet());
+    }
+
+    public FileVisitResult preVisitDirectory(Path subdir, BasicFileAttributes 
attrs)
+    {
+        if 
(subdir.getParent().getFileName().toString().equals(SNAPSHOT_SUBDIR))
+        {
+            logger.trace("Processing directory " + subdir);
+            Matcher snapshotDirMatcher = 
SNAPSHOT_DIR_PATTERN.matcher(subdir.toString());
+            if (snapshotDirMatcher.find())
+            {
+                try
+                {
+                    loadSnapshotFromDir(snapshotDirMatcher, subdir);
+                } catch (Throwable e)
+                {
+                    logger.warn("Could not load snapshot from {}.", subdir, e);
+                }
+            }
+            return FileVisitResult.SKIP_SUBTREE;
+        }
+
+        return subdir.getFileName().equals(Directories.BACKUPS_SUBDIR)
+               ? FileVisitResult.SKIP_SUBTREE
+               : FileVisitResult.CONTINUE;
+    }
+
+    private void loadSnapshotFromDir(Matcher snapshotDirMatcher, Path 
snapshotDir)
+    {
+        String keyspaceName = snapshotDirMatcher.group("keyspace");
+        String tableName = snapshotDirMatcher.group("tableName");
+        UUID tableId = parseUUID(snapshotDirMatcher.group("tableId"));
+        String tag = snapshotDirMatcher.group("tag");
+        String snapshotId = buildSnapshotId(keyspaceName, tableName, tableId, 
tag);
+        TableSnapshot.Builder builder = snapshots.computeIfAbsent(snapshotId, 
k -> new TableSnapshot.Builder(keyspaceName, tableName, tableId, tag));
+        builder.addSnapshotDir(new File(snapshotDir));
+    }
+
+    /**
+     * Given an UUID string without dashes (ie. 
c7e513243f0711ec9bbc0242ac130002)
+     * return an UUID object (ie. c7e51324-3f07-11ec-9bbc-0242ac130002)
+     */
+    protected static UUID parseUUID(String uuidWithoutDashes) throws 
IllegalArgumentException
+    {
+        assert uuidWithoutDashes.length() == 32 && 
!uuidWithoutDashes.contains("-");
+        String dashedUUID = 
uuidWithoutDashes.replaceFirst("([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]+)",
 "$1-$2-$3-$4-$5");
+        return UUID.fromString(dashedUUID);
+    }
+}
diff --git a/src/java/org/apache/cassandra/service/snapshot/TableSnapshot.java 
b/src/java/org/apache/cassandra/service/snapshot/TableSnapshot.java
index 185cd45c4b..7a2cdc0f89 100644
--- a/src/java/org/apache/cassandra/service/snapshot/TableSnapshot.java
+++ b/src/java/org/apache/cassandra/service/snapshot/TableSnapshot.java
@@ -17,50 +17,73 @@
  */
 package org.apache.cassandra.service.snapshot;
 
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.time.Instant;
 import java.util.Collection;
-import java.util.Map;
+import java.util.HashSet;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
+import java.util.UUID;
 
+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 final String keyspace;
-    private final String table;
+    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 Instant createdAt;
     private final Instant expiresAt;
 
     private final Set<File> snapshotDirs;
-    private final Function<File, Long> trueDiskSizeComputer;
 
-    public TableSnapshot(String keyspace, String table, String tag, Instant 
createdAt,
-                         Instant expiresAt, Set<File> snapshotDirs,
-                         Function<File, Long> trueDiskSizeComputer)
+    public TableSnapshot(String keyspaceName, String tableName, UUID tableId,
+                         String tag, Instant createdAt, Instant expiresAt,
+                         Set<File> snapshotDirs)
     {
-        this.keyspace = keyspace;
-        this.table = table;
+        this.keyspaceName = keyspaceName;
+        this.tableName = tableName;
+        this.tableId = tableId;
         this.tag = tag;
         this.createdAt = createdAt;
         this.expiresAt = expiresAt;
         this.snapshotDirs = snapshotDirs;
-        this.trueDiskSizeComputer = trueDiskSizeComputer;
     }
 
-    public String getKeyspace()
+    /**
+     * 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 keyspace;
+        return keyspaceName;
     }
 
-    public String getTable()
+    public String getTableName()
     {
-        return table;
+        return tableName;
     }
 
     public String getTag()
@@ -113,7 +136,21 @@ public class TableSnapshot
 
     public long computeTrueSizeBytes()
     {
-        return 
snapshotDirs.stream().mapToLong(trueDiskSizeComputer::apply).sum();
+        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()
@@ -121,17 +158,30 @@ public class TableSnapshot
         return snapshotDirs;
     }
 
-    @Override
-    public String toString()
+    public Optional<File> getManifestFile()
     {
-        return "TableSnapshot{" +
-               "keyspace='" + keyspace + '\'' +
-               ", table='" + table + '\'' +
-               ", tag='" + tag + '\'' +
-               ", createdAt=" + createdAt +
-               ", expiresAt=" + expiresAt +
-               ", snapshotDirs=" + snapshotDirs +
-               '}';
+        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
@@ -139,28 +189,121 @@ public class TableSnapshot
     {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
-        TableSnapshot that = (TableSnapshot) o;
-        return Objects.equals(keyspace, that.keyspace) && 
Objects.equals(table, that.table)
-               && Objects.equals(tag, that.tag) && Objects.equals(createdAt, 
that.createdAt)
-               && Objects.equals(expiresAt, that.expiresAt) && 
Objects.equals(snapshotDirs, that.snapshotDirs);
+        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);
     }
 
     @Override
     public int hashCode()
     {
-        return Objects.hash(keyspace, table, tag, createdAt, expiresAt, 
snapshotDirs);
+        return Objects.hash(keyspaceName, tableName, tableId, tag, createdAt, 
expiresAt, snapshotDirs);
+    }
+
+    @Override
+    public String toString()
+    {
+        return "TableSnapshot{" +
+               "keyspaceName='" + keyspaceName + '\'' +
+               ", tableName='" + tableName + '\'' +
+               ", tableId=" + tableId +
+               ", tag='" + tag + '\'' +
+               ", createdAt=" + createdAt +
+               ", expiresAt=" + expiresAt +
+               ", snapshotDirs=" + snapshotDirs +
+               '}';
+    }
+
+    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 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) {
+                loadTimestampsFromManifest(manifestFile);
+            }
+        }
+
+        private void loadTimestampsFromManifest(File manifestFile)
+        {
+            try
+            {
+                logger.debug("Loading snapshot manifest from {}", 
manifestFile);
+                SnapshotManifest manifest = 
SnapshotManifest.deserializeFromJsonFile(manifestFile);
+                createdAt = manifest.createdAt;
+                expiresAt = manifest.expiresAt;
+            }
+            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);
+        }
     }
 
-    public static Map<String, TableSnapshot> filter(Map<String, TableSnapshot> 
snapshots, Map<String, String> options)
+    protected static String buildSnapshotId(String keyspaceName, String 
tableName, UUID tableId, String tag)
     {
-        if (options == null)
-            return snapshots;
+        return String.format("%s:%s:%s:%s", keyspaceName, tableName, tableId, 
tag);
+    }
 
-        boolean skipExpiring = 
Boolean.parseBoolean(options.getOrDefault("no_ttl", "false"));
+    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();
+        }
+    }
 
-        return snapshots.entrySet()
-                        .stream()
-                        .filter(entry -> !skipExpiring || 
!entry.getValue().isExpiring())
-                        .collect(Collectors.toMap(Map.Entry::getKey, 
Map.Entry::getValue));
+    /**
+     * 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(new 
File(snapshotFilePath.getParent().toFile())))
+        {
+            // Snapshot file structure format is 
{data_dir}/snapshots/{snapshot_name}/.{index}/{sstable-component}.db
+            liveDir = Paths.get(liveDir.getParent().toString(), 
snapshotFilePath.getParent().getFileName().toString());
+        }
+        return new File(liveDir.toString(), 
snapshotFilePath.getFileName().toString());
     }
 }
diff --git a/src/java/org/apache/cassandra/utils/DirectorySizeCalculator.java 
b/src/java/org/apache/cassandra/utils/DirectorySizeCalculator.java
index f0cfdea021..94f9b229b4 100644
--- a/src/java/org/apache/cassandra/utils/DirectorySizeCalculator.java
+++ b/src/java/org/apache/cassandra/utils/DirectorySizeCalculator.java
@@ -27,17 +27,13 @@ import java.nio.file.attribute.BasicFileAttributes;
 /**
  * Walks directory recursively, summing up total contents of files within.
  */
-import org.apache.cassandra.io.util.File;
-
 public class DirectorySizeCalculator extends SimpleFileVisitor<Path>
 {
     private volatile long size = 0;
-    protected final File path;
 
-    public DirectorySizeCalculator(File path)
+    public DirectorySizeCalculator()
     {
         super();
-        this.path = path;
     }
 
     public boolean isAcceptable(Path file)
@@ -54,7 +50,7 @@ public class DirectorySizeCalculator extends 
SimpleFileVisitor<Path>
     }
 
     @Override
-    public FileVisitResult visitFileFailed(Path file, IOException exc) throws 
IOException
+    public FileVisitResult visitFileFailed(Path file, IOException exc)
     {
         return FileVisitResult.CONTINUE;
     }
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/SnapshotsTest.java 
b/test/distributed/org/apache/cassandra/distributed/test/SnapshotsTest.java
index c56adfe7b1..1029301fca 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/SnapshotsTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/SnapshotsTest.java
@@ -21,6 +21,7 @@ package org.apache.cassandra.distributed.test;
 import java.io.IOException;
 import java.util.Arrays;
 
+import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Assert;
 import org.junit.BeforeClass;
@@ -52,6 +53,14 @@ public class SnapshotsTest extends TestBaseImpl
         cluster = init(Cluster.build(1).withConfig(c -> 
c.with(Feature.GOSSIP)).start());
     }
 
+
+    @After
+    public void clearAllSnapshots()
+    {
+        cluster.schemaChange("DROP KEYSPACE IF EXISTS default;");
+        cluster.get(1).nodetoolResult("clearsnapshot", 
"--all").asserts().success();
+    }
+
     @AfterClass
     public static void after()
     {
@@ -182,6 +191,37 @@ public class SnapshotsTest extends TestBaseImpl
         listSnapshotsResult.stdoutNotContains("first");
     }
 
+    @Test
+    public void testListSnapshotOfDroppedTable()
+    {
+        IInvokableInstance instance = cluster.get(1);
+
+        cluster.schemaChange("CREATE KEYSPACE IF NOT EXISTS default WITH 
replication = {'class': 'SimpleStrategy', 'replication_factor': 2};");
+        cluster.schemaChange("CREATE TABLE default.tbl (key int, value text, 
PRIMARY KEY (key))");
+
+        populate(cluster);
+
+        instance.nodetoolResult("snapshot",
+                                      "-t", "tag1",
+                                      "-kt", 
"default.tbl").asserts().success();
+
+        // Check snapshot is listed when table is not dropped
+        
instance.nodetoolResult("listsnapshots").asserts().success().stdoutContains("tag1");
+
+        // Drop Table
+        cluster.schemaChange("DROP TABLE default.tbl;");
+
+        // Check snapshot is listed after table is dropped
+        
instance.nodetoolResult("listsnapshots").asserts().success().stdoutContains("tag1");
+
+        // Restart node
+        stopUnchecked(instance);
+        instance.startup();
+
+        // Check snapshot of dropped table still exists after restart
+        
instance.nodetoolResult("listsnapshots").asserts().success().stdoutContains("tag1");
+    }
+
     @Test
     public void testSameTimestampOnEachTableOfSnaphot()
     {
diff --git 
a/test/microbench/org/apache/cassandra/test/microbench/DirectorySizerBench.java 
b/test/microbench/org/apache/cassandra/test/microbench/DirectorySizerBench.java
index c4466e11b0..ad72f3d02c 100644
--- 
a/test/microbench/org/apache/cassandra/test/microbench/DirectorySizerBench.java
+++ 
b/test/microbench/org/apache/cassandra/test/microbench/DirectorySizerBench.java
@@ -66,7 +66,7 @@ public class DirectorySizerBench
 
         // Test w/25,600 files, 100x the load of a full default CommitLog 
(8192) divided by size (32 per)
         populateRandomFiles(tempDir, 25600);
-        sizer = new DirectorySizeCalculator(tempDir);
+        sizer = new DirectorySizeCalculator();
     }
 
     @TearDown
diff --git a/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java 
b/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java
index 633328d4c8..6d8bd0fe05 100644
--- a/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java
+++ b/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java
@@ -28,8 +28,10 @@ import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Iterators;
 import org.junit.Assert;
 import org.junit.Before;
@@ -261,7 +263,7 @@ public class ColumnFamilyStoreTest
     }
 
     @Test
-    public void testSnapshotSize()
+    public void testSnapshotSize() throws IOException
     {
         // cleanup any previous test gargbage
         ColumnFamilyStore cfs = 
Keyspace.open(KEYSPACE1).getColumnFamilyStore(CF_STANDARD1);
@@ -286,7 +288,7 @@ public class ColumnFamilyStoreTest
         // check that sizeOnDisk > trueSize = 0
         TableSnapshot details = snapshotDetails.get("basic");
         
assertThat(details.computeSizeOnDiskBytes()).isGreaterThan(details.computeTrueSizeBytes());
-        assertThat(details.computeTrueSizeBytes()).isZero();
+        
assertThat(details.computeTrueSizeBytes()).isEqualTo(getSnapshotManifestAndSchemaFileSizes(details));
 
         // compact base table to make trueSize > 0
         cfs.forceMajorCompaction();
@@ -296,8 +298,7 @@ public class ColumnFamilyStoreTest
         // Check that truesize now is > 0
         snapshotDetails = cfs.listSnapshots();
         details = snapshotDetails.get("basic");
-        
assertThat(details.computeSizeOnDiskBytes()).isGreaterThan(details.computeTrueSizeBytes());
-        assertThat(details.computeTrueSizeBytes()).isPositive();
+        
assertThat(details.computeSizeOnDiskBytes()).isEqualTo(details.computeTrueSizeBytes());
     }
 
     @Test
@@ -609,4 +610,18 @@ public class ColumnFamilyStoreTest
         assertEquals(0, ssTableFiles.size());
         cfs.clearUnsafe();
     }
+
+    @VisibleForTesting
+    public static long getSnapshotManifestAndSchemaFileSizes(TableSnapshot 
snapshot) throws IOException
+    {
+        Optional<File> schemaFile = snapshot.getSchemaFile();
+        Optional<File> manifestFile = snapshot.getManifestFile();
+
+        long schemaAndManifestFileSizes = 0;
+
+        schemaAndManifestFileSizes += schemaFile.isPresent() ? 
schemaFile.get().length() : 0;
+        schemaAndManifestFileSizes += manifestFile.isPresent() ? 
manifestFile.get().length() : 0;
+
+        return schemaAndManifestFileSizes;
+    }
 }
diff --git a/test/unit/org/apache/cassandra/db/DirectoriesTest.java 
b/test/unit/org/apache/cassandra/db/DirectoriesTest.java
index eb6a8b6cc3..3779eccbec 100644
--- a/test/unit/org/apache/cassandra/db/DirectoriesTest.java
+++ b/test/unit/org/apache/cassandra/db/DirectoriesTest.java
@@ -199,7 +199,7 @@ public class DirectoriesTest
         {
             Instant createdAt = manifest == null ? null : manifest.createdAt;
             Instant expiresAt = manifest == null ? null : manifest.expiresAt;
-            return new TableSnapshot(table.keyspace, table.name, tag, 
createdAt, expiresAt, Collections.singleton(snapshotDir), null);
+            return new TableSnapshot(table.keyspace, table.name, 
table.id.asUUID(), tag, createdAt, expiresAt, 
Collections.singleton(snapshotDir));
         }
     }
 
@@ -417,11 +417,8 @@ public class DirectoriesTest
         // check snapshot details
         Map<String, TableSnapshot> parentSnapshotDetail = 
parentDirectories.listSnapshots();
         assertTrue(parentSnapshotDetail.containsKey("test"));
-        assertEquals(30L, 
parentSnapshotDetail.get("test").computeTrueSizeBytes());
-
-        Map<String, TableSnapshot> indexSnapshotDetail = 
indexDirectories.listSnapshots();
-        assertTrue(indexSnapshotDetail.containsKey("test"));
-        assertEquals(40L, 
indexSnapshotDetail.get("test").computeTrueSizeBytes());
+        // CASSANDRA-17357: include indexes when computing true size of parent 
table
+        assertEquals(70L, 
parentSnapshotDetail.get("test").computeTrueSizeBytes());
 
         // check backup directory
         File parentBackupDirectory = 
Directories.getBackupsDirectory(parentDesc);
diff --git a/test/unit/org/apache/cassandra/index/sasi/SASIIndexTest.java 
b/test/unit/org/apache/cassandra/index/sasi/SASIIndexTest.java
index 8c223a5a7f..441d7b00ff 100644
--- a/test/unit/org/apache/cassandra/index/sasi/SASIIndexTest.java
+++ b/test/unit/org/apache/cassandra/index/sasi/SASIIndexTest.java
@@ -101,6 +101,7 @@ import com.google.common.util.concurrent.Uninterruptibles;
 import org.junit.*;
 
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static 
org.apache.cassandra.db.ColumnFamilyStoreTest.getSnapshotManifestAndSchemaFileSizes;
 
 public class SASIIndexTest
 {
@@ -211,7 +212,8 @@ public class SASIIndexTest
             TableSnapshot details = store.listSnapshots().get(snapshotName);
 
             // check that SASI components are included in the computation of 
snapshot size
-            Assert.assertEquals(tableSize + indexSize, 
details.computeTrueSizeBytes());
+            long snapshotSize = tableSize + indexSize + 
getSnapshotManifestAndSchemaFileSizes(details);
+            Assert.assertEquals(snapshotSize, details.computeTrueSizeBytes());
         }
         finally
         {
diff --git 
a/test/unit/org/apache/cassandra/service/snapshot/SnapshotLoaderTest.java 
b/test/unit/org/apache/cassandra/service/snapshot/SnapshotLoaderTest.java
new file mode 100644
index 0000000000..27f294e8d5
--- /dev/null
+++ b/test/unit/org/apache/cassandra/service/snapshot/SnapshotLoaderTest.java
@@ -0,0 +1,236 @@
+/*
+ * 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.Paths;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import org.apache.cassandra.config.DurationSpec;
+import org.apache.cassandra.db.Directories;
+import org.apache.cassandra.io.util.File;
+import org.assertj.core.util.Lists;
+
+import static 
org.apache.cassandra.service.snapshot.SnapshotLoader.SNAPSHOT_DIR_PATTERN;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SnapshotLoaderTest
+{
+    static String DATA_DIR_1 = "data1";
+    static String DATA_DIR_2 = "data2";
+    static String DATA_DIR_3 = "data3";
+    static String[] DATA_DIRS = new String[]{DATA_DIR_1, DATA_DIR_2, 
DATA_DIR_3};
+
+    static String KEYSPACE_1 = "ks1";
+    static String TABLE1_NAME = "table_1";
+    static UUID TABLE1_ID = UUID.randomUUID();
+    static String TABLE2_NAME = "table2";
+    static UUID TABLE2_ID = UUID.randomUUID();
+
+    static String KEYSPACE_2 = "ks2";
+    static String TABLE3_NAME = "table_3";
+    static UUID TABLE3_ID = UUID.randomUUID();
+
+    static String TAG1 = "tag1";
+    static String TAG2 = "tag2";
+    static String TAG3 = "tag3";
+
+    static String INVALID_NAME = "#test#";
+    static String INVALID_ID = "XPTO";
+
+    @ClassRule
+    public static TemporaryFolder tmpDir = new TemporaryFolder();
+
+    @Test
+    public void testMatcher()
+    {
+        String INDEX_SNAPSHOT = 
"/user/.ccm/test/node1/data0/ks/indexed_table-24b241e0c58f11eca526336fc2c671ab/snapshots/test";
+        
assertThat(SNAPSHOT_DIR_PATTERN.matcher(INDEX_SNAPSHOT).find()).isTrue();
+
+        String TABLE_SNAPSHOT = 
"/Users/pmottagomes/.ccm/test/node1/data0/ks/my_table-1a025b40c58f11eca526336fc2c671ab/snapshots/test";
+        
assertThat(SNAPSHOT_DIR_PATTERN.matcher(TABLE_SNAPSHOT).find()).isTrue();
+
+        String DROPPED_SNAPSHOT = 
"/Users/pmottagomes/.ccm/test/node1/data0/ks/my_table-e5c58330c58d11eca526336fc2c671ab/snapshots/dropped-1650997415751-my_table";
+        
assertThat(SNAPSHOT_DIR_PATTERN.matcher(DROPPED_SNAPSHOT).find()).isTrue();
+    }
+
+    @Test
+    public void testNoSnapshots() throws IOException
+    {
+        // Create table directories on all data directories without snapshots
+        File baseDir  = new File(tmpDir.newFolder());
+        for (String dataDir : DATA_DIRS)
+        {
+            createDir(baseDir, dataDir, KEYSPACE_1, tableDirName(TABLE1_NAME, 
TABLE1_ID));
+            createDir(baseDir, dataDir, KEYSPACE_1, tableDirName(TABLE2_NAME, 
TABLE2_ID));
+            createDir(baseDir, dataDir, KEYSPACE_2, tableDirName(TABLE3_NAME, 
TABLE3_ID));
+        }
+
+        // Check no snapshots are found
+        SnapshotLoader loader = new 
SnapshotLoader(Arrays.asList(Paths.get(baseDir.toString(), DATA_DIR_1),
+                                                                 
Paths.get(baseDir.toString(), DATA_DIR_2),
+                                                                 
Paths.get(baseDir.toString(), DATA_DIR_3)));
+        assertThat(loader.loadSnapshots()).isEmpty();
+    }
+
+    @Test
+    public void testSnapshotsWithoutManifests() throws IOException
+    {
+        Set<File> tag1Files = new HashSet<>();
+        Set<File> tag2Files = new HashSet<>();
+        Set<File> tag3Files = new HashSet<>();
+
+        // Create one snapshot per table - without manifests:
+        // - ks1.t1 : tag1
+        // - ks1.t2 : tag2
+        // - ks2.t3 : tag3
+        File baseDir  = new File(tmpDir.newFolder());
+        for (String dataDir : DATA_DIRS)
+        {
+            tag1Files.add(createDir(baseDir, dataDir, KEYSPACE_1, 
tableDirName(TABLE1_NAME, TABLE1_ID), Directories.SNAPSHOT_SUBDIR, TAG1));
+            tag2Files.add(createDir(baseDir, dataDir, KEYSPACE_1, 
tableDirName(TABLE2_NAME, TABLE2_ID), Directories.SNAPSHOT_SUBDIR, TAG2));
+            tag3Files.add(createDir(baseDir, dataDir, KEYSPACE_2, 
tableDirName(TABLE3_NAME, TABLE3_ID), Directories.SNAPSHOT_SUBDIR, TAG3));
+        }
+
+        // Verify all 3 snapshots are found correctly from data directories
+        SnapshotLoader loader = new 
SnapshotLoader(Arrays.asList(Paths.get(baseDir.toString(), DATA_DIR_1),
+                                                                 
Paths.get(baseDir.toString(), DATA_DIR_2),
+                                                                 
Paths.get(baseDir.toString(), DATA_DIR_3)));
+        Set<TableSnapshot> snapshots = loader.loadSnapshots();
+        assertThat(snapshots).hasSize(3);
+        assertThat(snapshots).contains(new TableSnapshot(KEYSPACE_1, 
TABLE1_NAME, TABLE1_ID, TAG1, null, null, tag1Files));
+        assertThat(snapshots).contains(new TableSnapshot(KEYSPACE_1, 
TABLE2_NAME, TABLE2_ID,  TAG2, null, null, tag2Files));
+        assertThat(snapshots).contains(new TableSnapshot(KEYSPACE_2, 
TABLE3_NAME, TABLE3_ID,  TAG3, null, null, tag3Files));
+    }
+
+    @Test
+    public void testSnapshotsWithManifests() throws IOException
+    {
+        Set<File> tag1Files = new HashSet<>();
+        Set<File> tag2Files = new HashSet<>();
+        Set<File> tag3Files = new HashSet<>();
+
+        // Create one snapshot per table:
+        // - ks1.t1 : tag1
+        // - ks1.t2 : tag2
+        // - ks2.t3 : tag3
+        File baseDir  = new File(tmpDir.newFolder());
+        for (String dataDir : DATA_DIRS)
+        {
+            tag1Files.add(createDir(baseDir, dataDir, KEYSPACE_1, 
tableDirName(TABLE1_NAME, TABLE1_ID), Directories.SNAPSHOT_SUBDIR, TAG1));
+            tag2Files.add(createDir(baseDir, dataDir, KEYSPACE_1, 
tableDirName(TABLE2_NAME, TABLE2_ID), Directories.SNAPSHOT_SUBDIR, TAG2));
+            tag3Files.add(createDir(baseDir, dataDir, KEYSPACE_2, 
tableDirName(TABLE3_NAME, TABLE3_ID), Directories.SNAPSHOT_SUBDIR, TAG3));
+        }
+
+        // Write manifest for snapshot tag1 on random location
+        Instant tag1Ts = Instant.now();
+        File tag1ManifestLocation = tag1Files.toArray(new 
File[0])[ThreadLocalRandom.current().nextInt(tag1Files.size())];
+        writeManifest(tag1ManifestLocation, tag1Ts, null);
+
+        // Write manifest for snapshot tag2 on random location
+        Instant tag2Ts = Instant.now().plusSeconds(10);
+        DurationSpec tag2Ttl = new DurationSpec("10h");
+        File tag2ManifestLocation = tag2Files.toArray(new 
File[0])[ThreadLocalRandom.current().nextInt(tag2Files.size())];
+        writeManifest(tag2ManifestLocation, tag2Ts, tag2Ttl);
+
+        // Write manifest for snapshot tag3 on random location
+        Instant tag3Ts = Instant.now().plusSeconds(20);
+        File tag3ManifestLocation = tag3Files.toArray(new 
File[0])[ThreadLocalRandom.current().nextInt(tag3Files.size())];
+        writeManifest(tag3ManifestLocation, tag3Ts, null);
+
+        // Verify all 3 snapshots are found correctly from data directories
+        SnapshotLoader loader = new 
SnapshotLoader(Arrays.asList(Paths.get(baseDir.toString(), DATA_DIR_1),
+                                                                 
Paths.get(baseDir.toString(), DATA_DIR_2),
+                                                                 
Paths.get(baseDir.toString(), DATA_DIR_3)));
+        Set<TableSnapshot> snapshots = loader.loadSnapshots();
+        assertThat(snapshots).hasSize(3);
+        assertThat(snapshots).contains(new TableSnapshot(KEYSPACE_1, 
TABLE1_NAME, TABLE1_ID, TAG1, tag1Ts, null, tag1Files));
+        assertThat(snapshots).contains(new TableSnapshot(KEYSPACE_1, 
TABLE2_NAME, TABLE2_ID,  TAG2, tag2Ts, tag2Ts.plusSeconds(tag2Ttl.toSeconds()), 
tag2Files));
+        assertThat(snapshots).contains(new TableSnapshot(KEYSPACE_2, 
TABLE3_NAME, TABLE3_ID,  TAG3, tag3Ts, null, tag3Files));
+    }
+
+    @Test
+    public void testInvalidSnapshotsAreNotLoaded() throws IOException
+    {
+        Set<File> tag1Files = new HashSet<>();
+        Set<File> tag2Files = new HashSet<>();
+        Set<File> tag3Files = new HashSet<>();
+
+        // Create invalid snapshot directory structure
+        // - /data_dir/#test#/table1-validuuid/snapshot/tag1
+        // - /data_dir/ks1/#test#-validuuid/snapshot/tag2
+        // - /data_dir/ks2/table3-invaliduuid/snapshot/tag3
+        File baseDir  = new File(tmpDir.newFolder());
+        for (String dataDir : DATA_DIRS)
+        {
+            tag1Files.add(createDir(baseDir, dataDir, INVALID_NAME, 
tableDirName(TABLE1_NAME, TABLE1_ID), Directories.SNAPSHOT_SUBDIR, TAG1));
+            tag2Files.add(createDir(baseDir, dataDir, KEYSPACE_1, 
tableDirName(INVALID_NAME, TABLE2_ID), Directories.SNAPSHOT_SUBDIR, TAG2));
+            tag3Files.add(createDir(baseDir, dataDir, KEYSPACE_2, 
String.format("%s-%s", TABLE3_NAME, INVALID_ID), Directories.SNAPSHOT_SUBDIR, 
TAG3));
+        }
+
+        // Check no snapshots are loaded
+        SnapshotLoader loader = new 
SnapshotLoader(Arrays.asList(Paths.get(baseDir.toString(), DATA_DIR_1),
+                                                                 
Paths.get(baseDir.toString(), DATA_DIR_2),
+                                                                 
Paths.get(baseDir.toString(), DATA_DIR_3)));
+        assertThat(loader.loadSnapshots()).isEmpty();
+    }
+
+    @Test
+    public void testParseUUID()
+    {
+        
assertThat(SnapshotLoader.parseUUID("c7e513243f0711ec9bbc0242ac130002")).isEqualTo(UUID.fromString("c7e51324-3f07-11ec-9bbc-0242ac130002"));
+    }
+
+    private void writeManifest(File snapshotDir, Instant creationTime, 
DurationSpec ttl) throws IOException
+    {
+        SnapshotManifest manifest = new 
SnapshotManifest(Lists.newArrayList("f1", "f2", "f3"), ttl, creationTime);
+        manifest.serializeToJsonFile(getManifestFile(snapshotDir));
+    }
+
+    private static File createDir(File baseDir, String... subdirs)
+    {
+        File file = new File(Paths.get(baseDir.toString(), 
subdirs).toString());
+        file.toJavaIOFile().mkdirs();
+        return file;
+    }
+
+    static String tableDirName(String tableName, UUID tableId)
+    {
+        return String.format("%s-%s", tableName, removeDashes(tableId));
+    }
+
+    static String removeDashes(UUID id)
+    {
+        return id.toString().replace("-", "");
+    }
+
+    public static File getManifestFile(File snapshotDir)
+    {
+        return new File(snapshotDir, "manifest.json");
+    }
+}
diff --git 
a/test/unit/org/apache/cassandra/service/snapshot/SnapshotManagerTest.java 
b/test/unit/org/apache/cassandra/service/snapshot/SnapshotManagerTest.java
index 9530c95e61..b07bb23487 100644
--- a/test/unit/org/apache/cassandra/service/snapshot/SnapshotManagerTest.java
+++ b/test/unit/org/apache/cassandra/service/snapshot/SnapshotManagerTest.java
@@ -21,6 +21,7 @@ package org.apache.cassandra.service.snapshot;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.List;
+import java.util.UUID;
 import java.util.stream.Stream;
 
 import org.junit.BeforeClass;
@@ -52,13 +53,13 @@ public class SnapshotManagerTest
 
     private TableSnapshot generateSnapshotDetails(String tag, Instant 
expiration) throws Exception {
         return new TableSnapshot(
-            "ks",
-            "tbl",
-            tag,
-            Instant.EPOCH,
-            expiration,
-            createFolders(temporaryFolder),
-            (file) -> 0L
+        "ks",
+        "tbl",
+        UUID.randomUUID(),
+        tag,
+        Instant.EPOCH,
+        expiration,
+        createFolders(temporaryFolder)
         );
     }
 
diff --git 
a/test/unit/org/apache/cassandra/service/snapshot/TableSnapshotTest.java 
b/test/unit/org/apache/cassandra/service/snapshot/TableSnapshotTest.java
index 0b77472e71..4bb1756c31 100644
--- a/test/unit/org/apache/cassandra/service/snapshot/TableSnapshotTest.java
+++ b/test/unit/org/apache/cassandra/service/snapshot/TableSnapshotTest.java
@@ -19,10 +19,12 @@
 package org.apache.cassandra.service.snapshot;
 
 import java.io.IOException;
+import java.nio.file.Paths;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.UUID;
 
 import org.junit.Before;
 import org.junit.ClassRule;
@@ -68,12 +70,11 @@ public class TableSnapshotTest
         TableSnapshot snapshot = new TableSnapshot(
         "ks",
         "tbl",
+        UUID.randomUUID(),
         "some",
         null,
         null,
-        folders,
-        (File file) -> 0L
-        );
+        folders);
 
         assertThat(snapshot.exists()).isTrue();
 
@@ -90,12 +91,11 @@ public class TableSnapshotTest
         TableSnapshot snapshot = new TableSnapshot(
         "ks",
         "tbl",
+        UUID.randomUUID(),
         "some",
         null,
         null,
-        folders,
-        (File file) -> 0L
-        );
+        folders);
 
         assertThat(snapshot.isExpiring()).isFalse();
         assertThat(snapshot.isExpired(now())).isFalse();
@@ -103,12 +103,11 @@ public class TableSnapshotTest
         snapshot = new TableSnapshot(
         "ks",
         "tbl",
+        UUID.randomUUID(),
         "some",
         now(),
         null,
-        folders,
-        (File file) -> 0L
-        );
+        folders);
 
         assertThat(snapshot.isExpiring()).isFalse();
         assertThat(snapshot.isExpired(now())).isFalse();
@@ -116,12 +115,11 @@ public class TableSnapshotTest
         snapshot = new TableSnapshot(
         "ks",
         "tbl",
+        UUID.randomUUID(),
         "some",
         now(),
         now().plusSeconds(1000),
-        folders,
-        (File file) -> 0L
-        );
+        folders);
 
         assertThat(snapshot.isExpiring()).isTrue();
         assertThat(snapshot.isExpired(now())).isFalse();
@@ -129,12 +127,11 @@ public class TableSnapshotTest
         snapshot = new TableSnapshot(
         "ks",
         "tbl",
+        UUID.randomUUID(),
         "some",
         now(),
         now().minusSeconds(1000),
-        folders,
-        (File file) -> 0L
-        );
+        folders);
 
         assertThat(snapshot.isExpiring()).isTrue();
         assertThat(snapshot.isExpired(now())).isTrue();
@@ -158,14 +155,11 @@ public class TableSnapshotTest
         TableSnapshot tableDetails = new TableSnapshot(
         "ks",
         "tbl",
+        UUID.randomUUID(),
         "some",
         null,
         null,
-        folders,
-        (File file) -> {
-            return 0L;
-        }
-        );
+        folders);
 
         Long res = 0L;
 
@@ -187,19 +181,19 @@ public class TableSnapshotTest
         TableSnapshot tableDetails = new TableSnapshot(
         "ks",
         "tbl",
+        UUID.randomUUID(),
         "some",
         null,
         null,
-        folders,
-        File::length
-        );
+        folders);
 
         Long res = 0L;
 
         for (File dir : folders)
         {
-            writeBatchToFile(new File(dir, "tmp"));
-            res += dir.length();
+            File file = new File(dir, "tmp");
+            writeBatchToFile(file);
+            res += file.length();
         }
 
         assertThat(tableDetails.computeTrueSizeBytes()).isGreaterThan(0L);
@@ -216,25 +210,41 @@ public class TableSnapshotTest
         TableSnapshot withCreatedAt = new TableSnapshot(
         "ks",
         "tbl",
+        UUID.randomUUID(),
         "some1",
         createdAt,
         null,
-        folders,
-        (File file) -> 0L
-        );
+        folders);
         assertThat(withCreatedAt.getCreatedAt()).isEqualTo(createdAt);
 
         // When createdAt is  null, it should return the snapshot folder 
minimum update time
         TableSnapshot withoutCreatedAt = new TableSnapshot(
         "ks",
         "tbl",
+        UUID.randomUUID(),
         "some1",
         null,
         null,
-        folders,
-        (File file) -> 0L
-        );
+        folders);
         
assertThat(withoutCreatedAt.getCreatedAt()).isEqualTo(Instant.ofEpochMilli(folders.stream().mapToLong(f
 -> f.lastModified()).min().getAsLong()));
     }
 
+    @Test
+    public void testGetLiveFileFromSnapshotFile()
+    {
+        
testGetLiveFileFromSnapshotFile("~/.ccm/test/node1/data0/test_ks/tbl-e03faca0813211eca100c705ea09b5ef/snapshots/1643481737850/me-1-big-Data.db",
+                                        
"~/.ccm/test/node1/data0/test_ks/tbl-e03faca0813211eca100c705ea09b5ef/me-1-big-Data.db");
+    }
+
+    @Test
+    public void testGetLiveFileFromSnapshotIndexFile()
+    {
+        
testGetLiveFileFromSnapshotFile("~/.ccm/test/node1/data0/test_ks/tbl-e03faca0813211eca100c705ea09b5ef/snapshots/1643481737850/.tbl_val_idx/me-1-big-Summary.db",
+                                        
"~/.ccm/test/node1/data0/test_ks/tbl-e03faca0813211eca100c705ea09b5ef/.tbl_val_idx/me-1-big-Summary.db");
+    }
+
+    public void testGetLiveFileFromSnapshotFile(String snapshotFile, String 
expectedLiveFile)
+    {
+        
assertThat(TableSnapshot.getLiveFileFromSnapshotFile(Paths.get(snapshotFile)).toString()).isEqualTo(expectedLiveFile);
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to