Asynchronous operations

This is internal documentation of Tinymail's libtinymail-camel. It has very few to do with what application developers need to know to develop an application on top of this.

Asynchronous operations in Tinymail end with _async. They obviously don't block so this means that something in the background must be happening. In Tinymail's libtinynmail-camel implementation, all network I/O is blocking. This means that a thread must deal with this. For that thread, a queue per store account has been put in place. The queue type for this is called TnyCamelQueue. There are actually two queues for each store account: one for normal operations and one for getting messages.

Asynchronous operations allow the developer to plug-in a callback handler. This is an handler that will happen in the mainloop of the application once the operation is finished. In Tinymail will the queue wait for the return of this callback handler before iterating to the next item in the queue. New _async operations will simply be added to the queue if they occur.

Each queue has only one thread and can therefore only do one operation at the time. This ensures locking order on the socket with the service (in case a remote network connection is needed to consume the service, of course).

Connecting

Connecting in Tinymail's libtinymail-camel is an operation like any other operation on the queue. This means that all operations on the queue must wait for the connection operation to be finished. At the point of wanting to connect is the "start connecting" operation simply put on the queue. That point is usually when the TnyDevice of the application emits a connection_changed signal. It can also happen at the initialization of the application, in case the TnyDevice's tny_device_is_online immediately returns TRUE.

Operations

Operations have a body and a callback. The body is implemented by Tinymail's libtinymail-camel. The callback is what the application developer implements. In Tinymail's libtinymail-camel will the queue for an operation wait for the callback to return before continuing to the next item on the queue.

Cancelling

Only a select group of operations are cancellable. Cancelling can either happen during an operation, or before the operation happened. In case during the cancel-callback and cancel-destroy wont be called, the normal ones that are to be called in your worker will be. Else the cancel-destroy and cancel-callback will be called.

How does this work?

The TnyCamelQueue type

The TnyCamelQueue type is a serialized queue. It can only do one thing at the same time. It ensures that all items will be processed. It does not ensure a priority or order, just that it will eventually process the items. The API, although only available internally in Tinymail's libtinymail-camel, looks like this:

TnyCamelQueue* _tny_camel_queue_new (void);
void _tny_camel_queue_launch (TnyCamelQueue *queue, GThreadFunc func, gpointer data);

They are created in the instance init of TnyCamelStoreAccount:

static void
tny_camel_store_account_instance_init (GTypeInstance *instance, gpointer g_class)
{
	TnyCamelStoreAccount *self = (TnyCamelStoreAccount *)instance;
	TnyCamelStoreAccountPriv *priv = TNY_CAMEL_STORE_ACCOUNT_GET_PRIVATE (self);

	priv->queue = _tny_camel_queue_new ();
	priv->msg_queue = _tny_camel_queue_new ();
	...
}

Dissecting the async operations themselves

When an async operation is called for, internally it goes like this:

This are some infrastructural pieces. Like a type that contains all the info that we want to pass. The "actual_callback" is of course usually part of the public API of Tinymail:

typedef void (*actual_callback) (MyType *self, ...);

typedef struct {
	MyType *self;
	TnySessionCamel *session;
	actual_callback callback;
	gpointer user_data;

	GCond* condition;      /* A conditional so that the queue 
                                * will wait for the callback */
	gboolean had_callback; /* In case the callback was faster 
                                * (in case the mainloop got scheduled 
                                * by the kernel, and the queue's thread
                                * got interrupted) */
	GMutex *mutex;         /* Mutex to protect had_callback */

	gboolean cancelled;

} my_op_info;

This is the callback that we internally have. It'll call the user's actual callback after setting the ui lock.

static gboolean 
my_op_async_callback (gpointer user_data)
{
	my_op_info *info = user_data;

	/* Launching the actual callback. This is implemented by the application
	 * developer. We have no control over the content of this method. */

	if (info->callback) {
		tny_lockable_lock (info->session->priv->ui_lock);
		info->actual_callback (info->self, info->cancelled, info->user_data);
		tny_lockable_unlock (info->session->priv->ui_lock);
	}
	return;
}

