Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Version Rollbacks for RocksDB (#6) #19

Merged
merged 3 commits into from
Sep 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugin-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 'mBaqR6fbWYPndHOw7CymPkR3KKb+f8pxLldBnTCjYAg='
knownHash = '+dpM9DHxYq73BGIktTSVRKPjZ1mu+Nw8NoYYvPgmSn4='
}
check.dependsOn('checkAPIChanges')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.exception.StorageException;

import java.io.Closeable;

/** Factory for creating key-value storage instances. */
@Unstable
public interface KeyValueStorageFactory {
public interface KeyValueStorageFactory extends Closeable {

/**
* Retrieves the identity of the key-value storage factory.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
package org.hyperledger.besu.plugin.services.storage.rocksdb;

import static com.google.common.base.Preconditions.checkNotNull;

import org.hyperledger.besu.plugin.services.BesuConfiguration;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.exception.StorageException;
Expand Down Expand Up @@ -40,22 +42,36 @@
public class RocksDBKeyValueStorageFactory implements KeyValueStorageFactory {

private static final Logger LOG = LogManager.getLogger();
private static final int DEFAULT_VERSION = 1;
private static final Set<Integer> SUPPORTED_VERSION = Set.of(0, 1);
private final int DEFAULT_VERSION;
private static final Set<Integer> SUPPORTED_VERSIONS = Set.of(0, 1);
private static final String NAME = "rocksdb";

private boolean isSegmentIsolationSupported;
private Integer databaseVersion;
private Boolean isSegmentIsolationSupported;
private SegmentedKeyValueStorage<?> segmentedStorage;
private KeyValueStorage unsegmentedStorage;
private RocksDBConfiguration rocksDBConfiguration;

private final Supplier<RocksDBFactoryConfiguration> configuration;
private final List<SegmentIdentifier> segments;

public RocksDBKeyValueStorageFactory(
RocksDBKeyValueStorageFactory(
final Supplier<RocksDBFactoryConfiguration> configuration,
final List<SegmentIdentifier> segments) {
final List<SegmentIdentifier> segments,
final int DEFAULT_VERSION) {
this.configuration = configuration;
this.segments = segments;
this.DEFAULT_VERSION = DEFAULT_VERSION;
}

public RocksDBKeyValueStorageFactory(
final Supplier<RocksDBFactoryConfiguration> configuration,
final List<SegmentIdentifier> segments) {
this(
configuration,
segments,
/** Source of truth for the default database version. */
1);
}

