Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.

Commit 00e66ec

Browse files
committed
Ensure shared process plugins shutdown cleanly
Fixes #1284
1 parent 683c53c commit 00e66ec

File tree

11 files changed

+249
-119
lines changed

11 files changed

+249
-119
lines changed

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"upnpclient>=0.0.8,<1",
3737
],
3838
'trinity': [
39+
"async-generator==1.10",
3940
"bloom-filter==1.3",
4041
"cachetools>=2.1.0,<3.0.0",
4142
"coincurve>=8.0.0,<9.0.0",

trinity/extensibility/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
BaseEvent
33
)
44
from trinity.extensibility.plugin import ( # noqa: F401
5-
BasePlugin,
5+
BaseAsyncStopPlugin,
6+
BaseMainProcessPlugin,
67
BaseIsolatedPlugin,
8+
BasePlugin,
9+
BaseSyncStopPlugin,
710
DebugPlugin,
811
PluginContext,
9-
PluginProcessScope,
1012
)
1113
from trinity.extensibility.plugin_manager import ( # noqa: F401
14+
BaseManagerProcessScope,
1215
MainAndIsolatedProcessScope,
1316
PluginManager,
14-
ManagerProcessScope,
1517
SharedProcessScope,
1618
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from trinity.exceptions import (
2+
BaseTrinityError,
3+
)
4+
5+
6+
class UnsuitableShutdownError(BaseTrinityError):
7+
"""
8+
Raised when `shutdown` was called on a ``PluginManager`` instance that operates
9+
in the ``MainAndIsolatedProcessScope`` or when ``shutdown_nowait`` was called on a
10+
``PluginManager`` instance that operates in the ``SharedProcessScope``.
11+
"""
12+
pass

trinity/extensibility/plugin.py

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@
77
Namespace,
88
_SubParsersAction,
99
)
10-
from enum import (
11-
auto,
12-
Enum,
13-
)
10+
import asyncio
1411
import logging
1512
from multiprocessing import (
1613
Process
@@ -48,20 +45,6 @@
4845
)
4946

5047

51-
class PluginProcessScope(Enum):
52-
"""
53-
Define the process model in which a plugin operates:
54-
55-
- ISOLATED: The plugin runs in its own separate process
56-
- MAIN: The plugin takes over the Trinity main process (e.g. attach)
57-
- SHARED: The plugin runs in a process that is shared with other plugins
58-
"""
59-
60-
ISOLATED = auto()
61-
MAIN = auto()
62-
SHARED = auto()
63-
64-
6548
class PluginContext:
6649
"""
6750
The ``PluginContext`` holds valuable contextual information such as the parsed
@@ -98,14 +81,6 @@ def name(self) -> str:
9881
"Must be implemented by subclasses"
9982
)
10083

101-
@property
102-
def process_scope(self) -> PluginProcessScope:
103-
"""
104-
Return the :class:`~trinity.extensibility.plugin.PluginProcessScope` that the plugin uses
105-
to operate. The default scope is ``PluginProcessScope.SHARED``.
106-
"""
107-
return PluginProcessScope.SHARED
108-
10984
@property
11085
def logger(self) -> logging.Logger:
11186
return logging.getLogger('trinity.extensibility.plugin.BasePlugin#{0}'.format(self.name))
@@ -147,21 +122,42 @@ def start(self) -> None:
147122
"""
148123
pass
149124

125+
126+
class BaseSyncStopPlugin(BasePlugin):
127+
"""
128+
A ``BaseSyncStopPlugin`` unwinds synchronoulsy, hence blocks until shut down is done.
129+
"""
150130
def stop(self) -> None:
151-
"""
152-
Called when the plugin gets stopped. Should be overwritten to perform cleanup
153-
work in case the plugin set up external resources.
154-
"""
155131
pass
156132

157133

158-
class BaseIsolatedPlugin(BasePlugin):
134+
class BaseAsyncStopPlugin(BasePlugin):
135+
"""
136+
A ``BaseAsyncStopPlugin`` unwinds asynchronoulsy, hence needs to be awaited.
137+
"""
159138

160-
_process: Process = None
139+
async def stop(self) -> None:
140+
pass
161141

162-
@property
163-
def process_scope(self) -> PluginProcessScope:
164-
return PluginProcessScope.ISOLATED
142+
143+
class BaseMainProcessPlugin(BasePlugin):
144+
"""
145+
A ``BaseMainProcessPlugin`` overtakes the whole main process before most of the Trinity boot
146+
process had a chance to start. In that sense it redefines the whole meaning of the ``trinity``
147+
process.
148+
"""
149+
pass
150+
151+
152+
class BaseIsolatedPlugin(BaseSyncStopPlugin):
153+
"""
154+
A ``BaseIsolatedPlugin`` runs in an isolated process and doesn't dictate whether its
155+
implementation is based on non-blocking asyncio or synchronous calls. When an isolated
156+
plugin is stopped it will first receive a SIGINT followed by a SIGTERM soon after.
157+
It is up to the plugin to handle these signals accordingly.
158+
"""
159+
160+
_process: Process = None
165161

166162
def _start(self) -> None:
167163
self._process = ctx.Process(
@@ -182,7 +178,7 @@ def stop(self) -> None:
182178
kill_process_gracefully(self._process, self.logger)
183179

184180

185-
class DebugPlugin(BasePlugin):
181+
class DebugPlugin(BaseAsyncStopPlugin):
186182
"""
187183
This is a dummy plugin useful for demonstration and debugging purposes
188184
"""
@@ -195,11 +191,22 @@ def configure_parser(self, arg_parser: ArgumentParser, subparser: _SubParsersAct
195191
arg_parser.add_argument("--debug-plugin", type=bool, required=False)
196192

197193
def handle_event(self, activation_event: BaseEvent) -> None:
198-
self.logger.info("Debug plugin: handle_event called: ", activation_event)
194+
self.logger.info("Debug plugin: handle_event called: %s", activation_event)
199195

200196
def should_start(self) -> bool:
201197
self.logger.info("Debug plugin: should_start called")
202198
return True
203199

204200
def start(self) -> None:
205201
self.logger.info("Debug plugin: start called")
202+
asyncio.ensure_future(self.count_forever())
203+
204+
async def count_forever(self) -> None:
205+
i = 0
206+
while True:
207+
self.logger.info(i)
208+
i += 1
209+
await asyncio.sleep(1)
210+
211+
async def stop(self) -> None:
212+
self.logger.info("Debug plugin: stop called")

0 commit comments

Comments
 (0)