4
4
BaseManager ,
5
5
BaseProxy ,
6
6
)
7
+ import inspect
7
8
import os
9
+ import traceback
10
+ from types import TracebackType
8
11
from typing import (
12
+ Any ,
13
+ Callable ,
14
+ List ,
9
15
Type
10
16
)
11
17
@@ -144,6 +150,65 @@ def initialize_database(chain_config: ChainConfig, chaindb: AsyncChainDB) -> Non
144
150
)
145
151
146
152
153
+ class TracebackRecorder :
154
+ """
155
+ Wrap the given instance, delegating all attribute accesses to it but if any method call raises
156
+ an exception it is converted into a ChainedExceptionWithTraceback that uses exception chaining
157
+ in order to retain the traceback that led to the exception in the remote process.
158
+ """
159
+
160
+ def __init__ (self , obj : Any ) -> None :
161
+ self .obj = obj
162
+
163
+ def __dir__ (self ) -> List [str ]:
164
+ return dir (self .obj )
165
+
166
+ def __getattr__ (self , name : str ) -> Any :
167
+ attr = getattr (self .obj , name )
168
+ if not inspect .ismethod (attr ):
169
+ return attr
170
+ else :
171
+ return record_traceback_on_error (attr )
172
+
173
+
174
+ # Need to "type: ignore" here because we run mypy with --disallow-any-generics
175
+ def record_traceback_on_error (attr : Callable ) -> Callable : # type: ignore
176
+ def wrapper (* args : Any , ** kwargs : Any ) -> Any :
177
+ try :
178
+ return attr (* args , ** kwargs )
179
+ except Exception as e :
180
+ # This is a bit of a hack based on https://bugs.python.org/issue13831 to record the
181
+ # original traceback (as a string, which is picklable unlike traceback instances) in
182
+ # the exception that will be sent to the remote process.
183
+ raise ChainedExceptionWithTraceback (e , e .__traceback__ )
184
+
185
+ return wrapper
186
+
187
+
188
+ class RemoteTraceback (Exception ):
189
+
190
+ def __init__ (self , tb : str ) -> None :
191
+ self .tb = tb
192
+
193
+ def __str__ (self ) -> str :
194
+ return self .tb
195
+
196
+
197
+ class ChainedExceptionWithTraceback (Exception ):
198
+
199
+ def __init__ (self , exc : Exception , tb : TracebackType ) -> None :
200
+ self .tb = '\n """\n %s"""' % '' .join (traceback .format_exception (type (exc ), exc , tb ))
201
+ self .exc = exc
202
+
203
+ def __reduce__ (self ) -> Any :
204
+ return rebuild_exc , (self .exc , self .tb )
205
+
206
+
207
+ def rebuild_exc (exc , tb ): # type: ignore
208
+ exc .__cause__ = RemoteTraceback (tb )
209
+ return exc
210
+
211
+
147
212
def serve_chaindb (chain_config : ChainConfig , base_db : BaseDB ) -> None :
148
213
chaindb = AsyncChainDB (base_db )
149
214
chain_class : Type [BaseChain ]
@@ -167,23 +232,25 @@ class DBManager(BaseManager):
167
232
168
233
# Typeshed definitions for multiprocessing.managers is incomplete, so ignore them for now:
169
234
# https://github.com/python/typeshed/blob/85a788dbcaa5e9e9a62e55f15d44530cd28ba830/stdlib/3/multiprocessing/managers.pyi#L3
170
- DBManager .register ('get_db' , callable = lambda : base_db , proxytype = DBProxy ) # type: ignore
235
+ DBManager .register ( # type: ignore
236
+ 'get_db' , callable = lambda : TracebackRecorder (base_db ), proxytype = DBProxy )
171
237
172
238
DBManager .register ( # type: ignore
173
239
'get_chaindb' ,
174
- callable = lambda : chaindb ,
240
+ callable = lambda : TracebackRecorder ( chaindb ) ,
175
241
proxytype = ChainDBProxy ,
176
242
)
177
- DBManager .register ('get_chain' , callable = lambda : chain , proxytype = ChainProxy ) # type: ignore
243
+ DBManager .register ( # type: ignore
244
+ 'get_chain' , callable = lambda : TracebackRecorder (chain ), proxytype = ChainProxy )
178
245
179
246
DBManager .register ( # type: ignore
180
247
'get_headerdb' ,
181
- callable = lambda : headerdb ,
248
+ callable = lambda : TracebackRecorder ( headerdb ) ,
182
249
proxytype = AsyncHeaderDBProxy ,
183
250
)
184
251
DBManager .register ( # type: ignore
185
252
'get_header_chain' ,
186
- callable = lambda : header_chain ,
253
+ callable = lambda : TracebackRecorder ( header_chain ) ,
187
254
proxytype = AsyncHeaderChainProxy ,
188
255
)
189
256
0 commit comments