static gboolean 
my_op_async_cancelled_callback (gpointer user_data)
{
	my_op_info *info = user_data;
	if (info->callback) {
		tny_lockable_lock (info->session->priv->ui_lock);
		info->actual_callback (info->self, TRUE, info->user_data);
		tny_lockable_unlock (info->session->priv->ui_lock);
	}
	return;
}

This one destroys the callback above and cleans up the info that we've been passing around:

static void
my_op_async_destroyer (gpointer user_data)
{
	my_op_info *info = user_data;

	/* We take away that reference that we added below. */

	g_object_unref (info->self);
	camel_object_unref (info->session);

	/* During destruction of the idle, we'll also let the 
	 * conditional continue: it's save for the queue to 
	 * continue */

	g_mutex_lock (info->mutex);
	g_cond_broadcast (info->condition);
	info->had_callback = TRUE;
	g_mutex_unlock (info->mutex);
}

static void
my_op_async_cancelled_destroyer (gpointer user_data)
{
	my_op_info *info = user_data;
	g_object_unref (info->self);
	camel_object_unref (info->session);
	g_slice_free (my_op_info, info);
}

This is the actual workhorse. It's suffixed by "thread" for historical reasons (it was not a queue function in the beginning of Tinymail). It happens in a queue which has one thread. The suffix "thread" is not really bad because it does actually happen in this thread indeed. Just don't g_thread_exit(x) here, else you'll also exit the queue's thread (and you don't want that behaviour from the queue, of course).:

static gpointer
my_op_async_thread (gpointer user_data)
{
	my_op_info *info = user_data;

	info->mutex = g_mutex_new ();
	info->condition = g_cond_new ();
	info->had_callback = FALSE;

	/* execute_callback is a g_idle_add_full wrapper in 
	 * Tinymail's libtinymail-camel. */

	/* We are actually throwing info->self to another thread
	 * here. Strictly spoken we should therefore add a reference.
	 * Because we wait for the conditional here, we don't have
	 * to do it here (this thread wont quit until the callback
	 * is completely finished anyway). */

	execute_callback (info->depth, G_PRIORITY_DEFAULT, 
  	 	my_op_async_callback, info, 
 	 	my_op_async_destroyer);

	/* We wait for the conditional. Note that this happens
	 * in the queue's context (in the queue's thread). We
	 * block the queue from doing any work until the application
	 * developer's callback is over and done. */

	g_mutex_lock (info->mutex);
	if (!info->had_callback)
		g_cond_wait (info->condition, info->mutex);
	g_mutex_unlock (info->mutex);

	g_mutex_free (info->mutex);
	g_cond_free (info->condition);

	g_slice_free (my_op_info, info);


	/* Reaching this means that we're finished. We don't do 
	 * the g_thread_exit (NULL) here because the queue's thread
	 * would, if we'd do this, actually exit. This is of course
	 * unwanted behaviour. We do the g_thread_exit in TnyCamelQueue
	 * when there are no more items to process in the queue. */

	return NULL;
}

This is the public API. It creates the queue item and puts it on the queue:

static void
my_op_async (MyType *self, actual_callback callback, gpointer user_data)
{
	MyTypePriv *priv = MY_TYPE_GET_PRIVATE (self);
	my_op_info *info = g_slice_new (my_op_info);

	/* For thread safety it's vital to add references when passing
	 * information to another thread. The queue is another thread,
	 * so we need to add a reference! */

	info->self = g_object_ref (self);
	info->session = priv->session;
	camel_object_ref (info->session);

	info->callback = callback;
	info->user_data = user_data;
	info->condition = NULL;

	/* Quite simple: collect data, throw a function on the queue
	 * together with the collected data. */

	_tny_camel_queue_launch (TNY_FOLDER_PRIV_GET_QUEUE (priv), 
		my_op_async_thread,              	/* The worker            */
		my_op_async_cancelled_callback, 	/* In case cancelled     */
		my_op_async_cancelled_destroyer, 	/* In case cancelled     */
		&info->cancelled,                	/* Variable that will be * 
                                                 	 * written if cancelled  */
		info, __FUNCTION__);             	/* The info and for      *
                                                 	 * debugging,a constchar */

}

Related pages