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

Cleanup and define consistent pattern for leap plugin startup/shutdown. #1231

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ee4e4b0
use appbase branch which calls `plugin_shutdown()` on exception.
greg7mdp May 25, 2023
ee29899
update appbase to new tip
greg7mdp May 25, 2023
3812d5b
Do not destroy `chain::controller` on `plugin_shutdown()`.
greg7mdp May 26, 2023
3210dfe
No need to catch exceptions to make sure `plugin_shutdown()` is execu…
greg7mdp May 26, 2023
81e5a81
Whitespace fix
greg7mdp May 26, 2023
a2157c4
In `plugin_startup()`, in case of error, better `throw` than call `ap…
greg7mdp May 26, 2023
97aa420
Merge branch 'main' of github.com:AntelopeIO/leap into gh-672
greg7mdp May 30, 2023
dc72da4
Remove unnecessary (now) try/catch block calling `plugin_shutdown()`
greg7mdp Jun 1, 2023
a35e793
Merge branch 'main' of github.com:AntelopeIO/leap into gh-672
greg7mdp Jun 2, 2023
a4f54a2
Add `usage_patterns.md` file
greg7mdp Jun 5, 2023
2bd8818
Merge branch 'main' of github.com:AntelopeIO/leap into gh-672
greg7mdp Jun 5, 2023
4869d63
Merge branch 'main' of github.com:AntelopeIO/leap into gh-672
greg7mdp Jun 5, 2023
4224df0
Update `usage_pattern.md`
greg7mdp Jun 5, 2023
48be938
fix typo
greg7mdp Jun 5, 2023
839f5c9
Merge branch 'main' of github.com:AntelopeIO/leap into gh-672
greg7mdp Jun 8, 2023
727175d
Fix merge issue
greg7mdp Jun 8, 2023
42ca516
Merge branch 'main' of github.com:AntelopeIO/leap into gh-672
greg7mdp Jun 12, 2023
5291108
Fix incorrect merge
greg7mdp Jun 12, 2023
ce96bf9
Merge branch 'main' of github.com:AntelopeIO/leap into gh-672
greg7mdp Jun 12, 2023
64ef9ea
Remove unneeded semicolumn
greg7mdp Jun 13, 2023
42e8824
Merge branch 'main' of github.com:AntelopeIO/leap into gh-672
greg7mdp Jun 14, 2023
2ac21bc
Merge branch 'main' of github.com:AntelopeIO/leap into gh-672
greg7mdp Jun 15, 2023
153ff89
Don't throw when `app().quit()` called during plugin_startup.
greg7mdp Jun 15, 2023
b45ef19
Update appbase to branch tip
greg7mdp Jun 15, 2023
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
8 changes: 1 addition & 7 deletions plugins/net_plugin/net_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4121,13 +4121,7 @@ namespace eosio {
}

void net_plugin::plugin_startup() {
try {
my->plugin_startup();
} catch( ... ) {
// always want plugin_shutdown even on exception
plugin_shutdown();
throw;
}
my->plugin_startup();
}


Expand Down
8 changes: 1 addition & 7 deletions plugins/producer_plugin/producer_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,7 @@ void producer_plugin_impl::plugin_initialize(const boost::program_options::varia
_options = &options;
LOAD_VALUE_SET(options, "producer-name", _producers)

chain::controller& chain = chain_plug->chain();
chain::controller& chain = chain_plug->chain();

if (options.count("signature-provider")) {
const std::vector<std::string> key_spec_pairs = options["signature-provider"].as<std::vector<std::string>>();
Expand Down Expand Up @@ -1277,7 +1277,6 @@ void producer_plugin::plugin_initialize(const boost::program_options::variables_
using namespace std::chrono_literals;
void producer_plugin_impl::plugin_startup() {
try {
try {
ilog("producer plugin: plugin_startup() begin");

_thread_pool.start(_thread_pool_size, [](const fc::exception& e) {
Expand Down Expand Up @@ -1345,11 +1344,6 @@ void producer_plugin_impl::plugin_startup() {
schedule_production_loop();

ilog("producer plugin: plugin_startup() end");
} catch (...) {
// always call plugin_shutdown, even on exception
plugin_shutdown();
throw;
}
}
FC_CAPTURE_AND_RETHROW()
}
Expand Down
30 changes: 13 additions & 17 deletions plugins/state_history_plugin/state_history_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -389,25 +389,21 @@ void state_history_plugin::plugin_initialize(const variables_map& options) {
handle_sighup(); // setup logging
my->plugin_initialize(options);
}

void state_history_plugin_impl::plugin_startup() {
try {
auto bsp = chain_plug->chain().head_block_state();
if( bsp && chain_state_log && chain_state_log->empty() ) {
fc_ilog( _log, "Storing initial state on startup, this can take a considerable amount of time" );
store_chain_state( bsp );
fc_ilog( _log, "Done storing initial state on startup" );
}
listen();
// use of executor assumes only one thread
thread_pool.start( 1, [](const fc::exception& e) {
fc_elog( _log, "Exception in SHiP thread pool, exiting: ${e}", ("e", e.to_detail_string()) );
app().quit();
});
plugin_started = true;
} catch (std::exception& ex) {
appbase::app().quit();
auto bsp = chain_plug->chain().head_block_state();
if (bsp && chain_state_log && chain_state_log->empty()) {
fc_ilog(_log, "Storing initial state on startup, this can take a considerable amount of time");
store_chain_state(bsp);
fc_ilog(_log, "Done storing initial state on startup");
}
listen();
// use of executor assumes only one thread
thread_pool.start(1, [](const fc::exception& e) {
fc_elog(_log, "Exception in SHiP thread pool, exiting: ${e}", ("e", e.to_detail_string()));
app().quit();
});
plugin_started = true;
}

void state_history_plugin::plugin_startup() {
Expand Down
2 changes: 1 addition & 1 deletion plugins/trace_api_plugin/trace_api_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ struct trace_api_common_impl {
}

void plugin_startup() {
store->start_maintenance_thread([](const std::string& msg ){
store->start_maintenance_thread([](const std::string& msg) {
fc_dlog( _log, msg );
});
}
Expand Down
199 changes: 199 additions & 0 deletions plugins/usage_pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
## Rules and considerations for writing an appbase application

### Appbase application lifetime

#### Plugin registration

* Register plugins by calling `appbase::application::register_plugin<plugin_name>();`. This needs to be done only once per executable.
* These plugins, and their dependents, will get registered when `initialize` is called on `appbase::scoped_app`
* plugin registration is required for every plugin we want to use.

#### Plugin initialization

* Precondition: plugins must have been registered
* Happens when `appbase::scoped_app::initialize(argc, argv)` is called
* Plugins to be initialized are those passed as options (using the `--plugin` command line option) or as template parameters to the `initialize()` call, as well as their dependents.
* if we try to initialize an unregistered plugin, we get an exception (`std::exception::what: unable to find plugin`).
* initialization order:
- for each plugin specified in command line, initialize its dependents (depth first) and then itself
- for each plugin specified as template parameter, initialize its dependents (depth first) and then itself
* Plugin state is updated to `initialized` *before* its descendents and itself are initialized.
So if there was a dependency cycle, the same plugin would not be initialized twice.
* After the plugin initialization, `app().plugin_initialized(this);` is called which allows appbase to add it to its list of initialized plugins.


#### Application initialization

Currently, `appbase::initialize()` can:

1. either return `false`
- if a specified option like `--help` or `--version` is specified, which does not require to continue the program execution
- if a configuration error is detected (for ex `--logconf` specified dir missing, `--config` file missing)
2. or throw in case of failure
- if any plugin throws during `plugin_initialize`, `appbase::initialize()` logs the error and rethrows the exception
3. or return `true` is initialization was successful and the application should proceed.


So an application should exit cleanly in cases 1 and 2 (without calling `app->startup();` or `app->exec();`), and continue in case 3.

Unfortunately, when `appbase::initialize()` returns false, it is not clear whether it indicates a normal exit or an error, so for example in `nodeos` we do:


```c++
try {
...
if(!app->initialize<chain_plugin, net_plugin, producer_plugin, resource_monitor_plugin>(argc, argv, initialize_logging)) {
const auto& opts = app->get_options();
if( opts.count("help") || opts.count("version") || opts.count("full-version") || opts.count("print-default-config") ) {
return SUCCESS;
}
return INITIALIZE_FAIL;
}
...
app->startup();
app->exec();
}
// other catch blocks
} catch( ... ) {
elog("unknown exception");
return OTHER_FAIL;
}
```

> Note that in the code above, we check the return value of `app->initialize()` (as we should) and we have a try/catch block in case `app->initialize()` (or any of the plugins) throws an exception

#### Application Startup

This is when `appbase::scoped_app::startup()` is called. Besides starting a thread for catching signals, the main task is to call `startup() on all initialized plugins.


