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

asf-gitbox-commits pushed a commit to branch cassandra-4.0
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/cassandra-4.0 by this push:
     new d0851786a5 Delete SSTable components oldest-first so a partially 
deleted SSTable's transaction log is not reported as corrupt
d0851786a5 is described below

commit d0851786a556b9334c33194c1f6fb2f40616fb20
Author: nivy <[email protected]>
AuthorDate: Tue Nov 3 11:18:29 2026 -0800

    Delete SSTable components oldest-first so a partially deleted SSTable's 
transaction log is not reported as corrupt
    
    A REMOVE record in a transaction log stores the newest last-modified time
    among the SSTable's files. On startup, LogFile compares that value against
    the files still on disk. If they differ, the log is reported as corrupt.
    
    SSTableTidier deleted the Data component first and then the remaining
    components in arbitrary order. If the node stopped partway through, the
    newest remaining file could be older than the recorded time, and startup
    failed.
    
    Sort the components by last-modified time and delete the oldest first,
    so after any partial delete the newest remaining file still matches
    the REMOVE record and the leftover files are cleaned up normally.
    
    patch by Nivy Kani; reviewed by Caleb Rackliffe and Jon Meredith for 
CASSANDRA-21649
---
 CHANGES.txt                                        |   1 +
 .../cassandra/db/lifecycle/LogTransaction.java     |  10 +-
 .../org/apache/cassandra/io/sstable/SSTable.java   |  24 ++--
 .../lifecycle/SSTableTidierPartialDeleteTest.java  | 133 +++++++++++++++++++++
 4 files changed, 150 insertions(+), 18 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 9ec6355e54..506e707cd4 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 4.0.22
+ * Delete SSTable files in order of last-updated timestamp (CASSANDRA-21649)
  * Prevent negative BufferPool overflow memory accounting (CASSANDRA-21815)
  * Respect cassandra.skip_sync in LogReplica, SSTable.appendTOC and 
ChecksumWriter (CASSANDRA-21835)
  * Fix cassandra-env.sh treating Xmx/Xms/Xmn inside paths as heap flags 
