1
0

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
This commit is contained in:
SomberNight
2026-01-20 16:07:14 +00:00
parent 57b26ba473
commit 50b10284ac
2 changed files with 10 additions and 6 deletions

View File

@@ -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):

View File

@@ -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())