Repository: cassandra Updated Branches: refs/heads/trunk d7d9a9af5 -> 036ddaf53
Re-enable memory-mapped I/O on Windows Patch by jmckenzie; reviewed by tjake for CASSANDRA-9658 Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/b7ae07e5 Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/b7ae07e5 Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/b7ae07e5 Branch: refs/heads/trunk Commit: b7ae07e5c3e4e87c374ee580890c9a00c113f320 Parents: b73aa8e Author: Joshua McKenzie <[email protected]> Authored: Thu Jul 2 17:15:14 2015 -0400 Committer: Joshua McKenzie <[email protected]> Committed: Thu Jul 2 17:15:14 2015 -0400 ---------------------------------------------------------------------- CHANGES.txt | 1 + .../cassandra/config/DatabaseDescriptor.java | 49 ++++---- .../org/apache/cassandra/db/Directories.java | 18 ++- .../db/WindowsFailedSnapshotTracker.java | 115 +++++++++++++++++++ .../org/apache/cassandra/io/util/FileUtils.java | 32 ++++++ .../cassandra/repair/messages/RepairOption.java | 15 ++- .../cassandra/service/CassandraDaemon.java | 4 + .../apache/cassandra/db/SystemKeyspaceTest.java | 44 ++++++- .../io/sstable/SSTableRewriterTest.java | 22 ++++ .../repair/messages/RepairOptionTest.java | 10 +- .../service/StorageServiceServerTest.java | 76 +++++++++++- 11 files changed, 356 insertions(+), 30 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7ae07e5/CHANGES.txt ---------------------------------------------------------------------- diff --git a/CHANGES.txt b/CHANGES.txt index 745cacb..a160b09 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 2.2.0-rc2 + * Re-enable memory-mapped I/O on Windows (CASSANDRA-9658) * Warn when an extra-large partition is compacted (CASSANDRA-9643) * (cqlsh) Allow setting the initial connection timeout (CASSANDRA-9601) * BulkLoader has --transport-factory option but does not use it (CASSANDRA-9675) http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7ae07e5/src/java/org/apache/cassandra/config/DatabaseDescriptor.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java index 632bf0a..5589bc2 100644 --- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java +++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java @@ -291,34 +291,23 @@ public class DatabaseDescriptor if (conf.commitlog_total_space_in_mb == null) conf.commitlog_total_space_in_mb = 8192; - // Always force standard mode access on Windows - CASSANDRA-6993. Windows won't allow deletion of hard-links to files that - // are memory-mapped which causes trouble with snapshots. - if (FBUtilities.isWindows()) + /* evaluate the DiskAccessMode Config directive, which also affects indexAccessMode selection */ + if (conf.disk_access_mode == Config.DiskAccessMode.auto) { - conf.disk_access_mode = Config.DiskAccessMode.standard; + conf.disk_access_mode = hasLargeAddressSpace() ? Config.DiskAccessMode.mmap : Config.DiskAccessMode.standard; indexAccessMode = conf.disk_access_mode; - logger.info("Windows environment detected. DiskAccessMode set to {}, indexAccessMode {}", conf.disk_access_mode, indexAccessMode); + logger.info("DiskAccessMode 'auto' determined to be {}, indexAccessMode is {}", conf.disk_access_mode, indexAccessMode); + } + else if (conf.disk_access_mode == Config.DiskAccessMode.mmap_index_only) + { + conf.disk_access_mode = Config.DiskAccessMode.standard; + indexAccessMode = Config.DiskAccessMode.mmap; + logger.info("DiskAccessMode is {}, indexAccessMode is {}", conf.disk_access_mode, indexAccessMode); } else { - /* evaluate the DiskAccessMode Config directive, which also affects indexAccessMode selection */ - if (conf.disk_access_mode == Config.DiskAccessMode.auto) - { - conf.disk_access_mode = hasLargeAddressSpace() ? Config.DiskAccessMode.mmap : Config.DiskAccessMode.standard; - indexAccessMode = conf.disk_access_mode; - logger.info("DiskAccessMode 'auto' determined to be {}, indexAccessMode is {}", conf.disk_access_mode, indexAccessMode); - } - else if (conf.disk_access_mode == Config.DiskAccessMode.mmap_index_only) - { - conf.disk_access_mode = Config.DiskAccessMode.standard; - indexAccessMode = Config.DiskAccessMode.mmap; - logger.info("DiskAccessMode is {}, indexAccessMode is {}", conf.disk_access_mode, indexAccessMode); - } - else - { - indexAccessMode = conf.disk_access_mode; - logger.info("DiskAccessMode is {}, indexAccessMode is {}", conf.disk_access_mode, indexAccessMode); - } + indexAccessMode = conf.disk_access_mode; + logger.info("DiskAccessMode is {}, indexAccessMode is {}", conf.disk_access_mode, indexAccessMode); } /* Authentication, authorization and role management backend, implementing IAuthenticator, IAuthorizer & IRoleMapper*/ @@ -1303,11 +1292,25 @@ public class DatabaseDescriptor return conf.disk_access_mode; } + // Do not use outside unit tests. + @VisibleForTesting + public static void setDiskAccessMode(Config.DiskAccessMode mode) + { + conf.disk_access_mode = mode; + } + public static Config.DiskAccessMode getIndexAccessMode() { return indexAccessMode; } + // Do not use outside unit tests. + @VisibleForTesting + public static void setIndexAccessMode(Config.DiskAccessMode mode) + { + indexAccessMode = mode; + } + public static void setDiskFailurePolicy(Config.DiskFailurePolicy policy) { conf.disk_failure_policy = policy; http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7ae07e5/src/java/org/apache/cassandra/db/Directories.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/db/Directories.java b/src/java/org/apache/cassandra/db/Directories.java index ee8ecde..4982407 100644 --- a/src/java/org/apache/cassandra/db/Directories.java +++ b/src/java/org/apache/cassandra/db/Directories.java @@ -49,6 +49,7 @@ import org.apache.cassandra.io.FSWriteError; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.sstable.*; import org.apache.cassandra.utils.ByteBufferUtil; +import org.apache.cassandra.utils.FBUtilities; import org.apache.cassandra.utils.Pair; /** @@ -697,7 +698,22 @@ public class Directories if (snapshotDir.exists()) { logger.debug("Removing snapshot directory {}", snapshotDir); - FileUtils.deleteRecursive(snapshotDir); + try + { + FileUtils.deleteRecursive(snapshotDir); + } + catch (FSWriteError e) + { + if (FBUtilities.isWindows()) + { + logger.warn("Failed to delete snapshot directory [{}]. Folder will be deleted on JVM shutdown or next node restart on crash. You can safely attempt to delete this folder but it will fail so long as readers are open on the files.", snapshotDir); + WindowsFailedSnapshotTracker.handleFailedSnapshot(snapshotDir); + } + else + { + throw e; + } + } } } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7ae07e5/src/java/org/apache/cassandra/db/WindowsFailedSnapshotTracker.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/db/WindowsFailedSnapshotTracker.java b/src/java/org/apache/cassandra/db/WindowsFailedSnapshotTracker.java new file mode 100644 index 0000000..ce89823 --- /dev/null +++ b/src/java/org/apache/cassandra/db/WindowsFailedSnapshotTracker.java @@ -0,0 +1,115 @@ +/* + * 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; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Paths; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.io.util.FileUtils; + + +public class WindowsFailedSnapshotTracker +{ + private static final Logger logger = LoggerFactory.getLogger(WindowsFailedSnapshotTracker.class); + private static PrintWriter _failedSnapshotFile; + + @VisibleForTesting + // Need to handle null for unit tests + public static final String TODELETEFILE = System.getenv("CASSANDRA_HOME") == null + ? ".toDelete" + : System.getenv("CASSANDRA_HOME") + File.separator + ".toDelete"; + + public static void deleteOldSnapshots() + { + if (new File(TODELETEFILE).exists()) + { + try + { + BufferedReader reader = new BufferedReader(new FileReader(TODELETEFILE)); + String snapshotDirectory; + while ((snapshotDirectory = reader.readLine()) != null) + { + File f = new File(snapshotDirectory); + + // Skip folders that aren't a subset of temp or a data folder. We don't want people to accidentally + // delete something important by virtue of adding something invalid to the .toDelete file. + boolean validFolder = FileUtils.isSubDirectory(new File(System.getenv("TEMP")), f); + for (String s : DatabaseDescriptor.getAllDataFileLocations()) + validFolder |= FileUtils.isSubDirectory(new File(s), f); + + if (!validFolder) + { + logger.warn("Skipping invalid directory found in .toDelete: {}. Only %TEMP% or data file subdirectories are valid.", f); + continue; + } + + // Could be a non-existent directory if deletion worked on previous JVM shutdown. + if (f.exists()) + { + logger.warn("Discovered obsolete snapshot. Deleting directory [{}]", snapshotDirectory); + FileUtils.deleteRecursive(new File(snapshotDirectory)); + } + } + reader.close(); + + // Only delete the old .toDelete file if we succeed in deleting all our known bad snapshots. + Files.delete(Paths.get(TODELETEFILE)); + } + catch (IOException e) + { + logger.warn("Failed to open {}. Obsolete snapshots from previous runs will not be deleted.", TODELETEFILE); + logger.warn("Exception: " + e); + } + } + + try + { + _failedSnapshotFile = new PrintWriter(new FileWriter(TODELETEFILE, true)); + } + catch (IOException e) + { + throw new RuntimeException(String.format("Failed to create failed snapshot tracking file [%s]. Aborting", TODELETEFILE)); + } + } + + public static synchronized void handleFailedSnapshot(File dir) + { + assert(_failedSnapshotFile != null); + FileUtils.deleteRecursiveOnExit(dir); + _failedSnapshotFile.println(dir.toString()); + _failedSnapshotFile.flush(); + } + + @VisibleForTesting + public static void resetForTests() + { + _failedSnapshotFile.close(); + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7ae07e5/src/java/org/apache/cassandra/io/util/FileUtils.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/io/util/FileUtils.java b/src/java/org/apache/cassandra/io/util/FileUtils.java index 2566952..3552ca9 100644 --- a/src/java/org/apache/cassandra/io/util/FileUtils.java +++ b/src/java/org/apache/cassandra/io/util/FileUtils.java @@ -379,6 +379,23 @@ public class FileUtils deleteWithConfirm(dir); } + /** + * Schedules deletion of all file and subdirectories under "dir" on JVM shutdown. + * @param dir Directory to be deleted + */ + public static void deleteRecursiveOnExit(File dir) + { + if (dir.isDirectory()) + { + String[] children = dir.list(); + for (String child : children) + deleteRecursiveOnExit(new File(dir, child)); + } + + logger.debug("Scheduling deferred deletion of file: " + dir); + dir.deleteOnExit(); + } + public static void skipBytesFully(DataInput in, int bytes) throws IOException { int n = 0; @@ -467,4 +484,19 @@ public class FileUtils out.write(buffer, 0, left); } } + + public static boolean isSubDirectory(File parent, File child) throws IOException + { + parent = parent.getCanonicalFile(); + child = child.getCanonicalFile(); + + File toCheck = child; + while (toCheck != null) + { + if (parent.equals(toCheck)) + return true; + toCheck = toCheck.getParentFile(); + } + return false; + } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7ae07e5/src/java/org/apache/cassandra/repair/messages/RepairOption.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/repair/messages/RepairOption.java b/src/java/org/apache/cassandra/repair/messages/RepairOption.java index a13d0fe..7b9a9af 100644 --- a/src/java/org/apache/cassandra/repair/messages/RepairOption.java +++ b/src/java/org/apache/cassandra/repair/messages/RepairOption.java @@ -22,10 +22,14 @@ import java.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.config.Config; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.repair.RepairParallelism; +import org.apache.cassandra.tools.nodetool.Repair; +import org.apache.cassandra.utils.FBUtilities; /** * Repair options. @@ -221,7 +225,16 @@ public class RepairOption public RepairOption(RepairParallelism parallelism, boolean primaryRange, boolean incremental, boolean trace, int jobThreads, Collection<Range<Token>> ranges) { - this.parallelism = parallelism; + if (FBUtilities.isWindows() && + (DatabaseDescriptor.getDiskAccessMode() != Config.DiskAccessMode.standard || DatabaseDescriptor.getIndexAccessMode() != Config.DiskAccessMode.standard) && + parallelism == RepairParallelism.SEQUENTIAL) + { + logger.warn("Sequential repair disabled when memory-mapped I/O is configured on Windows. Reverting to parallel."); + this.parallelism = RepairParallelism.PARALLEL; + } + else + this.parallelism = parallelism; + this.primaryRange = primaryRange; this.incremental = incremental; this.trace = trace; http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7ae07e5/src/java/org/apache/cassandra/service/CassandraDaemon.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/service/CassandraDaemon.java b/src/java/org/apache/cassandra/service/CassandraDaemon.java index 49e0c58..167d6b9 100644 --- a/src/java/org/apache/cassandra/service/CassandraDaemon.java +++ b/src/java/org/apache/cassandra/service/CassandraDaemon.java @@ -144,6 +144,10 @@ public class CassandraDaemon */ protected void setup() { + // Delete any failed snapshot deletions on Windows - see CASSANDRA-9658 + if (FBUtilities.isWindows()) + WindowsFailedSnapshotTracker.deleteOldSnapshots(); + logSystemInfo(); CLibrary.tryMlockall(); http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7ae07e5/test/unit/org/apache/cassandra/db/SystemKeyspaceTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/db/SystemKeyspaceTest.java b/test/unit/org/apache/cassandra/db/SystemKeyspaceTest.java index 093f359..b8aa161 100644 --- a/test/unit/org/apache/cassandra/db/SystemKeyspaceTest.java +++ b/test/unit/org/apache/cassandra/db/SystemKeyspaceTest.java @@ -22,6 +22,7 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.util.*; +import org.junit.BeforeClass; import org.junit.Test; import org.apache.cassandra.config.DatabaseDescriptor; @@ -38,6 +39,13 @@ import static org.junit.Assert.assertTrue; public class SystemKeyspaceTest { + @BeforeClass + public static void prepSnapshotTracker() + { + if (FBUtilities.isWindows()) + WindowsFailedSnapshotTracker.deleteOldSnapshots(); + } + @Test public void testLocalTokens() { @@ -78,6 +86,28 @@ public class SystemKeyspaceTest assert firstId.equals(secondId) : String.format("%s != %s%n", firstId.toString(), secondId.toString()); } + private void assertDeletedOrDeferred(int expectedCount) + { + if (FBUtilities.isWindows()) + assertEquals(expectedCount, getDeferredDeletionCount()); + else + assertTrue(getSystemSnapshotFiles().isEmpty()); + } + + private int getDeferredDeletionCount() + { + try + { + Class c = Class.forName("java.io.DeleteOnExitHook"); + LinkedHashSet<String> files = (LinkedHashSet<String>)FBUtilities.getProtectedField(c, "files").get(c); + return files.size(); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + @Test public void snapshotSystemKeyspaceIfUpgrading() throws IOException { @@ -86,13 +116,15 @@ public class SystemKeyspaceTest cfs.clearUnsafe(); Keyspace.clearSnapshot(null, SystemKeyspace.NAME); + int baseline = getDeferredDeletionCount(); + SystemKeyspace.snapshotOnVersionChange(); - assertTrue(getSystemSnapshotFiles().isEmpty()); + assertDeletedOrDeferred(baseline); // now setup system.local as if we're upgrading from a previous version setupReleaseVersion(getOlderVersionString()); Keyspace.clearSnapshot(null, SystemKeyspace.NAME); - assertTrue(getSystemSnapshotFiles().isEmpty()); + assertDeletedOrDeferred(baseline); // Compare versions again & verify that snapshots were created for all tables in the system ks SystemKeyspace.snapshotOnVersionChange(); @@ -104,7 +136,13 @@ public class SystemKeyspaceTest setupReleaseVersion(FBUtilities.getReleaseVersionString()); SystemKeyspace.snapshotOnVersionChange(); - assertTrue(getSystemSnapshotFiles().isEmpty()); + + // snapshotOnVersionChange for upgrade case will open a SSTR when the CFS is flushed. On Windows, we won't be + // able to delete hard-links to that file while segments are memory-mapped, so they'll be marked for deferred deletion. + // 10 files expected. + assertDeletedOrDeferred(baseline + 10); + + Keyspace.clearSnapshot(null, SystemKeyspace.NAME); } private String getOlderVersionString() http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7ae07e5/test/unit/org/apache/cassandra/io/sstable/SSTableRewriterTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/io/sstable/SSTableRewriterTest.java b/test/unit/org/apache/cassandra/io/sstable/SSTableRewriterTest.java index 5b4374e..dbf95c1 100644 --- a/test/unit/org/apache/cassandra/io/sstable/SSTableRewriterTest.java +++ b/test/unit/org/apache/cassandra/io/sstable/SSTableRewriterTest.java @@ -27,12 +27,15 @@ import java.util.concurrent.TimeUnit; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import org.junit.After; +import org.junit.AfterClass; import org.junit.BeforeClass; import com.google.common.util.concurrent.Uninterruptibles; import org.junit.Test; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.Util; +import org.apache.cassandra.config.Config; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.KSMetaData; import org.apache.cassandra.db.*; import org.apache.cassandra.db.compaction.AbstractCompactedRow; @@ -62,9 +65,21 @@ public class SSTableRewriterTest extends SchemaLoader private static final String KEYSPACE = "SSTableRewriterTest"; private static final String CF = "Standard1"; + private static Config.DiskAccessMode standardMode; + private static Config.DiskAccessMode indexMode; + @BeforeClass public static void defineSchema() throws ConfigurationException { + if (FBUtilities.isWindows()) + { + standardMode = DatabaseDescriptor.getDiskAccessMode(); + indexMode = DatabaseDescriptor.getIndexAccessMode(); + + DatabaseDescriptor.setDiskAccessMode(Config.DiskAccessMode.standard); + DatabaseDescriptor.setIndexAccessMode(Config.DiskAccessMode.standard); + } + SchemaLoader.prepareServer(); SchemaLoader.createKeyspace(KEYSPACE, SimpleStrategy.class, @@ -72,6 +87,13 @@ public class SSTableRewriterTest extends SchemaLoader SchemaLoader.standardCFMD(KEYSPACE, CF)); } + @AfterClass + public static void revertDiskAccess() + { + DatabaseDescriptor.setDiskAccessMode(standardMode); + DatabaseDescriptor.setIndexAccessMode(indexMode); + } + @After public void truncateCF() { http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7ae07e5/test/unit/org/apache/cassandra/repair/messages/RepairOptionTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/repair/messages/RepairOptionTest.java b/test/unit/org/apache/cassandra/repair/messages/RepairOptionTest.java index 557d0b1..11ae69f 100644 --- a/test/unit/org/apache/cassandra/repair/messages/RepairOptionTest.java +++ b/test/unit/org/apache/cassandra/repair/messages/RepairOptionTest.java @@ -24,11 +24,14 @@ import java.util.Set; import org.junit.Test; +import org.apache.cassandra.config.Config; +import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; import org.apache.cassandra.repair.RepairParallelism; +import org.apache.cassandra.utils.FBUtilities; import static org.junit.Assert.*; @@ -42,7 +45,12 @@ public class RepairOptionTest // parse with empty options RepairOption option = RepairOption.parse(new HashMap<String, String>(), partitioner); - assertTrue(option.getParallelism() == RepairParallelism.SEQUENTIAL); + + if (FBUtilities.isWindows() && (DatabaseDescriptor.getDiskAccessMode() != Config.DiskAccessMode.standard || DatabaseDescriptor.getIndexAccessMode() != Config.DiskAccessMode.standard)) + assertTrue(option.getParallelism() == RepairParallelism.PARALLEL); + else + assertTrue(option.getParallelism() == RepairParallelism.SEQUENTIAL); + assertFalse(option.isPrimaryRange()); assertFalse(option.isIncremental()); http://git-wip-us.apache.org/repos/asf/cassandra/blob/b7ae07e5/test/unit/org/apache/cassandra/service/StorageServiceServerTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/service/StorageServiceServerTest.java b/test/unit/org/apache/cassandra/service/StorageServiceServerTest.java index a20fa6c..4481501 100644 --- a/test/unit/org/apache/cassandra/service/StorageServiceServerTest.java +++ b/test/unit/org/apache/cassandra/service/StorageServiceServerTest.java @@ -20,7 +20,9 @@ package org.apache.cassandra.service; import java.io.File; +import java.io.FileWriter; import java.io.IOException; +import java.io.PrintWriter; import java.net.InetAddress; import java.util.*; @@ -37,7 +39,7 @@ import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.KSMetaData; import org.apache.cassandra.config.Schema; import org.apache.cassandra.db.Keyspace; -import org.apache.cassandra.schema.LegacySchemaTables; +import org.apache.cassandra.db.WindowsFailedSnapshotTracker; import org.apache.cassandra.db.SystemKeyspace; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; @@ -48,9 +50,12 @@ import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.PropertyFileSnitch; import org.apache.cassandra.locator.TokenMetadata; +import org.apache.cassandra.schema.LegacySchemaTables; +import org.apache.cassandra.utils.FBUtilities; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; @RunWith(OrderedJUnit4ClassRunner.class) public class StorageServiceServerTest @@ -95,6 +100,75 @@ public class StorageServiceServerTest StorageService.instance.takeSnapshot("snapshot"); } + private void checkTempFilePresence(File f, boolean exist) + { + for (int i = 0; i < 5; i++) + { + File subdir = new File(f, Integer.toString(i)); + subdir.mkdir(); + for (int j = 0; j < 5; j++) + { + File subF = new File(subdir, Integer.toString(j)); + assert(exist ? subF.exists() : !subF.exists()); + } + } + } + + @Test + public void testSnapshotFailureHandler() throws IOException + { + assumeTrue(FBUtilities.isWindows()); + + // Initial "run" of Cassandra, nothing in failed snapshot file + WindowsFailedSnapshotTracker.deleteOldSnapshots(); + + File f = new File(System.getenv("TEMP") + File.separator + Integer.toString(new Random().nextInt())); + f.mkdir(); + f.deleteOnExit(); + for (int i = 0; i < 5; i++) + { + File subdir = new File(f, Integer.toString(i)); + subdir.mkdir(); + for (int j = 0; j < 5; j++) + new File(subdir, Integer.toString(j)).createNewFile(); + } + + checkTempFilePresence(f, true); + + // Confirm deletion is recursive + for (int i = 0; i < 5; i++) + WindowsFailedSnapshotTracker.handleFailedSnapshot(new File(f, Integer.toString(i))); + + assert new File(WindowsFailedSnapshotTracker.TODELETEFILE).exists(); + + // Simulate shutdown and restart of C* node, closing out the list of failed snapshots. + WindowsFailedSnapshotTracker.resetForTests(); + + // Perform new run, mimicking behavior of C* at startup + WindowsFailedSnapshotTracker.deleteOldSnapshots(); + checkTempFilePresence(f, false); + + // Check to make sure we don't delete non-temp, non-datafile locations + WindowsFailedSnapshotTracker.resetForTests(); + PrintWriter tempPrinter = new PrintWriter(new FileWriter(WindowsFailedSnapshotTracker.TODELETEFILE, true)); + tempPrinter.println(".safeDir"); + tempPrinter.close(); + + File protectedDir = new File(".safeDir"); + protectedDir.mkdir(); + File protectedFile = new File(protectedDir, ".safeFile"); + protectedFile.createNewFile(); + + WindowsFailedSnapshotTracker.handleFailedSnapshot(protectedDir); + WindowsFailedSnapshotTracker.deleteOldSnapshots(); + + assert protectedDir.exists(); + assert protectedFile.exists(); + + protectedFile.delete(); + protectedDir.delete(); + } + @Test public void testColumnFamilySnapshot() throws IOException {
