diff --git a/community/lucene-index/src/main/java/org/neo4j/kernel/api/impl/index/storage/AbstractIndexStorage.java b/community/lucene-index/src/main/java/org/neo4j/kernel/api/impl/index/storage/AbstractIndexStorage.java deleted file mode 100644 index e19382ce32647..0000000000000 --- a/community/lucene-index/src/main/java/org/neo4j/kernel/api/impl/index/storage/AbstractIndexStorage.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2002-2016 "Neo Technology," - * Network Engine for Objects in Lund AB [http://neotechnology.com] - * - * This file is part of Neo4j. - * - * Neo4j is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.neo4j.kernel.api.impl.index.storage; - - -import org.apache.lucene.store.Directory; - -import java.io.File; -import java.io.IOException; - -import org.neo4j.io.fs.FileSystemAbstraction; -import org.neo4j.kernel.api.impl.index.storage.layout.FolderLayout; - -public abstract class AbstractIndexStorage -{ - protected final DirectoryFactory directoryFactory; - protected final FileSystemAbstraction fileSystem; - protected final FolderLayout folderLayout; - protected final FailureStorage failureStorage; - - public AbstractIndexStorage( DirectoryFactory directoryFactory, FileSystemAbstraction fileSystem, - FolderLayout folderLayout ) - { - this.fileSystem = fileSystem; - this.folderLayout = folderLayout; - this.directoryFactory = directoryFactory; - this.failureStorage = new FailureStorage( fileSystem, folderLayout ); - } - - public Directory openDirectory( File folder ) throws IOException - { - return directoryFactory.open( folder ); - } - - public File getPartitionFolder( int partition ) - { - return folderLayout.getPartitionFolder( partition ); - } - - public File getIndexFolder() - { - return folderLayout.getIndexFolder(); - } - - public void reserveIndexFailureStorage() throws IOException - { - failureStorage.reserveForIndex(); - } - - public void storeIndexFailure( String failure ) throws IOException - { - failureStorage.storeIndexFailure( failure ); - } - - public String getStoredIndexFailure() - { - return failureStorage.loadIndexFailure(); - } - - public void prepareFolder( File folder ) throws IOException - { - cleanupFolder( folder ); - fileSystem.mkdirs( folder ); - } - - public void cleanupFolder( File folder ) throws IOException - { - fileSystem.deleteRecursively( folder ); - } -} diff --git a/community/lucene-index/src/main/java/org/neo4j/kernel/api/impl/index/storage/PartitionedIndexStorage.java b/community/lucene-index/src/main/java/org/neo4j/kernel/api/impl/index/storage/PartitionedIndexStorage.java index 5e0e5634188a2..9c2eed0d54f7d 100644 --- a/community/lucene-index/src/main/java/org/neo4j/kernel/api/impl/index/storage/PartitionedIndexStorage.java +++ b/community/lucene-index/src/main/java/org/neo4j/kernel/api/impl/index/storage/PartitionedIndexStorage.java @@ -31,18 +31,136 @@ import org.neo4j.io.IOUtils; import org.neo4j.io.fs.FileSystemAbstraction; +import org.neo4j.io.fs.FileUtils; +import org.neo4j.kernel.api.impl.index.storage.layout.FolderLayout; import org.neo4j.kernel.api.impl.index.storage.layout.IndexFolderLayout; import static java.util.stream.Collectors.toList; -public class PartitionedIndexStorage extends AbstractIndexStorage +/** + * Utility class that manages directory structure for a partitioned lucene index. + * It is aware of the {@link FileSystemAbstraction file system} structure of all index related folders, lucene + * {@link Directory directories} and {@link FailureStorage failure storage}. + */ +public class PartitionedIndexStorage { + private final DirectoryFactory directoryFactory; + private final FileSystemAbstraction fileSystem; + private final FolderLayout folderLayout; + private final FailureStorage failureStorage; + public PartitionedIndexStorage( DirectoryFactory directoryFactory, FileSystemAbstraction fileSystem, File rootFolder, String identifier ) { - super( directoryFactory, fileSystem, new IndexFolderLayout( rootFolder, identifier ) ); + this.fileSystem = fileSystem; + this.folderLayout = new IndexFolderLayout( rootFolder, identifier ); + this.directoryFactory = directoryFactory; + this.failureStorage = new FailureStorage( fileSystem, folderLayout ); + } + + /** + * Opens a {@link Directory lucene directory} for the given folder. + * + * @param folder the folder that denotes a lucene directory. + * @return the lucene directory denoted by the given folder. + * @throws IOException if directory can't be opened. + */ + public Directory openDirectory( File folder ) throws IOException + { + return directoryFactory.open( folder ); + } + + /** + * Resolves a folder for the partition with the given index. + * + * @param partition the partition index. + * @return the folder where partition's lucene directory should be located. + */ + public File getPartitionFolder( int partition ) + { + return folderLayout.getPartitionFolder( partition ); + } + + /** + * Resolves root folder for the given index. + * + * @return the folder containing index partition folders. + */ + public File getIndexFolder() + { + return folderLayout.getIndexFolder(); + } + + /** + * Create a failure storage in the {@link #getIndexFolder() index folder}. + * + * @throws IOException if failure storage creation fails. + * @see FailureStorage#reserveForIndex() + */ + public void reserveIndexFailureStorage() throws IOException + { + failureStorage.reserveForIndex(); + } + + /** + * Writes index failure into the failure storage. + * + * @param failure the cause of the index failure. + * @throws IOException if writing to the failure storage file failed. + * @see FailureStorage#storeIndexFailure(String) + */ + public void storeIndexFailure( String failure ) throws IOException + { + failureStorage.storeIndexFailure( failure ); + } + + /** + * Retrieves stored index failure. + * + * @return index failure as string or {@code null} if there is no failure. + * @see FailureStorage#loadIndexFailure() + */ + public String getStoredIndexFailure() + { + return failureStorage.loadIndexFailure(); + } + + /** + * For the given {@link File folder} removes all nested folders from both {@link FileSystemAbstraction file system} + * and {@link Directory lucene directories}. + * + * @param folder the folder to clean up. + * @throws IOException if some removal operation fails. + */ + public void prepareFolder( File folder ) throws IOException + { + cleanupFolder( folder ); + fileSystem.mkdirs( folder ); + } + + /** + * For the given {@link File folder} removes the folder itself and all nested folders from both + * {@link FileSystemAbstraction file system} and {@link Directory lucene directories}. + * + * @param folder the folder to remove. + * @throws IOException if some removal operation fails. + */ + public void cleanupFolder( File folder ) throws IOException + { + List partitionFolders = listFolders( folder ); + for ( File partitionFolder : partitionFolders ) + { + cleanupLuceneDirectory( partitionFolder ); + } + fileSystem.deleteRecursively( folder ); } + /** + * Opens all {@link Directory lucene directories} contained in the {@link #getIndexFolder() index folder}. + * + * @return the map from file system {@link File directory} to the corresponding {@link Directory lucene directory}. + * @throws IOException if opening of some lucene directory (via {@link DirectoryFactory#open(File)}) fails. + */ public Map openIndexDirectories() throws IOException { Map directories = new LinkedHashMap<>(); @@ -68,11 +186,44 @@ public Map openIndexDirectories() throws IOException return directories; } + /** + * List all folders in the {@link #getIndexFolder() index folder}. + * + * @return the list of index partition folders or {@link Collections#emptyList() empty list} if index folder is + * empty. + */ public List listFolders() { - File[] files = fileSystem.listFiles( getIndexFolder() ); + return listFolders( getIndexFolder() ); + } + + private List listFolders( File rootFolder ) + { + File[] files = fileSystem.listFiles( rootFolder ); return files == null ? Collections.emptyList() : Stream.of( files ).filter( fileSystem::isDirectory ).collect( toList() ); } + + /** + * Removes content of the lucene directory denoted by the given {@link File file}. This might seem unnecessary + * since we cleanup the folder using {@link FileSystemAbstraction file system} but in fact for testing we often use + * in-memory directories whose content can't be removed via the file system. + *

+ * Uses {@link FileUtils#windowsSafeIOOperation(FileUtils.FileOperation)} underneath. + * + * @param folder the path to the directory to cleanup. + * @throws IOException if removal operation fails. + */ + private void cleanupLuceneDirectory( File folder ) throws IOException + { + try ( Directory dir = directoryFactory.open( folder ) ) + { + String[] indexFiles = dir.listAll(); + for ( String indexFile : indexFiles ) + { + FileUtils.windowsSafeIOOperation( () -> dir.deleteFile( indexFile ) ); + } + } + } } diff --git a/community/lucene-index/src/test/java/org/neo4j/kernel/api/impl/index/storage/PartitionedIndexStorageTest.java b/community/lucene-index/src/test/java/org/neo4j/kernel/api/impl/index/storage/PartitionedIndexStorageTest.java new file mode 100644 index 0000000000000..bb28630a9debc --- /dev/null +++ b/community/lucene-index/src/test/java/org/neo4j/kernel/api/impl/index/storage/PartitionedIndexStorageTest.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2002-2016 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.api.impl.index.storage; + +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.StringField; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.store.Directory; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +import org.neo4j.helpers.ArrayUtil; +import org.neo4j.io.IOUtils; +import org.neo4j.io.fs.DefaultFileSystemAbstraction; +import org.neo4j.io.fs.FileSystemAbstraction; +import org.neo4j.io.fs.StoreChannel; +import org.neo4j.kernel.api.impl.index.IndexWriterConfigs; +import org.neo4j.test.DefaultFileSystemRule; +import org.neo4j.test.TargetDirectory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.neo4j.helpers.collection.IteratorUtil.asSet; + +public class PartitionedIndexStorageTest +{ + private static final String INDEX_ID = "testIndex"; + + @Rule + public final DefaultFileSystemRule fsRule = new DefaultFileSystemRule(); + @Rule + public final TargetDirectory.TestDirectory testDir = TargetDirectory.testDirForTest( getClass(), fsRule.get() ); + + private DefaultFileSystemAbstraction fs; + private PartitionedIndexStorage storage; + + @Before + public void createIndexStorage() throws Exception + { + fs = fsRule.get(); + storage = new PartitionedIndexStorage( getOrCreateDirFactory( fs ), fs, testDir.graphDbDir(), INDEX_ID ); + } + + @Test + public void prepareFolderCreatesFolder() throws IOException + { + File folder = createRandomFolder( testDir.graphDbDir() ); + + storage.prepareFolder( folder ); + + assertTrue( fs.fileExists( folder ) ); + } + + @Test + public void prepareFolderRemovesFromFileSystem() throws IOException + { + File folder = createRandomFolder( testDir.graphDbDir() ); + createRandomFilesAndFolders( folder ); + + storage.prepareFolder( folder ); + + assertTrue( fs.fileExists( folder ) ); + assertTrue( ArrayUtil.isEmpty( fs.listFiles( folder ) ) ); + } + + @Test + public void prepareFolderRemovesFromLucene() throws IOException + { + File folder = createRandomFolder( testDir.graphDbDir() ); + Directory dir = createRandomLuceneDir( folder ); + + assertFalse( ArrayUtil.isEmpty( dir.listAll() ) ); + + storage.prepareFolder( folder ); + + assertTrue( fs.fileExists( folder ) ); + assertTrue( ArrayUtil.isEmpty( dir.listAll() ) ); + } + + @Test + public void openIndexDirectoriesForEmptyIndex() throws IOException + { + File indexFolder = storage.getIndexFolder(); + + Map directories = storage.openIndexDirectories(); + + assertTrue( directories.isEmpty() ); + } + + @Test + public void openIndexDirectories() throws IOException + { + File indexFolder = storage.getIndexFolder(); + createRandomLuceneDir( indexFolder ).close(); + createRandomLuceneDir( indexFolder ).close(); + + Map directories = storage.openIndexDirectories(); + try + { + assertEquals( 2, directories.size() ); + for ( Directory dir : directories.values() ) + { + assertFalse( ArrayUtil.isEmpty( dir.listAll() ) ); + } + } + finally + { + IOUtils.closeAll( directories.values() ); + } + } + + @Test + public void listFoldersForEmptyFolder() throws IOException + { + File indexFolder = storage.getIndexFolder(); + fs.mkdirs( indexFolder ); + + List folders = storage.listFolders(); + + assertTrue( folders.isEmpty() ); + } + + @Test + public void listFolders() throws IOException + { + File indexFolder = storage.getIndexFolder(); + fs.mkdirs( indexFolder ); + + createRandomFile( indexFolder ); + createRandomFile( indexFolder ); + File folder1 = createRandomFolder( indexFolder ); + File folder2 = createRandomFolder( indexFolder ); + + List folders = storage.listFolders(); + + assertEquals( asSet( folder1, folder2 ), new HashSet<>( folders ) ); + } + + private void createRandomFilesAndFolders( File rootFolder ) throws IOException + { + int count = ThreadLocalRandom.current().nextInt( 10 ) + 1; + for ( int i = 0; i < count; i++ ) + { + if ( ThreadLocalRandom.current().nextBoolean() ) + { + createRandomFile( rootFolder ); + } + else + { + createRandomFolder( rootFolder ); + } + } + } + + private Directory createRandomLuceneDir( File rootFolder ) throws IOException + { + File folder = createRandomFolder( rootFolder ); + DirectoryFactory directoryFactory = getOrCreateDirFactory( fs ); + Directory directory = directoryFactory.open( folder ); + try ( IndexWriter writer = new IndexWriter( directory, IndexWriterConfigs.standardConfig() ) ) + { + writer.addDocument( randomDocument() ); + writer.commit(); + } + return directory; + } + + private void createRandomFile( File rootFolder ) throws IOException + { + File file = new File( rootFolder, RandomStringUtils.randomAlphabetic( 5 ) ); + try ( StoreChannel channel = fs.create( file ) ) + { + channel.writeAll( ByteBuffer.allocate( 100 ) ); + } + } + + private File createRandomFolder( File rootFolder ) throws IOException + { + File folder = new File( rootFolder, RandomStringUtils.randomAlphabetic( 5 ) ); + fs.mkdirs( folder ); + return folder; + } + + private static Document randomDocument() + { + Document doc = new Document(); + doc.add( new StringField( "field", RandomStringUtils.randomAlphabetic( 5 ), Field.Store.YES ) ); + return doc; + } + + private static DirectoryFactory getOrCreateDirFactory( FileSystemAbstraction fs ) + { + return fs.getOrCreateThirdPartyFileSystem( DirectoryFactory.class, + clazz -> new DirectoryFactory.InMemoryDirectoryFactory() ); + } +}