Skip to content

Commit 7b37d91

Browse files
committed
Elaborate further in plugin guide
1 parent 5307ef6 commit 7b37d91

File tree

3 files changed

+136
-17
lines changed

3 files changed

+136
-17
lines changed

docs/guides/trinity/writing_plugins.rst

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ Every plugin needs to overwrite ``name`` so voilà, here's our first plugin!
176176

177177
.. literalinclude:: ../../../trinity/plugins/examples/peer_count_reporter/plugin.py
178178
:language: python
179-
:start-after: --START CLASS--
179+
:pyobject: PeerCountReporterPlugin
180180
:end-before: def configure_parser
181181

182182
Of course that doesn't do anything useful yet, bear with us.
@@ -229,8 +229,30 @@ code in a loop or any other kind of action.
229229
:class:`~trinity.extensibility.events.PluginStartedEvent` and the plugin won't be properly shut
230230
down with Trinity if the node closes.
231231

232-
Causing a plugin to start
233-
-------------------------
232+
Let's assume we want to create a plugin that simply periodically prints out the number of connected
233+
peers.
234+
235+
While it is absolutely possible to put this logic right into the plugin, the preferred way is to
236+
subclass :class:`~p2p.service.BaseService` and implement the core logic in such a standalone
237+
service.
238+
239+
.. literalinclude:: ../../../trinity/plugins/examples/peer_count_reporter/plugin.py
240+
:language: python
241+
:pyobject: PeerCountReporter
242+
243+
Then, the implementation of :meth:`~trinity.extensibility.plugin.BaseIsolatedPlugin.do_start` is
244+
only concerned about running the service on a fresh event loop.
245+
246+
.. literalinclude:: ../../../trinity/plugins/examples/peer_count_reporter/plugin.py
247+
:language: python
248+
:pyobject: PeerCountReporterPlugin.do_start
249+
250+
If the example may seem unnecessarily complex, it should be noted that plugins can be implemented
251+
in many different ways, but this example follows a pattern that is considered best practice within
252+
the Trinity Code Base.
253+
254+
Starting a plugin
255+
-----------------
234256

235257
As we've read in the previous section not all plugins should run at any point in time. In fact, the
236258
circumstances under which we want a plugin to begin its work may vary from plugin to plugin.
@@ -245,19 +267,69 @@ We may want a plugin to only start running if:
245267

246268
Hence, to actually start a plugin, the plugin needs to invoke the
247269
:meth:`~trinity.extensibility.plugin.BasePlugin.start` method at any moment when it is in its
248-
``READY`` state.
270+
``READY`` state. Let's assume a simple case in which we simply want to start the plugin if Trinity
271+
is started with the ``--report-peer-count`` flag.
272+
273+
.. literalinclude:: ../../../trinity/plugins/examples/peer_count_reporter/plugin.py
274+
:language: python
275+
:pyobject: PeerCountReporterPlugin.on_ready
276+
277+
In case of a :class:`~trinity.extensibility.plugin.BaseIsolatedProcessPlugin`, this will cause the
278+
:meth:`~trinity.extensibility.plugin.BaseIsolatedPlugin.do_start` method to run on an entirely
279+
separated, new process. In other cases
280+
:meth:`~trinity.extensibility.plugin.BaseIsolatedPlugin.do_start` will simply run in the same
281+
process as the plugin manager that the plugin is controlled by.
282+
249283

250284
Communication pattern
251285
~~~~~~~~~~~~~~~~~~~~~
252286

253-
Coming soon: Spoiler: Plugins can communicate with other parts of the application or even other
254-
plugins via the central event bus.
287+
For most plugins to be useful they need to be able to communicate with the rest of the application
288+
as well as other plugins. In addition to that, this kind of communication needs to work across
289+
process boundaries as plugins will often operate in independent processes.
255290

256-
Making plugins discoverable
257-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
291+
To achieve this, Trinity uses the
292+
`Lahja project <https://github.com/ethereum/lahja>`_, which enables us to operate
293+
a lightweight event bus that works across processes. An event bus is a software dedicated to the
294+
transmission of events from a broadcaster to interested parties.
258295

