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 7741eacc54 Add auto_snapshot_ttl configuration
7741eacc54 is described below

commit 7741eacc546f80fe4324c7821fcf2c029f64b1f9
Author: Paulo Motta <[email protected]>
AuthorDate: Fri Apr 29 20:20:19 2022 -0300

    Add auto_snapshot_ttl configuration
    
    Patch by Paulo Motta; Reviewed by Stefan Miklosovic for CASSANDRA-16790
    
    Co-authored-by: fibersel <[email protected]>
---
 CHANGES.txt                                        |   1 +
 conf/cassandra.yaml                                |   8 +
 src/java/org/apache/cassandra/config/Config.java   |   8 +
 .../cassandra/config/DatabaseDescriptor.java       |  24 +++
 .../org/apache/cassandra/db/ColumnFamilyStore.java |  11 +-
 .../distributed/test/AutoSnapshotTtlTest.java      | 160 ++++++++++++++++++++
 .../validation/operations/AutoSnapshotTest.java    | 162 +++++++++++++++++++++
 7 files changed, 371 insertions(+), 3 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 726cfad750..050767a943 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 4.1
+ * Add auto_snapshot_ttl configuration (CASSANDRA-16790)
  * 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)
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index f846c7148a..2879680ecf 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -876,6 +876,14 @@ snapshot_before_compaction: false
 # lose data on truncation or drop.
 auto_snapshot: true
 
+# Adds a time-to-live (TTL) to auto snapshots generated by table
+# truncation or drop (when enabled).
+# After the TTL is elapsed, the snapshot is automatically cleared.
+# By default, auto snapshots *do not* have TTL, uncomment the property below
+# to enable TTL on auto snapshots.
+# Accepted units: d (days), h (hours) or m (minutes)
+# auto_snapshot_ttl: 30d
+
 # The act of creating or clearing a snapshot involves creating or removing
 # potentially tens of thousands of links, which can cause significant 
performance
 # impact, especially on consumer grade SSDs. A non-zero value here can
diff --git a/src/java/org/apache/cassandra/config/Config.java 
b/src/java/org/apache/cassandra/config/Config.java
index 9bf9dfffe5..35c9f67e83 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -292,6 +292,14 @@ public class Config
 
     public boolean snapshot_before_compaction = false;
     public boolean auto_snapshot = true;
+
+    /**
+     * When auto_snapshot is true and this property
+     * is set, snapshots created by truncation or
+     * drop use this TTL.
+     */
+    public String auto_snapshot_ttl;
+
     public volatile long snapshot_links_per_second = 0;
 
     /* if the size of columns or super-columns are more than this, indexing 
will kick in */
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java 
b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index 5c661321a3..7ed6ce742f 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -160,6 +160,7 @@ public class DatabaseDescriptor
     private static boolean daemonInitialized;
 
     private static final int searchConcurrencyFactor = 
Integer.parseInt(System.getProperty(Config.PROPERTY_PREFIX + 
"search_concurrency_factor", "1"));
+    private static DurationSpec autoSnapshoTtl;
 
     private static volatile boolean disableSTCSInL0 = 
Boolean.getBoolean(Config.PROPERTY_PREFIX + "disable_stcs_in_l0");
     private static final boolean unsafeSystem = 
Boolean.getBoolean(Config.PROPERTY_PREFIX + "unsafesystem");
@@ -394,6 +395,18 @@ public class DatabaseDescriptor
         //InetAddressAndPort and get the right defaults
         InetAddressAndPort.initializeDefaultPort(getStoragePort());
 
