Force besu to stop on plugin initialization errors (#7662)

Signed-off-by: Gabriel-Trintinalia <gabriel.trintinalia@consensys.net>
This commit is contained in:
Gabriel-Trintinalia
2024-09-24 20:50:19 +10:00
committed by GitHub
parent 2aa3848950
commit e0518c6d94
11 changed files with 424 additions and 99 deletions

View File

@@ -8,6 +8,7 @@
- k8s (KUBERNETES) Nat method is now deprecated and will be removed in a future release
### Breaking Changes
- Besu will now fail to start if any plugins encounter errors during initialization. To allow Besu to continue running despite plugin errors, use the `--plugin-continue-on-error` option. [#7662](https://github.com/hyperledger/besu/pull/7662)
### Additions and Improvements
- Remove privacy test classes support [#7569](https://github.com/hyperledger/besu/pull/7569)

View File

@@ -503,8 +503,9 @@ public class ThreadBesuNodeRunner implements BesuNodeRunner {
besuPluginContext.addService(PermissioningService.class, permissioningService);
besuPluginContext.addService(PrivacyPluginService.class, new PrivacyPluginServiceImpl());
besuPluginContext.registerPlugins(
besuPluginContext.initialize(
new PluginConfiguration.Builder().pluginsDir(pluginsPath).build());
besuPluginContext.registerPlugins();
commandLine.parseArgs(extraCLIOptions.toArray(new String[0]));
// register built-in plugins

View File

@@ -1,5 +1,5 @@
/*
* Copyright ConsenSys AG.
* Copyright contributors to Hyperledger Besu.
*
* 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
@@ -32,16 +32,25 @@ import picocli.CommandLine.Option;
public class TestPicoCLIPlugin implements BesuPlugin {
private static final Logger LOG = LoggerFactory.getLogger(TestPicoCLIPlugin.class);
private static final String UNSET = "UNSET";
private static final String FAIL_REGISTER = "FAILREGISTER";
private static final String FAIL_BEFORE_EXTERNAL_SERVICES = "FAILBEFOREEXTERNALSERVICES";
private static final String FAIL_START = "FAILSTART";
private static final String FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP =
"FAILAFTEREXTERNALSERVICEPOSTMAINLOOP";
private static final String FAIL_STOP = "FAILSTOP";
private static final String PLUGIN_LIFECYCLE_PREFIX = "pluginLifecycle.";
@Option(
names = {"--Xplugin-test-option"},
hidden = true,
defaultValue = "UNSET")
defaultValue = UNSET)
String testOption = System.getProperty("testPicoCLIPlugin.testOption");
@Option(
names = {"--plugin-test-stable-option"},
hidden = true,
defaultValue = "UNSET")
defaultValue = UNSET)
String stableOption = "";
private String state = "uninited";
@@ -52,7 +61,7 @@ public class TestPicoCLIPlugin implements BesuPlugin {
LOG.info("Registering. Test Option is '{}'", testOption);
state = "registering";
if ("FAILREGISTER".equals(testOption)) {
if (FAIL_REGISTER.equals(testOption)) {
state = "failregister";
throw new RuntimeException("I was told to fail at registration");
}
@@ -66,12 +75,26 @@ public class TestPicoCLIPlugin implements BesuPlugin {
state = "registered";
}
@Override
public void beforeExternalServices() {
LOG.info("Before external services. Test Option is '{}'", testOption);
state = "beforeExternalServices";
if (FAIL_BEFORE_EXTERNAL_SERVICES.equals(testOption)) {
state = "failbeforeExternalServices";
throw new RuntimeException("I was told to fail before external services");
}
writeSignal("beforeExternalServices");
state = "beforeExternalServicesFinished";
}
@Override
public void start() {
LOG.info("Starting. Test Option is '{}'", testOption);
state = "starting";
if ("FAILSTART".equals(testOption)) {
if (FAIL_START.equals(testOption)) {
state = "failstart";
throw new RuntimeException("I was told to fail at startup");
}
@@ -80,12 +103,26 @@ public class TestPicoCLIPlugin implements BesuPlugin {
state = "started";
}
@Override
public void afterExternalServicePostMainLoop() {
LOG.info("After external services post main loop. Test Option is '{}'", testOption);
state = "afterExternalServicePostMainLoop";
if (FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP.equals(testOption)) {
state = "failafterExternalServicePostMainLoop";
throw new RuntimeException("I was told to fail after external services post main loop");
}
writeSignal("afterExternalServicePostMainLoop");
state = "afterExternalServicePostMainLoopFinished";
}
@Override
public void stop() {
LOG.info("Stopping. Test Option is '{}'", testOption);
state = "stopping";
if ("FAILSTOP".equals(testOption)) {
if (FAIL_STOP.equals(testOption)) {
state = "failstop";
throw new RuntimeException("I was told to fail at stop");
}
@@ -103,7 +140,7 @@ public class TestPicoCLIPlugin implements BesuPlugin {
@SuppressWarnings("ResultOfMethodCallIgnored")
private void writeSignal(final String signal) {
try {
final File callbackFile = new File(callbackDir, "pluginLifecycle." + signal);
final File callbackFile = new File(callbackDir, PLUGIN_LIFECYCLE_PREFIX + signal);
if (!callbackFile.getParentFile().exists()) {
callbackFile.getParentFile().mkdirs();
callbackFile.getParentFile().deleteOnExit();

View File

@@ -1,5 +1,5 @@
/*
* Copyright ConsenSys AG.
* Copyright contributors to Hyperledger Besu.
*
* 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
@@ -40,8 +40,31 @@ import org.junit.jupiter.api.Test;
public class BesuPluginContextImplTest {
private static final Path DEFAULT_PLUGIN_DIRECTORY = Paths.get(".");
private static final String TEST_PICO_CLI_PLUGIN = "TestPicoCLIPlugin";
private static final String TEST_PICO_CLI_PLUGIN_TEST_OPTION = "testPicoCLIPlugin.testOption";
private static final String FAIL_REGISTER = "FAILREGISTER";
private static final String FAIL_START = "FAILSTART";
private static final String FAIL_STOP = "FAILSTOP";
private static final String FAIL_BEFORE_EXTERNAL_SERVICES = "FAILBEFOREEXTERNALSERVICES";
private static final String FAIL_BEFORE_MAIN_LOOP = "FAILBEFOREMAINLOOP";
private static final String FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP =
"FAILAFTEREXTERNALSERVICEPOSTMAINLOOP";
private static final String NON_EXISTENT_PLUGIN = "NonExistentPlugin";
private static final String REGISTERED = "registered";
private static final String STARTED = "started";
private static final String STOPPED = "stopped";
private static final String FAIL_START_STATE = "failstart";
private static final String FAIL_STOP_STATE = "failstop";
private BesuPluginContextImpl contextImpl;
private static final PluginConfiguration DEFAULT_CONFIGURATION =
PluginConfiguration.builder()
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.externalPluginsEnabled(true)
.continueOnPluginError(true)
.build();
@BeforeAll
public static void createFakePluginDir() throws IOException {
if (System.getProperty("besu.plugins.dir") == null) {
@@ -53,7 +76,7 @@ public class BesuPluginContextImplTest {
@AfterEach
public void clearTestPluginState() {
System.clearProperty("testPicoCLIPlugin.testOption");
System.clearProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION);
}
@BeforeEach
@@ -64,31 +87,31 @@ public class BesuPluginContextImplTest {
@Test
public void verifyEverythingGoesSmoothly() {
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(
PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build());
contextImpl.initialize(DEFAULT_CONFIGURATION);
contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins()).isNotEmpty();
final Optional<TestPicoCLIPlugin> testPluginOptional =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(testPluginOptional).isPresent();
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered");
assertThat(testPicoCLIPlugin.getState()).isEqualTo(REGISTERED);
contextImpl.beforeExternalServices();
contextImpl.startPlugins();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("started");
assertThat(testPicoCLIPlugin.getState()).isEqualTo(STARTED);
contextImpl.stopPlugins();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("stopped");
assertThat(testPicoCLIPlugin.getState()).isEqualTo(STOPPED);
}
@Test
public void registrationErrorsHandledSmoothly() {
System.setProperty("testPicoCLIPlugin.testOption", "FAILREGISTER");
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_REGISTER);
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(
PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build());
contextImpl.initialize(DEFAULT_CONFIGURATION);
contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
contextImpl.beforeExternalServices();
@@ -103,11 +126,11 @@ public class BesuPluginContextImplTest {
@Test
public void startErrorsHandledSmoothly() {
System.setProperty("testPicoCLIPlugin.testOption", "FAILSTART");
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_START);
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(
PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build());
contextImpl.initialize(DEFAULT_CONFIGURATION);
contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins())
.extracting("class")
.contains(TestPicoCLIPlugin.class);
@@ -116,11 +139,11 @@ public class BesuPluginContextImplTest {
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(testPluginOptional).isPresent();
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered");
assertThat(testPicoCLIPlugin.getState()).isEqualTo(REGISTERED);
contextImpl.beforeExternalServices();
contextImpl.startPlugins();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("failstart");
assertThat(testPicoCLIPlugin.getState()).isEqualTo(FAIL_START_STATE);
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
contextImpl.stopPlugins();
@@ -129,11 +152,11 @@ public class BesuPluginContextImplTest {
@Test
public void stopErrorsHandledSmoothly() {
System.setProperty("testPicoCLIPlugin.testOption", "FAILSTOP");
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_STOP);
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(
PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build());
contextImpl.initialize(DEFAULT_CONFIGURATION);
contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins())
.extracting("class")
.contains(TestPicoCLIPlugin.class);
@@ -142,22 +165,20 @@ public class BesuPluginContextImplTest {
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(testPluginOptional).isPresent();
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered");
assertThat(testPicoCLIPlugin.getState()).isEqualTo(REGISTERED);
contextImpl.beforeExternalServices();
contextImpl.startPlugins();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("started");
assertThat(testPicoCLIPlugin.getState()).isEqualTo(STARTED);
contextImpl.stopPlugins();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("failstop");
assertThat(testPicoCLIPlugin.getState()).isEqualTo(FAIL_STOP_STATE);
}
@Test
public void lifecycleExceptions() throws Throwable {
final ThrowableAssert.ThrowingCallable registerPlugins =
() ->
contextImpl.registerPlugins(
PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build());
contextImpl.initialize(DEFAULT_CONFIGURATION);
final ThrowableAssert.ThrowingCallable registerPlugins = () -> contextImpl.registerPlugins();
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::startPlugins);
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::stopPlugins);
@@ -179,30 +200,27 @@ public class BesuPluginContextImplTest {
@Test
public void shouldRegisterAllPluginsWhenNoPluginsOption() {
final PluginConfiguration config =
PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build();
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(config);
contextImpl.initialize(DEFAULT_CONFIGURATION);
contextImpl.registerPlugins();
final Optional<TestPicoCLIPlugin> testPluginOptional =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(testPluginOptional).isPresent();
final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get();
assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered");
assertThat(testPicoCLIPlugin.getState()).isEqualTo(REGISTERED);
}
@Test
public void shouldRegisterOnlySpecifiedPluginWhenPluginsOptionIsSet() {
final PluginConfiguration config = createConfigurationForSpecificPlugin("TestPicoCLIPlugin");
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(config);
contextImpl.initialize(createConfigurationForSpecificPlugin(TEST_PICO_CLI_PLUGIN));
contextImpl.registerPlugins();
final Optional<TestPicoCLIPlugin> requestedPlugin =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class);
assertThat(requestedPlugin).isPresent();
assertThat(requestedPlugin.get().getState()).isEqualTo("registered");
assertThat(requestedPlugin.get().getState()).isEqualTo(REGISTERED);
final Optional<TestPicoCLIPlugin> nonRequestedPlugin =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestBesuEventsPlugin.class);
@@ -212,9 +230,9 @@ public class BesuPluginContextImplTest {
@Test
public void shouldNotRegisterUnspecifiedPluginsWhenPluginsOptionIsSet() {
final PluginConfiguration config = createConfigurationForSpecificPlugin("TestPicoCLIPlugin");
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
contextImpl.registerPlugins(config);
contextImpl.initialize(createConfigurationForSpecificPlugin(TEST_PICO_CLI_PLUGIN));
contextImpl.registerPlugins();
final Optional<TestPicoCLIPlugin> nonRequestedPlugin =
findTestPlugin(contextImpl.getRegisteredPlugins(), TestBesuEventsPlugin.class);
@@ -223,13 +241,12 @@ public class BesuPluginContextImplTest {
@Test
void shouldThrowExceptionIfExplicitlySpecifiedPluginNotFound() {
PluginConfiguration config = createConfigurationForSpecificPlugin("NonExistentPlugin");
contextImpl.initialize(createConfigurationForSpecificPlugin(NON_EXISTENT_PLUGIN));
String exceptionMessage =
assertThrows(NoSuchElementException.class, () -> contextImpl.registerPlugins(config))
assertThrows(NoSuchElementException.class, () -> contextImpl.registerPlugins())
.getMessage();
final String expectedMessage =
"The following requested plugins were not found: NonExistentPlugin";
"The following requested plugins were not found: " + NON_EXISTENT_PLUGIN;
assertThat(exceptionMessage).isEqualTo(expectedMessage);
assertThat(contextImpl.getRegisteredPlugins()).isEmpty();
}
@@ -241,19 +258,180 @@ public class BesuPluginContextImplTest {
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.externalPluginsEnabled(false)
.build();
contextImpl.registerPlugins(config);
contextImpl.initialize(config);
contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins().isEmpty()).isTrue();
}
@Test
void shouldRegisterPluginsIfExternalPluginsEnabled() {
contextImpl.initialize(DEFAULT_CONFIGURATION);
contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins().isEmpty()).isFalse();
}
@Test
void shouldHaltOnRegisterErrorWhenFlagIsFalse() {
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_REGISTER);
PluginConfiguration config =
PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(false)
.build();
contextImpl.initialize(config);
String errorMessage =
String.format("Error registering plugin of type %s", TestPicoCLIPlugin.class.getName());
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> contextImpl.registerPlugins())
.withMessageContaining(errorMessage);
}
@Test
void shouldNotHaltOnRegisterErrorWhenFlagIsTrue() {
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_REGISTER);
PluginConfiguration config =
PluginConfiguration.builder()
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.externalPluginsEnabled(true)
.continueOnPluginError(true)
.build();
contextImpl.registerPlugins(config);
assertThat(contextImpl.getRegisteredPlugins().isEmpty()).isFalse();
contextImpl.initialize(config);
contextImpl.registerPlugins();
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
}
@Test
void shouldHaltOnBeforeExternalServicesErrorWhenFlagIsFalse() {
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_BEFORE_EXTERNAL_SERVICES);
PluginConfiguration config =
PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(false)
.build();
contextImpl.initialize(config);
contextImpl.registerPlugins();
String errorMessage =
String.format(
"Error calling `beforeExternalServices` on plugin of type %s",
TestPicoCLIPlugin.class.getName());
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> contextImpl.beforeExternalServices())
.withMessageContaining(errorMessage);
}
@Test
void shouldNotHaltOnBeforeExternalServicesErrorWhenFlagIsTrue() {
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_BEFORE_EXTERNAL_SERVICES);
PluginConfiguration config =
PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(true)
.build();
contextImpl.initialize(config);
contextImpl.registerPlugins();
contextImpl.beforeExternalServices();
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
}
@Test
void shouldHaltOnBeforeMainLoopErrorWhenFlagIsFalse() {
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_START);
PluginConfiguration config =
PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(false)
.build();
contextImpl.initialize(config);
contextImpl.registerPlugins();
contextImpl.beforeExternalServices();
String errorMessage =
String.format("Error starting plugin of type %s", TestPicoCLIPlugin.class.getName());
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> contextImpl.startPlugins())
.withMessageContaining(errorMessage);
}
@Test
void shouldNotHaltOnBeforeMainLoopErrorWhenFlagIsTrue() {
System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_BEFORE_MAIN_LOOP);
PluginConfiguration config =
PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(true)
.build();
contextImpl.initialize(config);
contextImpl.registerPlugins();
contextImpl.beforeExternalServices();
contextImpl.startPlugins();
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
}
@Test
void shouldHaltOnAfterExternalServicePostMainLoopErrorWhenFlagIsFalse() {
System.setProperty(
TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP);
PluginConfiguration config =
PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(false)
.build();
contextImpl.initialize(config);
contextImpl.registerPlugins();
contextImpl.beforeExternalServices();
contextImpl.startPlugins();
String errorMessage =
String.format(
"Error calling `afterExternalServicePostMainLoop` on plugin of type %s",
TestPicoCLIPlugin.class.getName());
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> contextImpl.afterExternalServicesMainLoop())
.withMessageContaining(errorMessage);
}
@Test
void shouldNotHaltOnAfterExternalServicePostMainLoopErrorWhenFlagIsTrue() {
System.setProperty(
TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP);
PluginConfiguration config =
PluginConfiguration.builder()
.requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN)))
.pluginsDir(DEFAULT_PLUGIN_DIRECTORY)
.continueOnPluginError(true)
.build();
contextImpl.initialize(config);
contextImpl.registerPlugins();
contextImpl.beforeExternalServices();
contextImpl.startPlugins();
contextImpl.afterExternalServicesMainLoop();
assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class);
}
private PluginConfiguration createConfigurationForSpecificPlugin(final String pluginName) {

View File

@@ -118,7 +118,6 @@ import org.hyperledger.besu.ethereum.core.MiningParameters;
import org.hyperledger.besu.ethereum.core.MiningParametersMetrics;
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
import org.hyperledger.besu.ethereum.core.VersionMetadata;
import org.hyperledger.besu.ethereum.core.plugins.PluginConfiguration;
import org.hyperledger.besu.ethereum.eth.sync.SyncMode;
import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration;
@@ -1080,9 +1079,8 @@ public class BesuCommand implements DefaultCommandValues, Runnable {
private IExecutionStrategy createPluginRegistrationTask(final IExecutionStrategy nextStep) {
return parseResult -> {
PluginConfiguration configuration =
PluginsConfigurationOptions.fromCommandLine(parseResult.commandSpec().commandLine());
besuPluginContext.registerPlugins(configuration);
besuPluginContext.initialize(PluginsConfigurationOptions.fromCommandLine(commandLine));
besuPluginContext.registerPlugins();
commandLine.setExecutionStrategy(nextStep);
return commandLine.execute(parseResult.originalArgs().toArray(new String[0]));
};

View File

@@ -128,6 +128,9 @@ public interface DefaultCommandValues {
/** The constant DEFAULT_PLUGINS_OPTION_NAME. */
String DEFAULT_PLUGINS_OPTION_NAME = "--plugins";
/** The constant DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME. */
String DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME = "--plugin-continue-on-error";
/** The constant DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME. */
String DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME = "--Xplugins-external-enabled";

View File

@@ -14,6 +14,7 @@
*/
package org.hyperledger.besu.cli.options.stable;
import static org.hyperledger.besu.cli.DefaultCommandValues.DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME;
import static org.hyperledger.besu.cli.DefaultCommandValues.DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME;
import static org.hyperledger.besu.cli.DefaultCommandValues.DEFAULT_PLUGINS_OPTION_NAME;
@@ -27,7 +28,7 @@ import java.util.List;
import picocli.CommandLine;
/** The Plugins Options options. */
/** The Plugins options. */
public class PluginsConfigurationOptions implements CLIOptions<PluginConfiguration> {
@CommandLine.Option(
@@ -44,9 +45,17 @@ public class PluginsConfigurationOptions implements CLIOptions<PluginConfigurati
split = ",",
hidden = true,
converter = PluginInfoConverter.class,
arity = "1..*")
arity = "1")
private List<PluginInfo> plugins;
@CommandLine.Option(
names = {DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME},
description =
"Allow Besu startup even if any plugins fail to initialize correctly (default: ${DEFAULT-VALUE})",
defaultValue = "false",
arity = "1")
private final Boolean continueOnPluginError = false;
/** Default Constructor. */
public PluginsConfigurationOptions() {}
@@ -55,6 +64,7 @@ public class PluginsConfigurationOptions implements CLIOptions<PluginConfigurati
return new PluginConfiguration.Builder()
.externalPluginsEnabled(externalPluginsEnabled)
.requestedPlugins(plugins)
.continueOnPluginError(continueOnPluginError)
.build();
}
@@ -66,10 +76,15 @@ public class PluginsConfigurationOptions implements CLIOptions<PluginConfigurati
public void validate(final CommandLine commandLine) {
String errorMessage =
String.format(
"%s option can only be used when %s is true",
DEFAULT_PLUGINS_OPTION_NAME, DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME);
"%s and %s option can only be used when %s is true",
DEFAULT_PLUGINS_OPTION_NAME,
DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME,
DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME);
CommandLineUtils.failIfOptionDoesntMeetRequirement(
commandLine, errorMessage, externalPluginsEnabled, List.of(DEFAULT_PLUGINS_OPTION_NAME));
commandLine,
errorMessage,
externalPluginsEnabled,
List.of(DEFAULT_PLUGINS_OPTION_NAME, DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME));
}
@Override
@@ -92,9 +107,14 @@ public class PluginsConfigurationOptions implements CLIOptions<PluginConfigurati
CommandLineUtils.getOptionValueOrDefault(
commandLine, DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME, Boolean::parseBoolean);
boolean continueOnPluginError =
CommandLineUtils.getOptionValueOrDefault(
commandLine, DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME, Boolean::parseBoolean);
return new PluginConfiguration.Builder()
.requestedPlugins(plugins)
.externalPluginsEnabled(externalPluginsEnabled)
.continueOnPluginError(continueOnPluginError)
.build();
}
}

