diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java index 3655c1f0b24..18ba82c02dd 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java @@ -106,8 +106,8 @@ public void startNode(final BesuNode node) { } final StorageServiceImpl storageService = new StorageServiceImpl(); - final BesuConfiguration commonPluginConfiguration = - new BesuConfigurationImpl(Files.createTempDir().toPath()); + final Path path = Files.createTempDir().toPath(); + final BesuConfiguration commonPluginConfiguration = new BesuConfigurationImpl(path, path); final BesuPluginContextImpl besuPluginContext = besuPluginContextMap.computeIfAbsent( node, n -> buildPluginContext(node, storageService, commonPluginConfiguration)); diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index f0a88d7c731..cf228fa9253 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -771,7 +771,9 @@ public void run() { private void addConfigurationService() { if (pluginCommonConfiguration == null) { - pluginCommonConfiguration = new BesuConfigurationImpl(dataDir().resolve(DATABASE_PATH)); + final Path dataDir = dataDir(); + pluginCommonConfiguration = + new BesuConfigurationImpl(dataDir.resolve(DATABASE_PATH), dataDir); besuPluginContext.addService(BesuConfiguration.class, pluginCommonConfiguration); } } diff --git a/besu/src/main/java/org/hyperledger/besu/services/BesuConfigurationImpl.java b/besu/src/main/java/org/hyperledger/besu/services/BesuConfigurationImpl.java index aa59167034f..d64ff96d951 100644 --- a/besu/src/main/java/org/hyperledger/besu/services/BesuConfigurationImpl.java +++ b/besu/src/main/java/org/hyperledger/besu/services/BesuConfigurationImpl.java @@ -21,13 +21,24 @@ public class BesuConfigurationImpl implements BesuConfiguration { private final Path storagePath; + private final Path dataPath; public BesuConfigurationImpl(final Path storagePath) { + this(storagePath, storagePath); + } + + public BesuConfigurationImpl(final Path storagePath, final Path dataPath) { this.storagePath = storagePath; + this.dataPath = dataPath; } @Override public Path getStoragePath() { return storagePath; } + + @Override + public Path getDataPath() { + return dataPath; + } } diff --git a/build.gradle b/build.gradle index c9893b6ab4a..56478c9d47d 100644 --- a/build.gradle +++ b/build.gradle @@ -164,9 +164,7 @@ allprojects { } // Below this line are currently only license header tasks - format 'groovy', { - target '**/src/*/grovy/**/*.groovy' - } + format 'groovy', { target '**/src/*/grovy/**/*.groovy' } // Currently disabled due to referencetest issues // format 'bash', { diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index bc2c4602273..ea380a93f9e 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -56,7 +56,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = '7GpOlkPAtanchkveLlCKOTmafqC8cAa19/s/eLLGFyE=' + knownHash = 'f0SPUj4/aLZKyjAGcDYai021dO8pg5xLaNvALEWxoIg=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/BesuConfiguration.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/BesuConfiguration.java index 29599dd83dd..646841c514c 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/BesuConfiguration.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/BesuConfiguration.java @@ -25,4 +25,11 @@ public interface BesuConfiguration { * @return location of the storage in the file system of the client. */ Path getStoragePath(); + + /** + * Location of the data directory in the file system running the client. + * + * @return location of the data directory in the file system of the client. + */ + Path getDataPath(); } diff --git a/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/RocksDBKeyValueStorageFactory.java b/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/RocksDBKeyValueStorageFactory.java index cb465b19f94..4453a0d125a 100644 --- a/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/RocksDBKeyValueStorageFactory.java +++ b/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/RocksDBKeyValueStorageFactory.java @@ -147,17 +147,18 @@ private boolean requiresInit() { private int readDatabaseVersion(final BesuConfiguration commonConfiguration) throws IOException { final Path databaseDir = commonConfiguration.getStoragePath(); + final Path dataDir = commonConfiguration.getDataPath(); final boolean databaseExists = databaseDir.resolve("IDENTITY").toFile().exists(); final int databaseVersion; if (databaseExists) { - databaseVersion = DatabaseMetadata.fromDirectory(databaseDir).getVersion(); - LOG.debug("Existing database detected at {}. Version {}", databaseDir, databaseVersion); + databaseVersion = DatabaseMetadata.lookUpFrom(databaseDir, dataDir).getVersion(); + LOG.info("Existing database detected at {}. Version {}", dataDir, databaseVersion); } else { databaseVersion = DEFAULT_VERSION; - LOG.info( - "No existing database detected at {}. Using version {}", databaseDir, databaseVersion); + LOG.info("No existing database detected at {}. Using version {}", dataDir, databaseVersion); Files.createDirectories(databaseDir); - new DatabaseMetadata(databaseVersion).writeToDirectory(databaseDir); + Files.createDirectories(dataDir); + new DatabaseMetadata(databaseVersion).writeToDirectory(dataDir); } if (!SUPPORTED_VERSIONS.contains(databaseVersion)) { diff --git a/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/configuration/DatabaseMetadata.java b/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/configuration/DatabaseMetadata.java index 43777475757..4eab6e42be3 100644 --- a/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/configuration/DatabaseMetadata.java +++ b/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/configuration/DatabaseMetadata.java @@ -23,8 +23,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class DatabaseMetadata { + private static final Logger LOG = LogManager.getLogger(); + private static final String METADATA_FILENAME = "DATABASE_METADATA.json"; private static ObjectMapper MAPPER = new ObjectMapper(); private final int version; @@ -38,20 +42,41 @@ public int getVersion() { return version; } - public static DatabaseMetadata fromDirectory(final Path databaseDir) throws IOException { - final File metadataFile = getDefaultMetadataFile(databaseDir); + public static DatabaseMetadata lookUpFrom(final Path databaseDir, final Path dataDir) + throws IOException { + LOG.info("Lookup database metadata file in data directory: {}", dataDir.toString()); + File metadataFile = getDefaultMetadataFile(dataDir); + final boolean shouldLookupInDatabaseDir = !metadataFile.exists(); + if (shouldLookupInDatabaseDir) { + LOG.info( + "Database metadata file not found in data directory. Lookup in database directory: {}", + databaseDir.toString()); + metadataFile = getDefaultMetadataFile(databaseDir); + } + DatabaseMetadata databaseMetadata; try { - return MAPPER.readValue(metadataFile, DatabaseMetadata.class); + databaseMetadata = MAPPER.readValue(metadataFile, DatabaseMetadata.class); } catch (FileNotFoundException fnfe) { - return new DatabaseMetadata(0); + databaseMetadata = new DatabaseMetadata(0); } catch (JsonProcessingException jpe) { throw new IllegalStateException( String.format("Invalid metadata file %s", metadataFile.getAbsolutePath()), jpe); } + if (shouldLookupInDatabaseDir) { + LOG.warn( + "Database metadata file has been copied from old location (database directory). Be aware that the old file might be removed in future release."); + writeToDirectory(databaseMetadata, dataDir); + } + return databaseMetadata; } public void writeToDirectory(final Path databaseDir) throws IOException { - MAPPER.writeValue(getDefaultMetadataFile(databaseDir), this); + writeToDirectory(this, databaseDir); + } + + private static void writeToDirectory( + final DatabaseMetadata databaseMetadata, final Path databaseDir) throws IOException { + MAPPER.writeValue(getDefaultMetadataFile(databaseDir), databaseMetadata); } private static File getDefaultMetadataFile(final Path databaseDir) { diff --git a/plugins/rocksdb/src/test/java/org/hyperledger/besu/plugin/services/helper/Conditions.java b/plugins/rocksdb/src/test/java/org/hyperledger/besu/plugin/services/helper/Conditions.java new file mode 100644 index 00000000000..085e9313536 --- /dev/null +++ b/plugins/rocksdb/src/test/java/org/hyperledger/besu/plugin/services/helper/Conditions.java @@ -0,0 +1,41 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.besu.plugin.services.helper; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.assertj.core.api.Condition; + +public class Conditions { + public static final Condition FILE_EXISTS = + new Condition<>(path -> path != null && path.toFile().exists(), "File must exist."); + public static final Condition FILE_DOES_NOT_EXIST = + new Condition<>(path -> path == null || !path.toFile().exists(), "File must not exist."); + + public static Condition shouldContain(final String content) { + return new Condition<>( + path -> { + try { + return content.equals(Files.readString(path)); + } catch (IOException e) { + return false; + } + }, + "File should contain specified content."); + } +} diff --git a/plugins/rocksdb/src/test/java/org/hyperledger/besu/plugin/services/storage/rocksdb/RocksDBKeyValueStorageFactoryTest.java b/plugins/rocksdb/src/test/java/org/hyperledger/besu/plugin/services/storage/rocksdb/RocksDBKeyValueStorageFactoryTest.java index 8e0581e019c..20780ce79ae 100644 --- a/plugins/rocksdb/src/test/java/org/hyperledger/besu/plugin/services/storage/rocksdb/RocksDBKeyValueStorageFactoryTest.java +++ b/plugins/rocksdb/src/test/java/org/hyperledger/besu/plugin/services/storage/rocksdb/RocksDBKeyValueStorageFactoryTest.java @@ -54,8 +54,10 @@ public class RocksDBKeyValueStorageFactoryTest { @Test public void shouldCreateCorrectMetadataFileForLatestVersion() throws Exception { + final Path tempDataDir = temporaryFolder.newFolder().toPath().resolve("data"); final Path tempDatabaseDir = temporaryFolder.newFolder().toPath().resolve("db"); when(commonConfiguration.getStoragePath()).thenReturn(tempDatabaseDir); + when(commonConfiguration.getDataPath()).thenReturn(tempDataDir); final RocksDBKeyValueStorageFactory storageFactory = new RocksDBKeyValueStorageFactory(() -> rocksDbConfiguration, segments); @@ -63,47 +65,59 @@ public void shouldCreateCorrectMetadataFileForLatestVersion() throws Exception { // Side effect is creation of the Metadata version file storageFactory.create(segment, commonConfiguration, metricsSystem); - assertThat(DatabaseMetadata.fromDirectory(commonConfiguration.getStoragePath()).getVersion()) + assertThat( + DatabaseMetadata.lookUpFrom( + commonConfiguration.getStoragePath(), commonConfiguration.getDataPath()) + .getVersion()) .isEqualTo(DEFAULT_VERSION); } @Test public void shouldDetectVersion0DatabaseIfNoMetadataFileFound() throws Exception { + final Path tempDataDir = temporaryFolder.newFolder().toPath().resolve("data"); final Path tempDatabaseDir = temporaryFolder.newFolder().toPath().resolve("db"); Files.createDirectories(tempDatabaseDir); + Files.createDirectories(tempDataDir); tempDatabaseDir.resolve("IDENTITY").toFile().createNewFile(); when(commonConfiguration.getStoragePath()).thenReturn(tempDatabaseDir); + when(commonConfiguration.getDataPath()).thenReturn(tempDataDir); final RocksDBKeyValueStorageFactory storageFactory = new RocksDBKeyValueStorageFactory(() -> rocksDbConfiguration, segments); storageFactory.create(segment, commonConfiguration, metricsSystem); - assertThat(DatabaseMetadata.fromDirectory(tempDatabaseDir).getVersion()).isZero(); + assertThat(DatabaseMetadata.lookUpFrom(tempDatabaseDir, tempDataDir).getVersion()).isZero(); } @Test public void shouldDetectCorrectVersionIfMetadataFileExists() throws Exception { + final Path tempDataDir = temporaryFolder.newFolder().toPath().resolve("data"); final Path tempDatabaseDir = temporaryFolder.newFolder().toPath().resolve("db"); Files.createDirectories(tempDatabaseDir); - tempDatabaseDir.resolve("IDENTITY").toFile().createNewFile(); - new DatabaseMetadata(DEFAULT_VERSION).writeToDirectory(tempDatabaseDir); + Files.createDirectories(tempDataDir); when(commonConfiguration.getStoragePath()).thenReturn(tempDatabaseDir); + when(commonConfiguration.getDataPath()).thenReturn(tempDataDir); + final RocksDBKeyValueStorageFactory storageFactory = new RocksDBKeyValueStorageFactory(() -> rocksDbConfiguration, segments); storageFactory.create(segment, commonConfiguration, metricsSystem); - assertThat(DatabaseMetadata.fromDirectory(tempDatabaseDir).getVersion()) + assertThat(DatabaseMetadata.lookUpFrom(tempDatabaseDir, tempDataDir).getVersion()) .isEqualTo(DEFAULT_VERSION); assertThat(storageFactory.isSegmentIsolationSupported()).isTrue(); } @Test public void shouldDetectCorrectVersionInCaseOfRollback() throws Exception { + final Path tempDataDir = temporaryFolder.newFolder().toPath().resolve("data"); final Path tempDatabaseDir = temporaryFolder.newFolder().toPath().resolve("db"); Files.createDirectories(tempDatabaseDir); + Files.createDirectories(tempDataDir); when(commonConfiguration.getStoragePath()).thenReturn(tempDatabaseDir); + when(commonConfiguration.getDataPath()).thenReturn(tempDataDir); + final RocksDBKeyValueStorageFactory storageFactory = new RocksDBKeyValueStorageFactory(() -> rocksDbConfiguration, segments, 1); @@ -117,12 +131,14 @@ public void shouldDetectCorrectVersionInCaseOfRollback() throws Exception { @Test public void shouldThrowExceptionWhenVersionNumberIsInvalid() throws Exception { + final Path tempDataDir = temporaryFolder.newFolder().toPath().resolve("data"); final Path tempDatabaseDir = temporaryFolder.newFolder().toPath().resolve("db"); Files.createDirectories(tempDatabaseDir); + Files.createDirectories(tempDataDir); tempDatabaseDir.resolve("IDENTITY").toFile().createNewFile(); - new DatabaseMetadata(-1).writeToDirectory(tempDatabaseDir); when(commonConfiguration.getStoragePath()).thenReturn(tempDatabaseDir); - + when(commonConfiguration.getDataPath()).thenReturn(tempDataDir); + new DatabaseMetadata(-1).writeToDirectory(tempDatabaseDir); assertThatThrownBy( () -> new RocksDBKeyValueStorageFactory(() -> rocksDbConfiguration, segments) @@ -132,9 +148,13 @@ public void shouldThrowExceptionWhenVersionNumberIsInvalid() throws Exception { @Test public void shouldSetSegmentationFieldDuringCreation() throws Exception { + final Path tempDataDir = temporaryFolder.newFolder().toPath().resolve("data"); final Path tempDatabaseDir = temporaryFolder.newFolder().toPath().resolve("db"); Files.createDirectories(tempDatabaseDir); + Files.createDirectories(tempDataDir); when(commonConfiguration.getStoragePath()).thenReturn(tempDatabaseDir); + when(commonConfiguration.getDataPath()).thenReturn(tempDataDir); + final RocksDBKeyValueStorageFactory storageFactory = new RocksDBKeyValueStorageFactory(() -> rocksDbConfiguration, segments); storageFactory.create(segment, commonConfiguration, metricsSystem); @@ -143,10 +163,14 @@ public void shouldSetSegmentationFieldDuringCreation() throws Exception { @Test public void shouldThrowExceptionWhenMetaDataFileIsCorrupted() throws Exception { + final Path tempDataDir = temporaryFolder.newFolder().toPath().resolve("data"); final Path tempDatabaseDir = temporaryFolder.newFolder().toPath().resolve("db"); Files.createDirectories(tempDatabaseDir); - when(commonConfiguration.getStoragePath()).thenReturn(tempDatabaseDir); + Files.createDirectories(tempDataDir); tempDatabaseDir.resolve("IDENTITY").toFile().createNewFile(); + when(commonConfiguration.getStoragePath()).thenReturn(tempDatabaseDir); + when(commonConfiguration.getDataPath()).thenReturn(tempDataDir); + final String badVersion = "{\"🦄\":1}"; Files.write( tempDatabaseDir.resolve(METADATA_FILENAME), badVersion.getBytes(Charset.defaultCharset())); diff --git a/plugins/rocksdb/src/test/java/org/hyperledger/besu/plugin/services/storage/rocksdb/configuration/DatabaseMetadataTest.java b/plugins/rocksdb/src/test/java/org/hyperledger/besu/plugin/services/storage/rocksdb/configuration/DatabaseMetadataTest.java new file mode 100644 index 00000000000..1e90c890312 --- /dev/null +++ b/plugins/rocksdb/src/test/java/org/hyperledger/besu/plugin/services/storage/rocksdb/configuration/DatabaseMetadataTest.java @@ -0,0 +1,87 @@ +/* + * Copyright ConsenSys AG. + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.besu.plugin.services.storage.rocksdb.configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.plugin.services.helper.Conditions.FILE_DOES_NOT_EXIST; +import static org.hyperledger.besu.plugin.services.helper.Conditions.FILE_EXISTS; +import static org.hyperledger.besu.plugin.services.helper.Conditions.shouldContain; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class DatabaseMetadataTest { + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void getVersion() { + final DatabaseMetadata databaseMetadata = new DatabaseMetadata(42); + assertThat(databaseMetadata).isNotNull(); + assertThat(databaseMetadata.getVersion()).isEqualTo(42); + } + + @Test + public void metaFileShouldBeSoughtIntoDataDirFirst() throws Exception { + final Path tempDataDir = createAndWrite("data", "DATABASE_METADATA.json", "{\"version\":42}"); + final Path tempDatabaseDir = createAndWrite("db", "DATABASE_METADATA.json", "{\"version\":99}"); + final DatabaseMetadata databaseMetadata = + DatabaseMetadata.lookUpFrom(tempDatabaseDir, tempDataDir); + assertThat(databaseMetadata).isNotNull(); + assertThat(databaseMetadata.getVersion()).isEqualTo(42); + } + + @Test + public void metaFileNotFoundInDataDirShouldLookupIntoDbDir() throws Exception { + final Path tempDataDir = temporaryFolder.newFolder().toPath().resolve("data"); + Files.createDirectories(tempDataDir); + final Path tempDatabaseDir = createAndWrite("db", "DATABASE_METADATA.json", "{\"version\":42}"); + final Path metadataPathInDataDir = tempDataDir.resolve("DATABASE_METADATA.json"); + assertThat(metadataPathInDataDir).is(FILE_DOES_NOT_EXIST); + final DatabaseMetadata databaseMetadata = + DatabaseMetadata.lookUpFrom(tempDatabaseDir, tempDataDir); + assertThat(databaseMetadata).isNotNull(); + assertThat(databaseMetadata.getVersion()).isEqualTo(42); + assertThat(metadataPathInDataDir).is(FILE_EXISTS); + assertThat(metadataPathInDataDir).is(shouldContain("{\"version\":42}")); + } + + private Path createAndWrite(final String dir, final String file, final String content) + throws IOException { + return createAndWrite(temporaryFolder, dir, file, content); + } + + private Path createAndWrite( + final TemporaryFolder temporaryFolder, + final String dir, + final String file, + final String content) + throws IOException { + final Path tmpDir = temporaryFolder.newFolder().toPath().resolve(dir); + Files.createDirectories(tmpDir); + createAndWrite(tmpDir.resolve(file), content); + return tmpDir; + } + + private void createAndWrite(final Path path, final String content) throws IOException { + path.toFile().createNewFile(); + Files.writeString(path, content); + } +}