I've found myself writing this several times recently in the context of Telepathy, so I thought I'd make a blog post that I can refer people to.
D-Bus is defined (by the wire protocol spec)
to be an asynchronous message-passing system, and libdbus behaves
accordingly; there are no
blocking calls at the wire protocol level. However, libdbus provides
a "blocking" API (dbus_do_something_and_block
), via a mechanism I'll
refer to as "pseudo-blocking" for the purposes of this post. Most D-Bus
bindings (at least dbus-glib, dbus-python and QtDBus) expose this
pseudo-blocking behaviour to library users.
These pseudo-blocking calls work like this:
- send a method call message (call it M)
- while a reply to M has not been received:
- select() or poll() on the D-Bus socket, ignoring all other I/O
- whenever a whole message has been received:
- check whether the message is a reply to M
- if it is (call it R), stop
- if it is not, put it on the incoming message queue
The messages received between M and R are delivered when the main loop is next entered.
This can cause a number of problems:
Messages are re-ordered: messages received between M and R aren't delivered until after the reply, violating the ordering guarantee that the D-Bus daemon usually provides.
(This causes practical problems if a signal indicating object destruction is delayed - the client gets a method reply "UnknownMethod", has to guess that this is because the object has vanished, and can't know why it vanished until the signal indicating its destruction arrives with more details.)
The client is completely unresponsive until the service replies - if the service has somehow got wedged (e.g. telepathy-gabble is meant to be purely non-blocking and asynchronous, but there are cases where it will do blocking I/O on SSL connections due to Loudmouth bugs), the client will be unresponsive for (by default) 25 seconds until the call times out.
(Clients shouldn't crash or lock up, whatever happens to the services they depend on.)
The client can't parallelize calls - if a signal (e.g. an incoming Text message in Telepathy) causes method calls to be made, a client that uses pseudo-blocking calls can't even start processing the next message until those method calls return
If two processes make pseudo-blocking calls on each other, deadlock occurs. This is particularly tricky in the presence of a plugin architecture and shared D-Bus connections - a plugin that "knows" it's a client and not a service, and a plugin in the same process that "knows" it's a service and not a client, can end up sharing a connection, resulting in a process that is both a service and a client (and hence deadlock-prone).
(We've seen this happen in the OLPC Sugar environment and on Nokia internet tablets; it's not just a theoretical concern.)
As a result, telepathy-glib's code generation mechanism does not generate any pseudo-blocking code. There are several alternative modes of operation that we do support:
Fully asynchronous: call a method now, pass it a callback, get your callback called from the main loop later. This can be awkward to program with, but is the only way forward for most non-trivial projects.
Re-entrant main loop: re-enter the main loop, processing all messages (D-Bus, GUI, network, anything), and run it until the reply has been received. This is useful for trivial projects (like telepathy-glib's regression tests), but dangerous for non-trivial projects (like Empathy), and I'm somewhat regretting implementing this.
Ignoring the reply: when appropriate (it rarely is), you can make an asynchronous call with
callback = NULL
and the reply will be ignored completely
We make a couple of narrowly targeted exceptions to the "no pseudo-blocking" policy in internal code, by allowing telepathy-glib internals to make a small number of pseudo-blocking method calls to the dbus-daemon (which is the one component we can definitely trust to return results promptly).
Here are some examples of the same strategies in other libraries:
Pseudo-blocking: dbus-python method calls with no special keyword arguments; dbus-glib
dbus_g_proxy_call
; QDBus calls with modeQDBus::Block
Fully asynchronous: dbus-python method calls with
reply_callback
anderror_callback
keyword arguments; dbus-glibdbus_g_proxy_begin_call
;QDBusConnection::callWithCallback
Re-entrant: QDBus calls with mode
QDBus::BlockWithGui
Ignoring reply: dbus-python method calls with
ignore_reply
keyword argument; QDBus calls with modeQDBus::NoWaitForReply