Skip to content

Commit e27f55c

Browse files
authored
šŸ› Avoid importing and calling close in __del__ during shutdown (#7322)
Two related fixes to prevent 'Exception ignored in __del__' errors during Python shutdown: 1. Move 'import warnings' to module level in `RabbitmqBroker` and `StorageBackend` to avoid `ImportError` when `sys.meta_path` is `None` during shutdown. 2. Guard `__del__` with `sys.is_finalizing()` to skip the `close()` call during Python shutdown when asyncio/kiwipy event loop infrastructure is already torn down, preventing `AttributeError` in pytray's `await_` method. During shutdown: - `sys.meta_path` becomes `None`, making imports unavailable - Module globals are set to `None` before garbage collection - `asyncio.run_coroutine_threadsafe()` fails with `AttributeError` By deferring the import and skipping close during finalization, we avoid both issues since the process is exiting anyway. Fixes #7309
1 parent 62f105f commit e27f55c

4 files changed

Lines changed: 63 additions & 7 deletions

File tree

ā€Žsrc/aiida/brokers/rabbitmq/broker.pyā€Ž

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from __future__ import annotations
44

55
import functools
6+
import sys
67
import typing as t
8+
import warnings
79
from collections.abc import Iterator
810

911
from aiida.brokers.broker import Broker
@@ -49,10 +51,9 @@ def close(self) -> None:
4951

5052
def __del__(self) -> None:
5153
if self._communicator is not None:
52-
import warnings
53-
5454
warnings.warn(f'RabbitmqBroker was not closed explicitly: {self!r}', ResourceWarning, stacklevel=1)
55-
self.close()
55+
if not sys.is_finalizing():
56+
self.close()
5657

5758
def iterate_tasks(self) -> Iterator[t.Any]:
5859
"""Return an iterator over the tasks in the launch queue."""

ā€Žsrc/aiida/orm/implementation/storage_backend.pyā€Ž

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from __future__ import annotations
1212

1313
import abc
14+
import sys
15+
import warnings
1416
from collections.abc import Iterable
1517
from typing import TYPE_CHECKING, Any, ContextManager, List, Optional, TypeVar, Union
1618

@@ -142,10 +144,9 @@ def __del__(self):
142144
# covers cases where the backend implementation is not yet initialized but object is deleted
143145
return
144146
if not closed:
145-
import warnings
146-
147147
warnings.warn(f'StorageBackend was not closed explicitly: {self!r}', ResourceWarning, stacklevel=1)
148-
self.close()
148+
if not sys.is_finalizing():
149+
self.close()
149150

150151
@property
151152
@abc.abstractmethod

ā€Žtests/brokers/test_rabbitmq.pyā€Ž

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010

1111
import pathlib
1212
import uuid
13+
from unittest.mock import MagicMock
1314

1415
import pytest
1516
import requests
1617
from kiwipy.rmq import RmqThreadCommunicator
1718
from packaging.version import parse
1819

19-
from aiida.brokers.rabbitmq import client, utils
20+
from aiida.brokers.rabbitmq import RabbitmqBroker, client, utils
2021
from aiida.engine.processes import ProcessState, control
2122
from aiida.orm import Int
2223

@@ -36,6 +37,33 @@ def raise_connection_error():
3637
assert 'RabbitMQ @' in str(broker)
3738

3839

40+
def test_del_closes_broker_when_not_finalizing(aiida_profile, monkeypatch):
41+
"""Test `__del__` closes the broker when Python is not finalizing."""
42+
broker = RabbitmqBroker(aiida_profile)
43+
broker._communicator = MagicMock()
44+
close = MagicMock()
45+
monkeypatch.setattr(broker, 'close', close)
46+
47+
with pytest.warns(ResourceWarning, match='RabbitmqBroker was not closed explicitly'):
48+
broker.__del__()
49+
50+
close.assert_called_once_with()
51+
52+
53+
def test_del_skips_close_when_finalizing(aiida_profile, monkeypatch):
54+
"""Test ``__del__`` skips close when Python is finalizing."""
55+
broker = RabbitmqBroker(aiida_profile)
56+
broker._communicator = MagicMock()
57+
close = MagicMock()
58+
monkeypatch.setattr(broker, 'close', close)
59+
monkeypatch.setattr('sys.is_finalizing', lambda: True)
60+
61+
with pytest.warns(ResourceWarning, match='RabbitmqBroker was not closed explicitly'):
62+
broker.__del__()
63+
64+
close.assert_not_called()
65+
66+
3967
@pytest.mark.parametrize(
4068
('version', 'supported'),
4169
(

ā€Žtests/orm/implementation/test_backend.pyā€Ž

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import json
1414
import pathlib
1515
import uuid
16+
from unittest.mock import MagicMock
1617

1718
import pytest
1819

@@ -168,6 +169,31 @@ def test_delete_nodes_and_connections(self):
168169
assert len(group.nodes) == 0
169170

170171

172+
def test_del_closes_backend_when_not_finalizing(aiida_profile, monkeypatch):
173+
"""Test ``__del__`` closes the backend when Python is not finalizing."""
174+
backend = aiida_profile.storage_cls(aiida_profile)
175+
close = MagicMock()
176+
monkeypatch.setattr(backend, 'close', close)
177+
178+
with pytest.warns(ResourceWarning, match='StorageBackend was not closed explicitly'):
179+
backend.__del__()
180+
181+
close.assert_called_once_with()
182+
183+
184+
def test_del_skips_close_when_finalizing(aiida_profile, monkeypatch):
185+
"""Test ``__del__`` skips close when Python is finalizing."""
186+
backend = aiida_profile.storage_cls(aiida_profile)
187+
close = MagicMock()
188+
monkeypatch.setattr(backend, 'close', close)
189+
monkeypatch.setattr('sys.is_finalizing', lambda: True)
190+
191+
with pytest.warns(ResourceWarning, match='StorageBackend was not closed explicitly'):
192+
backend.__del__()
193+
194+
close.assert_not_called()
195+
196+
171197
def test_backup_not_implemented(aiida_config, backend, monkeypatch, tmp_path):
172198
"""Test the backup functionality if the plugin does not implement it."""
173199

0 commit comments

Comments
Ā (0)
⚔