Skip to content

fix!: defer socket.connect() from __init__ to connect()#570

Draft
agners wants to merge 2 commits intoBluetooth-Devices:mainfrom
agners:fix-sync-io-in-messagebus-init
Draft

fix!: defer socket.connect() from __init__ to connect()#570
agners wants to merge 2 commits intoBluetooth-Devices:mainfrom
agners:fix-sync-io-in-messagebus-init

Conversation

@agners
Copy link
Collaborator

@agners agners commented Dec 2, 2025

NOTE: I just had AI a go at #569. I guess this would need (another) major bump 😰 . Not sure if the GLib implementation really needs to be that complicated 🤷 .

BREAKING CHANGE: 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:

  • _setup_socket() now only creates the socket and stores the connection address, but does not connect. Socket is set to non-blocking immediately.
  • Added _connect_socket() for synchronous blocking connection (unused by current implementations but available for sync use cases)
  • aio MessageBus.connect() now uses await loop.sock_connect() for proper async socket connection
  • glib MessageBus.connect() now initiates non-blocking socket connection and uses a GLib source to wait for completion if needed
  • Added _sock_connect_address to slots and .pxd file
  • Updated tests to expect connection errors from connect() not init

Fixes blocking I/O detection errors like:
blockbuster.BlockingError: Blocking call to socket.socket.connect

🤖 Generated with Claude Code

Fixes: #569

@codecov
Copy link

codecov bot commented Dec 2, 2025

Codecov Report

❌ Patch coverage is 42.42424% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.36%. Comparing base (fb38783) to head (e7b2dc1).

Files with missing lines Patch % Lines
src/dbus_fast/glib/message_bus.py 24.44% 34 Missing ⚠️
src/dbus_fast/message_bus.py 75.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #570      +/-   ##
==========================================
- Coverage   85.31%   84.36%   -0.96%     
==========================================
  Files          28       28              
  Lines        3433     3479      +46     
  Branches      601      604       +3     
==========================================
+ Hits         2929     2935       +6     
- Misses        312      351      +39     
- Partials      192      193       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq
Copy link

codspeed-hq bot commented Dec 2, 2025

CodSpeed Performance Report

Merging #570 will not alter performance

Comparing agners:fix-sync-io-in-messagebus-init (e7b2dc1) with main (fb38783)

Summary

✅ 6 untouched

@agners
Copy link
Collaborator Author

agners commented Dec 2, 2025

Probably most problematic is that Exceptions now get raised with connect(). But I'd guess that most users use the code with something like this:

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.

BREAKING CHANGE: 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:
- _setup_socket() now only creates the socket and stores the connection
  address, but does not connect. Socket is set to non-blocking immediately.
- Added _connect_socket() for synchronous blocking connection (unused by
  current implementations but available for sync use cases)
- aio MessageBus.connect() now uses await loop.sock_connect() for proper
  async socket connection
- glib MessageBus.connect() now initiates non-blocking socket connection
  and uses a GLib source to wait for completion if needed
- Added _sock_connect_address to __slots__ and .pxd file
- Updated tests to expect connection errors from connect() not __init__

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>
@agners agners force-pushed the fix-sync-io-in-messagebus-init branch from e4bc481 to b9ada81 Compare December 2, 2025 18:11
return GLib.SOURCE_CONTINUE


class _ConnectSource(_GLibSource):
Copy link
Member

@bdraco bdraco Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe keep glib the same as it was before and only fix asyncio to contain scope. We could pass a flag to defer connect for asyncio.

I don't think any of the maintainers use the glib version so we try not to touch it

Copy link
Member

@dlech dlech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 the 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.

Comment on lines 717 to 744
@@ -726,25 +732,31 @@ def _setup_socket(self) -> None:
if "port" in options:
ip_port = int(options["port"])

try:
self._sock.connect((ip_addr, ip_port))
self._sock.setblocking(False)
except Exception as e:
last_err = e
else:
stack.pop_all()
return
# Store connect address for later; don't connect yet
self._sock_connect_address = (ip_addr, ip_port)
self._sock.setblocking(False)
stack.pop_all()
return

else:
raise InvalidAddressError(
f"got unknown address transport: {transport}"
)
raise InvalidAddressError(f"got unknown address transport: {transport}")

# Should not normally happen, but just in case
raise TypeError("empty list of bus addresses given") # pragma: no cover
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the try/excepts here breaks the automatic fallback to "tcp" if connecting the "unix" transport fails. This entire method will need to be moved to the connect method.

the DBus daemon failed.
- :class:`Exception` - If there was a connection error.
"""
try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs for this method need a ..versionchanged: X.Y.Z and to have the :raises: section updated.

And we should add ..versionchanged: X.Y.Z to class MessageBus as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blocking I/O in MessageBus init

3 participants

Comments