```c++
try {
for( auto plugin : initialized_plugins ) {
if( is_quiting() ) break;
plugin->startup();
}

} catch( ... ) {
clean_up_signal_thread();
shutdown();
throw;
}
```

If any plugin throws during plugin_startup, the startup process stops and an orderly shutdown is initiated, where all started plugins (including the one that threw) will be invoked with plugin_shutdown. After the orderly shutdown of all started plugins, the exception is rethrown, and it is expected that this exception will cause the application to terminate without calling `app->exec()`.

If a plugin calls `app().quit()` during plugin_startup (not recommended), the appbase framework will detect it and throw an exception, so we fall back to the same processing as in the above paragraph.

> We note that plugins are also started in a similar way as C++ objects are constructed, where the dependent plugins are started first, in a depth first fashion. Just like C++, shutdown proceeds in the reverse order.


## Rules and considerations for writing plugins


### Initialize

#### Typical actions in `plugin_initialize`

- get references (using `app().find_plugin<name>()`) to other plugins you depend on, and maybe store a reference to them.
Those dependencies should have been specified in the plugin class definition using `APPBASE_PLUGIN_REQUIRES`
- use the provided program options to configure the plugin
- possibly read additional configuration from files
- create new objects, directories, files, etc...
critically any deallocation/cleanup of these (or other objects created during the plugin execution) should be done in the plugin destructor, not in `plugin_shutdown()`.
- connect to signals from other plugins (ex: `chain.applied_transaction.connect([this](...) { ... });`)
- register provider methods (that others can connect to via signals).
Ex: `app().get_method<incoming::methods::transaction_async>().register_provider([this](...) { ... });`
- relay signals to channels.
Ex: `pre_accepted_block_connection = chain->pre_accepted_block.connect([this](...) { ...; pre_accepted_block_channel.publish(...); });`

