44 BaseManager ,
55 BaseProxy ,
66)
7+ import inspect
78import os
9+ import traceback
10+ from types import TracebackType
811from typing import (
12+ Any ,
13+ Callable ,
14+ List ,
915 Type
1016)
1117
@@ -144,6 +150,65 @@ def initialize_database(chain_config: ChainConfig, chaindb: AsyncChainDB) -> Non
144150 )
145151
146152
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+
147212def serve_chaindb (chain_config : ChainConfig , base_db : BaseDB ) -> None :
148213 chaindb = AsyncChainDB (base_db )
149214 chain_class : Type [BaseChain ]
@@ -167,23 +232,25 @@ class DBManager(BaseManager):
167232
168233 # Typeshed definitions for multiprocessing.managers is incomplete, so ignore them for now:
169234 # 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 )
171237
172238 DBManager .register ( # type: ignore
173239 'get_chaindb' ,
174- callable = lambda : chaindb ,
240+ callable = lambda : TracebackRecorder ( chaindb ) ,
175241 proxytype = ChainDBProxy ,
176242 )
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 )
178245
179246 DBManager .register ( # type: ignore
180247 'get_headerdb' ,
181- callable = lambda : headerdb ,
248+ callable = lambda : TracebackRecorder ( headerdb ) ,
182249 proxytype = AsyncHeaderDBProxy ,
183250 )
184251 DBManager .register ( # type: ignore
185252 'get_header_chain' ,
186- callable = lambda : header_chain ,
253+ callable = lambda : TracebackRecorder ( header_chain ) ,
187254 proxytype = AsyncHeaderChainProxy ,
188255 )
189256
0 commit comments