259-
Coming soon.
296+
This kind of architecture allows for efficient and decoupled communication between different parts
297+
of Trinity including plugins.
260298

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.**
299+
For instance, a plugin may be interested to perform some action every time that a new peer connects
300+
to our node. These kind of events get exposed on the EventBus and hence allow a wide range of
301+
plugins to make use of them.
302+
303+
For an event to be usable across processes it needs to be pickable and in general should be a
304+
shallow Data Transfer Object (`DTO <https://en.wikipedia.org/wiki/Data_transfer_object>`_)
305+
306+
Every plugin has access to the event bus via its
307+
:meth:`~trinity.extensibility.plugin.BasePlugin.event_bus` property and in fact we have already
308+
used it in the above example to get the current number of connected peers.
309+
310+
.. note::
311+
This guide will soon cover communication through the event bus in more detail. For now, the
312+
`Lahja documentation <https://github.com/ethereum/lahja/blob/master/README.md>`_ gives us some
313+
more information about the available APIs and how to use them.
314+
315+
Distributing plugins
316+
~~~~~~~~~~~~~~~~~~~~
317+
318+
Of course, plugins are more fun if we can share them and anyone can simply install them through
319+
``pip``. The good news is, it's not hard at all!
320+
321+
In this guide, we won't go into details about how to create Python packages as this is already
322+
`covered in the official Python docs <https://packaging.python.org/tutorials/packaging-projects/>`_
323+
.
324+
325+
Once we have a ``setup.py`` file, all we have to do is to expose our plugin under
326+
``trinity.plugins`` via the ``entry_points`` section.
327+
328+
.. literalinclude:: ../../../tests/trinity/integration/trinity_test_plugin/setup.py
329+
:language: python
330+
331+
Check out the `official documentation on entry points <https://packaging.python.org/guides/creating-and-discovering-plugins/#using-package-metadata>`_
332+
for a deeper explanation.
333+
334+
A plugin where the ``setup.py`` file is configured as described can be installed by
335+
``pip install <package-name>``` and is immediately available as a plugin in Trinity.
Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,39 @@
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.
1+
# This is an example plugin. It is exposed in the docs and intentionally
2+
# uses more concise style for imports
43

54
from argparse import ArgumentParser, _SubParsersAction
5+
import asyncio
66

7+
from p2p.events import PeerCountRequest
8+
from p2p.service import BaseService
9+
from lahja import Endpoint
710
from trinity.extensibility import BaseIsolatedPlugin
11+
from trinity.utils.shutdown import exit_with_service_and_endpoint
12+
13+
14+
class PeerCountReporter(BaseService):
15+
16+
def __init__(self, event_bus: Endpoint) -> None:
17+
super().__init__()
18+
self.event_bus = event_bus
19+
20+
async def _run(self) -> None:
21+
self.run_daemon_task(self._periodically_report_stats())
22+
await self.cancel_token.wait()
23+
24+
async def _periodically_report_stats(self) -> None:
25+
while self.is_operational:
26+
try:
27+
response = await asyncio.wait_for(
28+
self.event_bus.request(PeerCountRequest()),
29+
timeout=1.0
30+
)
31+
self.logger.info("CONNECTED PEERS: %s", response.peer_count)
32+
except asyncio.TimeoutError:
33+
self.logger.warning("TIMEOUT: Waiting on PeerPool to boot")
34+
await asyncio.sleep(5)
835

936

10-
# --START CLASS--
1137
class PeerCountReporterPlugin(BaseIsolatedPlugin):
1238

1339
@property
@@ -17,4 +43,20 @@ def name(self) -> str:
1743
def configure_parser(self,
1844
arg_parser: ArgumentParser,
1945
subparser: _SubParsersAction) -> None:
20-
arg_parser.add_argument("--report-peer-count", type=bool, required=False)
46+
arg_parser.add_argument(
47+
"--report-peer-count",
48+
action="store_true",
49+
help="Report peer count to console",
50+
)
51+
52+
def on_ready(self) -> None:
53+
if self.context.args.report_peer_count:
54+
self.start()
55+
56+
def do_start(self) -> None:
57+
loop = asyncio.get_event_loop()
58+
service = PeerCountReporter(self.event_bus)
59+
asyncio.ensure_future(exit_with_service_and_endpoint(service, self.event_bus))
60+
asyncio.ensure_future(service.run())
61+
loop.run_forever()
62+
loop.close()

trinity/plugins/registry.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
LightPeerChainBridgePlugin
2020
)
2121

22+
from trinity.plugins.examples.peer_count_reporter.plugin import (
23+
PeerCountReporterPlugin
24+
)
25+
2226

2327
def is_ipython_available() -> bool:
2428
try:
@@ -40,6 +44,7 @@ def is_ipython_available() -> bool:
4044
JsonRpcServerPlugin(),
4145
LightPeerChainBridgePlugin(),
4246
TxPlugin(),
47+
PeerCountReporterPlugin(),
4348
]
4449

4550
# Plugins need to define entrypoints at 'trinity.plugins' to automatically get loaded

0 commit comments

Comments
 (0)