diff --git a/community/graph-algo/src/test/java/org/neo4j/graphalgo/path/AStarPerformanceIT.java b/community/graph-algo/src/test/java/org/neo4j/graphalgo/path/AStarPerformanceIT.java index 56e11273352a7..69b4dfcc1a7e6 100644 --- a/community/graph-algo/src/test/java/org/neo4j/graphalgo/path/AStarPerformanceIT.java +++ b/community/graph-algo/src/test/java/org/neo4j/graphalgo/path/AStarPerformanceIT.java @@ -51,7 +51,7 @@ public class AStarPerformanceIT @Before public void setup() { - directory = testDirectory.directory( "graph-db" ); + directory = testDirectory.graphDbDir(); } @Test diff --git a/community/io/src/test/java/org/neo4j/test/rule/TestDirectory.java b/community/io/src/test/java/org/neo4j/test/rule/TestDirectory.java index 8da821473cac8..e3cbf7fa4767a 100644 --- a/community/io/src/test/java/org/neo4j/test/rule/TestDirectory.java +++ b/community/io/src/test/java/org/neo4j/test/rule/TestDirectory.java @@ -56,6 +56,8 @@ */ public class TestDirectory implements TestRule { + public static final String DATABASE_DIRECTORY = "graph-db"; + private final FileSystemAbstraction fileSystem; private File testClassBaseFolder; private Class owningTest; @@ -158,12 +160,12 @@ public File file( String name ) public File graphDbDir() { - return directory( "graph-db" ); + return directory( DATABASE_DIRECTORY ); } public File makeGraphDbDir() throws IOException { - return cleanDirectory( "graph-db" ); + return cleanDirectory( DATABASE_DIRECTORY ); } public void cleanup() throws IOException diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/PlatformModule.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/PlatformModule.java index 615aede27e02d..5e5362652cafd 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/PlatformModule.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/factory/PlatformModule.java @@ -209,6 +209,8 @@ protected FileWatcher createFileWatcher() FileWatcher watcher = fileSystem.fileWatcher(); watcher.addFileWatchEventListener( new DefaultFileDeletionEventListener( logging ) ); watcher.watch( storeDir ); + // register to watch store dir parent folder to see when store dir removed + watcher.watch( storeDir.getParentFile() ); return watcher; } catch ( Exception e ) diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/util/watcher/DefaultFileDeletionEventListener.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/util/watcher/DefaultFileDeletionEventListener.java index 8a84435ee8b89..98170870bbc28 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/util/watcher/DefaultFileDeletionEventListener.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/util/watcher/DefaultFileDeletionEventListener.java @@ -21,14 +21,19 @@ import org.neo4j.io.fs.watcher.event.FileWatchEventListenerAdapter; import org.neo4j.kernel.impl.logging.LogService; +import org.neo4j.kernel.impl.store.MetaDataStore; +import org.neo4j.kernel.impl.transaction.log.PhysicalLogFile; import org.neo4j.logging.Log; +import static java.lang.String.format; + /** * Listener that will print notification about deleted filename into internal log. */ public class DefaultFileDeletionEventListener extends FileWatchEventListenerAdapter { + private static final String EXTENSION_SEPARATOR = "."; private final Log internalLog; public DefaultFileDeletionEventListener( LogService logService ) @@ -39,7 +44,26 @@ public DefaultFileDeletionEventListener( LogService logService ) @Override public void fileDeleted( String fileName ) { - internalLog.info( "Store file '" + fileName + "' was deleted while database was online." ); + if ( isMonitoredFile( fileName ) ) + { + internalLog.info( format( "Store %s '%s' was deleted while database was running.", getFileType( fileName ), + fileName ) ); + } + } + + private static boolean isMonitoredFile( String fileName ) + { + return !fileName.startsWith( PhysicalLogFile.DEFAULT_NAME ); + } + + private static String getFileType( String fileName ) + { + return isFile( fileName ) ? "file" : "directory"; + } + + private static boolean isFile( String fileName ) + { + return fileName.startsWith( MetaDataStore.DEFAULT_NAME ) || fileName.contains( EXTENSION_SEPARATOR ); } } diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/util/watcher/DefaultFileDeletionEventListenerTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/util/watcher/DefaultFileDeletionEventListenerTest.java index 5918ca5ff7e6d..07fbdb0ab5c18 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/util/watcher/DefaultFileDeletionEventListenerTest.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/util/watcher/DefaultFileDeletionEventListenerTest.java @@ -22,6 +22,7 @@ import org.junit.Test; import org.neo4j.kernel.impl.logging.SimpleLogService; +import org.neo4j.kernel.impl.transaction.log.PhysicalLogFile; import org.neo4j.logging.AssertableLogProvider; import org.neo4j.logging.NullLogProvider; @@ -32,14 +33,30 @@ public class DefaultFileDeletionEventListenerTest public void notificationInLogAboutFileDeletion() throws Exception { AssertableLogProvider internalLogProvider = new AssertableLogProvider( false ); + DefaultFileDeletionEventListener listener = buildListener( internalLogProvider ); + listener.fileDeleted( "testFile.db" ); + listener.fileDeleted( "anotherDirectory" ); + + internalLogProvider.assertContainsMessageContaining( "Store file 'testFile.db' was " + + "deleted while database was running." ); + internalLogProvider.assertContainsMessageContaining( "Store directory 'anotherDirectory' was " + + "deleted while database was running." ); + } + + @Test + public void noNotificationForTransactionLogs() + { + AssertableLogProvider internalLogProvider = new AssertableLogProvider( false ); + DefaultFileDeletionEventListener listener = buildListener( internalLogProvider ); + listener.fileDeleted( PhysicalLogFile.DEFAULT_NAME + ".0" ); + listener.fileDeleted( PhysicalLogFile.DEFAULT_NAME + ".1" ); + + internalLogProvider.assertNoLoggingOccurred(); + } + + private DefaultFileDeletionEventListener buildListener( AssertableLogProvider internalLogProvider ) + { SimpleLogService logService = new SimpleLogService( NullLogProvider.getInstance(), internalLogProvider ); - DefaultFileDeletionEventListener listener = new DefaultFileDeletionEventListener( logService ); - listener.fileDeleted( "testFile" ); - listener.fileDeleted( "anotherFile" ); - - internalLogProvider.assertContainsMessageContaining( "Store file 'testFile' was " + - "deleted while database was online." ); - internalLogProvider.assertContainsMessageContaining( "Store file 'anotherFile' was " + - "deleted while database was online." ); + return new DefaultFileDeletionEventListener( logService ); } } diff --git a/community/neo4j/src/test/java/db/DatabaseShutdownTest.java b/community/neo4j/src/test/java/db/DatabaseShutdownTest.java index e7b5010e0d8a0..3ee89f2b106ac 100644 --- a/community/neo4j/src/test/java/db/DatabaseShutdownTest.java +++ b/community/neo4j/src/test/java/db/DatabaseShutdownTest.java @@ -25,7 +25,6 @@ import java.io.File; import java.io.IOException; import java.lang.reflect.Field; -import java.util.Map; import java.util.function.Supplier; import org.neo4j.graphdb.GraphDatabaseService; @@ -41,7 +40,6 @@ import org.neo4j.kernel.impl.factory.EditionModule; import org.neo4j.kernel.impl.factory.GraphDatabaseFacade; import org.neo4j.kernel.impl.factory.GraphDatabaseFacadeFactory; -import org.neo4j.kernel.impl.factory.GraphDatabaseFacadeFactory.Dependencies; import org.neo4j.kernel.impl.factory.PlatformModule; import org.neo4j.kernel.impl.logging.LogService; import org.neo4j.kernel.impl.query.QueryExecutionEngine; @@ -86,7 +84,7 @@ private static class TestGraphDatabaseFactoryWithFailingPageCacheFlush extends T private NeoStoreDataSource neoStoreDataSource; @Override - protected GraphDatabaseService newDatabase( File storeDir, Config config, + protected GraphDatabaseService newEmbeddedDatabase( File storeDir, Config config, GraphDatabaseFacadeFactory.Dependencies dependencies ) { return new GraphDatabaseFacadeFactory( DatabaseInfo.COMMUNITY, CommunityEditionModule::new ) diff --git a/community/neo4j/src/test/java/org/neo4j/store/watch/FileWatchIT.java b/community/neo4j/src/test/java/org/neo4j/store/watch/FileWatchIT.java index 895028c6420d1..8e40fb88aae90 100644 --- a/community/neo4j/src/test/java/org/neo4j/store/watch/FileWatchIT.java +++ b/community/neo4j/src/test/java/org/neo4j/store/watch/FileWatchIT.java @@ -19,27 +19,44 @@ */ package org.neo4j.store.watch; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import java.io.File; import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.neo4j.graphdb.DependencyResolver; import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Label; +import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Transaction; +import org.neo4j.graphdb.schema.IndexDefinition; +import org.neo4j.index.impl.lucene.legacy.LuceneDataSource; import org.neo4j.io.fs.DefaultFileSystemAbstraction; import org.neo4j.io.fs.FileUtils; import org.neo4j.io.fs.watcher.FileWatcher; -import org.neo4j.io.fs.watcher.event.FileWatchEventListener; +import org.neo4j.io.fs.watcher.event.FileWatchEventListenerAdapter; import org.neo4j.kernel.impl.store.MetaDataStore; +import org.neo4j.kernel.impl.store.StoreFactory; +import org.neo4j.kernel.impl.transaction.log.PhysicalLogFile; +import org.neo4j.kernel.impl.transaction.log.checkpoint.CheckPointer; +import org.neo4j.kernel.impl.transaction.log.checkpoint.SimpleTriggerInfo; +import org.neo4j.kernel.impl.util.watcher.DefaultFileDeletionEventListener; import org.neo4j.kernel.internal.GraphDatabaseAPI; import org.neo4j.logging.AssertableLogProvider; import org.neo4j.test.TestGraphDatabaseFactory; -import org.neo4j.test.rule.CleanupRule; import org.neo4j.test.rule.TestDirectory; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; public class FileWatchIT @@ -47,56 +64,242 @@ public class FileWatchIT @Rule public TestDirectory testDirectory = TestDirectory.testDirectory(); - @Rule - public CleanupRule cleanupRule = new CleanupRule(); + + private File storeDir; + private AssertableLogProvider logProvider; + private GraphDatabaseService database; + + @Before + public void setUp() + { + storeDir = testDirectory.graphDbDir(); + logProvider = new AssertableLogProvider( true ); + database = new TestGraphDatabaseFactory().setInternalLogProvider( logProvider ).newEmbeddedDatabase( storeDir ); + } + + @After + public void tearDown() + { + shutdownDatabaseSilently( database ); + } @Test public void notifyAboutStoreFileDeletion() throws Exception { - File storeDir = testDirectory.graphDbDir(); - AssertableLogProvider logProvider = new AssertableLogProvider( true ); - GraphDatabaseService database = - new TestGraphDatabaseFactory().setInternalLogProvider( logProvider ).newEmbeddedDatabase( storeDir ); - cleanupRule.add( database ); - - CountDownLatch modificationLatch = new CountDownLatch( 1 ); - CountDownLatch deletionLatch = new CountDownLatch( 1 ); - GraphDatabaseAPI databaseAPI = (GraphDatabaseAPI) database; - DependencyResolver dependencyResolver = databaseAPI.getDependencyResolver(); - FileWatcher fileWatcher = dependencyResolver.resolveDependency( FileWatcher.class ); - fileWatcher.addFileWatchEventListener( new TestLatchEventListener( deletionLatch, modificationLatch ) ); + String fileName = MetaDataStore.DEFAULT_NAME; + FileWatcher fileWatcher = getFileWatcher( (GraphDatabaseAPI) database ); + DeletionLatchEventListener deletionListener = new DeletionLatchEventListener( fileName ); + fileWatcher.addFileWatchEventListener( deletionListener ); - createTestNode( database ); - modificationLatch.await(); + createNode( database ); + deletionListener.awaitModificationNotification(); - deleteMetadataStore( storeDir ); - deletionLatch.await(); + deleteFile( storeDir, fileName ); + deletionListener.awaitDeletionNotification(); logProvider.assertContainsMessageContaining( - "Store file '" + MetaDataStore.DEFAULT_NAME + "' was deleted while database was online." ); + "Store file '" + fileName + "' was deleted while database was running." ); } @Test public void notifyWhenFileWatchingFailToStart() { AssertableLogProvider logProvider = new AssertableLogProvider( true ); - GraphDatabaseService database = new TestGraphDatabaseFactory() - .setInternalLogProvider( logProvider ) - .setFileSystem( new NonWatchableFileSystemAbstraction() ) - .newEmbeddedDatabase( testDirectory.graphDbDir() ); - cleanupRule.add( database ); + GraphDatabaseService db = null; + try + { + db = new TestGraphDatabaseFactory().setInternalLogProvider( logProvider ) + .setFileSystem( new NonWatchableFileSystemAbstraction() ) + .newEmbeddedDatabase( testDirectory.directory( "faied-start-db" ) ); + + logProvider.assertContainsMessageContaining( "Can not create file watcher for current file system. " + + "File monitoring capabilities for store files will be disabled." ); + } + finally + { + shutdownDatabaseSilently( db ); + } + } + + @Test + public void notifyAboutLegacyIndexFolderRemoval() throws InterruptedException, IOException + { + String monitoredDirectory = getLegacyIndexDirectory( storeDir ); - logProvider.assertContainsMessageContaining( "Can not create file watcher for current file system. " + - "File monitoring capabilities for store files will be disabled." ); + FileWatcher fileWatcher = getFileWatcher( (GraphDatabaseAPI) database ); + DeletionLatchEventListener deletionListner = new DeletionLatchEventListener( monitoredDirectory ); + fileWatcher.addFileWatchEventListener( deletionListner ); + + createNode( database ); + deletionListner.awaitModificationNotification(); + + deleteStoreDirectory( storeDir, monitoredDirectory ); + deletionListner.awaitDeletionNotification(); + + logProvider.assertContainsMessageContaining( + "Store directory '" + monitoredDirectory + "' was deleted while database was running." ); } - private void deleteMetadataStore( File storeDir ) + @Test + public void doNotNotifyAboutLuceneIndexFilesDeletion() throws InterruptedException, IOException { - File metadataStore = new File( storeDir, MetaDataStore.DEFAULT_NAME ); + DependencyResolver dependencyResolver = ((GraphDatabaseAPI) database).getDependencyResolver(); + FileWatcher fileWatcher = dependencyResolver.resolveDependency( FileWatcher.class ); + CheckPointer checkPointer = dependencyResolver.resolveDependency( CheckPointer.class ); + + String propertyStoreName = MetaDataStore.DEFAULT_NAME + StoreFactory.PROPERTY_STORE_NAME; + AccumulativeDeletionEventListener accumulativeListener = new AccumulativeDeletionEventListener(); + ModificationEventListener modificationListener = new ModificationEventListener( propertyStoreName ); + fileWatcher.addFileWatchEventListener( modificationListener ); + fileWatcher.addFileWatchEventListener( accumulativeListener ); + + String labelName = "labelName"; + String propertyName = "propertyName"; + Label testLabel = Label.label( labelName ); + createIndexes( database, propertyName, testLabel ); + createNode( database, propertyName, testLabel ); + forceCheckpoint( checkPointer ); + modificationListener.awaitModificationNotification(); + + fileWatcher.removeFileWatchEventListener( modificationListener ); + ModificationEventListener afterRemovalListener = new ModificationEventListener( propertyStoreName ); + fileWatcher.addFileWatchEventListener( afterRemovalListener ); + + dropAllIndexes( database ); + createNode( database, propertyName, testLabel ); + forceCheckpoint( checkPointer ); + afterRemovalListener.awaitModificationNotification(); + + accumulativeListener.assertDoesNotHaveAnyDeletions(); + } + + @Test + public void doNotMonitorTransactionLogFiles() throws InterruptedException + { + FileWatcher fileWatcher = getFileWatcher( (GraphDatabaseAPI) database ); + ModificationEventListener modificationEventListener = + new ModificationEventListener( MetaDataStore.DEFAULT_NAME ); + fileWatcher.addFileWatchEventListener( modificationEventListener ); + + createNode( database ); + modificationEventListener.awaitModificationNotification(); + + String fileName = PhysicalLogFile.DEFAULT_NAME + ".0"; + DeletionLatchEventListener deletionListener = new DeletionLatchEventListener( fileName ); + fileWatcher.addFileWatchEventListener( deletionListener ); + deleteFile( storeDir, fileName ); + deletionListener.awaitDeletionNotification(); + + AssertableLogProvider.LogMatcher logMatcher = + AssertableLogProvider.inLog( DefaultFileDeletionEventListener.class ) + .info( containsString( fileName ) ); + logProvider.assertNone( logMatcher ); + } + + @Test + public void notifyWhenWholeStoreDirectoryRemoved() throws IOException, InterruptedException + { + String fileName = MetaDataStore.DEFAULT_NAME; + FileWatcher fileWatcher = getFileWatcher( (GraphDatabaseAPI) database ); + ModificationEventListener modificationListener = new ModificationEventListener( fileName ); + fileWatcher.addFileWatchEventListener( modificationListener ); + createNode( database ); + + modificationListener.awaitModificationNotification(); + fileWatcher.removeFileWatchEventListener( modificationListener ); + + String storeDirectoryName = TestDirectory.DATABASE_DIRECTORY; + DeletionLatchEventListener eventListener = new DeletionLatchEventListener( storeDirectoryName ); + fileWatcher.addFileWatchEventListener( eventListener ); + FileUtils.deleteRecursively( storeDir ); + eventListener.awaitDeletionNotification(); + + logProvider.assertContainsMessageContaining( + "Store directory '" + storeDirectoryName + "' was deleted while database was running." ); + } + + private void shutdownDatabaseSilently( GraphDatabaseService databaseService ) + { + if ( databaseService != null ) + { + try + { + databaseService.shutdown(); + } + catch ( Exception expected ) + { + // ignored + } + } + } + + private void dropAllIndexes( GraphDatabaseService database ) + { + try ( Transaction transaction = database.beginTx() ) + { + for ( IndexDefinition definition : database.schema().getIndexes() ) + { + definition.drop(); + } + transaction.success(); + } + } + + private void createIndexes( GraphDatabaseService database, String propertyName, Label testLabel ) + { + try ( Transaction transaction = database.beginTx() ) + { + database.schema().indexFor( testLabel ).on( propertyName ).create(); + transaction.success(); + } + + try ( Transaction ignored = database.beginTx() ) + { + database.schema().awaitIndexesOnline( 1, TimeUnit.MINUTES ); + } + } + + private void forceCheckpoint( CheckPointer checkPointer ) throws IOException + { + checkPointer.forceCheckPoint( new SimpleTriggerInfo( "testForceCheckPoint" ) ); + } + + private String getLegacyIndexDirectory( File storeDir ) + { + File schemaIndexDirectory = LuceneDataSource.getLuceneIndexStoreDirectory( storeDir ); + Path relativeIndexPath = storeDir.toPath().relativize( schemaIndexDirectory.toPath() ); + return relativeIndexPath.getName( 0 ).toString(); + } + + private void createNode( GraphDatabaseService database, String propertyName, Label testLabel ) + { + try ( Transaction transaction = database.beginTx() ) + { + Node node = database.createNode( testLabel ); + node.setProperty( propertyName, "value" ); + transaction.success(); + } + } + + private FileWatcher getFileWatcher( GraphDatabaseAPI database ) + { + DependencyResolver dependencyResolver = database.getDependencyResolver(); + return dependencyResolver.resolveDependency( FileWatcher.class ); + } + + private void deleteFile( File storeDir, String fileName ) + { + File metadataStore = new File( storeDir, fileName ); FileUtils.deleteFile( metadataStore ); } - private void createTestNode( GraphDatabaseService database ) + private void deleteStoreDirectory( File storeDir, String directoryName ) throws IOException + { + File directory = new File( storeDir, directoryName ); + FileUtils.deleteRecursively( directory ); + } + + private void createNode( GraphDatabaseService database ) { try ( Transaction transaction = database.beginTx() ) { @@ -110,32 +313,71 @@ private static class NonWatchableFileSystemAbstraction extends DefaultFileSystem @Override public FileWatcher fileWatcher() throws IOException { - throw new IOException( "You can't watch me!"); + throw new IOException( "You can't watch me!" ); } } - private static class TestLatchEventListener implements FileWatchEventListener + private static class AccumulativeDeletionEventListener extends FileWatchEventListenerAdapter { - private final CountDownLatch deletionLatch; - private final CountDownLatch modificationLatch; + private List deletedFiles = new ArrayList<>(); + + @Override + public void fileDeleted( String fileName ) + { + deletedFiles.add( fileName ); + } - TestLatchEventListener( CountDownLatch deletionLatch, CountDownLatch modificationLatch ) + void assertDoesNotHaveAnyDeletions() { - this.deletionLatch = deletionLatch; - this.modificationLatch = modificationLatch; + assertThat( "Should not have any deletions registered", deletedFiles, Matchers.empty() ); + } + } + + private static class ModificationEventListener extends FileWatchEventListenerAdapter + { + final String expectedFileName; + private final CountDownLatch modificationLatch = new CountDownLatch( 1 ); + + ModificationEventListener( String expectedFileName ) + { + this.expectedFileName = expectedFileName; + } + + @Override + public void fileModified( String fileName ) + { + if ( expectedFileName.equals( fileName ) ) + { + modificationLatch.countDown(); + } + } + + void awaitModificationNotification() throws InterruptedException + { + modificationLatch.await(); + } + } + + private static class DeletionLatchEventListener extends ModificationEventListener + { + private final CountDownLatch deletionLatch = new CountDownLatch( 1 ); + + DeletionLatchEventListener( String expectedFileName ) + { + super( expectedFileName ); } @Override public void fileDeleted( String fileName ) { - assertTrue( fileName.endsWith( MetaDataStore.DEFAULT_NAME ) ); + assertTrue( fileName.endsWith( expectedFileName ) ); deletionLatch.countDown(); } - @Override - public void fileModified( String fileName ) + void awaitDeletionNotification() throws InterruptedException { - modificationLatch.countDown(); + deletionLatch.await(); } + } }