+        if (conf.auto_snapshot_ttl != null)
+        {
+            try
+            {
+                autoSnapshoTtl = new DurationSpec(conf.auto_snapshot_ttl);
+            }
+            catch (IllegalArgumentException e)
+            {
+                throw new ConfigurationException("Invalid value of 
auto_snapshot_ttl: " + conf.auto_snapshot_ttl, false);
+            }
+        }
+
         if (conf.commitlog_sync == null)
         {
             throw new ConfigurationException("Missing required directive 
CommitLogSync", false);
@@ -2805,6 +2818,17 @@ public class DatabaseDescriptor
         return conf.auto_snapshot;
     }
 
+    public static DurationSpec getAutoSnapshotTtl()
+    {
+        return autoSnapshoTtl;
+    }
+
+    @VisibleForTesting
+    public static void setAutoSnapshotTtl(DurationSpec newTtl)
+    {
+        autoSnapshoTtl = newTtl;
+    }
+
     @VisibleForTesting
     public static void setAutoSnapshot(boolean autoSnapshot)
     {
diff --git a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java 
b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java
index d46b20421a..0057300690 100644
--- a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java
+++ b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java
@@ -2184,7 +2184,12 @@ public class ColumnFamilyStore implements 
ColumnFamilyStoreMBean, Memtable.Owner
      */
     public TableSnapshot snapshot(String snapshotName)
     {
-        return snapshot(snapshotName, false, null, null, now());
+        return snapshot(snapshotName, null);
+    }
+
+    public TableSnapshot snapshot(String snapshotName, DurationSpec ttl)
+    {
+        return snapshot(snapshotName, false, ttl, null, now());
     }
 
     /**
@@ -2627,7 +2632,7 @@ public class ColumnFamilyStore implements 
ColumnFamilyStoreMBean, Memtable.Owner
                 data.notifyTruncated(truncatedAt);
 
             if (!noSnapshot && DatabaseDescriptor.isAutoSnapshot())
-                snapshot(Keyspace.getTimestampedSnapshotNameWithPrefix(name, 
SNAPSHOT_TRUNCATE_PREFIX));
+                snapshot(Keyspace.getTimestampedSnapshotNameWithPrefix(name, 
SNAPSHOT_TRUNCATE_PREFIX), DatabaseDescriptor.getAutoSnapshotTtl());
 
             discardSSTables(truncatedAt);
 
@@ -3189,7 +3194,7 @@ public class ColumnFamilyStore implements 
ColumnFamilyStoreMBean, Memtable.Owner
         
CompactionManager.instance.interruptCompactionForCFs(concatWithIndexes(), 
(sstable) -> true, true);
 
         if (DatabaseDescriptor.isAutoSnapshot())
-            snapshot(Keyspace.getTimestampedSnapshotNameWithPrefix(name, 
ColumnFamilyStore.SNAPSHOT_DROP_PREFIX));
+            snapshot(Keyspace.getTimestampedSnapshotNameWithPrefix(name, 
ColumnFamilyStore.SNAPSHOT_DROP_PREFIX), 
DatabaseDescriptor.getAutoSnapshotTtl());
 
         
CommitLog.instance.forceRecycleAllSegments(Collections.singleton(metadata.id));
 
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/AutoSnapshotTtlTest.java
 
b/test/distributed/org/apache/cassandra/distributed/test/AutoSnapshotTtlTest.java
new file mode 100644
index 0000000000..ace3b8fc26
--- /dev/null
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/AutoSnapshotTtlTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.distributed.test;
+
+import java.io.IOException;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.config.CassandraRelevantProperties;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.distributed.api.Feature;
+import org.apache.cassandra.distributed.api.IInvokableInstance;
+import org.apache.cassandra.distributed.shared.WithProperties;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.apache.cassandra.db.ColumnFamilyStore.SNAPSHOT_DROP_PREFIX;
+import static 
org.apache.cassandra.db.ColumnFamilyStore.SNAPSHOT_TRUNCATE_PREFIX;
+import static org.apache.cassandra.distributed.Cluster.build;
+import static org.awaitility.Awaitility.await;
+
+public class AutoSnapshotTtlTest extends TestBaseImpl
+{
+    public static final Integer SNAPSHOT_CLEANUP_PERIOD_SECONDS = 1;
+    public static final Integer FIVE_SECONDS = 5;
+    private static WithProperties properties = new WithProperties();
+
+    @BeforeClass
+    public static void beforeClass() throws Throwable
+    {
+        TestBaseImpl.beforeClass();
+        
properties.set(CassandraRelevantProperties.SNAPSHOT_CLEANUP_INITIAL_DELAY_SECONDS,
 0);
+        
properties.set(CassandraRelevantProperties.SNAPSHOT_CLEANUP_PERIOD_SECONDS, 
SNAPSHOT_CLEANUP_PERIOD_SECONDS);
+        
properties.set(CassandraRelevantProperties.SNAPSHOT_MIN_ALLOWED_TTL_SECONDS, 
FIVE_SECONDS);
+    }
+
+    @AfterClass
+    public static void after()
+    {
+        properties.close();
+    }
+
+    /**
+     * Check that when auto_snapshot_ttl=5s, snapshots created from TRUNCATE 
are expired after 10s
+     */
+    @Test
+    public void testAutoSnapshotTTlOnTruncate() throws IOException
+    {
+        try (Cluster cluster = init(build().withNodes(1)
+                                      .withConfig(c -> c.with(Feature.GOSSIP)
+                                                        
.set("auto_snapshot_ttl", String.format("%ds", FIVE_SECONDS)))
+                                      .start()))
+        {
+            IInvokableInstance instance = cluster.get(1);
+
+            cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (key int, 
value text, PRIMARY KEY (key))"));
+
+            populate(cluster);
+
+            // Truncate Table
+            cluster.schemaChange(withKeyspace("TRUNCATE %s.tbl;"));
+
+            // Check snapshot is listed after table is truncated
+            
instance.nodetoolResult("listsnapshots").asserts().success().stdoutContains(SNAPSHOT_TRUNCATE_PREFIX);
+
+            // Check snapshot is removed after 10s
+            await().timeout(10, SECONDS)
+                   .pollInterval(1, SECONDS)
+                   .until(() -> 
!instance.nodetoolResult("listsnapshots").getStdout().contains(SNAPSHOT_DROP_PREFIX));
+        }
+    }
+
+    /**
+     * Check that when auto_snapshot_ttl=5s, snapshots created from TRUNCATE 
are expired after 10s
+     */
+    @Test
+    public void testAutoSnapshotTTlOnDrop() throws IOException
+    {
+        try (Cluster cluster = init(build().withNodes(1)
+                                      .withConfig(c -> c.with(Feature.GOSSIP)
+                                                                  
.set("auto_snapshot_ttl", String.format("%ds", FIVE_SECONDS)))
+                                      .start()))
+        {
+            IInvokableInstance instance = cluster.get(1);
+
+            cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (key int, 
value text, PRIMARY KEY (key))"));
+
+            populate(cluster);
+
+            // Drop Table
+            cluster.schemaChange(withKeyspace("DROP TABLE %s.tbl;"));
+
+            // Check snapshot is listed after table is dropped
+            
instance.nodetoolResult("listsnapshots").asserts().success().stdoutContains(SNAPSHOT_DROP_PREFIX);
+
+            // Check snapshot is removed after 10s
+            await().timeout(10, SECONDS)
+                   .pollInterval(1, SECONDS)
+                   .until(() -> 
!instance.nodetoolResult("listsnapshots").getStdout().contains(SNAPSHOT_DROP_PREFIX));
+        }
+    }
+
+    /**
+     * Check that when auto_snapshot_ttl is unset, snapshots created from DROP 
or TRUNCATE do not expire
+     */
+    @Test
+    public void testAutoSnapshotTtlDisabled() throws IOException, 
InterruptedException
+    {
+        try (Cluster cluster = init(build().withNodes(1)
+                                      .withConfig(c -> c.with(Feature.GOSSIP))
+                                      .start()))
+        {
+            IInvokableInstance instance = cluster.get(1);
+
+            cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (key int, 
value text, PRIMARY KEY (key))"));
+
+            populate(cluster);
+
+            // Truncate Table
+            cluster.schemaChange(withKeyspace("TRUNCATE %s.tbl;"));
+
+            // Drop Table
+            cluster.schemaChange(withKeyspace("DROP TABLE %s.tbl;"));
+
+            // Check snapshots are created after table is truncated and dropped
+            
instance.nodetoolResult("listsnapshots").asserts().success().stdoutContains(SNAPSHOT_TRUNCATE_PREFIX);
+            
instance.nodetoolResult("listsnapshots").asserts().success().stdoutContains(SNAPSHOT_DROP_PREFIX);
+
+            // Check snapshot are *NOT* expired after 10s
+            Thread.sleep(2 * FIVE_SECONDS * 1000L);
+            
instance.nodetoolResult("listsnapshots").asserts().success().stdoutContains(ColumnFamilyStore.SNAPSHOT_TRUNCATE_PREFIX);
+            
instance.nodetoolResult("listsnapshots").asserts().success().stdoutContains(ColumnFamilyStore.SNAPSHOT_DROP_PREFIX);
+        }
+    }
+
+    protected static void populate(Cluster cluster)
+    {
+        for (int i = 0; i < 100; i++)
+            cluster.coordinator(1).execute(withKeyspace("INSERT INTO %s.tbl 
(key, value) VALUES (?, 'txt')"), ConsistencyLevel.ONE, i);
+    }
+}
diff --git 
a/test/unit/org/apache/cassandra/cql3/validation/operations/AutoSnapshotTest.java
 
b/test/unit/org/apache/cassandra/cql3/validation/operations/AutoSnapshotTest.java
new file mode 100644
index 0000000000..839ca44b28
--- /dev/null
+++ 
b/test/unit/org/apache/cassandra/cql3/validation/operations/AutoSnapshotTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.cql3.validation.operations;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.DurationSpec;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.service.snapshot.TableSnapshot;
+import org.assertj.core.api.Condition;
+
+import static org.apache.cassandra.db.ColumnFamilyStore.SNAPSHOT_DROP_PREFIX;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RunWith(Parameterized.class)
+public class AutoSnapshotTest extends CQLTester
+{
+    static int TTL_SECS = 1;
+
+    public static Boolean enabledBefore;
+    public static DurationSpec ttlBefore;
+
+    @BeforeClass
+    public static void beforeClass()
+    {
+        enabledBefore = DatabaseDescriptor.isAutoSnapshot();
+        ttlBefore = DatabaseDescriptor.getAutoSnapshotTtl();
+    }
+
+    @AfterClass
+    public static void afterClass()
+    {
+        DatabaseDescriptor.setAutoSnapshot(enabledBefore);
+        DatabaseDescriptor.setAutoSnapshotTtl(ttlBefore);
+    }
+
+    // Dynamic parameters used during tests
+    @Parameterized.Parameter(0)
+    public Boolean autoSnapshotEnabled;
+
+    @Parameterized.Parameter(1)
+    public DurationSpec autoSnapshotTTl;
+
+    @Before
+    public void beforeTest() throws Throwable
+    {
+        super.beforeTest();
+        // Make sure we're testing the correct parameterized settings
+        DatabaseDescriptor.setAutoSnapshot(autoSnapshotEnabled);
+        DatabaseDescriptor.setAutoSnapshotTtl(autoSnapshotTTl);
+    }
+
+    // Test for all values of [auto_snapshot=[true,false], ttl=[1s, null]
+    @Parameterized.Parameters( name = "enabled={0},ttl={1}" )
+    public static Collection options() {
+        return Arrays.asList(new Object[][] {
+        { true, DurationSpec.inSeconds(TTL_SECS) },
+        { false, DurationSpec.inSeconds(TTL_SECS) },
+        { true, null },
+        { false, null },
+        });
+    }
+
+    @Test
+    public void testAutoSnapshotOnTrucate() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, PRIMARY KEY(a, 
b))");
+        // Check there are no snapshots
+        ColumnFamilyStore tableDir = getCurrentColumnFamilyStore();
+        assertThat(tableDir.listSnapshots()).isEmpty();
+
+        execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 0, 0, 0);
+        execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 0, 1, 1);
+
+        flush();
+
+        execute("DROP TABLE %s");
+
+        verifyAutoSnapshot(SNAPSHOT_DROP_PREFIX, tableDir);
+    }
+
+    @Test
+    public void testAutoSnapshotOnDrop() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a int, b int, c int, PRIMARY KEY(a, 
b))");
+        // Check there are no snapshots
+        ColumnFamilyStore tableDir = getCurrentColumnFamilyStore();
+        assertThat(tableDir.listSnapshots()).isEmpty();
+
+        execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 0, 0, 0);
+        execute("INSERT INTO %s (a, b, c) VALUES (?, ?, ?)", 0, 1, 1);
+
+        flush();
+
+        execute("DROP TABLE %s");
+
+        verifyAutoSnapshot(SNAPSHOT_DROP_PREFIX, tableDir);
+    }
+
+    /**
+     * Verify that:
+     * - A snapshot is created when auto_snapshot = true.
+     * - TTL is added to the snapshot when auto_snapshot_ttl != null
+     */
+    private void verifyAutoSnapshot(String snapshotPrefix, ColumnFamilyStore 
tableDir)
+    {
+        Map<String, TableSnapshot> snapshots = tableDir.listSnapshots();
+        if (autoSnapshotEnabled)
+        {
+            assertThat(snapshots).hasSize(1);
+            assertThat(snapshots).hasKeySatisfying(new Condition<>(k -> 
k.startsWith(snapshotPrefix), "is dropped snapshot"));
+            TableSnapshot snapshot = snapshots.values().iterator().next();
+            assertThat(snapshot.getTableName()).isEqualTo(currentTable());
+            if (autoSnapshotTTl == null)
+            {
+                // check that the snapshot has NO TTL
+                assertThat(snapshot.isExpiring()).isFalse();
+            }
+            else
+            {
+                // check that snapshot has TTL and is expired after 1 second
+                assertThat(snapshot.isExpiring()).isTrue();
+                Uninterruptibles.sleepUninterruptibly(TTL_SECS, 
TimeUnit.SECONDS);
+                assertThat(snapshot.isExpired(Instant.now())).isTrue();
+            }
+        }
+        else
+        {
+            // No snapshot should be created when auto_snapshot = false
+            assertThat(snapshots).isEmpty();
+        }
+    }
+}


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

Reply via email to