@Override
Expand All @@ -71,62 +87,64 @@ public KeyValueStorage create(
throws StorageException {

if (requiresInit()) {
init(commonConfiguration, metricsSystem);
init(commonConfiguration);
}

return isSegmentIsolationSupported
? new SegmentedKeyValueStorageAdapter<>(segment, segmentedStorage)
: unsegmentedStorage;
}

@Override
public boolean isSegmentIsolationSupported() {
return isSegmentIsolationSupported;
}

public void close() throws IOException {
if (segmentedStorage != null) {
segmentedStorage.close();
}
if (unsegmentedStorage != null) {
unsegmentedStorage.close();
// It's probably a good idea for the creation logic to be entirely dependent on the database
// version. Introducing intermediate booleans that represent database properties and dispatching
// creation logic based on them is error prone.
switch (databaseVersion) {
case 0:
{
segmentedStorage = null;
if (unsegmentedStorage == null) {
unsegmentedStorage = new RocksDBKeyValueStorage(rocksDBConfiguration, metricsSystem);
}
return unsegmentedStorage;
}
case 1:
{
unsegmentedStorage = null;
if (segmentedStorage == null) {
segmentedStorage =
new RocksDBColumnarKeyValueStorage(rocksDBConfiguration, segments, metricsSystem);
}
return new SegmentedKeyValueStorageAdapter<>(segment, segmentedStorage);
}
default:
{
throw new IllegalStateException(
String.format(
"Developer error: A supported database version (%d) was detected but there is no associated creation logic.",
databaseVersion));
}
}
}

protected Path storagePath(final BesuConfiguration commonConfiguration) {
return commonConfiguration.getStoragePath();
}

private boolean requiresInit() {
return segmentedStorage == null && unsegmentedStorage == null;
}

private void init(
final BesuConfiguration commonConfiguration, final MetricsSystem metricsSystem) {
private void init(final BesuConfiguration commonConfiguration) {
try {
this.isSegmentIsolationSupported = databaseVersion(commonConfiguration) == DEFAULT_VERSION;
databaseVersion = readDatabaseVersion(commonConfiguration);
} catch (final IOException e) {
LOG.error("Failed to retrieve the RocksDB database meta version: {}", e.getMessage());
throw new StorageException(e.getMessage(), e);
}

final RocksDBConfiguration rocksDBConfiguration =
isSegmentIsolationSupported = databaseVersion >= 1;
rocksDBConfiguration =
RocksDBConfigurationBuilder.from(configuration.get())
.databaseDir(storagePath(commonConfiguration))
.build();
}

if (isSegmentIsolationSupported) {
this.unsegmentedStorage = null;
this.segmentedStorage =
new RocksDBColumnarKeyValueStorage(rocksDBConfiguration, segments, metricsSystem);
} else {
this.unsegmentedStorage = new RocksDBKeyValueStorage(rocksDBConfiguration, metricsSystem);
this.segmentedStorage = null;
}
private boolean requiresInit() {
return segmentedStorage == null && unsegmentedStorage == null;
}

private int databaseVersion(final BesuConfiguration commonConfiguration) throws IOException {
final Path databaseDir = storagePath(commonConfiguration);
private int readDatabaseVersion(final BesuConfiguration commonConfiguration) throws IOException {
final Path databaseDir = commonConfiguration.getStoragePath();
final boolean databaseExists = databaseDir.resolve("IDENTITY").toFile().exists();
final int databaseVersion;
if (databaseExists) {
Expand All @@ -140,12 +158,29 @@ private int databaseVersion(final BesuConfiguration commonConfiguration) throws
new DatabaseMetadata(databaseVersion).writeToDirectory(databaseDir);
}

if (!SUPPORTED_VERSION.contains(databaseVersion)) {
if (!SUPPORTED_VERSIONS.contains(databaseVersion)) {
final String message = "Unsupported RocksDB Metadata version of: " + databaseVersion;
LOG.error(message);
throw new StorageException(message);
}

return databaseVersion;
}

@Override
public void close() throws IOException {
if (unsegmentedStorage != null) {
unsegmentedStorage.close();
}
if (segmentedStorage != null) {
segmentedStorage.close();
}
}

@Override
public boolean isSegmentIsolationSupported() {
return checkNotNull(
isSegmentIsolationSupported,
"Whether segment isolation is supported will be determined during creation. Call a creation method first");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package org.hyperledger.besu.plugin.services.storage.rocksdb;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.when;

Expand Down Expand Up @@ -96,6 +97,22 @@ public void shouldDetectCorrectVersionIfMetadataFileExists() throws Exception {
assertThat(storageFactory.isSegmentIsolationSupported()).isTrue();
}

@Test
public void shouldDetectCorrectVersionInCaseOfRollback() throws Exception {
final Path tempDatabaseDir = temporaryFolder.newFolder().toPath().resolve("db");
Files.createDirectories(tempDatabaseDir);
when(commonConfiguration.getStoragePath()).thenReturn(tempDatabaseDir);
final RocksDBKeyValueStorageFactory storageFactory =
new RocksDBKeyValueStorageFactory(() -> rocksDbConfiguration, segments, 1);

storageFactory.create(segment, commonConfiguration, metricsSystem);
storageFactory.close();

final RocksDBKeyValueStorageFactory rolledbackStorageFactory =
new RocksDBKeyValueStorageFactory(() -> rocksDbConfiguration, segments, 0);
rolledbackStorageFactory.create(segment, commonConfiguration, metricsSystem);
}

@Test
public void shouldThrowExceptionWhenVersionNumberIsInvalid() throws Exception {
final Path tempDatabaseDir = temporaryFolder.newFolder().toPath().resolve("db");
Expand All @@ -111,6 +128,17 @@ public void shouldThrowExceptionWhenVersionNumberIsInvalid() throws Exception {
.isInstanceOf(StorageException.class);
}

@Test
public void shouldSetSegmentationFieldDuringCreation() throws Exception {
final Path tempDatabaseDir = temporaryFolder.newFolder().toPath().resolve("db");
Files.createDirectories(tempDatabaseDir);
when(commonConfiguration.getStoragePath()).thenReturn(tempDatabaseDir);
final RocksDBKeyValueStorageFactory storageFactory =
new RocksDBKeyValueStorageFactory(() -> rocksDbConfiguration, segments);
storageFactory.create(segment, commonConfiguration, metricsSystem);
assertThatCode(storageFactory::isSegmentIsolationSupported).doesNotThrowAnyException();
}

@Test
public void shouldThrowExceptionWhenMetaDataFileIsCorrupted() throws Exception {
final Path tempDatabaseDir = temporaryFolder.newFolder().toPath().resolve("db");
Expand Down