View File

@@ -56,6 +56,8 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
private enum Lifecycle {
/** Uninitialized lifecycle. */
UNINITIALIZED,
/** Initialized lifecycle. */
INITIALIZED,
/** Registering lifecycle. */
REGISTERING,
/** Registered lifecycle. */
@@ -83,6 +85,7 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
private final List<BesuPlugin> registeredPlugins = new ArrayList<>();
private final List<String> pluginVersions = new ArrayList<>();
private PluginConfiguration config;
/** Instantiates a new Besu plugin context. */
public BesuPluginContextImpl() {}
@@ -116,19 +119,30 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
return StreamSupport.stream(serviceLoader.spliterator(), false).toList();
}
/**
* Initializes the plugin context with the provided {@link PluginConfiguration}.
*
* @param config the plugin configuration
* @throws IllegalStateException if the system is not in the UNINITIALIZED state.
*/
public void initialize(final PluginConfiguration config) {
checkState(
state == Lifecycle.UNINITIALIZED,
"Besu plugins have already been initialized. Cannot register additional plugins.");
this.config = config;
state = Lifecycle.INITIALIZED;
}
/**
* Registers plugins based on the provided {@link PluginConfiguration}. This method finds plugins
* according to the configuration settings, filters them if necessary and then registers the
* filtered or found plugins
*
* @param config The configuration settings used to find and filter plugins for registration. The
* configuration includes the plugin directory and any configured plugin identifiers if
* applicable.
* @throws IllegalStateException if the system is not in the UNINITIALIZED state.
*/
public void registerPlugins(final PluginConfiguration config) {
public void registerPlugins() {
checkState(
state == Lifecycle.UNINITIALIZED,
state == Lifecycle.INITIALIZED,
"Besu plugins have already been registered. Cannot register additional plugins.");
state = Lifecycle.REGISTERING;
@@ -192,14 +206,18 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
private boolean registerPlugin(final BesuPlugin plugin) {
try {
plugin.register(this);
LOG.info("Registered plugin of type {}.", plugin.getClass().getName());
pluginVersions.add(plugin.getVersion());
LOG.info("Registered plugin of type {}.", plugin.getClass().getName());
} catch (final Exception e) {
LOG.error(
"Error registering plugin of type "
+ plugin.getClass().getName()
+ ", start and stop will not be called.",
e);
if (config.isContinueOnPluginError()) {
LOG.error(
"Error registering plugin of type {}, start and stop will not be called.",
plugin.getClass().getName(),
e);
} else {
throw new RuntimeException(
"Error registering plugin of type " + plugin.getClass().getName(), e);
}
return false;
}
return true;
@@ -223,15 +241,20 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
LOG.debug(
"beforeExternalServices called on plugin of type {}.", plugin.getClass().getName());
} catch (final Exception e) {
LOG.error(
"Error calling `beforeExternalServices` on plugin of type "
+ plugin.getClass().getName()
+ ", stop will not be called.",
e);
pluginsIterator.remove();
if (config.isContinueOnPluginError()) {
LOG.error(
"Error calling `beforeExternalServices` on plugin of type {}, start will not be called.",
plugin.getClass().getName(),
e);
pluginsIterator.remove();
} else {
throw new RuntimeException(
"Error calling `beforeExternalServices` on plugin of type "
+ plugin.getClass().getName(),
e);
}
}
}
LOG.debug("Plugin startup complete.");
state = Lifecycle.BEFORE_EXTERNAL_SERVICES_FINISHED;
}
@@ -253,12 +276,16 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
plugin.start();
LOG.debug("Started plugin of type {}.", plugin.getClass().getName());
} catch (final Exception e) {
LOG.error(
"Error starting plugin of type "
+ plugin.getClass().getName()
+ ", stop will not be called.",
e);
pluginsIterator.remove();
if (config.isContinueOnPluginError()) {
LOG.error(
"Error starting plugin of type {}, stop will not be called.",
plugin.getClass().getName(),
e);
pluginsIterator.remove();
} else {
throw new RuntimeException(
"Error starting plugin of type " + plugin.getClass().getName(), e);
}
}
}
@@ -279,8 +306,20 @@ public class BesuPluginContextImpl implements BesuContext, PluginVersionsProvide
final BesuPlugin plugin = pluginsIterator.next();
try {
plugin.afterExternalServicePostMainLoop();
} finally {
pluginsIterator.remove();
} catch (final Exception e) {
if (config.isContinueOnPluginError()) {
LOG.error(
"Error calling `afterExternalServicePostMainLoop` on plugin of type "
+ plugin.getClass().getName()
+ ", stop will not be called.",
e);
pluginsIterator.remove();
} else {
throw new RuntimeException(
"Error calling `afterExternalServicePostMainLoop` on plugin of type "
+ plugin.getClass().getName(),
e);
}
}
}
}

