|
| 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.** |
0 commit comments