From 5d1df960205da39036451e5863ee70648e27f496 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 26 Sep 2025 15:53:41 +0000 Subject: [PATCH] tests: clear util.callback_mgr between test cases util.callback_mgr.callbacks was not getting properly cleared between tests. Every time an Abstract_Wallet or an LNWorker (or many other subclasses of EventListener) is instantiated, self.register_callbacks() is called in __init__, which puts callbacks into util.callback_mgr.callbacks. These are only cleaned up if we explicitly call Abstract_Wallet.stop() or LNWorker.stop() later, which we usually do not do in the tests. As a result, when running multiple unit tests in a row, lots of objects created in a given testcase are never GC-ed and leak into subsequent tests. This is not only a memory leak, but wastes compute too: when events are triggered and cbs get called, these old objects also have their cbs called. After running all (~1061) unit tests, I observe util.callback_mgr.callbacks had 30 events with a total of 3156 callbacks stored. On my laptop, running all unit tests previously took ~115 sec, and now it takes ~73 sec. --- electrum/util.py | 12 ++++++++---- tests/__init__.py | 1 + tests/test_lnpeer.py | 16 ++++------------ 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/electrum/util.py b/electrum/util.py index b019b0868..cc2733637 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1953,20 +1953,24 @@ class CallbackManager(Logger): def __init__(self): Logger.__init__(self) self.callback_lock = threading.Lock() - self.callbacks = defaultdict(list) # note: needs self.callback_lock + self.callbacks = defaultdict(list) # type: Dict[str, List[Callable]] # note: needs self.callback_lock - def register_callback(self, func, events): + def register_callback(self, func: Callable, events: Sequence[str]) -> None: with self.callback_lock: for event in events: self.callbacks[event].append(func) - def unregister_callback(self, callback): + def unregister_callback(self, callback: Callable) -> None: with self.callback_lock: for callbacks in self.callbacks.values(): if callback in callbacks: callbacks.remove(callback) - def trigger_callback(self, event, *args): + def clear_all_callbacks(self) -> None: + with self.callback_lock: + self.callbacks.clear() + + def trigger_callback(self, event: str, *args) -> None: """Trigger a callback with given arguments. Can be called from any thread. The callback itself will get scheduled on the event loop. diff --git a/tests/__init__.py b/tests/__init__.py index 663dc6080..106323ede 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -75,6 +75,7 @@ class ElectrumTestCase(unittest.IsolatedAsyncioTestCase, Logger): util._asyncio_event_loop = loop def tearDown(self): + util.callback_mgr.clear_all_callbacks() shutil.rmtree(self.electrum_path) super().tearDown() util._asyncio_event_loop = None # cleared here, at the ~last possible moment. asyncTearDown is too early. diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py index 01b3074a6..b1bc6c5af 100644 --- a/tests/test_lnpeer.py +++ b/tests/test_lnpeer.py @@ -1117,12 +1117,8 @@ class TestPeerDirect(TestPeer): util.register_callback(on_htlc_fulfilled, ["htlc_fulfilled"]) util.register_callback(on_htlc_failed, ["htlc_failed"]) - try: - with self.assertRaises(SuccessfulTest): - await f() - finally: - util.unregister_callback(on_htlc_fulfilled) - util.unregister_callback(on_htlc_failed) + with self.assertRaises(SuccessfulTest): + await f() async def test_payment_recv_mpp_confusion2(self): """Regression test for https://github.com/spesmilo/electrum/security/advisories/GHSA-8r85-vp7r-hjxf""" @@ -1191,12 +1187,8 @@ class TestPeerDirect(TestPeer): util.register_callback(on_htlc_fulfilled, ["htlc_fulfilled"]) util.register_callback(on_htlc_failed, ["htlc_failed"]) - try: - with self.assertRaises(SuccessfulTest): - await f() - finally: - util.unregister_callback(on_htlc_fulfilled) - util.unregister_callback(on_htlc_failed) + with self.assertRaises(SuccessfulTest): + await f() async def test_legacy_shutdown_low(self): await self._test_shutdown(alice_fee=100, bob_fee=150)