#### Rules to follow in `plugin_initialize`

- in case of irrecoverable error, appropriate action is to throw an exception (possibly after logging the error).
This will cause the application to terminate.
- `io_context`s should not be running, and threads should not be started that would run on any `io_context` before `plugin_startup`.
- plugins should not post any action involving callbacks (timer, io, etc), either on the main appbbase thread or on plugin-specific thread pools, during `plugin_initialize`.

#### Guarantees provided by appbase to `plugin_initialize`

- dependent plugins (as specified by `APPBASE_PLUGIN_REQUIRES`) are always initialized before the plugins that depends on them.
- if any plugin throws during `plugin_initialize`, the plugin initialization process stops immediately, an error is logged, and the exception is rethrown. `plugin_shutdown` is not called for any plugin, the expectation being that any object or resource that was allocated during `plugin_initialize` will be freed when the plugin destructor is executed.



### Startup

`plugin_startup()` is intended to allow the plugins to activate their `io_context`s, thread pools, timers, establish connections with dependent plugins, and start the intended processing of the plugin, in order to make sure that all the resources necessary are available. If critical resources required for the application correct functioning are not available, the plugin should throw an exception.

Ideally, all resource allocation is completed before `plugin_startup` returns.

If the plugin was to start a thread, which allocates resources upon startup, this could trigger an exception while another plugin is executing `plugin_startup`, making the log harder to decipher as it would intermingle messages from different sources.

