Skip to content

Commit 455678e

Browse files
committed
Add rudimentary plugin guide
Relates to #1103
1 parent 99f44ec commit 455678e

File tree

7 files changed

+307
-3
lines changed

7 files changed

+307
-3
lines changed

docs/guides/trinity/architecture.rst

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ that all database reads and writes are done by a single process.
109109
Networking Process
110110
------------------
111111

112-
The networking process is what kicks of the peer to peer communication, starts the syncing
113-
process and also serves the JSON-RPC API. It does so by running an instance of
112+
The networking process is what kicks of the peer to peer communication and starts the syncing
113+
process. It does so by running an instance of
114114
:func:`~trinity.nodes.base.Node` in an event loop.
115115

116116
Notice that the instance of :func:`~trinity.nodes.base.Node` has access to the APIs that the
@@ -119,3 +119,14 @@ connections to other peers, starts of the syncing process but will call APIs tha
119119
the database processes when it comes to actual importing of blocks or reading and writing of other
120120
things from the database.
121121

122+
The networking process also host an instance of the
123+
:class:`~trinity.extensibility.plugin_manager.PluginManager` to run plugins that need to deeply
124+
integrate with the networking process (Further reading:
125+
:doc:`Writing Plugins</guides/trinity/writing_plugins>`).
126+
127+
Plugin Processes
128+
----------------
129+
130+
Apart from running these three core processes, there may be additional processes for plugins that
131+
run in isolated processes. Isolated plugins are explained in depth in the
132+
:doc:`Writing Plugins</guides/trinity/writing_plugins>` guide.

docs/guides/trinity/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ This section aims to provide hands-on guides to demonstrate how to use Trinity.
99
:caption: Guides
1010

