util: cleanup asyncio event loop after stopping
What changed, and why it matters
This commit fixes a housekeeping issue in Electrum's background task manager. When the program stopped an internal 'event loop,' it previously left it partially open, causing Python resource warnings. The patch makes sure remaining tasks are cancelled, generators are shut down, the executor is stopped, and the loop is fully closed. There is no direct security vulnerability described; it is a cleanup that may improve stability and reduce resource leaks during testing.
Treat as a normal stability/maintenance improvement. Review whether the new cleanup order could hide exceptions during shutdown, but no urgent security action is indicated.
Security signals we found
Resource leak cleanup in asyncio event loop shutdown
No explicit security bug or exploit path described
Improves graceful teardown of background tasks and executor threads
Evidence from the diff
The change is in electrum/util.py inside create_and_start_event_loop(). The existing cleanup block now cancels all pending tasks with asyncio.gather(*asyncio.all_tasks(loop), return_exceptions=True), suppresses CancelledError, runs loop.shutdown_asyncgens(), runs loop.shutdown_default_executor() for BaseEventLoop, logs any exception, sets the global loop reference to None, and finally calls loop.close(). The commit message frames this as resolving ResourceWarning messages observed in regtests with PYTHONASYNCIODEBUG=1 and PYTHONDEVMODE=1.
Changed components
electrum/util.pycreate_and_start_event_loop()Inspect captured patch +13 / −1
diff --git a/electrum/util.py b/electrum/util.py
index cc27336..08e60e5 100644
--- a/electrum/util.py
+++ b/electrum/util.py
@@ -52,7 +52,7 @@ import functools
from functools import partial
from abc import abstractmethod, ABC
import enum
-from contextlib import nullcontext
+from contextlib import nullcontext, suppress
import traceback
import inspect
@@ -1704,8 +1704,20 @@ def create_and_start_event_loop() -> Tuple[asyncio.AbstractEventLoop,
loop.run_until_complete(stopping_fut)
finally:
# clean-up
+ try:
+ pending_tasks = asyncio.gather(*asyncio.all_tasks(loop), return_exceptions=True)
+ pending_tasks.cancel()
+ with suppress(asyncio.CancelledError):
+ loop.run_until_complete(pending_tasks)
+ loop.run_until_complete(loop.shutdown_asyncgens())
+ if isinstance(loop, asyncio.BaseEventLoop):
+ loop.run_until_complete(loop.shutdown_default_executor())
+ except Exception as e:
+ _logger.debug(f"exception when cleaning up asyncio event loop: {e}")
+
global _asyncio_event_loop
_asyncio_event_loop = None
+ loop.close()
loop.set_exception_handler(on_exception)
_set_custom_task_factory(loop)
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.