bluez: Allow accessing Bleak from multiple individual event loops (not simultaneously) #1031
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Hi! I have a small patch to the Bluez backend for your consideration.
Background
In the default configuration, pytest-asyncio creates a new event loop to execute each test coroutine in a clean environment. Similarly, there can be other cases where more than one event loop exists during the lifetime of a Python program.
If Bleak object are instantiated in the context of more than one active event loop then a RuntimeError appears in the second event loop, thrown from the dbus-fast layer. This happens even if all Bleak objects associated with the first event loop have already been garbage collected.
Steps to Reproduce
If pytest-asyncio is also installed:
Error looks like:
Root Cause
get_global_bluez_manager()
will be used by all callers.dbus_fast.aio.message_bus.MessageBus
object and this saves a reference to the current event loop at the time it is constructed, and uses it to call self._loop.create_future() as seen above.As far as I can tell, MessageBus needs to hold a lifetime reference to a single event loop because of MessageWriter, which is associated with one particular event loop.
This solution
Save the current event loop associated when
get_global_bluez_manager()
is first called. If it's called again from a new event loop, and the saved event loop is closed, then instantiate a new BlueZManager instance.I'm not sure that is the best solution, but it does allow continuing to use Bleak in the context of a new event loop and with pytest-asyncio in the default configuration.
It's still not possible to have Bleak objects accessed in the context of more than one event loop simultaneously, but the code will now log a warning if this happens.
Other possible solutions
I have another branch which implements a per-event-loop cache of the Manager objects. This works alright, but there are still concurrency issues with trying to use Bleak from multiple event loops simultaneously. I don't know enough about DBus to quickly debug these, and it seems like a much less practical use case, but I'm happy to look into it more if you prefer that approach.
Another possible solution would be to have a global deinit function for Bleak, that could be called between executions? It's possible to mock this up now by calling
del get_global_bluez_manager.instance
and this works, so a clean wrapper around this would also solve this problem (and leaves managing any leftover Bleak objects to the caller.)