(CASSANDRA-21804)
diff --git a/src/java/org/apache/cassandra/db/lifecycle/LogTransaction.java 
b/src/java/org/apache/cassandra/db/lifecycle/LogTransaction.java
index 43b9e1f771..0a1804324b 100644
--- a/src/java/org/apache/cassandra/db/lifecycle/LogTransaction.java
+++ b/src/java/org/apache/cassandra/db/lifecycle/LogTransaction.java
@@ -386,18 +386,14 @@ class LogTransaction extends 
Transactional.AbstractTransactional implements Tran
             {
                 try
                 {
-                    // If we can't successfully delete the DATA component, set 
the task to be retried later: see TransactionTidier
-                    File datafile = new File(desc.filenameFor(Component.DATA));
-
+                    // If we can't successfully delete all components, set the 
task to be retried later: see TransactionTidier
                     if (logger.isTraceEnabled())
                         logger.trace("Tidier running for old sstable {}", 
desc.baseFilename());
 
-                    if (datafile.exists())
-                        delete(datafile);
-                    else if (!wasNew)
+                    if (!new File(desc.filenameFor(Component.DATA)).exists() 
&& !wasNew)
                         logger.error("SSTableTidier ran with no existing data 
file for an sstable that was not new");
 
-                    // let the remainder be cleaned up by delete
+                    // delete all components, DATA included, oldest first so 
the REMOVE record stays valid on partial deletes
                     SSTable.delete(desc, SSTable.discoverComponentsFor(desc));
                 }
                 catch (Throwable t)
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTable.java 
b/src/java/org/apache/cassandra/io/sstable/SSTable.java
index 7bbcedc9fa..f80b66cc71 100644
--- a/src/java/org/apache/cassandra/io/sstable/SSTable.java
+++ b/src/java/org/apache/cassandra/io/sstable/SSTable.java
@@ -110,19 +110,21 @@ public abstract class SSTable
     public static boolean delete(Descriptor desc, Set<Component> components)
     {
         logger.info("Deleting sstable: {}", desc);
-        // remove the DATA component first if it exists
-        if (components.contains(Component.DATA))
-            FileUtils.deleteWithConfirm(desc.filenameFor(Component.DATA));
-        for (Component component : components)
-        {
-            if (component.equals(Component.DATA) || 
component.equals(Component.SUMMARY))
-                continue;
 
-            FileUtils.deleteWithConfirm(desc.filenameFor(component));
-        }
+        // delete older files first so the overall SSTable timestamp stays the 
same on partial deletes
+        List<Component> sorted = new ArrayList<>(components);
+        Map<Component, Long> lastModified = new HashMap<>();
+        for (Component component : sorted)
+            lastModified.put(component, new 
File(desc.filenameFor(component)).lastModified());
+        sorted.sort(Comparator.comparingLong(lastModified::get));
 
-        if (components.contains(Component.SUMMARY))
-            FileUtils.delete(desc.filenameFor(Component.SUMMARY));
+        for (Component component : sorted)
+        {
+            if (component.equals(Component.SUMMARY))
+                FileUtils.delete(desc.filenameFor(component));
+            else
+                FileUtils.deleteWithConfirm(desc.filenameFor(component));
+        }
 
         return true;
     }
diff --git 
a/test/unit/org/apache/cassandra/db/lifecycle/SSTableTidierPartialDeleteTest.java
 
b/test/unit/org/apache/cassandra/db/lifecycle/SSTableTidierPartialDeleteTest.java
new file mode 100644
index 0000000000..573464d9b2
--- /dev/null
+++ 
b/test/unit/org/apache/cassandra/db/lifecycle/SSTableTidierPartialDeleteTest.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.db.lifecycle;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.jboss.byteman.contrib.bmunit.BMRule;
+import org.jboss.byteman.contrib.bmunit.BMRules;
+import org.jboss.byteman.contrib.bmunit.BMUnitRunner;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.Directories;
+import org.apache.cassandra.db.commitlog.CommitLog;
+import org.apache.cassandra.db.compaction.OperationType;
+import org.apache.cassandra.io.sstable.Component;
+import org.apache.cassandra.io.sstable.format.SSTableReader;
+import org.apache.cassandra.schema.MockSchema;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Verifies that when the {@link LogTransaction.SSTableTidier} fails partway 
through deleting an sstable,
+ * the transaction log is kept and still matches the files left on disk, so 
both the tidier's retry
+ * and the startup cleanup finish the deletion.
+ */
+@RunWith(BMUnitRunner.class)
+@BMRules(rules = {
+    @BMRule(name = "Arm a delete failure after the given number of successful 
deletes",
+            targetClass = 
"org.apache.cassandra.db.lifecycle.SSTableTidierPartialDeleteTest",
+            targetMethod = "runTidier",
+            targetLocation = "AT ENTRY",
+            action = "createCountDown(\"deletes\", $1);"),
+    @BMRule(name = "Fail the delete once the countdown runs out",
+            targetClass = "org.apache.cassandra.io.util.FileUtils",
+            targetMethod = "deleteWithConfirm(java.lang.String)",
+            targetLocation = "AT ENTRY",
+            condition = "countDown(\"deletes\")",
+            action = "throw new org.apache.cassandra.io.FSWriteError(new 
java.io.IOException(\"simulated delete failure\"), $1);")
+})
+public class SSTableTidierPartialDeleteTest
+{
+    @BeforeClass
+    public static void setUp()
+    {
+        DatabaseDescriptor.daemonInitialization();
+        CommitLog.instance.start();
+        MockSchema.cleanup();
+    }
+
+    @Test
+    public void testPartialDeleteIsFinishedByRetryOrStartupCleanup()
+    {
+        // fail after each number of successful deletes that leaves files 
behind (there are 4 components in MockSchema)
+        for (int deletes = 1; deletes < 4; deletes++)
+        {
+            assertPartialDeleteFinished(deletes, false);
+            assertPartialDeleteFinished(deletes, true);
+        }
+    }
+
+    private static void assertPartialDeleteFinished(int deletes, boolean 
restart)
+    {
+        ColumnFamilyStore cfs = MockSchema.newCFS();
+        SSTableReader sstableOld = MockSchema.sstable(0, true, cfs);
+        SSTableReader sstableNew = MockSchema.sstable(1, true, cfs);
+        List<File> files = 
sstableOld.getAllFilePaths().stream().map(File::new).collect(Collectors.toList());
+
+        // make DATA the newest file, so deleting it early would cause the 
REMOVE record to have a different timestamp
+        File data = new 
File(sstableOld.descriptor.filenameFor(Component.DATA));
+        long newest = 
files.stream().mapToLong(File::lastModified).max().getAsLong();
+        assertTrue(data.setLastModified(newest + 1000));
+
+        LogTransaction log = new LogTransaction(OperationType.COMPACTION);
+        log.trackNew(sstableNew);
+        LogTransaction.SSTableTidier tidier = log.obsoleted(sstableOld);
+        List<File> logFiles = log.txnFile().getFiles();
+        log.finish();
+
+        runTidier(deletes, tidier, sstableOld);
+
+        // the tidier failed partway, but DATA is deleted last so the txn log 
still matches the files on disk
+        assertEquals(files.size() - deletes, 
files.stream().filter(File::exists).count());
+        assertTrue("DATA should be deleted last", data.exists());
+        for (File logFile : logFiles)
+            assertTrue("txn log invalid after " + deletes + " deletes", 
LogFile.make(logFile).verify());
+
+        if (restart)
+        {
+            // the startup txn log replay finishes the deletion, and the old 
sstable isn't picked up as live
+            
assertTrue(LogTransaction.removeUnfinishedLeftovers(cfs.metadata()));
+            assertEquals(Collections.singleton(sstableNew.descriptor),
+                         new 
Directories(cfs.metadata()).sstableLister(Directories.OnTxnErr.THROW).list().keySet());
+        }
+
+        // the tidier's retry finishes the deletion (or finds nothing left 
after a restart) and releases the txn log
+        sstableNew.selfRef().release();
+        LogTransaction.rescheduleFailedDeletions();
+        LogTransaction.waitForDeletions();
+        assertTrue(Stream.concat(files.stream(), 
logFiles.stream()).noneMatch(File::exists));
+    }
+
+    // the BMRules above use deletesBeforeFailure to fail the next delete
+    private static void runTidier(int deletesBeforeFailure, 
LogTransaction.SSTableTidier tidier, SSTableReader sstable)
+    {
+        sstable.markObsolete(tidier);
+        sstable.selfRef().release();
+        LogTransaction.waitForDeletions();
+    }
+}


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

Reply via email to