diff --git a/server/docs/configuration.md b/server/docs/configuration.md index 342bc5af..577d6fa3 100644 --- a/server/docs/configuration.md +++ b/server/docs/configuration.md @@ -11,7 +11,8 @@ defaults and can be left unchanged. It is recommended to browse the properties b | Environment Variable | Description | Default Value | |:---|:---|---:| -| PERSISTENCE_STORAGE_ROOT_PATH | The root path for the storage, if not provided will attempt to create a `data` on the working dir of the app. | | +| PERSISTENCE_STORAGE_LIVE_ROOT_PATH | The root path for the live storage. The provided path must be absolute! | | +| PERSISTENCE_STORAGE_ARCHIVE_ROOT_PATH | The root path for the archive storage. The provided path must be absolute! | | | PERSISTENCE_STORAGE_TYPE | Type of the persistence storage | BLOCK_AS_LOCAL_FILE | | CONSUMER_TIMEOUT_THRESHOLD_MILLIS | Time to wait for subscribers before disconnecting in milliseconds | 1500 | | SERVICE_DELAY_MILLIS | Service shutdown delay in milliseconds | 500 | diff --git a/server/src/main/java/com/hedera/block/server/Constants.java b/server/src/main/java/com/hedera/block/server/Constants.java index 047243a4..1870db7e 100644 --- a/server/src/main/java/com/hedera/block/server/Constants.java +++ b/server/src/main/java/com/hedera/block/server/Constants.java @@ -18,8 +18,11 @@ /** Constants used in the BlockNode service. */ public final class Constants { - /** Constant mapped to the semantic name of the Block Node root directory */ - public static final String BLOCK_NODE_ROOT_DIRECTORY_SEMANTIC_NAME = "Block Node Root Directory"; + /** Constant mapped to the semantic name of the Block Node live root directory */ + public static final String BLOCK_NODE_LIVE_ROOT_DIRECTORY_SEMANTIC_NAME = "Block Node Live Root Directory"; + + /** Constant mapped to the semantic name of the Block Node archive root directory */ + public static final String BLOCK_NODE_ARCHIVE_ROOT_DIRECTORY_SEMANTIC_NAME = "Block Node Archive Root Directory"; /** Constant mapped to PbjProtocolProvider.CONFIG_NAME in the PBJ Helidon Plugin */ public static final String PBJ_PROTOCOL_PROVIDER_CONFIG_NAME = "pbj"; diff --git a/server/src/main/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializer.java b/server/src/main/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializer.java index f43fa5e4..cb9da650 100644 --- a/server/src/main/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializer.java +++ b/server/src/main/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializer.java @@ -28,7 +28,8 @@ public final class ServerMappedConfigSourceInitializer { private static final List MAPPINGS = List.of( new ConfigMapping("consumer.timeoutThresholdMillis", "CONSUMER_TIMEOUT_THRESHOLD_MILLIS"), - new ConfigMapping("persistence.storage.rootPath", "PERSISTENCE_STORAGE_ROOT_PATH"), + new ConfigMapping("persistence.storage.liveRootPath", "PERSISTENCE_STORAGE_LIVE_ROOT_PATH"), + new ConfigMapping("persistence.storage.archiveRootPath", "PERSISTENCE_STORAGE_ARCHIVE_ROOT_PATH"), new ConfigMapping("persistence.storage.type", "PERSISTENCE_STORAGE_TYPE"), new ConfigMapping("service.delayMillis", "SERVICE_DELAY_MILLIS"), new ConfigMapping("mediator.ringBufferSize", "MEDIATOR_RING_BUFFER_SIZE"), diff --git a/server/src/main/java/com/hedera/block/server/persistence/PersistenceInjectionModule.java b/server/src/main/java/com/hedera/block/server/persistence/PersistenceInjectionModule.java index 191f12bd..b9c3916f 100644 --- a/server/src/main/java/com/hedera/block/server/persistence/PersistenceInjectionModule.java +++ b/server/src/main/java/com/hedera/block/server/persistence/PersistenceInjectionModule.java @@ -103,7 +103,8 @@ static BlockReader providesBlockReader(@NonNull final Persistence final StorageType persistenceType = config.type(); return switch (persistenceType) { case BLOCK_AS_LOCAL_FILE -> BlockAsFileReaderBuilder.newBuilder().build(); - case BLOCK_AS_LOCAL_DIRECTORY -> BlockAsDirReaderBuilder.newBuilder(config).build(); + case BLOCK_AS_LOCAL_DIRECTORY -> BlockAsDirReaderBuilder.newBuilder(config) + .build(); case NO_OP -> new NoOpBlockReader(); }; } @@ -140,7 +141,7 @@ static BlockRemover providesBlockRemover( @Singleton static BlockPathResolver providesPathResolver(@NonNull final PersistenceStorageConfig config) { final StorageType persistenceType = config.type(); - final Path blockStorageRoot = Path.of(config.rootPath()); + final Path blockStorageRoot = Path.of(config.liveRootPath()); return switch (persistenceType) { case BLOCK_AS_LOCAL_FILE -> new BlockAsFilePathResolver(blockStorageRoot); case BLOCK_AS_LOCAL_DIRECTORY -> new BlockAsDirPathResolver(blockStorageRoot); diff --git a/server/src/main/java/com/hedera/block/server/persistence/storage/PersistenceStorageConfig.java b/server/src/main/java/com/hedera/block/server/persistence/storage/PersistenceStorageConfig.java index fd28db0e..e80e55d6 100644 --- a/server/src/main/java/com/hedera/block/server/persistence/storage/PersistenceStorageConfig.java +++ b/server/src/main/java/com/hedera/block/server/persistence/storage/PersistenceStorageConfig.java @@ -16,32 +16,38 @@ package com.hedera.block.server.persistence.storage; -import static com.hedera.block.server.Constants.BLOCK_NODE_ROOT_DIRECTORY_SEMANTIC_NAME; -import static java.lang.System.Logger.Level.ERROR; +import static com.hedera.block.server.Constants.BLOCK_NODE_ARCHIVE_ROOT_DIRECTORY_SEMANTIC_NAME; +import static com.hedera.block.server.Constants.BLOCK_NODE_LIVE_ROOT_DIRECTORY_SEMANTIC_NAME; import static java.lang.System.Logger.Level.INFO; -import com.hedera.block.common.utils.FileUtilities; +import com.hedera.block.common.utils.StringUtilities; import com.swirlds.config.api.ConfigData; import com.swirlds.config.api.ConfigProperty; +import edu.umd.cs.findbugs.annotations.NonNull; import java.io.IOException; import java.io.UncheckedIOException; +import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Objects; /** - * Use this configuration across the persistent storage package + * Use this configuration across the persistence storage package. * - * @param rootPath provides the root path for saving block data, if you want to override it need to - * set it as persistence.storage.rootPath + * @param liveRootPath provides the root path for saving blocks live + * @param archiveRootPath provides the root path for archived blocks * @param type use a predefined type string to replace the persistence component implementation. - * Non-PRODUCTION values should only be used for troubleshooting and development purposes. + * Non-PRODUCTION values should only be used for troubleshooting and development purposes. */ @ConfigData("persistence.storage") public record PersistenceStorageConfig( - @ConfigProperty(defaultValue = "") String rootPath, + @ConfigProperty(defaultValue = "") String liveRootPath, + @ConfigProperty(defaultValue = "") String archiveRootPath, @ConfigProperty(defaultValue = "BLOCK_AS_LOCAL_FILE") StorageType type) { private static final System.Logger LOGGER = System.getLogger(PersistenceStorageConfig.class.getName()); + private static final String LIVE_ROOT_PATH = + Path.of("hashgraph/blocknode/data/live/").toAbsolutePath().toString(); + private static final String ARCHIVE_ROOT_PATH = + Path.of("hashgraph/blocknode/data/archive/").toAbsolutePath().toString(); /** * Constructor to set the default root path if not provided, it will be set to the data @@ -49,32 +55,86 @@ public record PersistenceStorageConfig( */ public PersistenceStorageConfig { Objects.requireNonNull(type); - Objects.requireNonNull(rootPath); + liveRootPath = resolvePath(liveRootPath, LIVE_ROOT_PATH, BLOCK_NODE_LIVE_ROOT_DIRECTORY_SEMANTIC_NAME); + archiveRootPath = + resolvePath(archiveRootPath, ARCHIVE_ROOT_PATH, BLOCK_NODE_ARCHIVE_ROOT_DIRECTORY_SEMANTIC_NAME); + LOGGER.log(INFO, "Persistence Storage Configuration: persistence.storage.type=" + type); + LOGGER.log(INFO, "Persistence Storage Configuration: persistence.storage.liveRootPath=" + liveRootPath); + LOGGER.log(INFO, "Persistence Storage Configuration: persistence.storage.archiveRootPath=" + archiveRootPath); + } - // verify rootPath prop - Path path = Path.of(rootPath); - if (rootPath.isBlank()) { - path = Paths.get("").toAbsolutePath().resolve("data"); + /** + * This method attempts to resolve a given configured path. If the input + * path is blank, a default path is used. The resolved path must be + * absolute! If the path resolution is successful, at attempt is made to + * create the directory path. If the directory path cannot be created, an + * {@link UncheckedIOException} is thrown. + * + * @param pathToResolve the path to resolve + * @param defaultIfBlank the default path if the path to resolve is blank + * @param semanticPathName the semantic name of the path used for logging + * @return the resolved path + * @throws IllegalArgumentException if the resolved path is not absolute + * @throws UncheckedIOException if the resolved path cannot be created + */ + @NonNull + private String resolvePath( + final String pathToResolve, @NonNull final String defaultIfBlank, @NonNull final String semanticPathName) { + final Path normalized = getNormalizedPath(pathToResolve, defaultIfBlank, semanticPathName); + createDirectoryPath(normalized, semanticPathName); + return normalized.toString(); + } + + /** + * This method normalizes a given path. If the path to normalize is blank, + * a default path is used. The normalized path must be absolute! If the + * normalized path is not absolute, an {@link IllegalArgumentException} is + * thrown. + * + * @param pathToNormalize the path to normalize + * @param defaultIfBlank the default path if the path to normalize is blank + * @param semanticPathName the semantic name of the path used for logging + * @return the normalized path + * @throws IllegalArgumentException if the path to normalize is not absolute + */ + @NonNull + private Path getNormalizedPath( + final String pathToNormalize, + @NonNull final String defaultIfBlank, + @NonNull final String semanticPathName) { + final Path result; + if (StringUtilities.isBlank(pathToNormalize)) { + result = Path.of(defaultIfBlank).toAbsolutePath(); + } else { + result = Path.of(pathToNormalize); } - // Check if absolute - if (!path.isAbsolute()) { - throw new IllegalArgumentException(rootPath + " Root path must be absolute"); + if (!result.isAbsolute()) { + throw new IllegalArgumentException("Path provided for [%s] must be absolute!".formatted(semanticPathName)); + } else { + return result; } + } - // Create Directory if it does not exist + /** + * This method creates a directory path at the given target path. If the + * directory path cannot be created, an {@link UncheckedIOException} is + * thrown. + * + * @param targetPath the target path to create the directory path + * @param semanticPathName the semantic name of the path used for logging + * @throws UncheckedIOException if the directory path cannot be created + */ + private void createDirectoryPath(@NonNull final Path targetPath, @NonNull final String semanticPathName) { try { - FileUtilities.createFolderPathIfNotExists(path, ERROR, BLOCK_NODE_ROOT_DIRECTORY_SEMANTIC_NAME); + Files.createDirectories(targetPath); } catch (final IOException e) { + final String classname = this.getClass().getName(); final String message = - "Unable to instantiate [%s]! Unable to create the root directory for the block storage [%s]" - .formatted(this.getClass().getName(), path); + "Unable to instantiate [%s]! Unable to create the [%s] path for the block storage at [%s]" + .formatted(classname, semanticPathName, targetPath); throw new UncheckedIOException(message, e); } - - rootPath = path.toString(); - LOGGER.log(INFO, "Persistence Storage Configuration: persistence.storage.rootPath=" + path); - LOGGER.log(INFO, "Persistence Storage Configuration: persistence.storage.type=" + type); } /** diff --git a/server/src/main/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReader.java b/server/src/main/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReader.java index 3a082c4b..db7927ef 100644 --- a/server/src/main/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReader.java +++ b/server/src/main/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReader.java @@ -64,7 +64,7 @@ class BlockAsDirReader implements BlockReader { final FileAttribute> folderPermissions) { LOGGER.log(INFO, "Initializing FileSystemBlockReader"); - final Path blockNodeRootPath = Path.of(config.rootPath()); + final Path blockNodeRootPath = Path.of(config.liveRootPath()); LOGGER.log(INFO, config.toString()); LOGGER.log(INFO, "Block Node Root Path: " + blockNodeRootPath); diff --git a/server/src/main/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriter.java b/server/src/main/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriter.java index bcd503d6..669ef766 100644 --- a/server/src/main/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriter.java +++ b/server/src/main/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriter.java @@ -17,7 +17,7 @@ package com.hedera.block.server.persistence.storage.write; import static com.hedera.block.server.Constants.BLOCK_FILE_EXTENSION; -import static com.hedera.block.server.Constants.BLOCK_NODE_ROOT_DIRECTORY_SEMANTIC_NAME; +import static com.hedera.block.server.Constants.BLOCK_NODE_LIVE_ROOT_DIRECTORY_SEMANTIC_NAME; import static com.hedera.block.server.metrics.BlockNodeMetricTypes.Counter.BlocksPersisted; import static java.lang.System.Logger; import static java.lang.System.Logger.Level.DEBUG; @@ -92,7 +92,7 @@ class BlockAsDirWriter implements LocalBlockWriter> { final PersistenceStorageConfig config = blockNodeContext.configuration().getConfigData(PersistenceStorageConfig.class); - this.blockNodeRootPath = Path.of(config.rootPath()); + this.blockNodeRootPath = Path.of(config.liveRootPath()); LOGGER.log(INFO, "Block Node Root Path: " + blockNodeRootPath); if (Objects.nonNull(folderPermissions)) { @@ -111,7 +111,7 @@ class BlockAsDirWriter implements LocalBlockWriter> { // Initialize the block node root directory if it does not exist FileUtilities.createFolderPathIfNotExists( - blockNodeRootPath, INFO, this.folderPermissions, BLOCK_NODE_ROOT_DIRECTORY_SEMANTIC_NAME); + blockNodeRootPath, INFO, this.folderPermissions, BLOCK_NODE_LIVE_ROOT_DIRECTORY_SEMANTIC_NAME); } /** @@ -197,7 +197,7 @@ private void resetState(@NonNull final BlockHeader blockHeader) throws IOExcepti blockPathResolver.resolvePathToBlock(currentBlockNumber), DEBUG, folderPermissions, - BLOCK_NODE_ROOT_DIRECTORY_SEMANTIC_NAME); + BLOCK_NODE_LIVE_ROOT_DIRECTORY_SEMANTIC_NAME); // Reset blockNodeFileNameIndex = 0; diff --git a/server/src/main/resources/app.properties b/server/src/main/resources/app.properties index 10b96628..342b6ac5 100644 --- a/server/src/main/resources/app.properties +++ b/server/src/main/resources/app.properties @@ -10,5 +10,6 @@ prometheus.endpointPortNumber=9999 #consumer.timeoutThresholdMillis=1500 #Persistence Storage Config -#persistence.storage.rootPath= +#persistence.storage.liveRootPath= +#persistence.storage.archiveRootPath= #persistence.storage.type=BLOCK_AS_LOCAL_DIRECTORY diff --git a/server/src/test/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializerTest.java b/server/src/test/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializerTest.java index 5f2caf6e..1a4a15eb 100644 --- a/server/src/test/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializerTest.java +++ b/server/src/test/java/com/hedera/block/server/config/ServerMappedConfigSourceInitializerTest.java @@ -30,7 +30,8 @@ class ServerMappedConfigSourceInitializerTest { private static final ConfigMapping[] SUPPORTED_MAPPINGS = { new ConfigMapping("consumer.timeoutThresholdMillis", "CONSUMER_TIMEOUT_THRESHOLD_MILLIS"), - new ConfigMapping("persistence.storage.rootPath", "PERSISTENCE_STORAGE_ROOT_PATH"), + new ConfigMapping("persistence.storage.liveRootPath", "PERSISTENCE_STORAGE_LIVE_ROOT_PATH"), + new ConfigMapping("persistence.storage.archiveRootPath", "PERSISTENCE_STORAGE_ARCHIVE_ROOT_PATH"), new ConfigMapping("persistence.storage.type", "PERSISTENCE_STORAGE_TYPE"), new ConfigMapping("service.delayMillis", "SERVICE_DELAY_MILLIS"), new ConfigMapping("mediator.ringBufferSize", "MEDIATOR_RING_BUFFER_SIZE"), diff --git a/server/src/test/java/com/hedera/block/server/grpc/BlockAccessServiceTest.java b/server/src/test/java/com/hedera/block/server/grpc/BlockAccessServiceTest.java index a2fd72cf..10577b29 100644 --- a/server/src/test/java/com/hedera/block/server/grpc/BlockAccessServiceTest.java +++ b/server/src/test/java/com/hedera/block/server/grpc/BlockAccessServiceTest.java @@ -87,7 +87,7 @@ class BlockAccessServiceTest { public void setUp() throws IOException { blockNodeContext = - TestConfigUtil.getTestBlockNodeContext(Map.of("persistence.storage.rootPath", testPath.toString())); + TestConfigUtil.getTestBlockNodeContext(Map.of("persistence.storage.liveRootPath", testPath.toString())); config = blockNodeContext.configuration().getConfigData(PersistenceStorageConfig.class); blockAccessService = new PbjBlockAccessServiceProxy(serviceStatus, blockReader, blockNodeContext); diff --git a/server/src/test/java/com/hedera/block/server/pbj/PbjBlockStreamServiceIntegrationTest.java b/server/src/test/java/com/hedera/block/server/pbj/PbjBlockStreamServiceIntegrationTest.java index 1e981089..81b3641e 100644 --- a/server/src/test/java/com/hedera/block/server/pbj/PbjBlockStreamServiceIntegrationTest.java +++ b/server/src/test/java/com/hedera/block/server/pbj/PbjBlockStreamServiceIntegrationTest.java @@ -146,7 +146,7 @@ public class PbjBlockStreamServiceIntegrationTest { @BeforeEach public void setUp() throws IOException { Map properties = new HashMap<>(); - properties.put("persistence.storage.rootPath", testPath.toString()); + properties.put("persistence.storage.liveRootPath", testPath.toString()); blockNodeContext = TestConfigUtil.getTestBlockNodeContext(properties); } diff --git a/server/src/test/java/com/hedera/block/server/persistence/PersistenceInjectionModuleTest.java b/server/src/test/java/com/hedera/block/server/persistence/PersistenceInjectionModuleTest.java index 25dcd55c..c45db868 100644 --- a/server/src/test/java/com/hedera/block/server/persistence/PersistenceInjectionModuleTest.java +++ b/server/src/test/java/com/hedera/block/server/persistence/PersistenceInjectionModuleTest.java @@ -91,7 +91,7 @@ void testProvidesBlockWriter_IOException() { final BlockNodeContext blockNodeContext = mock(BlockNodeContext.class); final PersistenceStorageConfig persistenceStorageConfig = mock(PersistenceStorageConfig.class); - when(persistenceStorageConfig.rootPath()).thenReturn("/invalid_path/:invalid_directory"); + when(persistenceStorageConfig.liveRootPath()).thenReturn("/invalid_path/:invalid_directory"); when(persistenceStorageConfig.type()).thenReturn(StorageType.BLOCK_AS_LOCAL_DIRECTORY); final Configuration configuration = mock(Configuration.class); diff --git a/server/src/test/java/com/hedera/block/server/persistence/storage/PersistenceStorageConfigTest.java b/server/src/test/java/com/hedera/block/server/persistence/storage/PersistenceStorageConfigTest.java index 47201b32..66c35ca3 100644 --- a/server/src/test/java/com/hedera/block/server/persistence/storage/PersistenceStorageConfigTest.java +++ b/server/src/test/java/com/hedera/block/server/persistence/storage/PersistenceStorageConfigTest.java @@ -16,77 +16,245 @@ package com.hedera.block.server.persistence.storage; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.from; import com.hedera.block.server.persistence.storage.PersistenceStorageConfig.StorageType; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; +import java.util.Arrays; import java.util.Comparator; import java.util.stream.Stream; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +/** + * Test class that tests the functionality of the + * {@link PersistenceStorageConfig} + */ class PersistenceStorageConfigTest { + private static final Path HASHGRAPH_ROOT_ABSOLUTE_PATH = + Path.of("hashgraph/").toAbsolutePath(); + private static final Path PERSISTENCE_STORAGE_ROOT_ABSOLUTE_PATH = + HASHGRAPH_ROOT_ABSOLUTE_PATH.resolve("blocknode/data/"); - final String TEMP_DIR = "block-node-unit-test-dir"; + @AfterEach + void tearDown() { + if (!Files.exists(HASHGRAPH_ROOT_ABSOLUTE_PATH)) { + return; + } + try (final Stream walk = Files.walk(HASHGRAPH_ROOT_ABSOLUTE_PATH)) { + walk.sorted(Comparator.reverseOrder()).forEach(p -> { + try { + Files.delete(p); + } catch (final IOException e) { + throw new RuntimeException(e); + } + }); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } - @Test - void testPersistenceStorageConfig_happyPath() throws IOException { + /** + * This test aims to verify that the {@link PersistenceStorageConfig} class + * correctly returns the storage type that was set in the constructor. + * + * @param storageType parameterized, the storage type to test + */ + @ParameterizedTest + @MethodSource("storageTypes") + void testPersistenceStorageConfigStorageTypes(final StorageType storageType) { + final PersistenceStorageConfig actual = new PersistenceStorageConfig("", "", storageType); + assertThat(actual).returns(storageType, from(PersistenceStorageConfig::type)); + } - Path testPath = Files.createTempDirectory(TEMP_DIR); + /** + * This test aims to verify that the {@link PersistenceStorageConfig} class + * correctly sets the live and archive root paths. + * + * @param liveRootPathToTest parameterized, the live root path to test + * @param expectedLiveRootPathToTest parameterized, the expected live root + * @param archiveRootPathToTest parameterized, the archive root path to test + * @param expectedArchiveRootPathToTest parameterized, the expected archive + * root + */ + @ParameterizedTest + @MethodSource({"validAbsoluteDefaultRootPaths", "validAbsoluteNonDefaultRootPaths"}) + void testPersistenceStorageConfigHappyPaths( + final String liveRootPathToTest, + final String expectedLiveRootPathToTest, + final String archiveRootPathToTest, + final String expectedArchiveRootPathToTest) { + final PersistenceStorageConfig actual = new PersistenceStorageConfig( + liveRootPathToTest, archiveRootPathToTest, StorageType.BLOCK_AS_LOCAL_FILE); + assertThat(actual) + .returns(expectedLiveRootPathToTest, from(PersistenceStorageConfig::liveRootPath)) + .returns(expectedArchiveRootPathToTest, from(PersistenceStorageConfig::archiveRootPath)); + } - PersistenceStorageConfig persistenceStorageConfig = - new PersistenceStorageConfig(testPath.toString(), StorageType.BLOCK_AS_LOCAL_FILE); - assertEquals(testPath.toString(), persistenceStorageConfig.rootPath()); + /** + * This test aims to verify that the {@link PersistenceStorageConfig} class + * correctly throws an {@link UncheckedIOException} when either the live or + * archive root paths are invalid. + * + * @param invalidLiveRootPathToTest parameterized, the invalid live root + * path to test + * @param invalidArchiveRootPathToTest parameterized, the invalid archive + * root path to test + */ + @ParameterizedTest + @MethodSource({"invalidRootPaths"}) + void testPersistenceStorageConfigInvalidRootPaths( + final String invalidLiveRootPathToTest, final String invalidArchiveRootPathToTest) { + assertThatExceptionOfType(UncheckedIOException.class) + .isThrownBy(() -> new PersistenceStorageConfig( + invalidLiveRootPathToTest, invalidArchiveRootPathToTest, StorageType.BLOCK_AS_LOCAL_FILE)); } - @Test - void testPersistenceStorageConfig_emptyRootPath() throws IOException { - final String expectedDefaultRootPath = - Paths.get("").toAbsolutePath().resolve("data_empty").toString(); - // delete if exists - deleteDirectory(Paths.get(expectedDefaultRootPath)); + /** + * This test aims to verify that the {@link PersistenceStorageConfig} class + * correctly throws an {@link IllegalArgumentException} when either the live + * or archive root paths are relative. + * + * @param invalidLiveRootPathToTest parameterized, the invalid live root + * path to test + * @param invalidArchiveRootPathToTest parameterized, the invalid archive + * root path to test + */ + @ParameterizedTest + @MethodSource({"validNonAbsolutePaths"}) + void testPersistenceStorageConfigRelativeRootPaths( + final String invalidLiveRootPathToTest, final String invalidArchiveRootPathToTest) { + assertThatIllegalArgumentException() + .isThrownBy(() -> new PersistenceStorageConfig( + invalidLiveRootPathToTest, invalidArchiveRootPathToTest, StorageType.BLOCK_AS_LOCAL_FILE)); + } - PersistenceStorageConfig persistenceStorageConfig = - new PersistenceStorageConfig(getAbsoluteFolder("data_empty"), StorageType.BLOCK_AS_LOCAL_FILE); - assertEquals(expectedDefaultRootPath, persistenceStorageConfig.rootPath()); + /** + * All storage types dynamically provided. + */ + private static Stream storageTypes() { + return Arrays.stream(StorageType.values()).map(Arguments::of); } - @Test - void persistenceStorageConfig_throwsExceptionForRelativePath() { - IllegalArgumentException exception = assertThrows( - IllegalArgumentException.class, - () -> new PersistenceStorageConfig("relative/path", StorageType.BLOCK_AS_LOCAL_FILE)); - assertEquals("relative/path Root path must be absolute", exception.getMessage()); + /** + * The default absolute paths. We expect these to allow the persistence + * config to be instantiated. Providing a blank string is accepted, it will + * create the config instance with it's internal defaults. + */ + @SuppressWarnings("all") + private static Stream validAbsoluteDefaultRootPaths() { + final Path defaultLiveRootAbsolutePath = + PERSISTENCE_STORAGE_ROOT_ABSOLUTE_PATH.resolve("live/").toAbsolutePath(); + final Path defaultArchiveRootAbsolutePath = + PERSISTENCE_STORAGE_ROOT_ABSOLUTE_PATH.resolve("archive/").toAbsolutePath(); + + // test against the default liveRootPath and archiveRootPath + final String liveToTest1 = defaultLiveRootAbsolutePath.toString(); + final String liveExpected1 = defaultLiveRootAbsolutePath.toString(); + final String archiveToTest1 = defaultArchiveRootAbsolutePath.toString(); + final String archiveExpected1 = defaultArchiveRootAbsolutePath.toString(); + + // blank liveRootPath results in the default liveRootPath to be used + final String liveToTest2 = ""; + final String liveExpected2 = defaultLiveRootAbsolutePath.toString(); + final String archiveToTest2 = defaultArchiveRootAbsolutePath.toString(); + final String archiveExpected2 = defaultArchiveRootAbsolutePath.toString(); + + // blank archiveRootPath results in the default archiveRootPath to be used + final String liveToTest3 = defaultLiveRootAbsolutePath.toString(); + final String liveExpected3 = defaultLiveRootAbsolutePath.toString(); + final String archiveToTest3 = ""; + final String archiveExpected3 = defaultArchiveRootAbsolutePath.toString(); + + // null liveRootPath results in the default liveRootPath to be used + final String liveToTest4 = null; + final String liveExpected4 = defaultLiveRootAbsolutePath.toString(); + final String archiveToTest4 = defaultArchiveRootAbsolutePath.toString(); + final String archiveExpected4 = defaultArchiveRootAbsolutePath.toString(); + + // null archiveRootPath results in the default archiveRootPath to be used + final String liveToTest5 = defaultLiveRootAbsolutePath.toString(); + final String liveExpected5 = defaultLiveRootAbsolutePath.toString(); + final String archiveToTest5 = null; + final String archiveExpected5 = defaultArchiveRootAbsolutePath.toString(); + + // blank liveRootPath and archiveRootPath results in the default liveRootPath and archiveRootPath to be used + final String liveToTest6 = ""; + final String liveExpected6 = defaultLiveRootAbsolutePath.toString(); + final String archiveToTest6 = ""; + final String archiveExpected6 = defaultArchiveRootAbsolutePath.toString(); + + // null liveRootPath and archiveRootPath results in the default liveRootPath and archiveRootPath to be used + final String liveToTest7 = null; + final String liveExpected7 = defaultLiveRootAbsolutePath.toString(); + final String archiveToTest7 = null; + final String archiveExpected7 = defaultArchiveRootAbsolutePath.toString(); + + return Stream.of( + Arguments.of(liveToTest1, liveExpected1, archiveToTest1, archiveExpected1), + Arguments.of(liveToTest2, liveExpected2, archiveToTest2, archiveExpected2), + Arguments.of(liveToTest3, liveExpected3, archiveToTest3, archiveExpected3), + Arguments.of(liveToTest4, liveExpected4, archiveToTest4, archiveExpected4), + Arguments.of(liveToTest5, liveExpected5, archiveToTest5, archiveExpected5), + Arguments.of(liveToTest6, liveExpected6, archiveToTest6, archiveExpected6), + Arguments.of(liveToTest7, liveExpected7, archiveToTest7, archiveExpected7)); } - @Test - void persistenceStorageConfig_throwsRuntimeExceptionOnIOException() { - Path invalidPath = Paths.get("/invalid/path"); + /** + * Somve valid absolute paths that are not the default paths. We expect + * these to allow the persistence config to be instantiated. + */ + @SuppressWarnings("all") + private static Stream validAbsoluteNonDefaultRootPaths() { + final String liveToTest1 = PERSISTENCE_STORAGE_ROOT_ABSOLUTE_PATH + .resolve("nondefault/live/path/") + .toString(); + final String archiveToTest1 = PERSISTENCE_STORAGE_ROOT_ABSOLUTE_PATH + .resolve("nondefault/archive/path/") + .toString(); - RuntimeException exception = assertThrows( - RuntimeException.class, - () -> new PersistenceStorageConfig(invalidPath.toString(), StorageType.BLOCK_AS_LOCAL_FILE)); - assertInstanceOf(IOException.class, exception.getCause()); + final String liveToTest2 = PERSISTENCE_STORAGE_ROOT_ABSOLUTE_PATH + .resolve("another/nondefault/live/path/") + .toString(); + final String archiveToTest2 = PERSISTENCE_STORAGE_ROOT_ABSOLUTE_PATH + .resolve("another/nondefault/archive/path/") + .toString(); + + return Stream.of( + Arguments.of(liveToTest1, liveToTest1, archiveToTest1, archiveToTest1), + Arguments.of(liveToTest2, liveToTest2, archiveToTest2, archiveToTest2)); } - public static void deleteDirectory(Path path) throws IOException { - if (!Files.exists(path)) { - return; - } - try (Stream walk = Files.walk(path)) { - walk.sorted(Comparator.reverseOrder()).forEach(p -> { - try { - Files.delete(p); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } + /** + * Supplying blank is valid, both must be valid paths in order to be able + * to create the config instance. If either liveRootPath or archiveRootPath + * is invalid, we expect to fail. If we provide external paths, they must be + * absolute. + */ + private static Stream validNonAbsolutePaths() { + // these must fail, if we provide external paths, they must be absolute + final String liveToTest = "hashgraph/blocknode/data/relative/live/path/"; + final String archiveToTest = "hashgraph/blocknode/data/relative/archive/path/"; + return Stream.of( + Arguments.of("", archiveToTest), Arguments.of(liveToTest, ""), Arguments.of(liveToTest, archiveToTest)); } - private String getAbsoluteFolder(String relativePath) { - return Paths.get(relativePath).toAbsolutePath().toString(); + /** + * Supplying blank is valid, both must be valid paths in order to be able + * to create the config instance. If either liveRootPath or archiveRootPath + * is invalid, we expect to fail. There cannot be invalid paths supplied. + */ + private static Stream invalidRootPaths() { + final String invalidPath = "/invalid_path/:invalid_directory"; + return Stream.of( + Arguments.of("", invalidPath), Arguments.of(invalidPath, ""), Arguments.of(invalidPath, invalidPath)); } } diff --git a/server/src/test/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReaderTest.java b/server/src/test/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReaderTest.java index ab37c55d..b41343f6 100644 --- a/server/src/test/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReaderTest.java +++ b/server/src/test/java/com/hedera/block/server/persistence/storage/read/BlockAsDirReaderTest.java @@ -66,7 +66,7 @@ public class BlockAsDirReaderTest { @BeforeEach public void setUp() throws IOException { blockNodeContext = - TestConfigUtil.getTestBlockNodeContext(Map.of("persistence.storage.rootPath", testPath.toString())); + TestConfigUtil.getTestBlockNodeContext(Map.of("persistence.storage.liveRootPath", testPath.toString())); config = blockNodeContext.configuration().getConfigData(PersistenceStorageConfig.class); blockItems = generateBlockItemsUnparsed(1); } @@ -135,7 +135,7 @@ blockNodeContext, mock(BlockRemover.class), mock(BlockPathResolver.class)) @Test public void testPathIsNotDirectory() throws IOException, ParseException { - final Path blockNodeRootPath = Path.of(config.rootPath()); + final Path blockNodeRootPath = Path.of(config.liveRootPath()); // Write a file named "1" where a directory should be writeBlockItemToPath(blockNodeRootPath.resolve(Path.of("1")), blockItems.getFirst()); @@ -170,7 +170,7 @@ blockNodeContext, mock(BlockRemover.class), mock(BlockPathResolver.class)) public void testBlockNodePathReadFails() throws IOException, ParseException { // Remove read perm on the root path - removePathReadPerms(Path.of(config.rootPath())); + removePathReadPerms(Path.of(config.liveRootPath())); // Use a spy on a subclass of the BlockAsDirReader to proxy calls // to the actual methods but to also throw an IOException when @@ -197,7 +197,7 @@ blockNodeContext, mock(BlockRemover.class), mock(BlockPathResolver.class)) final PersistenceStorageConfig persistenceStorageConfig = blockNodeContext.configuration().getConfigData(PersistenceStorageConfig.class); - final Path blockNodeRootPath = Path.of(persistenceStorageConfig.rootPath()); + final Path blockNodeRootPath = Path.of(persistenceStorageConfig.liveRootPath()); Path blockPath = blockNodeRootPath.resolve(String.valueOf(1)); byte[] bytes; @@ -219,7 +219,7 @@ blockNodeContext, mock(BlockRemover.class), mock(BlockPathResolver.class)) } public static void removeBlockReadPerms(int blockNumber, final PersistenceStorageConfig config) throws IOException { - final Path blockNodeRootPath = Path.of(config.rootPath()); + final Path blockNodeRootPath = Path.of(config.liveRootPath()); final Path blockPath = blockNodeRootPath.resolve(String.valueOf(blockNumber)); removePathReadPerms(blockPath); } @@ -230,7 +230,7 @@ static void removePathReadPerms(final Path path) throws IOException { private void removeBlockItemReadPerms(int blockNumber, int blockItem, PersistenceStorageConfig config) throws IOException { - final Path blockNodeRootPath = Path.of(config.rootPath()); + final Path blockNodeRootPath = Path.of(config.liveRootPath()); final Path blockPath = blockNodeRootPath.resolve(String.valueOf(blockNumber)); final Path blockItemPath = blockPath.resolve(blockItem + BLOCK_FILE_EXTENSION); Files.setPosixFilePermissions(blockItemPath, TestUtils.getNoRead().value()); diff --git a/server/src/test/java/com/hedera/block/server/persistence/storage/remove/BlockAsDirRemoverTest.java b/server/src/test/java/com/hedera/block/server/persistence/storage/remove/BlockAsDirRemoverTest.java index 623d3d03..edded667 100644 --- a/server/src/test/java/com/hedera/block/server/persistence/storage/remove/BlockAsDirRemoverTest.java +++ b/server/src/test/java/com/hedera/block/server/persistence/storage/remove/BlockAsDirRemoverTest.java @@ -51,7 +51,7 @@ public class BlockAsDirRemoverTest { @BeforeEach public void setUp() throws IOException { blockNodeContext = - TestConfigUtil.getTestBlockNodeContext(Map.of("persistence.storage.rootPath", testPath.toString())); + TestConfigUtil.getTestBlockNodeContext(Map.of("persistence.storage.liveRootPath", testPath.toString())); testConfig = blockNodeContext.configuration().getConfigData(PersistenceStorageConfig.class); } diff --git a/server/src/test/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriterTest.java b/server/src/test/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriterTest.java index f4886b14..dde3a102 100644 --- a/server/src/test/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriterTest.java +++ b/server/src/test/java/com/hedera/block/server/persistence/storage/write/BlockAsDirWriterTest.java @@ -63,7 +63,7 @@ public class BlockAsDirWriterTest { private static final String TEMP_DIR = "block-node-unit-test-dir"; - private static final String PERSISTENCE_STORAGE_ROOT_PATH_KEY = "persistence.storage.rootPath"; + private static final String PERSISTENCE_STORAGE_ROOT_PATH_KEY = "persistence.storage.liveRootPath"; private final Logger LOGGER = System.getLogger(getClass().getName()); private Path testPath; private BlockNodeContext blockNodeContext; @@ -302,17 +302,17 @@ public void testPartialBlockRemoval() throws IOException, ParseException { } private void removeRootWritePerms(final PersistenceStorageConfig config) throws IOException { - final Path blockNodeRootPath = Path.of(config.rootPath()); + final Path blockNodeRootPath = Path.of(config.liveRootPath()); Files.setPosixFilePermissions(blockNodeRootPath, TestUtils.getNoWrite().value()); } private void removeRootReadPerms(final PersistenceStorageConfig config) throws IOException { - final Path blockNodeRootPath = Path.of(config.rootPath()); + final Path blockNodeRootPath = Path.of(config.liveRootPath()); Files.setPosixFilePermissions(blockNodeRootPath, TestUtils.getNoRead().value()); } private void removeBlockAllPerms(final int blockNumber, final PersistenceStorageConfig config) throws IOException { - final Path blockNodeRootPath = Path.of(config.rootPath()); + final Path blockNodeRootPath = Path.of(config.liveRootPath()); final Path blockPath = blockNodeRootPath.resolve(String.valueOf(blockNumber)); Files.setPosixFilePermissions(blockPath, TestUtils.getNoPerms().value()); }