From 50b10284ac7bc5371b4a5006085894e734a0431b Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 20 Jan 2026 16:07:14 +0000 Subject: [PATCH] util.CallbackManager: use sets instead of lists - to gracefully take duplicate calls of register_callbacks(): should be idempotent now - as a side-effect, the order of the callbacks is changed and not guaranteed - not like anyone should have been relying on it before though --- electrum/util.py | 12 ++++++++---- tests/test_callbackmgr.py | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/electrum/util.py b/electrum/util.py index 5223d8570..45608bdc1 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 concurrent.futures +import copy from dataclasses import dataclass import logging import os @@ -1953,12 +1954,12 @@ class CallbackManager(Logger): def __init__(self): Logger.__init__(self) self.callback_lock = threading.Lock() - self.callbacks = defaultdict(list) # type: Dict[str, List[Callable]] # note: needs self.callback_lock + self.callbacks = defaultdict(set) # type: Dict[str, Set[Callable]] # note: needs self.callback_lock def register_callback(self, func: Callable, events: Sequence[str]) -> None: with self.callback_lock: for event in events: - self.callbacks[event].append(func) + self.callbacks[event].add(func) def unregister_callback(self, callback: Callable) -> None: with self.callback_lock: @@ -1978,7 +1979,7 @@ class CallbackManager(Logger): loop = get_asyncio_loop() assert loop.is_running(), "event loop not running" with self.callback_lock: - callbacks = self.callbacks[event][:] + callbacks = copy.copy(self.callbacks[event]) for callback in callbacks: if inspect.iscoroutinefunction(callback): # async cb fut = asyncio.run_coroutine_threadsafe(callback(*args), loop) @@ -2004,8 +2005,11 @@ _event_listeners = defaultdict(set) # type: Dict[str, Set[str]] class EventListener: """Use as a mixin for a class that has methods to be triggered on events. - Methods that receive the callbacks should be named "on_event_*" and decorated with @event_listener. - - register_callbacks() should be called exactly once per instance of EventListener, e.g. in __init__ + - register_callbacks() should be called once per instance of EventListener, e.g. in __init__ - unregister_callbacks() should be called at least once, e.g. when the instance is destroyed + - if register_callbacks() is called in __init__, as opposed to a separate start() method, + extra care is needed that the call to unregister_callbacks() is not forgotten, + otherwise we will leak memory """ def _list_callbacks(self): diff --git a/tests/test_callbackmgr.py b/tests/test_callbackmgr.py index 1a390e718..cd5cd10ef 100644 --- a/tests/test_callbackmgr.py +++ b/tests/test_callbackmgr.py @@ -49,13 +49,13 @@ class TestCallbackMgr(ElectrumTestCase): el2.start() self.assertEqual(4, _count_all_callbacks()) el1.start() - self.assertEqual(6, _count_all_callbacks()) - el1.stop() self.assertEqual(4, _count_all_callbacks()) el1.stop() self.assertEqual(2, _count_all_callbacks()) el1.stop() self.assertEqual(2, _count_all_callbacks()) + el1.stop() + self.assertEqual(2, _count_all_callbacks()) el2.stop() self.assertEqual(0, _count_all_callbacks())