Camel.Operation

A CamelOperation is a bi-directional rendezvous mechanism used to communicate status information from long-running tasks, and to cancel them from the client. It is used extensively by evolution-mail to display status messages, and is the core mechanism used to implement the "Cancel" button.

Usually, client code will allocate a CamelOperation, and then in a given worker thread, register the operation as the default for this thread. Subsequent calls to camel functions which are cancellable, or provide progress reporting, are able to find the registered operation for this thread, and communicate with it. They can report progress on various tasks, find out if the operation has been cancelled, or retrieve a file descriptor (or PRFileDesc'riptor for Mozilla NSS usage) to perform multiple i/o wait on cancellation or some other i/o event, without blocking or polling.

The API is a tad over-engineered, there are probably some functions which just aren't used (anymore?), or are redundant.

Allocating

When allocating an operation, an optional status function is supplied.

 typedef void (*CamelOperationStatusFunc)(struct _CamelOperation *op, const char *what, int pc, void *data);
 
 typedef enum _camel_operation_status_t {
        CAMEL_OPERATION_START = -1,
        CAMEL_OPERATION_END = -2,
 } camel_operation_status_t;
 
 CamelOperation *camel_operation_new(CamelOperationStatusFunc status, void *status_data);
 void camel_operation_ref(CamelOperation *cc);
 void camel_operation_unref(CamelOperation *cc);

This will allocate a new CamelOperation object, and provide the usual reference counting facilities. Any status information sent to the operation will be fed to the <code>status</code> callback as appropriate, and the various cancellation functions can be called on it directly. For custom code, it is possible to just use the returned operation pointer directly, without registering it for the current thread, but the power of the system is that the operation can then be registered for the current thread and doesn't need to be passed around as an argument to every function.

 CamelOperation *camel_operation_register(CamelOperation *cc);
 void camel_operation_unregister(CamelOperation *cc);

These functions will register the given cc pointer for the current thread. If an operation is registered in one thread, it must be unregistered after it is no longer needed. Usually in the same stack frame from which it was registered. Internally these functions use pthread_setspecific to track the operation.

Cancellation

Cancellation works when a thread calls cancel on the specific operation, or all operations.

 void camel_operation_cancel(CamelOperation *cc);
 void camel_operation_uncancel(CamelOperation *cc);

Passing NULL to <code>camel_operation_cancel</code> will infact cancel every currently allocated and live CamelOperation handle. Passing NULL to camel_operation_uncancel will just uncancel the operation currently registered for this thread, if one exists.

Operations may need to be uncancelled if a cancel has been detected, but some clean-up operations are required that might be testing for cancellation. It is important that any cleanup operations do not themselves get into a state where they need to be cancelled, otherwise it may confuse the user. Perhaps this is a redundant function, and a cancel block should be used?

For the code which implements cancellation, a number of functions allow access to the cancellation information:

 int camel_operation_cancel_check(CamelOperation *cc);
 int camel_operation_cancel_fd(CamelOperation *cc);
 #ifdef HAVE_NSS
 struct PRFileDesc *camel_operation_cancel_prfd(CamelOperation *cc);
 #endif

Check simply returns TRUE (in C89, 1), if cancellation has been invoked on the operation. This is very quick and efficient, and can be used to poll the cancellation status at a convenient time.

If you are doing more complex i/o, such as socket i/o, which may block for large periods of time, then you should first check if the operation is cancelled, and then follow it up with a poll(2) or select(2) on the socket as well as the cancellation file descriptor and wait for an event to occur. You simply need to check for a READ or POLLIN event on the cancellation descriptor, and then confirm with a camel_operation_cancel_check. Any code which returns an exception should return an exception code of CAMEL_EXCEPTION_USER_CANCEL. Code which returns it's status through errno must set errno = EINTR;. camel/camel-tcp-stream-raw.c has some particularly complete and complex examples of this usage.

Progress reporting

For the client, the progress will be reported to the callback supplied in the new function. A percentage value of the progress will be supplied in pc (a true percentage, i.e. out of 100, not a fraction) along with a description of the task in <code>what</code>. Two special percentage values, CAMEL_OPERATION_START and <code>CAMEL_OPERATION_END</code> indicate hard start/finish values, and will always be supplied in sequence, and only ever once.

For the implementation code, progress reporting is a little more complex. In a complex function call, there may be multiple levels of long-running tasks. This is represented by a 'stack' of progress jobs which run concurrently. To make this work in any practical way, this stack is flattened into multiple operation status callback invocations. Each task doesn't need to know about other tasks, they just start a task, report on it's progress, and then must end it. If a sub-task also reports progress, it will be handled internally and may or may not make it to the client code depending on how long it takes.

All of these functions may be given NULL as their cc argument, in which case they will use the registered operation on this thread; or simply NOOP.

These functions indicate the start of an operation. They take printf-style arguments.

 void camel_operation_start(CamelOperation *cc, char *what, ...);
 void camel_operation_start_transient(CamelOperation *cc, char *what, ...);

The transient version indicates that this might be a quick task, that the client needn't display immediately. This is to avoid ugly and expensive progress reporting of unecessary jobs. If the job happens to take a long time sometimes, then the reporting will be switched to the transient job until it is finished, otherwise the client callback will never hear about it. A good example of this is the POP download reporting, which sometimes jumps to reporting an individual message download.

Then there are two progress reporting functions.

 void camel_operation_progress(CamelOperation *cc, int pc);
 void camel_operation_progress_count(CamelOperation *cc, int sofar);

The first is preferable, as it supplies a percentage value to display in a progress bar. The second is intended for situations where the extent of the operation is not known, but some indication that things are still happening is required. I think this api needs to change, it should probably be camel_operation_step, and another special percentage value CAMEL_OPERATION_STEP should be passed to the progress callback.

And finally after the operation is complete camel_operation_end must be called every time.

 void camel_operation_end(CamelOperation *cc);

As a matter of good programming practice, camel_operation_start and camel_operation_end should be called from the same function and stack frame.

Control functions

Cancellation may be blocked in critical sections that should not be canceled.

 void camel_operation_cancel_block(CamelOperation *cc);
 void camel_operation_cancel_unblock(CamelOperation *cc);

This is probably one of those over-engineered aspects. I dont think this is used anywhere.

Other functions

 CamelOperation *camel_operation_registered(void);

Find out if and what operation is registered on the current thread. This is useful for implementation code that may need to make sure some operation is registered in the current thread.

 void camel_operation_mute(CamelOperation *cc);

Will permanently stop any new notifications from arriving on the operation status callback. This is only required in shut-down and finalisation processing where you must no longer receive callback invocations against a callback data pointer which is no longer valid.

Apps/Evolution/Camel.Operation (last edited 2013-08-08 22:50:07 by WilliamJonMcCann)