
2001-07-02 Peter Williams <peterw@ximian.com> * README.async: Add a warning about how wrong this document is. svn path=/trunk/; revision=10695
366 lines
14 KiB
Plaintext
366 lines
14 KiB
Plaintext
*******
|
|
This document is absurdly, obscenely out of date. Don't read it.
|
|
|
|
-- Peter Williams 7/2/2001
|
|
*******
|
|
|
|
Asynchronous Mailer Information
|
|
Peter Williams <peterw@helixcode.com>
|
|
8/4/2000
|
|
|
|
1. INTRODUCTION
|
|
|
|
It's pretty clear that the Evolution mailer needs to be asynchronous in
|
|
some manner. Blocking the UI completely on IMAP calls or large disk reads
|
|
is unnacceptable, and it's really nice to be able to thread the message
|
|
view in the background, or do other things while a mailbox is downloading.
|
|
|
|
The problem in making Evolution asynchronous is Camel. Camel is not
|
|
asynchronous for a few reasons. All of its interfaces are synchronous --
|
|
calls like camel_store_get_folder, camel_folder_get_message, etc. can
|
|
take a very long time if they're being performed over a network or with
|
|
a large mbox mailbox file. However, these functions have no mechanism
|
|
for specifying that the operation is in progress but not complete, and
|
|
no mechanism for signaling when to operation does complete.
|
|
|
|
2. WHY I DIDN'T MAKE CAMEL ASYNCHRONOUS
|
|
|
|
It seems like it would be a good idea, then, to rewrite Camel to be
|
|
asynchonous. This presents several problems:
|
|
|
|
* Many interfaces must be rewritten to support "completed"
|
|
callbacks, etc. Some of these interfaces are connected to
|
|
synchronous CORBA calls.
|
|
* Everything must be rewritten to be asynchonous. This includes
|
|
the CamelStore, CamelFolder, CamelMimeMessage, CamelProvider,
|
|
every subclass thereof, and all the code that touches these.
|
|
These include the files in camel/, mail/, filter/, and
|
|
composer/. The change would be a complete redesign for any
|
|
provider implementation.
|
|
* All the work on providers (IMAP, mh, mbox, nntp) up to this
|
|
point would be wasted. While they were being rewritten
|
|
evolution-mail would be useless.
|
|
|
|
However, it is worth noting that the solution I chose is not optimal,
|
|
and I think that it would be worthwhile to write a libcamel2 or some
|
|
such thing that was designed from the ground up to work asynchronously.
|
|
Starting fresh from such a design would work, but trying to move the
|
|
existing code over would be more trouble than it's worth.
|
|
|
|
3. WHY I MADE CAMEL THREADED
|
|
|
|
If Camel was not going to be made asynchronous, really the only other
|
|
choice was to make it multithreaded. [1] No one has been particularly
|
|
excited by this plan, as debugging and writing MT-safe code is hard.
|
|
But there wasn't much of a choice, and I believed that a simple thread
|
|
wrapper could be written around Camel.
|
|
|
|
The important thing to know is that while Camel is multithreaded, we
|
|
DO NOT and CANNOT share objects between threads. Instead,
|
|
evolution-mail sends a request to a dispatching thread, which performs
|
|
the action or queues it to be performed. (See section 4 for details)
|
|
|
|
The goal that I was working towards is that there should be no calls
|
|
to camel made, ever, in the main thread. I didn't expect to and
|
|
didn't do this, but that was the intent.
|
|
|
|
[1]. Well, we could fork off another process, but they share so much
|
|
data that this would be pretty impractical.
|
|
|
|
4. IMPLEMENTATION
|
|
|
|
a. CamelObject
|
|
|
|
Threading presented a major problem regarding Camel. Camel is based
|
|
on the GTK Object system, and uses signals to communicate events. This
|
|
is okay in a nonthreaded application, but the GTK Object system is
|
|
not thread-safe.
|
|
|
|
Particularly, signals and object allocations use static data. Using
|
|
either one inside Camel would guarantee that we'd be stuck with
|
|
random crashes forevermore. That's Bad (TM).
|
|
|
|
There were two choices: make sure to limit our usage of GTK, or stop
|
|
using the GTK Object system. I decided to do the latter, as the
|
|
former would lead to a mess of "what GTK calls can we make" and
|
|
GDK_THREADS_ENTER and accidentally calling UI functions and upgrades
|
|
to GTK breaking everything.
|
|
|
|
So I wrote a very very simple CamelObject system. It had three goals:
|
|
|
|
* Be really straightforward, just encapsulate the type
|
|
heirarchy without all that GtkArg silliness or anything.
|
|
* Be as compatible as possible with the GTK Object system
|
|
to make porting easy
|
|
* Be threadsafe
|
|
|
|
It supports:
|
|
|
|
* Type inheritance
|
|
* Events (signals)
|
|
* Type checking
|
|
* Normal refcounting
|
|
* No unref/destroy messiness
|
|
* Threadsafety
|
|
* Class functions
|
|
|
|
The entire code to the object system is in camel/camel-object.c. It's
|
|
a naive implementation and not full of features, but intentionally that
|
|
way. The main differences between GTK Objects and Camel Objects are:
|
|
|
|
* s,gtk,camel,i of course
|
|
* Finalize is no longer a GtkObjectClass function. You specify
|
|
a finalize function along with an init function when declaring
|
|
a type, and it is called automatically and chained automatically.
|
|
* Declaring a type is a slightly different API
|
|
* The signal system is replaced with a not-so-clever event system.
|
|
Every event is equivalent to a NONE__POINTER signal. The syntax
|
|
is slightly different: a class "declares" an event and specifies
|
|
a name and a "prep func", that is called before the event is
|
|
triggered and can cancel it.
|
|
* There is only one CamelXXXClass in existence for every type.
|
|
All objects share it.
|
|
|
|
There is a shell script, tools/make-camel-object.sh that will do all of
|
|
the common substitutions to make a file CamelObject-compatible. Usually
|
|
all that needs to be done is move the implementation of the finalize
|
|
event out of the class init, modify the get_type function, and replace
|
|
signals with events.
|
|
|
|
Pitfalls in the transition that I ran into were:
|
|
|
|
* gtk_object_ref -> camel_object_ref or you coredump
|
|
* some files return 'guint' instead of GtkType and must be changed
|
|
* Remove the #include <gtk/gtk.h>
|
|
* gtk_object_set_datas must be changed (This happened once; I
|
|
added a static hashtable)
|
|
* signals have to be fudged a bit to match the gpointer input
|
|
* the BAST_CASTARD option is on, meaning failed typecasts will
|
|
return NULL, almost guaranteeing a segfault -- gets those
|
|
bugs fixed double-quick!
|
|
|
|
b. API -- mail_operation_spec
|
|
|
|
I worked by creating a very specific definition of a "mail operation"
|
|
and wrote an engine to queue and dispatch them.
|
|
|
|
A mail operation is defined by a structure mail_operation_spec
|
|
prototyped in mail-threads.h. It comes in three logical parts -- a
|
|
"setup" phase, executed in the main thread; a "do" phase, executed
|
|
in the dispatch thread; and a "cleanup" phase, executed in the main
|
|
thread. These three phases are guaranteed to be performed in order
|
|
and atomically with respect to other mail operations.
|
|
|
|
Each of these phases is represented by a function pointer in the
|
|
mail_operation_spec structure. The function mail_operation_queue() is
|
|
called and passed a pointer to a mail_operation_spec and a user_data-style
|
|
pointer that fills in the operation's parameters. The "setup" callback
|
|
is called immediately, though that may change.
|
|
|
|
Each callback is passed three parameters: a pointer to the user_data,
|
|
a pointer to the "operation data", and a pointer to a CamelException.
|
|
The "operation data" is allocated automatically and freed when the operation
|
|
completes. Internal data that needs to be shared between phases should
|
|
be stored here. The size allocated is specified in the mail_operation_spec
|
|
structure.
|
|
|
|
Because all of the callbacks use Camel calls at some point, the
|
|
CamelException is provided as utility. The dispatcher will catch exceptions
|
|
and display error dialogs, unlike the synchronous code which lets
|
|
exceptions fall through the cracks fairly easily.
|
|
|
|
I tried to implement all the operations following this convention. Basically
|
|
I used this skeleton code for all the operations, just filling in the
|
|
specifics:
|
|
|
|
===================================
|
|
|
|
typedef struct operation_name_input_s {
|
|
parameters to operation
|
|
} operation_name_input_t;
|
|
|
|
typedef struct operation_name_data_s {
|
|
internal data to operation, if any
|
|
(if none, omit the structure and set opdata_size to 0)
|
|
} operation_name_data_t;
|
|
|
|
static gchar *describe_operation_name (gpointer in_data, gboolean gerund);
|
|
static void setup_operation_name (gpointer in_data, gpointer op_data, CamelException *ex);
|
|
static void do_operation_name (gpointer in_data, gpointer op_data, CamelException *ex);
|
|
static void cleanup_operation_name (gpointer in_data, gpointer op_data, CamelException *ex);
|
|
|
|
static gchar *describe_operation_name (gpointer in_data, gboolean gerund)
|
|
{
|
|
operation_name_input_t *input = (operation_name_input_t *) in_data;
|
|
|
|
if (gerund) {
|
|
return a g_strdup'ed string describing what we're doing
|
|
} else {
|
|
return a g_strdup'ed string describing what we're about to do
|
|
}
|
|
}
|
|
|
|
static void setup_operation_name (gpointer in_data, gpointer op_data, CamelException *ex)
|
|
{
|
|
operation_name_input_t *input = (operation_name_input_t *) in_data;
|
|
operation_name_data_t *data = (operation_name_data_t *) op_data;
|
|
|
|
verify that parameters are valid
|
|
|
|
initialize op_data
|
|
|
|
reference objects
|
|
}
|
|
|
|
static void do_operation_name (gpointer in_data, gpointer op_data, CamelException *ex)
|
|
{
|
|
operation_name_input_t *input = (operation_name_input_t *) in_data;
|
|
operation_name_data_t *data = (operation_name_data_t *) op_data;
|
|
|
|
perform camel operations
|
|
}
|
|
|
|
static void cleanup_operation_name (gpointer in_data, gpointer op_data, CamelException *ex)
|
|
{
|
|
operation_name_input_t *input = (operation_name_input_t *) in_data;
|
|
operation_name_data_t *data = (operation_name_data_t *) op_data;
|
|
|
|
perform UI updates
|
|
|
|
free allocations
|
|
|
|
dereference objects
|
|
}
|
|
|
|
static const mail_operation_spec op_operation_name = {
|
|
describe_operation_name,
|
|
sizeof (operation_name_data_t),
|
|
setup_operation_name,
|
|
do_operation_name,
|
|
cleanup_operation_name
|
|
};
|
|
|
|
void
|
|
mail_do_operation_name (parameters)
|
|
{
|
|
operation_name_input_t *input;
|
|
|
|
input = g_new (operation_name_input_t, 1);
|
|
|
|
store parameters in input
|
|
|
|
mail_operation_queue (&op_operation_name, input, TRUE);
|
|
}
|
|
|
|
===========================================
|
|
|
|
c. mail-ops.c
|
|
|
|
Has been drawn and quartered. It has been split into:
|
|
|
|
* mail-callbacks.c: the UI callbacks
|
|
* mail-tools.c: useful sequences wrapping common Camel operations
|
|
* mail-ops.c: implementations of all the mail_operation_specs
|
|
|
|
An important part of mail-ops.c are the global functions
|
|
mail_tool_camel_lock_{up,down}. These simulate a recursize mutex around
|
|
camel. There are an extreme few, supposedly safe, calls to Camel made in
|
|
the main thread. These functions should go around evey call to Camel or
|
|
group thereof. I don't think they're necessary but it's nice to know
|
|
they're there.
|
|
|
|
If you look at mail-tools.c, you'll notice that all the Camel calls are
|
|
protected with these functions. Remember that a mail tool is really
|
|
just another Camel call, so don't use them in the main thread either.
|
|
|
|
All the mail operations are implemented in mail-ops.c EXCEPT:
|
|
|
|
* filter-driver.c: the filter_mail operation
|
|
* message-list.c: the regenerate_messagelist operation
|
|
* message-thread.c: the thread_messages operation
|
|
|
|
d. Using the operations
|
|
|
|
The mail operations as implemented are very specific to evolution-mail. I
|
|
was thinking about leaving them mostly generic and then allowing extra
|
|
callbacks to be added to perform the more specific UI touches, but this
|
|
seemed kind of pointless.
|
|
|
|
I basically looked through the code, found references to Camel, and split
|
|
the code into three parts -- the bit before the Camel calls, the bit after,
|
|
and the Camel calls. These were mapped onto the template, given a name,
|
|
and added to mail-ops.c. Additionally, I simplified the common tasks that
|
|
were taken care of in mail-tools.c, making some functions much simpler.
|
|
|
|
Ninety-nine percent of the time, whatever operation is being done is being
|
|
done in a callback, so all that has to be done is this:
|
|
|
|
==================
|
|
|
|
void my_callback (GtkObject *obj, gchar *uid)
|
|
{
|
|
camel_do_something (uid);
|
|
}
|
|
|
|
==== becomes ====
|
|
|
|
void my_callback (GtkObject *obj, gchar *uid)
|
|
{
|
|
mail_do_do_something (uid);
|
|
}
|
|
|
|
=================
|
|
|
|
There are, however, a few belligerents. Particularly, the function
|
|
mail_uri_to_folder returns a CamelFolder and yet should really be
|
|
asynchronous. This is called in a CORBA call that is sychronous, and
|
|
additionally is used in the filter code.
|
|
|
|
I changed the first usage to return the folder immediately but
|
|
still fetch the CamelFolder asyncrhonously, and in the second case,
|
|
made filtering asynchronous, so the fact that the call is synchronous
|
|
doesn't matter.
|
|
|
|
The function was renamed to mail_tool_uri_to_folder to emphasize that
|
|
it's a synchronous Camel call.
|
|
|
|
e. The dispatcher
|
|
|
|
mail_operation_queue () takes its parameters and assembles them in a
|
|
closure_t structure, which I abbreviate clur. It sets a timeout to
|
|
display a progress window if an operation is still running one second
|
|
later (we're not smart enough to check if it's the same operation,
|
|
but the issue is not a big deal). The other thread and some communication
|
|
pipes are created.
|
|
|
|
The dispatcher thread sits in a loop reading from a pipe. Every time
|
|
the main thread queues an operation, it writes the closure_t into the pipe.
|
|
The dispatcher reads the closure, sends a STARTING message to the main
|
|
thread (see below for explanation), calls the callback specified in the
|
|
closure, and sends a FINISHED message. It then goes back to reading
|
|
from its pipe; it will either block until another operation comes along,
|
|
or find one right away and start it. This the pipe takes care of queueing
|
|
operations.
|
|
|
|
The dispatch thread communicates with the main thread with another pipe;
|
|
however, the main thread has other things to do than read from the pipe,
|
|
so it adds registers a GIOReader that checks for messages in the glib
|
|
main loop. In addition to starting and finishing messages, the other
|
|
thread can communicate to the user using messages and a progress bar.
|
|
(This is currently implemented but unused.)
|
|
|
|
5. ISSUES
|
|
|
|
* Operations are queued and dequeued stupidly. Like if you click
|
|
on one message then click on another, the first will be retrieved
|
|
and displayed then overwritten by the second. Operations that could
|
|
be performed at the same time safely aren't.
|
|
* The CamelObject system is workable, but it'd be nice to work with
|
|
something established like the GtkObject
|
|
* The whole threading idea is not great. Concensus is that an
|
|
asynchronous interface is the Right Thing, eventually.
|
|
* Care still needs to be taken when designing evolution-mail code to
|
|
work with the asynchronous mail_do_ functions
|
|
* Some of the operations are extremely hacky.
|
|
* IMAP's timeout to send a NOOP had to be removed because we can't
|
|
use GTK. We need an alternative for this. |