View File

@@ -33,7 +33,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test
public void shouldParsePluginOptionForSinglePlugin() {
parseCommand("--plugins", "pluginA");
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture());
verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().getRequestedPlugins())
.isEqualTo(List.of("pluginA"));
assertThat(commandOutput.toString(UTF_8)).isEmpty();
@@ -43,7 +43,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test
public void shouldParsePluginOptionForMultiplePlugins() {
parseCommand("--plugins", "pluginA,pluginB");
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture());
verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().getRequestedPlugins())
.isEqualTo(List.of("pluginA", "pluginB"));
@@ -54,7 +54,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test
public void shouldNotUsePluginOptionWhenNoPluginsSpecified() {
parseCommand();
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture());
verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().getRequestedPlugins())
.isEqualTo(List.of());
assertThat(commandOutput.toString(UTF_8)).isEmpty();
@@ -64,7 +64,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test
public void shouldNotParseAnyPluginsWhenPluginOptionIsEmpty() {
parseCommand("--plugins", "");
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture());
verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().getRequestedPlugins())
.isEqualTo(List.of());
assertThat(commandOutput.toString(UTF_8)).isEmpty();
@@ -74,7 +74,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test
public void shouldParsePluginsExternalEnabledOptionWhenFalse() {
parseCommand("--Xplugins-external-enabled=false");
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture());
verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().isExternalPluginsEnabled())
.isEqualTo(false);
@@ -86,7 +86,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test
public void shouldParsePluginsExternalEnabledOptionWhenTrue() {
parseCommand("--Xplugins-external-enabled=true");
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture());
verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().isExternalPluginsEnabled())
.isEqualTo(true);
@@ -98,7 +98,7 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test
public void shouldEnablePluginsExternalByDefault() {
parseCommand();
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture());
verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().isExternalPluginsEnabled())
.isEqualTo(true);
@@ -109,10 +109,43 @@ public class PluginsOptionsTest extends CommandTestAbstract {
@Test
public void shouldFailWhenPluginsIsDisabledAndPluginsExplicitlyRequested() {
parseCommand("--Xplugins-external-enabled=false", "--plugins", "pluginA");
verify(mockBesuPluginContext).registerPlugins(pluginConfigurationArgumentCaptor.capture());
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8))
.contains("--plugins option can only be used when --Xplugins-external-enabled is true");
.contains(
"--plugins and --plugin-continue-on-error option can only be used when --Xplugins-external-enabled is true");
}
@Test
public void shouldHaveContinueOnErrorFalseByDefault() {
parseCommand();
verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().isContinueOnPluginError())
.isEqualTo(false);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void shouldUseContinueOnErrorWhenTrue() {
parseCommand("--plugin-continue-on-error=true");
verify(mockBesuPluginContext).initialize(pluginConfigurationArgumentCaptor.capture());
assertThat(pluginConfigurationArgumentCaptor.getValue().isContinueOnPluginError())
.isEqualTo(true);
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8)).isEmpty();
}
@Test
public void shouldFailWhenPluginsIsDisabledAnHaltOnErrorTrue() {
parseCommand("--Xplugins-external-enabled=false", "--plugin-continue-on-error=true");
assertThat(commandOutput.toString(UTF_8)).isEmpty();
assertThat(commandErrorOutput.toString(UTF_8))
.contains(
"--plugins and --plugin-continue-on-error option can only be used when --Xplugins-external-enabled is true");
}
}

