Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

Plugin Framework #1435

Merged
merged 22 commits into from
May 17, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
49ebfc3
rough implementation
shemnon May 10, 2019
0dead54
stop
shemnon May 10, 2019
fb486a2
Merge branch 'master' into plugin_framework
shemnon May 12, 2019
4f0b4ef
Merge branch 'master' of github.com:PegaSysEng/pantheon into plugin_f…
shemnon May 13, 2019
6dfb20a
Merge branch 'master' of github.com:PegaSysEng/pantheon into plugin_f…
shemnon May 14, 2019
613705b
Demo plugin with some error handling
shemnon May 14, 2019
ce7928b
Merge remote-tracking branch 'origin/plugin_framework' into plugin_fr…
shemnon May 14, 2019
042150d
acceptance test for process execution only
shemnon May 15, 2019
3ae2fce
spotless
shemnon May 15, 2019
b9b5e46
jenkins debug
shemnon May 15, 2019
3afabf6
more jenkins debugging
shemnon May 15, 2019
efeabf7
a different jenkins approach, SourceSets
shemnon May 15, 2019
4640c8c
spotless
shemnon May 16, 2019
94f0ba8
Merge branch 'master' into plugin_framework
shemnon May 16, 2019
92b383b
Update plugins/src/main/java/tech/pegasys/pantheon/plugins/internal/P…
shemnon May 16, 2019
581c842
CLI tests
shemnon May 16, 2019
e5a4583
Merge branch 'master' of github.com:PegaSysEng/pantheon into plugin_f…
shemnon May 16, 2019
ebd4246
update to properties cleanup
shemnon May 16, 2019
a79fb04
Update acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acc…
shemnon May 16, 2019
9c96867
Merge branch 'master' of github.com:PegaSysEng/pantheon into plugin_f…
shemnon May 17, 2019
2c81b2b
Merge branch 'master' of github.com:PegaSysEng/pantheon into plugin_f…
shemnon May 17, 2019
b0d0774
spotless
shemnon May 17, 2019
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
Next Next commit
rough implementation
  • Loading branch information