1111
quickstart
12-
architecture
12+
architecture
13+
writing_plugins
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
Writing Plugins
2+
===============
3+
4+
Trinity aims to be a highly flexible Ethereum node to support lots of different use cases
5+
beyond just participating in the regular networking traffic.
6+
7+
To support this goal, Trinity allows developers to create plugins that hook into the system to
8+
extend its functionality. In fact, Trinity dogfoods its Plugin API in the sense that several
9+
built-in features are written as plugins that just happen to be shipped among the rest of the core
10+
modules. For instance, the JSON-RPC API, the Transaction Pool as well as the ``trinity attach``
11+
command that provides an interactive REPL with `Web3` integration are all built as plugins.
12+
13+
Trinity tries to follow the practice: If something can be written as a plugin, it should be written
14+
as a plugin.
15+
16+
17+
What can plugins do?
18+
~~~~~~~~~~~~~~~~~~~~
19+
20+
Plugin support in Trinity is still very new and the API hasn't stabilized yet. That said, plugins
21+
are already pretty powerful and are only becoming more so as the APIs of the underlying services
22+
improve over time.
23+
24+
Here's a list of functionality that is currently provided by plugins:
25+
26+
- JSON-RPC API
27+
- Transaction Pool
28+
- EthStats Reporting
29+
- Interactive REPL with Web3 integration
30+
- Crash Recovery Command
31+
32+
33+
Understanding the different plugin categories
34+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
35+
36+
There are currently three different types of plugins that we'll all cover in this guide.
37+
38+
- Plugins that overtake and redefine the entire ``trinity`` command
39+
- Plugins that spawn their own new isolated process
40+
- Plugins that run in the shared `networking` process
41+
42+
43+
44+
Plugins that redefine the Trinity process
45+
-----------------------------------------
46+
47+
This is the simplest category of plugins as it doesn't really *hook* into the Trinity process but
48+
hijacks it entirely instead. We may be left wonderering: Why would one want to do that?
49+
50+
The only reason to write such a plugin is to execute some code that we want to group under the
51+
``trinity`` command. A great example for such a plugin is the ``trinity attach`` command that gives
52+
us a REPL attached to a running Trinity instance. This plugin could have easily be written as a
53+
standalone program and associated with a command such as ``trinity-attach``. However, using a
54+
subcommand ``attach`` is the more idiomatic approach and this type of plugin gives us simple way
55+
to develop exactly that.
56+
57+
We build this kind of plugin by subclassing from
58+
:class:`~trinity.extensibility.plugin.BaseMainProcessPlugin`. A detailed example will follow soon.
59+
60+
61+
Plugins that spawn their own new isolated process
62+
-------------------------------------------------
63+
64+
Of course, if all what plugins could do is to hijack the `trinity` command, there wouldn't be
65+
much room to actually extend the *runtime functionality* of Trinity. If we want to create plugins
66+
that boot with and run alongside the main node activity, we need to write a different kind of
67+
plugin. These type of plugins can respond to events such as a peers connecting/disconnecting and
68+
can access information that is only available within the running application.
69+
70+
The JSON-RPC API is a great example as it exposes information such as the current count
71+
of connected peers which is live information that can only be accessed by talking to other parts
72+
of the application at runtime.
73+
74+
This is the default type of plugin we want to build if:
75+
76+
- we want to execute logic **together** with the command that boots Trinity (as opposed
77+
to executing it in a separate command)
78+
- we want to execute logic that integrates with parts of Trinity that can only be accessed at
79+
runtime (as opposed to e.g. just reading things from the database)
80+
81+
We build this kind of plugin subclassing from
82+
:class:`~trinity.extensibility.plugin.BaseIsolatedPlugin`. A detailed example will follow soon.
83+
84+
85+
Plugins that run inside the networking process
86+
----------------------------------------------
87+
88+
If the previous category sounded as if it could handle every possible use case, it's because it's
89+
actually meant to. In reality though, not all internal APIs yet work well across process
90+
boundaries. In practice, this means that sometimes we want to make sure that a plugin runs in the
91+
same process as the rest of the networking code.
92+
93+
.. warning::
94+
The need to run plugins in the networking process is declining as the internals of Trinity become
95+
more and more multi-process friendly over time. While it isn't entirely clear yet, there's a fair
96+
chance this type of plugin will become obsolete at some point and may eventually be removed.
97+
98+
We should only choose this type of plugin category if what we are trying to build cannot be built
99+
with a :class:`~trinity.extensibility.plugin.BaseIsolatedPlugin`.
100+
101+
We build this kind of plugin subclassing from
102+
:class:`~trinity.extensibility.plugin.BaseAsyncStopPlugin`. A detailed example will follow soon.
103+
104+
105+
The plugin lifecycle
106+
~~~~~~~~~~~~~~~~~~~~
107+
108+
Plugins can be in one of the following status at a time:
109+
110+
- ``NOT_READY``
111+
- ``READY``
112+
- ``STARTED``
113+
- ``STOPPED``
114+
115+
The current status of a plugin is also reflected in the
116+
:meth:`~trinity.extensibility.plugin.BasePlugin.status` property.
117+
118+
.. note::
119+
120+
Strictly speaking, there's also a special state that only applies to the
121+
:class:`~trinity.extensibility.plugin.BaseMainProcessPlugin` which comes into effect when such a
122+
plugin hijacks the Trinity process entirely. That being said, in that case, the resulting process
123+
is in fact something entirely different than Trinity and the whole plugin infrastruture doesn't
124+
even continue to exist from the moment on where that plugin takes over the Trinity process. This
125+
is why we do not list it as an actual state of the regular plugin lifecycle.
126+
127+
Plugin state: ``NOT_READY``
128+
---------------------------
129+
130+
Every plugin starts out being in the ``NOT_READY`` state. This state begins with the instantiation
131+
of the plugin and lasts until the :meth:`~trinity.extensibility.plugin.BasePlugin.on_ready` hook
132+
was called which happens as soon the core infrastructure of Trinity is ready.
133+
134+
Plugin state: ``READY``
135+
-----------------------
136+
137+
After Trinity has finished setting up the core infrastructure, every plugin has its
138+
:class:`~trinity.extensibility.plugin.PluginContext` set and
139+
:meth:`~trinity.extensibility.plugin.BasePlugin.on_ready` is called. At this point the plugin has
140+
access to important information such as the parsed arguments or the
141+
:class:`~trinity.config.TrinityConfig`. It also has access to the central event bus via its
142+
:meth:`~trinity.extensibility.plugin.BasePlugin.event_bus` property which enables the plugin to
143+
communicate with other parts of the application including other plugins.
144+
145+
Plugin state: ``STARTED``
146+
-------------------------
147+
148+
A plugin is in the ``STARTED`` state after the
149+
:meth:`~trinity.extensibility.plugin.BasePlugin.start` method was called. Plugins call this method
150+
themselves whenever they want to start which may be based on some condition like Trinity being
151+
started with certain parameters or some event being propagated on the central event bus.
152+
153+
.. note::
154+
Calling :meth:`~trinity.extensibility.plugin.BasePlugin.start` while the plugin is in the
155+
``NOT_READY`` state or when it is already in ``STARTED`` will cause an exception to be raised.
156+
157+
158+
Plugin state: ``STOPPED``
159+
-------------------------
160+
161+
A plugin is in the ``STOPPED`` state after the
162+
:meth:`~trinity.extensibility.plugin.BasePlugin.stop` method was called and finished any tear down
163+
work.
164+
165+
Defining plugins
166+
~~~~~~~~~~~~~~~~
167+
168+
We define a plugin by deriving from either
169+
:class:`~trinity.extensibility.plugin.BaseMainProcessPlugin`,
170+
:class:`~trinity.extensibility.plugin.BaseIsolatedPlugin` or
171+
:class:`~trinity.extensibility.plugin.BaseAsyncStopPlugin` depending on the kind of plugin that we
172+
intend to write. For now, we'll stick to :class:`~trinity.extensibility.plugin.BaseIsolatedPlugin`
173+
which is the most commonly used plugin category.
174+
175+
Every plugin needs to overwrite ``name`` so voilà, here's our first plugin!
176+
177+
.. literalinclude:: ../../../trinity/plugins/examples/peer_count_reporter/plugin.py
178+
:language: python
179+
:start-after: --START CLASS--
180+
:end-before: def configure_parser
181+
182+
Of course that doesn't do anything useful yet, bear with us.
183+
184+
Configuring Command Line Arguments
185+
----------------------------------
186+
187+
More often than not we want to have control over if or when a plugin should start. Adding
188+
command-line arguments that are specific to such a plugin, which we then check, validate, and act
189+
on, is a good way to deal with that. Implementing
190+
:meth:`~trinity.extensibility.plugin.BasePlugin.configure_parser` enables us to do exactly that.
191+
192+
This method is called when Trinity starts and bootstraps the plugin system, in other words,
193+
**before** the start of any plugin. It is passed a :class:`~argparse.ArgumentParser` as well as a
194+
:class:`~argparse._SubParsersAction` which allows it to amend the configuration of Trinity's
195+
command line arguments in many different ways.
196+
197+
For example, here we are adding a boolean flag ``--report-peer-count`` to Trinity.
198+
199+
.. literalinclude:: ../../../trinity/plugins/examples/peer_count_reporter/plugin.py
200+
:language: python
201+
:pyobject: PeerCountReporterPlugin.configure_parser
202+
203+
To be clear, this does not yet cause our plugin to automatically start if ``--report-peer-count``
204+
is passed, it simply changes the parser to be aware of such flag and hence allows us to check for
205+
its existence later.
206+
207+
.. note::
208+
209+
For a more advanced example, that also configures a subcommand, checkout the ``trinity attach``
210+
plugin.
211+
212+
213+
Defining a plugins starting point
214+
---------------------------------
215+
216+
Every plugin needs to have a well defined starting point. The exact mechanics slightly differ
217+
in case of a :class:`~trinity.extensibility.plugin.BaseMainProcessPlugin` but remain fairly similar
218+
for the other types of plugins which we'll be focussing on for now.
219+
220+
Plugins need to implement the :meth:`~trinity.extensibility.plugin.BasePlugin.do_start` method
221+
to define their own bootstrapping logic. This logic may involve setting up event listeners, running
222+
code in a loop or any other kind of action.
223+
224+
.. warning::
225+
226+
Technically, there's nothing preventing a plugin from performing starting logic in the
227+
:meth:`~trinity.extensibility.plugin.BasePlugin.on_ready` hook. However, doing that is an anti
228+
pattern as the plugin infrastructure won't know about the running plugin, can't propagate the
229+
:class:`~trinity.extensibility.events.PluginStartedEvent` and the plugin won't be properly shut
230+
down with Trinity if the node closes.
231+
232+
Causing a plugin to start
233+
-------------------------
234+
235+
As we've read in the previous section not all plugins should run at any point in time. In fact, the
236+
circumstances under which we want a plugin to begin its work may vary from plugin to plugin.
237+
238+
We may want a plugin to only start running if:
239+
240+
- a certain (combination) of command line arguments was given
241+
- another plugin or group of plugins started
242+
- a certain number of connected peers was exceeded / undershot
243+
- a certain block number was reached
244+
- ...
245+
246+
Hence, to actually start a plugin, the plugin needs to invoke the
247+
:meth:`~trinity.extensibility.plugin.BasePlugin.start` method at any moment when it is in its
248+
``READY`` state.
249+
250+
Communication pattern
251+
~~~~~~~~~~~~~~~~~~~~~
252+
253+
Coming soon: Spoiler: Plugins can communicate with other parts of the application or even other
254+
plugins via the central event bus.
255+
256+
Making plugins discoverable
257+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
258+
259+
Coming soon.
260+
261+
.. warning::
262+
**Wait?! This is it? No! This is draft version of the plugin guide as small DEVCON IV gitft.
263+
This will turn into a much more detailed guide shortly after the devcon craze is over.**
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from trinity.plugins.examples import (
2+
PeerCountReporterPlugin,
3+
)
4+
5+
6+
def test_can_instantiate_examples():
7+
plugin = PeerCountReporterPlugin()
8+
assert plugin.name == "Peer Count Reporter"

trinity/plugins/examples/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .peer_count_reporter.plugin import PeerCountReporterPlugin # noqa: F401

trinity/plugins/examples/peer_count_reporter/__init__.py

Whitespace-only changes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# This might end up as a temporary place for this. The following code is
2+
# included in the documentation (literalinclude!) and uses a more concise
3+
# form of imports.
4+
5+
from argparse import ArgumentParser, _SubParsersAction
6+
7+
from trinity.extensibility import BaseIsolatedPlugin
8+
9+
10+
# --START CLASS--
11+
class PeerCountReporterPlugin(BaseIsolatedPlugin):
12+
13+
@property
14+
def name(self) -> str:
15+
return "Peer Count Reporter"
16+
17+
def configure_parser(self,
18+
arg_parser: ArgumentParser,
19+
subparser: _SubParsersAction) -> None:
20+
arg_parser.add_argument("--report-peer-count", type=bool, required=False)

0 commit comments

Comments
 (0)