ResourceWarnings in test_reconnects_are_stable
jaraco opened this issue · 6 comments
irc/tests/test_bot.py::TestBot::test_reconnects_are_stable
/Users/jaraco/code/jaraco/irc/irc/tests/test_bot.py:139: ResourceWarning: unclosed <socket.socket fd=12, family=2, type=1, proto=0, laddr=('127.0.0.1', 54781)>
bot.reactor.process_once()
Enable tracemalloc to get traceback where the object was allocated.
See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.
irc/tests/test_bot.py::TestBot::test_reconnects_are_stable
/Users/jaraco/code/jaraco/irc/irc/tests/test_bot.py:139: ResourceWarning: unclosed <socket.socket fd=12, family=2, type=1, proto=0, laddr=('127.0.0.1', 54782)>
bot.reactor.process_once()
Enable tracemalloc to get traceback where the object was allocated.
See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
Here's the more info.
irc main [5] 🐚 env PYTHONTRACEMALLOC=20 .tox/py/bin/pytest -p no:cov -p no:mypy -p no:ruff -k reconnects
=============================================================== test session starts ===============================================================
platform darwin -- Python 3.12.4, pytest-8.2.2, pluggy-1.5.0
rootdir: /Users/jaraco/code/jaraco/irc
configfile: pytest.ini
plugins: typeguard-4.3.0, checkdocs-2.13.0, enabler-3.1.1
collected 36 items / 35 deselected / 1 selected
irc/tests/test_bot.py . [100%]
================================================================ warnings summary =================================================================
irc/tests/test_bot.py::TestBot::test_reconnects_are_stable
/Users/jaraco/code/jaraco/irc/irc/tests/test_bot.py:139: ResourceWarning: unclosed <socket.socket fd=14, family=2, type=1, proto=0, laddr=('127.0.0.1', 64398)>
bot.reactor.process_once()
Object allocated at:
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_callers.py", line 103
res = hook_impl.function(*args)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/main.py", line 364
item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_hooks.py", line 513
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_manager.py", line 120
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_callers.py", line 103
res = hook_impl.function(*args)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/runner.py", line 116
runtestprotocol(item, nextitem=nextitem)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/runner.py", line 135
reports.append(call_and_report(item, "call", log))
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/runner.py", line 240
call = CallInfo.from_call(
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/runner.py", line 341
result: Optional[TResult] = func()
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/runner.py", line 241
lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_hooks.py", line 513
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_manager.py", line 120
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_callers.py", line 103
res = hook_impl.function(*args)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/runner.py", line 173
item.runtest()
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/python.py", line 1632
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_hooks.py", line 513
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_manager.py", line 120
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_callers.py", line 103
res = hook_impl.function(*args)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/python.py", line 162
result = testfunction(**testargs)
File "/Users/jaraco/code/jaraco/irc/irc/tests/test_bot.py", line 139
bot.reactor.process_once()
irc/tests/test_bot.py::TestBot::test_reconnects_are_stable
/Users/jaraco/code/jaraco/irc/irc/tests/test_bot.py:139: ResourceWarning: unclosed <socket.socket fd=14, family=2, type=1, proto=0, laddr=('127.0.0.1', 64399)>
bot.reactor.process_once()
Object allocated at:
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_callers.py", line 103
res = hook_impl.function(*args)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/main.py", line 364
item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_hooks.py", line 513
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_manager.py", line 120
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_callers.py", line 103
res = hook_impl.function(*args)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/runner.py", line 116
runtestprotocol(item, nextitem=nextitem)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/runner.py", line 135
reports.append(call_and_report(item, "call", log))
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/runner.py", line 240
call = CallInfo.from_call(
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/runner.py", line 341
result: Optional[TResult] = func()
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/runner.py", line 241
lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_hooks.py", line 513
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_manager.py", line 120
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_callers.py", line 103
res = hook_impl.function(*args)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/runner.py", line 173
item.runtest()
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/python.py", line 1632
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_hooks.py", line 513
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_manager.py", line 120
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/pluggy/_callers.py", line 103
res = hook_impl.function(*args)
File "/Users/jaraco/code/jaraco/irc/.tox/py/lib/python3.12/site-packages/_pytest/python.py", line 162
result = testfunction(**testargs)
File "/Users/jaraco/code/jaraco/irc/irc/tests/test_bot.py", line 139
bot.reactor.process_once()
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================================== 1 passed, 35 deselected, 2 warnings in 5.12s ===================================================
I tried adding a disconnect at the end.
diff --git a/irc/tests/test_bot.py b/irc/tests/test_bot.py
index 213eca3..c91a897 100644
--- a/irc/tests/test_bot.py
+++ b/irc/tests/test_bot.py
@@ -139,6 +139,7 @@ class TestBot:
bot.reactor.process_once()
time.sleep(0.01)
assert len(bot.reactor.scheduler.queue) <= 1
+ bot.disconnect()
def test_version():
But that made matters worse:
=============================================================== test session starts ===============================================================
platform darwin -- Python 3.12.4, pytest-8.2.2, pluggy-1.5.0
rootdir: /Users/jaraco/code/jaraco/irc
configfile: pytest.ini
plugins: typeguard-4.3.0, checkdocs-2.13.0, enabler-3.1.1
collected 36 items / 35 deselected / 1 selected
irc/tests/test_bot.py . [100%]
================================================================ warnings summary =================================================================
irc/tests/test_bot.py::TestBot::test_reconnects_are_stable
/Users/jaraco/code/jaraco/irc/irc/tests/test_bot.py:139: ResourceWarning: unclosed <socket.socket fd=14, family=2, type=1, proto=0, laddr=('127.0.0.1', 64871)>
bot.reactor.process_once()
Enable tracemalloc to get traceback where the object was allocated.
See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.
irc/tests/test_bot.py::TestBot::test_reconnects_are_stable
/Users/jaraco/code/jaraco/irc/irc/tests/test_bot.py:139: ResourceWarning: unclosed <socket.socket fd=14, family=2, type=1, proto=0, laddr=('127.0.0.1', 64872)>
bot.reactor.process_once()
Enable tracemalloc to get traceback where the object was allocated.
See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.
irc/tests/test_bot.py::TestBot::test_reconnects_are_stable
/Users/jaraco/code/jaraco/irc/irc/client.py:542: ResourceWarning: unclosed <socket.socket fd=15, family=2, type=1, proto=0, laddr=('127.0.0.1', 64873)>
del self.socket
Enable tracemalloc to get traceback where the object was allocated.
See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
================================================== 1 passed, 35 deselected, 3 warnings in 0.64s ===================================================
I managed to get a bit more detail about what's happening by passing --log-cli-level 0
.
------------------------------------------------------------------ live log call ------------------------------------------------------------------
DEBUG irc.client:client.py:164 connect(server='127.0.0.1', port=50516, nickname='reconnect_test', ...)
DEBUG irc.client:client.py:672 TO SERVER: NICK reconnect_test
DEBUG irc.client:client.py:672 TO SERVER: USER reconnect_test 0 * :reconnect_test
Level 8 irc.client:client.py:888 process_once()
Level 8 irc.client:client.py:854 process_data()
DEBUG irc.client:client.py:1218 _dispatcher: disconnect
Level 8 irc.client:client.py:888 process_once()
DEBUG irc.client:client.py:164 connect(server='127.0.0.1', port=50516, nickname='reconnect_test', ...)
DEBUG irc.client:client.py:672 TO SERVER: NICK reconnect_test
DEBUG irc.client:client.py:672 TO SERVER: USER reconnect_test 0 * :reconnect_test
Level 8 irc.client:client.py:888 process_once()
Level 8 irc.client:client.py:854 process_data()
DEBUG irc.client:client.py:1218 _dispatcher: disconnect
DEBUG irc.client:client.py:164 connect(server='127.0.0.1', port=50516, nickname='reconnect_test', ...)
DEBUG irc.client:client.py:672 TO SERVER: NICK reconnect_test
DEBUG irc.client:client.py:672 TO SERVER: USER reconnect_test 0 * :reconnect_test
Level 8 irc.client:client.py:888 process_once()
Level 8 irc.client:client.py:854 process_data()
DEBUG irc.client:client.py:1218 _dispatcher: disconnect
PASSED [100%]
I suspect the issue lies in the Client.disconnect method:
Lines 523 to 543 in 2f6ba71
After learning a bit about the relationship between socket.shutdown and socket.close, I'm pretty sure the issue is that socket.close()
never gets called when the peer disconnected because self.quit()
fails or socket.shutdown
fails.
Indeed, applying the following diff, there are no more resource warnings:
diff --git a/irc/client.py b/irc/client.py
index 4d269a9..52262f3 100644
--- a/irc/client.py
+++ b/irc/client.py
@@ -532,13 +532,15 @@ class ServerConnection(Connection):
except AttributeError:
return
- self.quit(message)
+ # self.quit(message)
try:
self.socket.shutdown(socket.SHUT_WR)
- self.socket.close()
except OSError:
pass
+
+ self.socket.close()
+
del self.socket
self._handle_event(Event("disconnect", self.server, "", [message]))
Oof. The Connection.quit
logic is tricky, because .quit
will call .disconnect
which calls .quit
. The only thing that prevents that from becoming an infinite loop is the check for self.connected
😬 .
Oh! I don't need to be concerned about Connection.quit
for this issue. It didn't need to be commented out to address the warnings.