From 5e1e7dd9b3ca42ea38202a07f96c6a8a1becd155 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 16 Aug 2023 16:27:17 +0000 Subject: [PATCH] util.CallbackManager: make sure exceptions in cbs are logged this resolves the existing FIXME --- electrum/util.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/electrum/util.py b/electrum/util.py index 933b71698..acfd1f220 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -21,6 +21,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import binascii +import concurrent.futures import os, sys, re, json from collections import defaultdict, OrderedDict from typing import (NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any, @@ -1729,11 +1730,12 @@ def randrange(bound: int) -> int: return secrets.randbelow(bound - 1) + 1 -class CallbackManager: +class CallbackManager(Logger): # callbacks set by the GUI or any thread # guarantee: the callbacks will always get triggered from the asyncio thread. def __init__(self): + Logger.__init__(self) self.callback_lock = threading.Lock() self.callbacks = defaultdict(list) # note: needs self.callback_lock self._running_cb_futs = set() @@ -1759,18 +1761,25 @@ class CallbackManager: with self.callback_lock: callbacks = self.callbacks[event][:] for callback in callbacks: - # FIXME: if callback throws, we will lose the traceback - if asyncio.iscoroutinefunction(callback): + if asyncio.iscoroutinefunction(callback): # async cb fut = asyncio.run_coroutine_threadsafe(callback(*args), loop) # keep strong references around to avoid GC issues: self._running_cb_futs.add(fut) - fut.add_done_callback(lambda fut_: self._running_cb_futs.remove(fut_)) - elif get_running_loop() == loop: - # run callback immediately, so that it is guaranteed - # to have been executed when this method returns - callback(*args) - else: - loop.call_soon_threadsafe(callback, *args) + def on_done(fut_: concurrent.futures.Future): + assert fut_.done() + self._running_cb_futs.remove(fut_) + if exc := fut_.exception(): + self.logger.error(f"cb errored. {event=}. {exc=}", exc_info=exc) + fut.add_done_callback(on_done) + else: # non-async cb + # note: the cb needs to run in the asyncio thread + if get_running_loop() == loop: + # run callback immediately, so that it is guaranteed + # to have been executed when this method returns + callback(*args) + else: + # note: if cb raises, asyncio will log the exception + loop.call_soon_threadsafe(callback, *args) callback_mgr = CallbackManager()