fix!: defer socket.connect() from __init__ to connect()#570
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #570 +/- ##
==========================================
+ Coverage 89.90% 90.31% +0.40%
==========================================
Files 29 29
Lines 3527 3561 +34
Branches 606 605 -1
==========================================
+ Hits 3171 3216 +45
+ Misses 211 204 -7
+ Partials 145 141 -4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Probably most problematic is that Exceptions now get raised with try:
connected_bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
except OSError as e:
...in which case it ends up not to matter. But breaking change nonetheless. |
e4bc481 to
b9ada81
Compare
There was a problem hiding this comment.
This isn't the worst breaking change ever since it is mostly only about where the exception is raised.
I'm not sure I like the suggestion of adding a paramter to opt into the new behavior. The new behavior is the more "correct" behavoir, we it would be best if we just did the right thing without having to depend on the user knowing what is best. We could add a deprecation warning to help users find this out if they don't set the option. But then we are stuck with the option forever, or we have to have another deprecation period and another breaking change to remove it.
Maybe we could write a quick script to check libraries that depend on dbus-fast to see if anyone is actually catching the exception from the constructor. If there aren't any, maybe we don't worry about it. Or if there are just a few, we could notify them of the change.
…eBus BREAKING CHANGE: For the aio MessageBus, connection errors are now raised from connect() instead of __init__. Code that catches exceptions from MessageBus() instantiation must be updated to catch them from connect() instead. Previously, BaseMessageBus.__init__() called _setup_socket() which performed a blocking socket.connect() call. This violated async design principles - an async library should not perform blocking I/O in __init__. This caused issues with tools like blockbuster that detect blocking calls in async contexts (e.g., Home Assistant Supervisor). Changes: - BaseMessageBus.__init__() no longer calls _setup_socket(); subclasses are responsible for triggering socket setup. - glib MessageBus.__init__() explicitly calls _setup_socket() to preserve its existing blocking-connect behavior (the glib path is intentionally left unchanged otherwise). - aio MessageBus.connect() now iterates the configured transports, creates a socket for each, and awaits loop.sock_connect(), falling back to the next transport on failure. The unix -> tcp fallback is preserved. - Factored out _create_socket_for_transport() helper so the sync (_setup_socket) and async (connect) paths share transport parsing. - _setup_socket is cpdef so it can be called from Python subclasses. - Added .. versionchanged:: 5.0.0 notes and expanded :raises: on aio MessageBus.connect() and the class docstring. Fixes blocking I/O detection errors like: blockbuster.BlockingError: Blocking call to socket.socket.connect 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
e7b2dc1 to
cde3fd4
Compare
There was a problem hiding this comment.
Pull request overview
Defers blocking socket connection work from BaseMessageBus.__init__() into connect() for the asyncio implementation, so MessageBus() construction no longer performs blocking I/O and connection errors surface from connect() instead.
Changes:
- Removed eager
_setup_socket()call fromBaseMessageBus.__init__()and introduced_create_socket_for_transport()to share per-transport socket construction. - Updated
aio.MessageBus.connect()to perform async connect attempts (including transport fallback) and updated docs to mark the breaking change. - Adjusted GLib initialization to preserve prior behavior (explicit
_setup_socket()inglib.MessageBus.__init__) and updated tests to expect failures fromconnect().
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_send_reply.py | Removes socket connect patching; uses a mock bus that pre-populates _sock/_stream now that init no longer connects. |
| tests/test_message_bus.py | Updates failure expectations to raise from connect() and asserts no socket/stream stored on failed connect. |
| src/dbus_fast/message_bus.py | Stops connecting in __init__; adds _create_socket_for_transport() and refactors _setup_socket() to use it. |
| src/dbus_fast/message_bus.pxd | Makes _setup_socket cpdef to keep it callable from Python subclasses. |
| src/dbus_fast/glib/message_bus.py | Explicitly calls _setup_socket() in __init__ to preserve existing blocking-connect semantics. |
| src/dbus_fast/aio/message_bus.py | Moves socket creation/connection into connect() using loop.sock_connect() and updates docstring/versionchanged notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…nsport Move option parsing (unix path/abstract, tcp port conversion) before the socket.socket() and sock.makefile() calls so a malformed address raises InvalidAddressError/ValueError without having allocated any resources to leak. Previously, makefile() was called before validation and the except handler only closed the socket, leaving the stream's refcount on the fd. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two tests exercising the unix -> tcp fallback path: - aio MessageBus.connect() with a multi-transport address where the first entry fails and the next also fails, verifying the loop iterates and raises the last error. - BaseMessageBus._setup_socket() directly, covering the sync fallback path used by the glib bus. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dlech
left a comment
There was a problem hiding this comment.
I think this looks in pretty good shape now. Just a few more comments...
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@bluetoothbot review |
Close socket/stream and reset state in aio MessageBus.connect() if _authenticate() or subsequent setup raises, so a failed connect does not leak the socket. Remove the test of the private _setup_socket() — exercising private API isn't useful, and the aio fallback test already covers the public contract.
PR Review — fix!: defer socket.connect() from init to connect()Solid PR that addresses the underlying issue (blocking I/O in 🟢 Suggestions1. Cleanup operations are unprotected — one failure masks the original (`src/dbus_fast/aio/message_bus.py`, L333-341)The cleanup block runs four operations sequentially with no per-step error handling. If except BaseException:
with contextlib.suppress(Exception):
self._loop.remove_reader(self._fd)
with contextlib.suppress(Exception):
self._stream.close()
with contextlib.suppress(Exception):
self._sock.close()
self._sock = self._stream = self._fd = self._writer = None
raiseThis matches the pattern already used in 2. ExitStack callback ordering closes socket before stream (reverse of the old enter_context order) (`src/dbus_fast/aio/message_bus.py`, L264-271)The old stack.callback(stream.close)
stack.callback(sock.close)inserts CPython's Checklist
SummarySolid PR that addresses the underlying issue (blocking I/O in Automated review by Kōancde3fd4 |
|
@bluetoothbot review |
|
I think its in good shape now but need to reconcile all the review comments |
…nit__ Per review feedback, match the new aio pattern where socket resources are populated in connect() rather than at construction time. Glib still sets up its socket in __init__ (intentionally unchanged), but MockBus is not bound to either implementation and the connect() form documents the new pattern more clearly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BaseMessageBus.__init__ no longer performs any I/O and ProxyObject already defaults to None, so the test can construct the bus normally with an explicit bus_address (which also avoids depending on DBUS_SESSION_BUS_ADDRESS in the environment). The __new__ workaround that bypassed __init__ is no longer necessary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The tcp path calls int(options["port"]) without try/except, so a non-integer port (e.g. "tcp:port=abc") escapes as ValueError rather than InvalidAddressError. Document this rather than wrapping it, since parse_address itself does not validate option values.
|
I've went through all the review comments and double checked if they are addressed/addressed missing ones. From my point of view the PR is ready for another round of review. |
dlech
left a comment
There was a problem hiding this comment.
I didn't have time to really go through the cleanup logic again, but given there are enough tests for it now, I think this is good now.
Just one comment on making tests more readable.
Replace the two thin wrappers around _assert_makefile_failure_closes with a single parametrized test so the unix and tcp cases are visible in one place and the helper indirection goes away.
f682807 to
37f5d1f
Compare
|
Quick review on my laptop and the code looks right. Its a bit hard to follow so I want to do a deep 2 screen check on my desktop before approving. Than have AI check to make sure we didn't miss any review comments. I'll also ask the bot to do another pass. |
|
@bluetoothbot review |
| ip_addr = options.get("host", "") | ||
| ip_port = int(options["port"]) if "port" in options else 0 | ||
| try: | ||
| ip_port = int(options["port"]) if "port" in options else 0 |
There was a problem hiding this comment.
Not sure why this isn't ip_port = int(options.get("port", 0)).
Fixes #569.
Summary
The aio
MessageBusconstructor used to perform a blockingsocket.connect()insideBaseMessageBus.__init__()(via_setup_socket). That violates async hygiene and trips blocking-call detectors like blockbuster in async contexts (notably Home Assistant Supervisor).This PR moves socket creation and connection for the aio bus into
MessageBus.connect(), soMessageBus()is now side-effect-free and connection errors surface fromconnect()instead of construction. The glib bus is intentionally left with its blocking-in-__init__behavior — only the shared helper has been factored out — because we don't know who relies on the existing glib semantics and didn't want to break them on a hunch.BREAKING CHANGE (aio only)
Connection errors that previously came from
aio.MessageBus(...)instantiation are now raised fromconnect(). Code using the common pattern is unaffected:Code that wraps the constructor itself in
try/exceptneeds to move thetryto coverconnect(). The glib bus is unchanged.Approach
BaseMessageBus._create_socket_for_transport(transport, options)(@staticmethod) creates an unconnected socket+stream and returns the address. Used by bothaio.MessageBus.connect()andglib.MessageBus.__init__(). Options are parsed before any resources are allocated so malformed addresses fail without leaking.connect(): iterates transports withawait loop.sock_connect(), usesExitStackper attempt so aCancelledErrordoesn't leak the freshly-created sock/stream. The unix→tcp fallback is preserved; on total failure the last error is raised (aREVISIT:comment notes we could surface all of them viaExceptionGrouponce Python 3.10 is dropped).connect()post-connect cleanup: writer creation,_authenticate(), and reader registration are wrapped intry/except BaseExceptionthat closes the socket/stream and clears_sock/_stream/_fd/_writeron failure, so an auth error no longer leaks the socket._writer: constructed insideconnect()after the socket is set, instead of being created in__init__withNonereferences and back-patched.__init__just setsself._writer = None.__init__inlines the per-transport setup loop using the same shared helper. No_setup_socketmethod on the base class anymore... versionchanged:: v5.0.0notes onaio.MessageBus.connect()and the class docstring._create_socket_for_transportdocuments bothInvalidAddressErrorandValueError(for non-integertcp:port=...).Relationship to #592
#592 (
async with buscontext manager) is easier on top of this PR —__aenter__no longer has to work around the constructor connecting the socket. dlech confirmed in #592 that this should land first.Tests
test_aio_connect_falls_back_between_transports— multi-transport address, all fail, last error raised.test_aio_connect_cleanup_after_socket_connected— auth failure after socket is connected clears all bus state.test_aio_connect_hello_error_propagates_and_clears_state— error reply to the initial Hello tears down sock/stream/fd/writer.test_create_socket_for_transport_{unix,tcp}_makefile_failure_closes_socket— verifies the makefile-failure cleanup path.test_glib_low_levelgets a transport-fallback test covering the glib path.test_send_replyMockBusupdated to populate_sock/_streaminconnect()rather than__init__, matching the new pattern.Fixes blocking-I/O detection errors like:
Fixes: #569