View File

@@ -247,4 +247,5 @@ Xevm-jumpdest-cache-weight-kb=32000
# plugins
Xplugins-external-enabled=true
plugins=["none"]
plugins=["none"]
plugin-continue-on-error=false

View File

@@ -23,14 +23,17 @@ public class PluginConfiguration {
private final List<PluginInfo> requestedPlugins;
private final Path pluginsDir;
private final boolean externalPluginsEnabled;
private final boolean continueOnPluginError;
public PluginConfiguration(
final List<PluginInfo> requestedPlugins,
final Path pluginsDir,
final boolean externalPluginsEnabled) {
final boolean externalPluginsEnabled,
final boolean continueOnPluginError) {
this.requestedPlugins = requestedPlugins;
this.pluginsDir = pluginsDir;
this.externalPluginsEnabled = externalPluginsEnabled;
this.continueOnPluginError = continueOnPluginError;
}
public List<String> getRequestedPlugins() {
@@ -47,6 +50,10 @@ public class PluginConfiguration {
return externalPluginsEnabled;
}
public boolean isContinueOnPluginError() {
return continueOnPluginError;
}
public static Path defaultPluginsDir() {
String pluginsDirProperty = System.getProperty("besu.plugins.dir");
return pluginsDirProperty == null
@@ -62,6 +69,7 @@ public class PluginConfiguration {
private List<PluginInfo> requestedPlugins;
private Path pluginsDir;
private boolean externalPluginsEnabled = true;
private boolean continueOnPluginError = false;
public Builder requestedPlugins(final List<PluginInfo> requestedPlugins) {
this.requestedPlugins = requestedPlugins;
@@ -78,11 +86,17 @@ public class PluginConfiguration {
return this;
}
public Builder continueOnPluginError(final boolean continueOnPluginError) {
this.continueOnPluginError = continueOnPluginError;
return this;
}
public PluginConfiguration build() {
if (pluginsDir == null) {
pluginsDir = PluginConfiguration.defaultPluginsDir();
}
return new PluginConfiguration(requestedPlugins, pluginsDir, externalPluginsEnabled);
return new PluginConfiguration(
requestedPlugins, pluginsDir, externalPluginsEnabled, continueOnPluginError);
}
}
}