> in some case, a delayed startup is desired, because we would like other plugins to finish their startup before we start our own services (for example the http_plugin may want to start listening for API requests only after the blockchain replay is completed). One way to achieve this is to post a lambda implementing the plugin startup on the appbase queue, as in: `app().executor().post(appbase::priority::high, [this] () { ... });`

#### Typical actions in `plugin_startup`

Example of actions in `plugin_startup`:

- an `io_context`, task queue and a main thread is provided by appbase, and plugins can schedule tasks to be executed on it using `app().executor().post()`. However, if necessary, plugins can create their own `io_context` and thread pools, and `plugin_startup()` is an appropriate time to activate these.
- if the plugin starts its own thread pool, and these threads allocate resources upon startup, it is recommented for the plugin to wait till all the threads are ready to accept work (using a synchronization primitive such as a `condition_variable`) before returning from `plugin_startup`.
- a plugin which communicates with other nodes may want to open network connections, or start listening for incoming connections.


#### Rules to follow in `plugin_startup`

- in case of an error during `plugin_startup()`, the plugin should throw, not call `app().quit()`
- any code in `plugin_shutdown()` should not depend on `plugin_startup` having fully executed.

#### Guarantees provided by appbase to `plugin_startup`

- `plugin_startup()` is called in a depth-first fashion, according to the stated dependency graph specified by `APPBASE_PLUGIN_REQUIRES`.
- `plugin_startup()` is called within a try/catch block.
- If an exception is thrown while `plugin_startup()` is executed on any plugin, `plugin_shutdown()` is called on it immediately, and an application shutdown is triggerred.
- but if the plugin does call `app().quit()` during `plugin_startup()`, the framework will throw, ensuring no other plugin is started and application terminates cleanly.
- Before `plugin_startup()` is called, the plugin is added to the list of running plugins, which ensures that `plugin_shutdown()` will be called on it (either at application shutdown, or in case of an exception occuring before that)
- because `plugin_shutdown()` will be executed regardless of whether `plugin_startup()` fully (or at all) executed, it is critical that `plugin_shutdown()`'s actions do not require or expect anything from `plugin_startup()`. For example, calling `cancel()` on a timer that has not been `async_wait`'ed on is fine.



### Shutdown

#### Typical actions in `plugin_shutdown`

`plugin_shutdown()` is intended to allow the plugins to terminate its processing, for example:

- cancel timers (typically `boost::asio::steady_timer` or `boost::asio::deadline_timer`
- stop thread pools (stop the `io_context`, join all threads)
- disconnect from other plugins if needed (this is probably not needed).

#### Rules to follow in `plugin_shutdown`

- plugin created objects, or the plugin itself, should *not* be destroyed during `plugin_shutdown()`.
This must be done in the plugin destructor.

#### Guarantees provided by appbase to `plugin_shutdown`

- `plugin_shutdown()` is called within a try/catch block for each running plugin, starting from the last registered first.
This means that `plugin_shutdown()` is called on the inverse order of `plugin_startup()`,
- If an exception is thrown during `plugin_shutdown()`, it is logged on `std::cerr`, but doesn't prevent `plugin_shutdown()` from being called on the remaining running plugins.
- The appbase framework will destruct the running plugins in a second phase of shutdown (last one added to the running list is destructed first).


## Action items

1. If a plugin (or one of its dependents) throws during `startup()`, `plugin_shutdown()` should be called on it.
=> update producer_plugin and other plugins which may (needlessly) call `plugin_shutdown()` when there is an exception in `plugin_startup()`

```
**done** [appbase 65bb056] ensure plugin_shutdown called when plugin_shutdown throws.
[leap/gh-672 3210dfe48] net_plugin: no need to catch exceptions to make sure `plugin_shutdown()` is executed.
[leap/gh-672 dc72da4e5] producer_plugin: remove unnecessary try/catch block calling `plugin_shutdown()`
```

2. many appbase apps do not check the return value of `app->initialize()` or catch exceptions.

3. have `app->initialize()` return `enum class result { all_done, success, failure }`, so an application knows whether to just return or continue with `app->startup();`.