1*******
2This document is absurdly, obscenely out of date. Don't read it.
3
4 -- Peter Williams 7/2/2001
5*******
6
7Asynchronous Mailer Information
8Peter Williams <peterw@helixcode.com>
98/4/2000
10
111. INTRODUCTION
12
13It's pretty clear that the Evolution mailer needs to be asynchronous in
14some manner. Blocking the UI completely on IMAP calls or large disk reads
15is unnacceptable, and it's really nice to be able to thread the message
16view in the background, or do other things while a mailbox is downloading.
17
18The problem in making Evolution asynchronous is Camel. Camel is not
19asynchronous for a few reasons. All of its interfaces are synchronous --
20calls like camel_store_get_folder, camel_folder_get_message, etc. can
21take a very long time if they're being performed over a network or with
22a large mbox mailbox file. However, these functions have no mechanism
23for specifying that the operation is in progress but not complete, and
24no mechanism for signaling when to operation does complete.
25
262. WHY I DIDN'T MAKE CAMEL ASYNCHRONOUS
27
28It seems like it would be a good idea, then, to rewrite Camel to be
29asynchonous. This presents several problems:
30
31 * Many interfaces must be rewritten to support "completed"
32 callbacks, etc. Some of these interfaces are connected to
33 synchronous CORBA calls.
34 * Everything must be rewritten to be asynchonous. This includes
35 the CamelStore, CamelFolder, CamelMimeMessage, CamelProvider,
36 every subclass thereof, and all the code that touches these.
37 These include the files in camel/, mail/, filter/, and
38 composer/. The change would be a complete redesign for any
39 provider implementation.
40 * All the work on providers (IMAP, mh, mbox, nntp) up to this
41 point would be wasted. While they were being rewritten
42 evolution-mail would be useless.
43
44However, it is worth noting that the solution I chose is not optimal,
45and I think that it would be worthwhile to write a libcamel2 or some
46such thing that was designed from the ground up to work asynchronously.
47Starting fresh from such a design would work, but trying to move the
48existing code over would be more trouble than it's worth.
49
503. WHY I MADE CAMEL THREADED
51
52If Camel was not going to be made asynchronous, really the only other
53choice was to make it multithreaded. [1] No one has been particularly
54excited by this plan, as debugging and writing MT-safe code is hard.
55But there wasn't much of a choice, and I believed that a simple thread
56wrapper could be written around Camel.
57
58The important thing to know is that while Camel is multithreaded, we
59DO NOT and CANNOT share objects between threads. Instead,
60evolution-mail sends a request to a dispatching thread, which performs
61the action or queues it to be performed. (See section 4 for details)
62
63The goal that I was working towards is that there should be no calls
64to camel made, ever, in the main thread. I didn't expect to and
65didn't do this, but that was the intent.
66
67[1]. Well, we could fork off another process, but they share so much
68data that this would be pretty impractical.
69
704. IMPLEMENTATION
71
72a. CamelObject
73
74Threading presented a major problem regarding Camel. Camel is based
75on the GTK Object system, and uses signals to communicate events. This
76is okay in a nonthreaded application, but the GTK Object system is
77not thread-safe.
78
79Particularly, signals and object allocations use static data. Using
80either one inside Camel would guarantee that we'd be stuck with
81random crashes forevermore. That's Bad (TM).
82
83There were two choices: make sure to limit our usage of GTK, or stop
84using the GTK Object system. I decided to do the latter, as the
85former would lead to a mess of "what GTK calls can we make" and
86GDK_THREADS_ENTER and accidentally calling UI functions and upgrades
87to GTK breaking everything.
88
89So I wrote a very very simple CamelObject system. It had three goals:
90
91 * Be really straightforward, just encapsulate the type
92 heirarchy without all that GtkArg silliness or anything.
93 * Be as compatible as possible with the GTK Object system
94 to make porting easy
95 * Be threadsafe
96
97It supports:
98
99 * Type inheritance
100 * Events (signals)
101 * Type checking
102 * Normal refcounting
103 * No unref/destroy messiness
104 * Threadsafety
105 * Class functions
106
107The entire code to the object system is in camel/camel-object.c. It's
108a naive implementation and not full of features, but intentionally that
109way. The main differences between GTK Objects and Camel Objects are:
110
111 * s,gtk,camel,i of course
112 * Finalize is no longer a GtkObjectClass function. You specify
113 a finalize function along with an init function when declaring
114 a type, and it is called automatically and chained automatically.
115 * Declaring a type is a slightly different API
116 * The signal system is replaced with a not-so-clever event system.
117 Every event is equivalent to a NONE__POINTER signal. The syntax
118 is slightly different: a class "declares" an event and specifies
119 a name and a "prep func", that is called before the event is
120 triggered and can cancel it.
121 * There is only one CamelXXXClass in existence for every type.
122 All objects share it.
123
124There is a shell script, tools/make-camel-object.sh that will do all of
125the common substitutions to make a file CamelObject-compatible. Usually
126all that needs to be done is move the implementation of the finalize
127event out of the class init, modify the get_type function, and replace
128signals with events.
129
130Pitfalls in the transition that I ran into were:
131
132 * gtk_object_ref -> camel_object_ref or you coredump
133 * some files return 'guint' instead of GtkType and must be changed
134 * Remove the #include <gtk/gtk.h>
135 * gtk_object_set_datas must be changed (This happened once; I
136 added a static hashtable)
137 * signals have to be fudged a bit to match the gpointer input
138 * the BAST_CASTARD option is on, meaning failed typecasts will
139 return NULL, almost guaranteeing a segfault -- gets those
140 bugs fixed double-quick!
141
142b. API -- mail_operation_spec
143
144I worked by creating a very specific definition of a "mail operation"
145and wrote an engine to queue and dispatch them.
146
147A mail operation is defined by a structure mail_operation_spec
148prototyped in mail-threads.h. It comes in three logical parts -- a
149"setup" phase, executed in the main thread; a "do" phase, executed
150in the dispatch thread; and a "cleanup" phase, executed in the main
151thread. These three phases are guaranteed to be performed in order
152and atomically with respect to other mail operations.
153
154Each of these phases is represented by a function pointer in the
155mail_operation_spec structure. The function mail_operation_queue() is
156called and passed a pointer to a mail_operation_spec and a user_data-style
157pointer that fills in the operation's parameters. The "setup" callback
158is called immediately, though that may change.
159
160Each callback is passed three parameters: a pointer to the user_data,
161a pointer to the "operation data", and a pointer to a CamelException.
162The "operation data" is allocated automatically and freed when the operation
163completes. Internal data that needs to be shared between phases should
164be stored here. The size allocated is specified in the mail_operation_spec
165structure.
166
167Because all of the callbacks use Camel calls at some point, the
168CamelException is provided as utility. The dispatcher will catch exceptions
169and display error dialogs, unlike the synchronous code which lets
170exceptions fall through the cracks fairly easily.
171
172I tried to implement all the operations following this convention. Basically
173I used this skeleton code for all the operations, just filling in the
174specifics:
175
176===================================
177
178typedef struct operation_name_input_s {
179 parameters to operation
180} operation_name_input_t;
181
182typedef struct operation_name_data_s {
183 internal data to operation, if any
184 (if none, omit the structure and set opdata_size to 0)
185} operation_name_data_t;
186
187static gchar *describe_operation_name (gpointer in_data, gboolean gerund);
188static void setup_operation_name (gpointer in_data, gpointer op_data, CamelException *ex);
189static void do_operation_name (gpointer in_data, gpointer op_data, CamelException *ex);
190static void cleanup_operation_name (gpointer in_data, gpointer op_data, CamelException *ex);
191
192static gchar *describe_operation_name (gpointer in_data, gboolean gerund)
193{
194 operation_name_input_t *input = (operation_name_input_t *) in_data;
195
196 if (gerund) {
197 return a g_strdup'ed string describing what we're doing
198 } else {
199 return a g_strdup'ed string describing what we're about to do
200 }
201}
202
203static void setup_operation_name (gpointer in_data, gpointer op_data, CamelException *ex)
204{
205 operation_name_input_t *input = (operation_name_input_t *) in_data;
206 operation_name_data_t *data = (operation_name_data_t *) op_data;
207
208 verify that parameters are valid
209
210 initialize op_data
211
212 reference objects
213}
214
215static void do_operation_name (gpointer in_data, gpointer op_data, CamelException *ex)
216{
217 operation_name_input_t *input = (operation_name_input_t *) in_data;
218 operation_name_data_t *data = (operation_name_data_t *) op_data;
219
220 perform camel operations
221}
222
223static void cleanup_operation_name (gpointer in_data, gpointer op_data, CamelException *ex)
224{
225 operation_name_input_t *input = (operation_name_input_t *) in_data;
226 operation_name_data_t *data = (operation_name_data_t *) op_data;
227
228 perform UI updates
229
230 free allocations
231
232 dereference objects
233}
234
235static const mail_operation_spec op_operation_name = {
236 describe_operation_name,
237 sizeof (operation_name_data_t),
238 setup_operation_name,
239 do_operation_name,
240 cleanup_operation_name
241};
242
243void
244mail_do_operation_name (parameters)
245{
246 operation_name_input_t *input;
247
248 input = g_new (operation_name_input_t, 1);
249
250 store parameters in input
251
252 mail_operation_queue (&op_operation_name, input, TRUE);
253}
254
255===========================================
256
257c. mail-ops.c
258
259Has been drawn and quartered. It has been split into:
260
261 * mail-callbacks.c: the UI callbacks
262 * mail-tools.c: useful sequences wrapping common Camel operations
263 * mail-ops.c: implementations of all the mail_operation_specs
264
265An important part of mail-ops.c are the global functions
266mail_tool_camel_lock_{up,down}. These simulate a recursize mutex around
267camel. There are an extreme few, supposedly safe, calls to Camel made in
268the main thread. These functions should go around evey call to Camel or
269group thereof. I don't think they're necessary but it's nice to know
270they're there.
271
272If you look at mail-tools.c, you'll notice that all the Camel calls are
273protected with these functions. Remember that a mail tool is really
274just another Camel call, so don't use them in the main thread either.
275
276All the mail operations are implemented in mail-ops.c EXCEPT:
277
278 * filter-driver.c: the filter_mail operation
279 * message-list.c: the regenerate_messagelist operation
280 * message-thread.c: the thread_messages operation
281
282d. Using the operations
283
284The mail operations as implemented are very specific to evolution-mail. I
285was thinking about leaving them mostly generic and then allowing extra
286callbacks to be added to perform the more specific UI touches, but this
287seemed kind of pointless.
288
289I basically looked through the code, found references to Camel, and split
290the code into three parts -- the bit before the Camel calls, the bit after,
291and the Camel calls. These were mapped onto the template, given a name,
292and added to mail-ops.c. Additionally, I simplified the common tasks that
293were taken care of in mail-tools.c, making some functions much simpler.
294
295Ninety-nine percent of the time, whatever operation is being done is being
296done in a callback, so all that has to be done is this:
297
298==================
299
300void my_callback (GtkObject *obj, gchar *uid)
301{
302 camel_do_something (uid);
303}
304
305==== becomes ====
306
307void my_callback (GtkObject *obj, gchar *uid)
308{
309 mail_do_do_something (uid);
310}
311
312=================
313
314There are, however, a few belligerents. Particularly, the function
315mail_uri_to_folder returns a CamelFolder and yet should really be
316asynchronous. This is called in a CORBA call that is sychronous, and
317additionally is used in the filter code.
318
319I changed the first usage to return the folder immediately but
320still fetch the CamelFolder asyncrhonously, and in the second case,
321made filtering asynchronous, so the fact that the call is synchronous
322doesn't matter.
323
324The function was renamed to mail_tool_uri_to_folder to emphasize that
325it's a synchronous Camel call.
326
327e. The dispatcher
328
329mail_operation_queue () takes its parameters and assembles them in a
330closure_t structure, which I abbreviate clur. It sets a timeout to
331display a progress window if an operation is still running one second
332later (we're not smart enough to check if it's the same operation,
333but the issue is not a big deal). The other thread and some communication
334pipes are created.
335
336The dispatcher thread sits in a loop reading from a pipe. Every time
337the main thread queues an operation, it writes the closure_t into the pipe.
338The dispatcher reads the closure, sends a STARTING message to the main
339thread (see below for explanation), calls the callback specified in the
340closure, and sends a FINISHED message. It then goes back to reading
341from its pipe; it will either block until another operation comes along,
342or find one right away and start it. This the pipe takes care of queueing
343operations.
344
345The dispatch thread communicates with the main thread with another pipe;
346however, the main thread has other things to do than read from the pipe,
347so it adds registers a GIOReader that checks for messages in the glib
348main loop. In addition to starting and finishing messages, the other
349thread can communicate to the user using messages and a progress bar.
350(This is currently implemented but unused.)
351
3525. ISSUES
353
354 * Operations are queued and dequeued stupidly. Like if you click
355 on one message then click on another, the first will be retrieved
356 and displayed then overwritten by the second. Operations that could
357 be performed at the same time safely aren't.
358 * The CamelObject system is workable, but it'd be nice to work with
359 something established like the GtkObject
360 * The whole threading idea is not great. Concensus is that an
361 asynchronous interface is the Right Thing, eventually.
362 * Care still needs to be taken when designing evolution-mail code to
363 work with the asynchronous mail_do_ functions
364 * Some of the operations are extremely hacky.
365 * IMAP's timeout to send a NOOP had to be removed because we can't
366 use GTK. We need an alternative for this.