Wednesday, August 11, 2010

The gen_client API overview

I've been using gen_client in few real projects, which helped to weed out many bugs, shape the design and evaluate features. I feel that it's now ready for public usage, so I would like to briefly explain some concepts used by gen_client and show how they might help to make XMPP client programming easier.

Starting the client.

start(Jid, Host, Port, Password) 
start(Jid, Host, Port, Password, Options)
start(Account, Domain, Host, Port, Password, Options)
start(Account, Domain, Resourse, Host, Port, Password, Options)

Above calls create a client session and return {ok, ClientRef} tuple in case everything went well. ClientRef (which is a Pid of gen_server process associated with the client session) then can be used to make gen_client API calls. For example:

{ok, Client} = gen_client:start("gen_client@jabber.ru", "jabber.ru", 5222, "test",
[{debug, true}, {presence, {true, "I'm online."}}, {reconnect, 15000}]),
gen_client:add_plugin(Client, disco_plugin, [test_disco, []]).

Options describe different aspect of the client, such as (default values go first):

  • {debug, false | true} - printing out debug info;
  • {presence, {true, Msg} | false} - should the client send a presence, and if yes, specify the presence message;
  • {reconnect, Timeout} - should the client reconnect after losing the connection, and the timeout for reconnection;
  • {log_in, true | false} - should the client log in automatically; useful when you want to choose between logging in and registration;
  • more to come... 


Handlers


add_handler(Client, Handler)
add_handler(Client, Handler, Priority)
remove_handler(Client, HandlerKey)


Handler is a callback function that handles incoming stanzas. Handlers can be added to (or removed from) the client session at will. add_handler/2,3 calls return the key value which could be used to remove the handler later, if needed. Each handler could be assigned a priority at the time it's added to the client. When stanza is received, it will be applied to the chain of handlers according to handlers' priority and/or output. For example, a handler can interrupt the chain of subsequent handler calls by returning stop.

Plugins


add_plugin(Client, Plugin, Args)
add_plugin(Client, Plugin, Args, Priority)
remove_plugin(Client, PluginKey) 


Theoretically handlers should be sufficient for processing of any incoming stanzas.
However, in many cases handling of stanzas involves fair amount of repetitive code. For example, responses to discovery requests (disco_info and disco_items) have to have certain headers, workflow sequences that utilize ad-hoc commands need to keep state etc. Plugins are meant to encapsulate such  common processing blocks, letting developers to focus on specifics.
To be a plugin, the module has to implement gen_client_plugin behavior, namely init/1, terminate/1 and handle/3 functions.  The gen_client:add_plugin/3 makes  The idea is that handle/3 will contain the bulk of boilerplate code, at the same time letting the plugin users to customize it by passing arguments to either init/1 or handle/3 functions.
If above explanation sounds somewhat obscure, hopefully looking at the code of disco_plugin and test example in test_gen_client:test/0 will make things a bit clearer. The other available plugin is  adhoc_plugin, which I am planning to talk about in more details in following posts.

Blocking and non-blocking requests

XMPP messaging is asynchronous, and that's great. However, quite often your code needs to get an immediate response before the flow can continue. The examples are discovery, ping and command requests, pubsub retrival requests and many others. Of course, it's possible to allocate a callback for the expected response, but this does make the code harder to write and understand.
gen_client supports both asynchronous and synchronous requests. The former is simply a wrapper of exmpp:send_packet/2:

send_packet(ClientRef, Packet)

where ClientRef is a client session reference created by one of gen_client:start functions (see above).

The synchronous request is a bit more interesting. Here are definitions:

send_sync_packet(Client, Packet, Timeout)
send_sync_packet(Client, Packet, Trigger, Timeout)

Here's how it works:

The calling process sends the packet and timeout value to the client session process. The client session process creates a temporary handler that would "look" for "matching" incoming message, and then sends the packet to the server and waits for the response to arrive within specified timeout.
How do we describe "matching"? By defining a "trigger" function that tests incoming message against some condition. For requests, "matching" means that response message will have the same id attribute as the request. So send_sync_packet/3 does just that: creates a function that looks at id attribute of incoming message and if it happens to be equal to request id, signals the client session that response has arrived. 
So as we can see, send_sync_packet/3 could be treated as "send iq and wait for response", which makes it somewhat close to sendIQ function in Strophe.js
In case your "matching criteria" is different from simple id matching, you can use send_sync_packet/4 that allows to define arbitrary "trigger" function.

Important: even though the calling process will be blocked, the active handlers will still work, because they will be called in separate (exmpp controlling) process. In other words, while your main process waits for response to a particular request, another kinds of incoming messages will still be handled in parallel.

That's all for now. Make sure to check out the code and please let me know what do you think.