shemnon committed May 10, 2019
commit 49ebfc376eb2c6b2737585faf84ac237d377d463
3 changes: 2 additions & 1 deletion pantheon/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies {
implementation project(':consensus:clique')
implementation project(':consensus:ibft')
implementation project(':consensus:ibftlegacy')
implementation project(':enclave')
implementation project(':ethereum:blockcreation')
implementation project(':ethereum:core')
implementation project(':ethereum:eth')
Expand All @@ -40,8 +41,8 @@ dependencies {
implementation project(':ethereum:permissioning')
implementation project(':ethereum:p2p')
implementation project(':ethereum:rlp')
implementation project(':plugins')
implementation project(':metrics:core')
implementation project(':enclave')
implementation project(':services:kvstore')

implementation 'com.graphql-java:graphql-java'
Expand Down
4 changes: 3 additions & 1 deletion pantheon/src/main/java/tech/pegasys/pantheon/Pantheon.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import tech.pegasys.pantheon.controller.PantheonController;
import tech.pegasys.pantheon.ethereum.eth.EthereumWireProtocolConfiguration;
import tech.pegasys.pantheon.ethereum.eth.sync.SynchronizerConfiguration;
import tech.pegasys.pantheon.plugins.internal.PantheonPluginContextImpl;
import tech.pegasys.pantheon.services.kvstore.RocksDbConfiguration;
import tech.pegasys.pantheon.util.BlockImporter;

Expand All @@ -37,7 +38,8 @@ public static void main(final String... args) {
new PantheonController.Builder(),
new SynchronizerConfiguration.Builder(),
EthereumWireProtocolConfiguration.builder(),
new RocksDbConfiguration.Builder());
new RocksDbConfiguration.Builder(),
new PantheonPluginContextImpl());

pantheonCommand.parse(
new RunLast().andExit(SUCCESS_EXIT_CODE),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.PrometheusMetricsSystem;
import tech.pegasys.pantheon.metrics.vertx.VertxMetricsAdapterFactory;
import tech.pegasys.pantheon.plugins.internal.PantheonPluginContextImpl;
import tech.pegasys.pantheon.plugins.services.PicoCLIOptions;
import tech.pegasys.pantheon.services.kvstore.RocksDbConfiguration;
import tech.pegasys.pantheon.util.BlockImporter;
import tech.pegasys.pantheon.util.InvalidConfigurationException;
Expand Down Expand Up @@ -134,6 +136,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable {
private final RocksDbConfiguration.Builder rocksDbConfigurationBuilder;
private final RunnerBuilder runnerBuilder;
private final PantheonController.Builder controllerBuilderFactory;
private final PantheonPluginContextImpl pantheonPluginContext;

protected KeyLoader getKeyLoader() {
return KeyPairUtil::loadKeyPair;
Expand Down Expand Up @@ -564,14 +567,16 @@ public PantheonCommand(
final PantheonController.Builder controllerBuilderFactory,
final SynchronizerConfiguration.Builder synchronizerConfigurationBuilder,
final EthereumWireProtocolConfiguration.Builder ethereumWireConfigurationBuilder,
final RocksDbConfiguration.Builder rocksDbConfigurationBuilder) {
final RocksDbConfiguration.Builder rocksDbConfigurationBuilder,
final PantheonPluginContextImpl pantheonPluginContext) {
this.logger = logger;
this.blockImporter = blockImporter;
this.runnerBuilder = runnerBuilder;
this.controllerBuilderFactory = controllerBuilderFactory;
this.synchronizerConfigurationBuilder = synchronizerConfigurationBuilder;
this.ethereumWireConfigurationBuilder = ethereumWireConfigurationBuilder;
this.rocksDbConfigurationBuilder = rocksDbConfigurationBuilder;
this.pantheonPluginContext = pantheonPluginContext;
}

private StandaloneCommand standaloneCommands;
Expand Down Expand Up @@ -621,6 +626,13 @@ public void parse(
"Ethereum Wire Protocol",
ethereumWireConfigurationBuilder));

pantheonPluginContext.addService(
PicoCLIOptions.class,
(PicoCLIOptions)
(namespace, optionObject) ->
commandLine.addMixin("pantheonplugin-" + namespace, optionObject));
pantheonPluginContext.registerPlugins();

// Create a handler that will search for a config file option and use it for
// default values
// and eventually it will run regular parsing of the remaining options.
Expand Down Expand Up @@ -697,6 +709,8 @@ public void run() {
ensureAllNodesAreInWhitelist(
staticNodes.stream().map(EnodeURL::toURI).collect(Collectors.toList()), p));

pantheonPluginContext.startPlugins();

synchronize(
buildController(),
p2pEnabled,
Expand Down Expand Up @@ -1059,8 +1073,8 @@ private void addShutdownHook(final Runner runner) {
new Thread(
() -> {
try {
pantheonPluginContext.stopPlugins();
runner.close();

LogManager.shutdown();
} catch (final Exception e) {
logger.error("Failed to stop Pantheon");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration;
import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration;
import tech.pegasys.pantheon.metrics.prometheus.MetricsConfiguration;
import tech.pegasys.pantheon.plugins.internal.PantheonPluginContextImpl;
import tech.pegasys.pantheon.services.kvstore.RocksDbConfiguration;
import tech.pegasys.pantheon.util.BlockImporter;

Expand Down Expand Up @@ -84,6 +85,7 @@ public abstract class CommandTestAbstract {
@Mock PantheonController<Object> mockController;
@Mock BlockImporter mockBlockImporter;
@Mock Logger mockLogger;
@Mock PantheonPluginContextImpl mockPantheonPluginContext;

@Captor ArgumentCaptor<Collection<String>> stringListArgumentCaptor;
@Captor ArgumentCaptor<Path> pathArgumentCaptor;
Expand Down Expand Up @@ -195,7 +197,8 @@ private CommandLine.Model.CommandSpec parseCommand(
mockSyncConfBuilder,
mockEthereumWireProtocolConfigurationBuilder,
mockRocksDbConfBuilder,
keyLoader);
keyLoader,
mockPantheonPluginContext);

// parse using Ansi.OFF to be able to assert on non formatted output results
pantheonCommand.parse(
Expand Down Expand Up @@ -224,15 +227,17 @@ protected KeyLoader getKeyLoader() {
final SynchronizerConfiguration.Builder mockSyncConfBuilder,
final EthereumWireProtocolConfiguration.Builder mockEthereumConfigurationMockBuilder,
final RocksDbConfiguration.Builder mockRocksDbConfBuilder,
final KeyLoader keyLoader) {
final KeyLoader keyLoader,
final PantheonPluginContextImpl pantheonPluginContext) {
super(
mockLogger,
mockBlockImporter,
mockRunnerBuilder,
controllerBuilderFactory,
mockSyncConfBuilder,
mockEthereumConfigurationMockBuilder,
mockRocksDbConfBuilder);
mockRocksDbConfBuilder,
pantheonPluginContext);
this.keyLoader = keyLoader;
}
}
Expand Down
28 changes: 28 additions & 0 deletions plugins/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2018 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.
*/

apply plugin: 'java-library'

jar {
baseName 'pantheon-plugins'
manifest {
attributes(
'Specification-Title': baseName,
'Specification-Version': project.version,
'Implementation-Title': baseName,
'Implementation-Version': calculateVersion()
)
}
}

dependencies { implementation('com.google.guava:guava') }
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2019 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.
*/
package tech.pegasys.pantheon.plugins;

import java.util.Optional;

public interface PantheonContext {

<T> Optional<T> getService(Class<T> serviceType);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious - why go with this generic pattern rather than just explicitly exposing some services like registerCLIOptions, getPantheonEvents, etc? Is the idea that this makes it easier for us to remove services in the future?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also wondering if we shouldn't just return the service class directly rather than wrapping in it in an Optional? And in the case where a plugin asks for a service that isn't registered, we could just throw an exception and disable that plugin. Seems safer to disable plugins that might be missing required services.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so we can expose more services later and not have to mutate the API. In a future revision the plugins themselves could expose services.

The Optional is there in case plugins want to do graceful degraded service. A plugin may want a service but not require it. For example a plugin that interacts with consensus engines would only expect one to be operating but would go down the list of possible engines that expose services to the plugin architecture.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2019 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.
*/
package tech.pegasys.pantheon.plugins;

public interface PantheonPlugin {

void register(PantheonContext context);

void start();

void stop();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2019 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.
*/
package tech.pegasys.pantheon.plugins.internal;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;

import tech.pegasys.pantheon.plugins.PantheonContext;
import tech.pegasys.pantheon.plugins.PantheonPlugin;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;

public class PantheonPluginContextImpl implements PantheonContext {

private enum Lifecycle {
UNINITIALZED,
REGISTERING,
REGISTERED,
STARTING,
STARTED,
STOPPING,
STOPPED
}

private Lifecycle state = Lifecycle.UNINITIALZED;
private final Map<Class<?>, ? super Object> serviceRegistry = new HashMap<>();
private ServiceLoader<PantheonPlugin> plugins;

public void addService(final Class<?> serviceType, final Object service) {
checkArgument(serviceType.isInterface(), "Services must be Java interfaces.");
checkArgument(
serviceType.isInstance(service),
"The service registered with a type must implement that type");
serviceRegistry.put(serviceType, service);
}

@SuppressWarnings("unchecked")
@Override
public <T> Optional<T> getService(final Class<T> serviceType) {
return (Optional<T>) Optional.ofNullable((Map<Class<T>, T>) serviceRegistry.get(serviceType));
}

public void registerPlugins() {
checkState(state == Lifecycle.UNINITIALZED, "PantheonContext has already beein registered.");
state = Lifecycle.REGISTERING;
plugins = ServiceLoader.load(PantheonPlugin.class);
for (final PantheonPlugin plugin : plugins) {
plugin.register(this);
}
state = Lifecycle.REGISTERED;
}

public void startPlugins() {
checkState(
state == Lifecycle.REGISTERED,
"PantheonContext should be in state %s but it was in %s",
Lifecycle.REGISTERED,
state);
state = Lifecycle.STARTING;
plugins = ServiceLoader.load(PantheonPlugin.class);
for (final PantheonPlugin plugin : plugins) {
plugin.start();
}
state = Lifecycle.STARTED;
}

public void stopPlugins() {
checkState(
state == Lifecycle.STARTED,
"PantheonContext should be in state %s but it was in %s",
Lifecycle.STARTED,
state);
state = Lifecycle.STARTING;
plugins = ServiceLoader.load(PantheonPlugin.class);
for (final PantheonPlugin plugin : plugins) {
plugin.stop();
}
state = Lifecycle.STARTED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** This package will be hidden from external users once Pantheon migrates to Java 11. */
package tech.pegasys.pantheon.plugins.internal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2019 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.
*/
package tech.pegasys.pantheon.plugins.services;

import java.util.function.Consumer;

public interface PantheonEvents {

/**
* Returns the raw RLP of a block that Pantheon has receieved and that has passed basic validation
* checks.
*
* @param blockJSONListener The listener that will accept a JSON string as the event.
* @return an object to be used as an identifier when de-registering the event.
*/
Object addBlockAddedListener(Consumer<String> blockJSONListener);

/**
* Remove the blockAdded listener from pantheon notifications.
*
* @param listenerIdentifier The instance that was returned from addBlockAddedListener;
*/
void removeBlockAddedObserver(Object listenerIdentifier);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2019 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.
*/
package tech.pegasys.pantheon.plugins.services;

/** This service will be available during the registration callbacks. */
public interface PicoCLIOptions {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking my understanding here. Seems like the idea in tying this class to PicoCLI rather than making it something more generic like CLIOptions is that in the future we might use a different CLI system, that will be supported by a completely different service / api?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, strongly coupled to Pico cli library.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, graceful migration.


/**
* During the registration callback plugins can register CLI options that should be added to
* Pantheon's CLI startup.
*
* @param namespace A namespace prefix. All registered options must start with this prefix
* @param optionObject The instance of the object to be inspected. PicoCLI will reflect the fields
* of this object to extract the CLI options.
*/
void addPicoCLIOptions(String namespace, Object optionObject);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
void addPicoCLIOptions(String namespace, Object optionObject);
void addCLIOptions(String namespace, Object optionObject);

(optional) We could still probably make the method name more generic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it is very tied to PicoCLI's style I feel we should communicate that wherever possible.

}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ include 'ethereum:trie'
include 'metrics:core'
include 'metrics:rocksdb'
include 'pantheon'
include 'plugins'
include 'services:kvstore'
include 'services:pipeline'
include 'services:tasks'
Expand Down