From a774034afff7b71c941341533a355a56034c2267 Mon Sep 17 00:00:00 2001 From: Jim Nelson Date: Tue, 28 Jun 2011 16:33:27 -0700 Subject: [PATCH] More responsive loading of folders: #3702 This commit introduces lazy loading of folder contents, which allows the Engine to report back email in chunks rather than all at once (which might require a round-trip to the server). This allows for the local database results to be returned to the caller right away while background I/O is occuring. --- src/client/ui/main-window.vala | 10 +- src/engine/api/geary-abstract-folder.vala | 91 +++++ src/engine/api/geary-engine-folder.vala | 345 ++++++++++++++---- src/engine/api/geary-folder.vala | 36 +- .../common/common-nonblocking-semaphore.vala | 73 ++++ src/engine/imap/api/imap-folder.vala | 20 +- .../imap-client-session-manager.vala | 38 +- src/engine/sqlite/api/sqlite-folder.vala | 21 +- src/wscript | 2 + 9 files changed, 531 insertions(+), 105 deletions(-) create mode 100644 src/engine/api/geary-abstract-folder.vala create mode 100644 src/engine/common/common-nonblocking-semaphore.vala diff --git a/src/client/ui/main-window.vala b/src/client/ui/main-window.vala index a0610afa..e12ac478 100644 --- a/src/client/ui/main-window.vala +++ b/src/client/ui/main-window.vala @@ -198,12 +198,18 @@ public class MainWindow : Gtk.Window { yield current_folder.open_async(true); - Gee.List? email = yield current_folder.list_email_async(1, 100, - Geary.Email.Field.ENVELOPE); + current_folder.lazy_list_email_async(1, 1000, Geary.Email.Field.ENVELOPE, + on_list_email_ready); + } + + private void on_list_email_ready(Gee.List? email, Error? err) { if (email != null && email.size > 0) { foreach (Geary.Email envelope in email) message_list_store.append_envelope(envelope); } + + if (err != null) + debug("Error while listing email: %s", err.message); } private void on_select_folder_completed(Object? source, AsyncResult result) { diff --git a/src/engine/api/geary-abstract-folder.vala b/src/engine/api/geary-abstract-folder.vala new file mode 100644 index 00000000..dbb0c1c9 --- /dev/null +++ b/src/engine/api/geary-abstract-folder.vala @@ -0,0 +1,91 @@ +/* Copyright 2011 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public abstract class Geary.AbstractFolder : Object, Geary.Folder { + protected virtual void notify_opened() { + opened(); + } + + protected virtual void notify_closed(Geary.Folder.CloseReason reason) { + closed(reason); + } + + protected virtual void notify_email_added_removed(Gee.List? added, + Gee.List? removed) { + email_added_removed(added, removed); + } + + protected virtual void notify_updated() { + updated(); + } + + public abstract string get_name(); + + public abstract Geary.FolderProperties? get_properties(); + + public abstract async void open_async(bool readonly, Cancellable? cancellable = null) throws Error; + + public abstract async void close_async(Cancellable? cancellable = null) throws Error; + + public abstract async int get_email_count(Cancellable? cancellable = null) throws Error; + + public abstract async void create_email_async(Geary.Email email, Cancellable? cancellable = null) + throws Error; + + public abstract async Gee.List? list_email_async(int low, int count, + Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; + + public virtual void lazy_list_email_async(int low, int count, Geary.Email.Field required_fields, + EmailCallback cb, Cancellable? cancellable = null) { + do_lazy_list_email_async.begin(low, count, required_fields, cb, cancellable); + } + + private async void do_lazy_list_email_async(int low, int count, Geary.Email.Field required_fields, + EmailCallback cb, Cancellable? cancellable = null) { + try { + Gee.List? list = yield list_email_async(low, count, required_fields, + cancellable); + + if (list != null && list.size > 0) + cb(list, null); + + cb(null, null); + } catch (Error err) { + cb(null, err); + } + } + + public abstract async Gee.List? list_email_sparse_async(int[] by_position, + Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; + + public virtual void lazy_list_email_sparse_async(int[] by_position, + Geary.Email.Field required_fields, EmailCallback cb, Cancellable? cancellable = null) { + do_lazy_list_email_sparse_async.begin(by_position, required_fields, cb, cancellable); + } + + private async void do_lazy_list_email_sparse_async(int[] by_position, + Geary.Email.Field required_fields, EmailCallback cb, Cancellable? cancellable = null) { + try { + Gee.List? list = yield list_email_sparse_async(by_position, + required_fields, cancellable); + + if (list != null && list.size > 0) + cb(list, null); + + cb(null, null); + } catch(Error err) { + cb(null, err); + } + } + + public abstract async Geary.Email fetch_email_async(int position, Geary.Email.Field required_fields, + Cancellable? cancellable = null) throws Error; + + public virtual string to_string() { + return get_name(); + } +} + diff --git a/src/engine/api/geary-engine-folder.vala b/src/engine/api/geary-engine-folder.vala index 1c4a5994..8d09214e 100644 --- a/src/engine/api/geary-engine-folder.vala +++ b/src/engine/api/geary-engine-folder.vala @@ -4,11 +4,15 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -private class Geary.EngineFolder : Object, Geary.Folder { +private class Geary.EngineFolder : Geary.AbstractFolder { + private const int REMOTE_FETCH_CHUNK_COUNT = 10; + private RemoteAccount remote; private LocalAccount local; - private RemoteFolder remote_folder; + private RemoteFolder? remote_folder = null; private LocalFolder local_folder; + private bool opened = false; + private Geary.Common.NonblockingSemaphore remote_semaphore = new Geary.Common.NonblockingSemaphore(); public EngineFolder(RemoteAccount remote, LocalAccount local, LocalFolder local_folder) { this.remote = remote; @@ -19,67 +23,166 @@ private class Geary.EngineFolder : Object, Geary.Folder { } ~EngineFolder() { + if (opened) + warning("Folder %s destroyed without closing", get_name()); + local_folder.updated.disconnect(on_local_updated); } - public string get_name() { + public override string get_name() { return local_folder.get_name(); } - public Geary.FolderProperties? get_properties() { + public override Geary.FolderProperties? get_properties() { return null; } - public async void create_email_async(Geary.Email email, Cancellable? cancellable) throws Error { + public override async void create_email_async(Geary.Email email, Cancellable? cancellable) throws Error { throw new EngineError.READONLY("Engine currently read-only"); } - public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { + public override async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { + if (opened) + throw new EngineError.ALREADY_OPEN("Folder %s already open", get_name()); + yield local_folder.open_async(readonly, cancellable); - if (remote_folder == null) { - remote_folder = (RemoteFolder) yield remote.fetch_folder_async(null, local_folder.get_name(), + // Rather than wait for the remote folder to open (which blocks completion of this method), + // attempt to open in the background and treat this folder as "opened". If the remote + // doesn't open, this folder remains open but only able to work with the local cache. + // + // Note that any use of remote_folder in this class should first call + // wait_for_remote_to_open(), which uses a NonblockingSemaphore to indicate that the remote + // is open (or has failed to open). This allows for early calls to list and fetch emails + // can work out of the local cache until the remote is ready. + RemoteFolder folder = (RemoteFolder) yield remote.fetch_folder_async(null, local_folder.get_name(), cancellable); - remote_folder.updated.connect(on_remote_updated); - } + open_remote_async.begin(folder, readonly, cancellable, on_open_remote_completed); - yield remote_folder.open_async(readonly, cancellable); + opened = true; notify_opened(); } - public async void close_async(Cancellable? cancellable = null) throws Error { - yield local_folder.close_async(cancellable); + private async void open_remote_async(RemoteFolder folder, bool readonly, Cancellable? cancellable) + throws Error { + yield folder.open_async(readonly, cancellable); - if (remote_folder != null) { - remote_folder.updated.disconnect(on_remote_updated); - yield remote_folder.close_async(cancellable); - remote_folder = null; + remote_folder = folder; + remote_folder.updated.connect(on_remote_updated); + + remote_semaphore.notify(); + } + + private void on_open_remote_completed(Object? source, AsyncResult result) { + try { + open_remote_async.end(result); + } catch (Error err) { + debug("Unable to open remote folder %s: %s", to_string(), err.message); - notify_closed(CloseReason.FOLDER_CLOSED); + remote_folder = null; + try { + remote_semaphore.notify(); + } catch (Error err) { + debug("Unable to notify remote folder ready: %s", err.message); + } } } - public async int get_email_count(Cancellable? cancellable = null) throws Error { + private async void wait_for_remote_to_open() throws Error { + yield remote_semaphore.wait_async(); + } + + public override async void close_async(Cancellable? cancellable = null) throws Error { + yield local_folder.close_async(cancellable); + + // if the remote folder is open, close it in the background so the caller isn't waiting for + // this method to complete (much like open_async()) + if (remote_folder != null) { + yield remote_semaphore.wait_async(); + remote_semaphore = new Geary.Common.NonblockingSemaphore(); + + remote_folder.updated.disconnect(on_remote_updated); + RemoteFolder? folder = remote_folder; + remote_folder = null; + + folder.close_async.begin(cancellable); + + notify_closed(CloseReason.FOLDER_CLOSED); + } + + opened = false; + } + + public override async int get_email_count(Cancellable? cancellable = null) throws Error { // TODO return 0; } - public async Gee.List? list_email_async(int low, int count, + public override async Gee.List? list_email_async(int low, int count, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error { - assert(low >= 1); - assert(count >= 0); - if (count == 0) return null; - Gee.List? local_list = yield local_folder.list_email_async(low, count, - required_fields, cancellable); - int local_list_size = (local_list != null) ? local_list.size : 0; - debug("local list found %d", local_list_size); + // block on do_list_email_async(), using an accumulator to gather the emails and return + // them all at once to the caller + Gee.List accumulator = new Gee.ArrayList(); + yield do_list_email_async(low, count, required_fields, accumulator, null, cancellable); - if (remote_folder == null || local_list_size == count) - return local_list; + return accumulator; + } + + public override void lazy_list_email_async(int low, int count, Geary.Email.Field required_fields, + EmailCallback cb, Cancellable? cancellable = null) { + // schedule do_list_email_async(), using the callback to drive availability of email + do_list_email_async.begin(low, count, required_fields, null, cb, cancellable); + } + + private async void do_list_email_async(int low, int count, Geary.Email.Field required_fields, + Gee.List? accumulator, EmailCallback? cb, Cancellable? cancellable = null) + throws Error { + assert(low >= 1); + assert(count >= 0); + + if (!opened) + throw new EngineError.OPEN_REQUIRED("%s is not open", get_name()); + + if (count == 0) { + // signal finished + if (cb != null) + cb(null, null); + + return; + } + + Gee.List? local_list = null; + try { + local_list = yield local_folder.list_email_async(low, count, required_fields, + cancellable); + } catch (Error local_err) { + if (cb != null) + cb (null, local_err); + + throw local_err; + } + + int local_list_size = (local_list != null) ? local_list.size : 0; + + if (local_list_size > 0) { + if (accumulator != null) + accumulator.add_all(local_list); + + if (cb != null) + cb(local_list, null); + } + + if (local_list_size == count) { + // signal finished + if (cb != null) + cb(null, null); + + return; + } // go through the positions from (low) to (low + count) and see if they're not already // present in local_list; whatever isn't present needs to be fetched @@ -93,26 +196,91 @@ private class Geary.EngineFolder : Object, Geary.Folder { needed_by_position += position; } - if (needed_by_position.length == 0) - return local_list; + if (needed_by_position.length == 0) { + // signal finished + if (cb != null) + cb(null, null); + + return; + } - Gee.List? remote_list = yield remote_list_email(needed_by_position, - required_fields, cancellable); + Gee.List? remote_list = null; + try { + // if cb != null, it will be called by remote_list_email(), so don't call again with + // returned list + remote_list = yield remote_list_email(needed_by_position, required_fields, cb, cancellable); + } catch (Error remote_err) { + if (cb != null) + cb(null, remote_err); + + throw remote_err; + } - return combine_lists(local_list, remote_list); + if (accumulator != null && remote_list != null && remote_list.size > 0) + accumulator.add_all(remote_list); + + // signal finished + if (cb != null) + cb(null, null); } - public async Gee.List? list_email_sparse_async(int[] by_position, + public override async Gee.List? list_email_sparse_async(int[] by_position, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error { if (by_position.length == 0) return null; - Gee.List? local_list = yield local_folder.list_email_sparse_async(by_position, - required_fields, cancellable); + Gee.List accumulator = new Gee.ArrayList(); + yield do_list_email_sparse_async(by_position, required_fields, accumulator, null, + cancellable); + + return accumulator; + } + + public override void lazy_list_email_sparse_async(int[] by_position, Geary.Email.Field required_fields, + EmailCallback cb, Cancellable? cancellable = null) { + // schedule listing in the background, using the callback to drive availability of email + do_list_email_sparse_async.begin(by_position, required_fields, null, cb, cancellable); + } + + private async void do_list_email_sparse_async(int[] by_position, Geary.Email.Field required_fields, + Gee.List? accumulator, EmailCallback? cb, Cancellable? cancellable = null) + throws Error { + if (!opened) + throw new EngineError.OPEN_REQUIRED("%s is not open", get_name()); + + if (by_position.length == 0) { + // signal finished + if (cb != null) + cb(null, null); + + return; + } + + Gee.List? local_list = null; + try { + local_list = yield local_folder.list_email_sparse_async(by_position, required_fields, + cancellable); + } catch (Error local_err) { + if (cb != null) + cb(null, local_err); + + throw local_err; + } + int local_list_size = (local_list != null) ? local_list.size : 0; - if (remote_folder == null || local_list_size == by_position.length) - return local_list; + if (local_list_size == by_position.length) { + if (accumulator != null) + accumulator.add_all(local_list); + + // report and signal finished + if (cb != null) { + cb(local_list, null); + cb(null, null); + } + + return; + } // go through the list looking for anything not already in the sparse by_position list // to fetch from the server; since by_position is not guaranteed to be sorted, the local @@ -136,28 +304,71 @@ private class Geary.EngineFolder : Object, Geary.Folder { needed_by_position += position; } - if (needed_by_position.length == 0) - return local_list; + if (needed_by_position.length == 0) { + if (local_list != null && local_list.size > 0) { + if (accumulator != null) + accumulator.add_all(local_list); + + if (cb != null) + cb(local_list, null); + } + + // signal finished + if (cb != null) + cb(null, null); + + return; + } - Gee.List? remote_list = yield remote_list_email(needed_by_position, - required_fields, cancellable); + Gee.List? remote_list = null; + try { + // if cb != null, it will be called by remote_list_email(), so don't call again with + // returned list + remote_list = yield remote_list_email(needed_by_position, required_fields, cb, cancellable); + } catch (Error remote_err) { + if (cb != null) + cb(null, remote_err); + + throw remote_err; + } - return combine_lists(local_list, remote_list); + if (accumulator != null && remote_list != null && remote_list.size > 0) + accumulator.add_all(remote_list); + + // signal finished + if (cb != null) + cb(null, null); } private async Gee.List? remote_list_email(int[] needed_by_position, - Geary.Email.Field required_fields, Cancellable? cancellable) throws Error { + Geary.Email.Field required_fields, EmailCallback? cb, Cancellable? cancellable) throws Error { + // possible to call remote multiple times, wait for it to open once and go + yield wait_for_remote_to_open(); + debug("Background fetching %d emails for %s", needed_by_position.length, get_name()); - Gee.List? remote_list = yield remote_folder.list_email_sparse_async( - needed_by_position, required_fields, cancellable); + Gee.List full = new Gee.ArrayList(); - if (remote_list != null && remote_list.size == 0) - remote_list = null; - - // if any were fetched, store locally - // TODO: Bulk writing - if (remote_list != null) { + int index = 0; + while (index <= needed_by_position.length) { + // if a callback is specified, pull the messages down in chunks, so they can be reported + // incrementally + unowned int[] list; + if (cb != null) { + int list_count = int.min(REMOTE_FETCH_CHUNK_COUNT, needed_by_position.length - index); + list = needed_by_position[index:index + list_count]; + } else { + list = needed_by_position; + } + + Gee.List? remote_list = yield remote_folder.list_email_sparse_async( + list, required_fields, cancellable); + + if (remote_list == null || remote_list.size == 0) + break; + + // if any were fetched, store locally + // TODO: Bulk writing foreach (Geary.Email email in remote_list) { bool exists_in_system = false; if (email.message_id != null) { @@ -193,14 +404,21 @@ private class Geary.EngineFolder : Object, Geary.Folder { yield local_folder.update_email_async(email, false, cancellable); } } + + if (cb != null) + cb(remote_list, null); + + full.add_all(remote_list); + + index += list.length; } - return remote_list; + return full; } - public async Geary.Email fetch_email_async(int num, Geary.Email.Field fields, + public override async Geary.Email fetch_email_async(int num, Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { - if (remote_folder == null) + if (!opened) throw new EngineError.OPEN_REQUIRED("Folder %s not opened", get_name()); try { @@ -220,6 +438,7 @@ private class Geary.EngineFolder : Object, Geary.Folder { fields = fields.set(Geary.Email.Field.REFERENCES); // fetch from network + yield wait_for_remote_to_open(); Geary.Email email = yield remote_folder.fetch_email_async(num, fields, cancellable); // save to local store @@ -233,19 +452,5 @@ private class Geary.EngineFolder : Object, Geary.Folder { private void on_remote_updated() { } - - private Gee.List? combine_lists(Gee.List? a, Gee.List? b) { - if (a == null) - return b; - - if (b == null) - return a; - - Gee.List combined = new Gee.ArrayList(); - combined.add_all(a); - combined.add_all(b); - - return combined; - } } diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala index 50065c25..67dd601c 100644 --- a/src/engine/api/geary-folder.vala +++ b/src/engine/api/geary-folder.vala @@ -4,6 +4,8 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ +public delegate void Geary.EmailCallback(Gee.List? emails, Error? err); + public interface Geary.Folder : Object { public enum CloseReason { LOCAL_CLOSE, @@ -109,7 +111,8 @@ public interface Geary.Folder : Object { * * Note that this only returns the number of messages available to the backing medium. In the * case of the local store, this might be less than the number on the network server. Folders - * created by Engine are aggregating objects and will return the true count. + * created by Engine are aggregating objects and will return the true count. However, this + * might require a round-trip to the server. * * Also note that local folders may be sparsely populated. get_count() returns the last position * available, but not all emails from 1 to n may be available. @@ -151,7 +154,7 @@ public interface Geary.Folder : Object { * * In the case of a Folder returned by Engine, it will use what's available in the local store * and fetch from the network only what it needs, so that the caller gets a full list. - * Note that this means the call may take some time to complete. + * Note that this means the call may require a round-trip to the server. * * TODO: Delayed listing methods (where what's available are reported via a callback after the * async method has completed) will be implemented in the future for more responsive behavior. @@ -164,6 +167,19 @@ public interface Geary.Folder : Object { public abstract async Gee.List? list_email_async(int low, int count, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; + /** + * Similar in contract to list_email_async(), however instead of the emails being returned all + * at once at completion time, the emails are delivered to the caller in chunks via the + * EmailCallback. The method indicates when all the message have been fetched by passing a null + * for the first parameter. If an Error occurs while processing, it will be passed as the + * second parameter. There's no guarantess of the order the messages will be delivered to the + * caller. + * + * The Folder must be opened prior to attempting this operation. + */ + public abstract void lazy_list_email_async(int low, int count, Geary.Email.Field required_fields, + EmailCallback cb, Cancellable? cancellable = null); + /** * Like list_email_async(), but the caller passes a sparse list of email by it's ordered * position in the folder. If any of the positions in the sparse list are out of range, @@ -180,6 +196,18 @@ public interface Geary.Folder : Object { public abstract async Gee.List? list_email_sparse_async(int[] by_position, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; + /** + * Similar in contract to list_email_sparse_async(), but like lazy_list_email_async(), the + * messages are passed back to the caller in chunks as they're retrieved. When null is passed + * as the first parameter, all the messages have been fetched. If an Error occurs during + * processing, it's passed as the second parameter. There's no guarantee of the returned + * message's order. + * + * The Folder must be opened prior to attempting this operation. + */ + public abstract void lazy_list_email_sparse_async(int[] by_position, + Geary.Email.Field required_fields, EmailCallback cb, Cancellable? cancellable = null); + /** * Returns a single email that fulfills the required_fields flag at the ordered position in * the folder. If position is invalid for the folder's contents, an EngineError.NOT_FOUND @@ -196,8 +224,6 @@ public interface Geary.Folder : Object { /** * Used for debugging. Should not be used for user-visible labels. */ - public virtual string to_string() { - return get_name(); - } + public abstract string to_string(); } diff --git a/src/engine/common/common-nonblocking-semaphore.vala b/src/engine/common/common-nonblocking-semaphore.vala new file mode 100644 index 00000000..30f366eb --- /dev/null +++ b/src/engine/common/common-nonblocking-semaphore.vala @@ -0,0 +1,73 @@ +/* Copyright 2011 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class Geary.Common.NonblockingSemaphore { + private class Pending { + public SourceFunc cb; + + public Pending(SourceFunc cb) { + this.cb = cb; + } + } + + private Cancellable? cancellable; + private bool passed = false; + private Gee.List pending_queue = new Gee.LinkedList(); + + public NonblockingSemaphore(Cancellable? cancellable = null) { + this.cancellable = cancellable; + + if (cancellable != null) + cancellable.cancelled.connect(on_cancelled); + } + + ~NonblockingSemaphore() { + if (pending_queue.size > 0) + warning("Nonblocking semaphore destroyed with %d pending callers", pending_queue.size); + } + + private void trigger_all() { + foreach (Pending pending in pending_queue) + Idle.add(pending.cb); + + pending_queue.clear(); + } + + public void notify() throws Error { + check_cancelled(); + + passed = true; + trigger_all(); + } + + // TODO: Allow the caller to pass their own cancellable in if they want to be able to cancel + // this particular wait (and not all waiting threads of execution) + public async void wait_async() throws Error { + for (;;) { + check_cancelled(); + + if (passed) + return; + + pending_queue.add(new Pending(wait_async.callback)); + yield; + } + } + + public bool is_cancelled() { + return (cancellable != null) ? cancellable.is_cancelled() : false; + } + + private void check_cancelled() throws Error { + if (is_cancelled()) + throw new IOError.CANCELLED("Semaphore cancelled"); + } + + private void on_cancelled() { + trigger_all(); + } +} + diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala index 1c5382e1..837a47a2 100644 --- a/src/engine/imap/api/imap-folder.vala +++ b/src/engine/imap/api/imap-folder.vala @@ -4,7 +4,7 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Imap.Folder : Object, Geary.Folder, Geary.RemoteFolder { +public class Geary.Imap.Folder : Geary.AbstractFolder, Geary.RemoteFolder { private ClientSessionManager session_mgr; private MailboxInformation info; private string name; @@ -21,7 +21,7 @@ public class Geary.Imap.Folder : Object, Geary.Folder, Geary.RemoteFolder { properties = new Imap.FolderProperties(null, info.attrs); } - public string get_name() { + public override string get_name() { return name; } @@ -29,11 +29,11 @@ public class Geary.Imap.Folder : Object, Geary.Folder, Geary.RemoteFolder { return readonly; } - public Geary.FolderProperties? get_properties() { + public override Geary.FolderProperties? get_properties() { return properties; } - public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { + public override async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { if (mailbox != null) throw new EngineError.ALREADY_OPEN("%s already open", to_string()); @@ -46,7 +46,7 @@ public class Geary.Imap.Folder : Object, Geary.Folder, Geary.RemoteFolder { notify_opened(); } - public async void close_async(Cancellable? cancellable = null) throws Error { + public override async void close_async(Cancellable? cancellable = null) throws Error { if (mailbox == null) return; @@ -57,21 +57,21 @@ public class Geary.Imap.Folder : Object, Geary.Folder, Geary.RemoteFolder { notify_closed(CloseReason.FOLDER_CLOSED); } - public async int get_email_count(Cancellable? cancellable = null) throws Error { + public override async int get_email_count(Cancellable? cancellable = null) throws Error { if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); return mailbox.count; } - public async void create_email_async(Geary.Email email, Cancellable? cancellable = null) throws Error { + public override async void create_email_async(Geary.Email email, Cancellable? cancellable = null) throws Error { if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); throw new EngineError.READONLY("IMAP currently read-only"); } - public async Gee.List? list_email_async(int low, int count, Geary.Email.Field fields, + public override async Gee.List? list_email_async(int low, int count, Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); @@ -79,7 +79,7 @@ public class Geary.Imap.Folder : Object, Geary.Folder, Geary.RemoteFolder { return yield mailbox.list_set_async(new MessageSet.range(low, count), fields, cancellable); } - public async Gee.List? list_email_sparse_async(int[] by_position, + public override async Gee.List? list_email_sparse_async(int[] by_position, Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); @@ -87,7 +87,7 @@ public class Geary.Imap.Folder : Object, Geary.Folder, Geary.RemoteFolder { return yield mailbox.list_set_async(new MessageSet.sparse(by_position), fields, cancellable); } - public async Geary.Email fetch_email_async(int position, Geary.Email.Field fields, + public override async Geary.Email fetch_email_async(int position, Geary.Email.Field fields, Cancellable? cancellable = null) throws Error { if (mailbox == null) throw new EngineError.OPEN_REQUIRED("%s not opened", to_string()); diff --git a/src/engine/imap/transport/imap-client-session-manager.vala b/src/engine/imap/transport/imap-client-session-manager.vala index 89017529..6689b898 100644 --- a/src/engine/imap/transport/imap-client-session-manager.vala +++ b/src/engine/imap/transport/imap-client-session-manager.vala @@ -5,6 +5,8 @@ */ public class Geary.Imap.ClientSessionManager { + public const int MIN_POOL_SIZE = 2; + private Credentials cred; private uint default_port; private Gee.HashSet sessions = new Gee.HashSet(); @@ -15,6 +17,20 @@ public class Geary.Imap.ClientSessionManager { public ClientSessionManager(Credentials cred, uint default_port) { this.cred = cred; this.default_port = default_port; + + adjust_session_pool.begin(); + } + + // TODO: Need a more thorough and bulletproof system for maintaining a pool of ready + // authorized sessions. + private async void adjust_session_pool() { + while (sessions.size < MIN_POOL_SIZE) { + try { + yield create_new_authorized_session(null); + } catch (Error err) { + debug("Unable to create authorized session to %s: %s", cred.server, err.message); + } + } } /** @@ -115,13 +131,7 @@ public class Geary.Imap.ClientSessionManager { context.session.close_mailbox_async.begin(); } - private async ClientSession get_authorized_session(Cancellable? cancellable = null) throws Error { - foreach (ClientSession session in sessions) { - string? mailbox; - if (session.get_context(out mailbox) == ClientSession.Context.AUTHORIZED) - return session; - } - + private async ClientSession create_new_authorized_session(Cancellable? cancellable) throws Error { debug("Creating new session to %s", cred.server); ClientSession new_session = new ClientSession(cred.server, default_port); @@ -135,11 +145,23 @@ public class Geary.Imap.ClientSessionManager { sessions.add(new_session); + debug("Created new session to %s, %d total", cred.server, sessions.size); + return new_session; } + private async ClientSession get_authorized_session(Cancellable? cancellable) throws Error { + foreach (ClientSession session in sessions) { + string? mailbox; + if (session.get_context(out mailbox) == ClientSession.Context.AUTHORIZED) + return session; + } + + return yield create_new_authorized_session(cancellable); + } + private async ClientSession select_examine_async(string folder, bool is_select, - out SelectExamineResults results, Cancellable? cancellable = null) throws Error { + out SelectExamineResults results, Cancellable? cancellable) throws Error { ClientSession.Context needed_context = (is_select) ? ClientSession.Context.SELECTED : ClientSession.Context.EXAMINED; foreach (ClientSession session in sessions) { diff --git a/src/engine/sqlite/api/sqlite-folder.vala b/src/engine/sqlite/api/sqlite-folder.vala index f5e4f444..1357e0e2 100644 --- a/src/engine/sqlite/api/sqlite-folder.vala +++ b/src/engine/sqlite/api/sqlite-folder.vala @@ -7,7 +7,7 @@ // TODO: This class currently deals with generic email storage as well as IMAP-specific issues; in // the future, to support other email services, will need to break this up. -public class Geary.Sqlite.Folder : Object, Geary.Folder, Geary.LocalFolder { +public class Geary.Sqlite.Folder : Geary.AbstractFolder, Geary.LocalFolder { private MailDatabase db; private FolderRow folder_row; private MessageTable message_table; @@ -32,15 +32,15 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder, Geary.LocalFolder { throw new EngineError.OPEN_REQUIRED("%s not open", to_string()); } - public string get_name() { + public override string get_name() { return name; } - public Geary.FolderProperties? get_properties() { + public override Geary.FolderProperties? get_properties() { return null; } - public async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { + public override async void open_async(bool readonly, Cancellable? cancellable = null) throws Error { if (opened) throw new EngineError.ALREADY_OPEN("%s already open", to_string()); @@ -48,7 +48,7 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder, Geary.LocalFolder { notify_opened(); } - public async void close_async(Cancellable? cancellable = null) throws Error { + public override async void close_async(Cancellable? cancellable = null) throws Error { if (!opened) return; @@ -56,14 +56,15 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder, Geary.LocalFolder { notify_closed(CloseReason.FOLDER_CLOSED); } - public async int get_email_count(Cancellable? cancellable = null) throws Error { + public override async int get_email_count(Cancellable? cancellable = null) throws Error { check_open(); // TODO return 0; } - public async void create_email_async(Geary.Email email, Cancellable? cancellable = null) throws Error { + public override async void create_email_async(Geary.Email email, Cancellable? cancellable = null) + throws Error { check_open(); Geary.Imap.EmailLocation location = (Geary.Imap.EmailLocation) email.location; @@ -90,7 +91,7 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder, Geary.LocalFolder { yield imap_location_table.create_async(imap_location_row, cancellable); } - public async Gee.List? list_email_async(int low, int count, + public override async Gee.List? list_email_async(int low, int count, Geary.Email.Field required_fields, Cancellable? cancellable) throws Error { assert(low >= 1); assert(count >= 1); @@ -103,7 +104,7 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder, Geary.LocalFolder { return yield list_email(list, required_fields, cancellable); } - public async Gee.List? list_email_sparse_async(int[] by_position, + public override async Gee.List? list_email_sparse_async(int[] by_position, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error { check_open(); @@ -146,7 +147,7 @@ public class Geary.Sqlite.Folder : Object, Geary.Folder, Geary.LocalFolder { return (emails.size > 0) ? emails : null; } - public async Geary.Email fetch_email_async(int position, Geary.Email.Field required_fields, + public override async Geary.Email fetch_email_async(int position, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error { assert(position >= 1); diff --git a/src/wscript b/src/wscript index 7d0354c0..cfcd40bd 100644 --- a/src/wscript +++ b/src/wscript @@ -13,6 +13,7 @@ def build(bld): bld.common_packages = ['glib-2.0', 'unique-1.0', 'posix' ] bld.engine_src = [ + '../engine/api/geary-abstract-folder.vala', '../engine/api/geary-account.vala', '../engine/api/geary-credentials.vala', '../engine/api/geary-email-location.vala', @@ -29,6 +30,7 @@ def build(bld): '../engine/common/common-interfaces.vala', '../engine/common/common-message-data.vala', + '../engine/common/common-nonblocking-semaphore.vala', '../engine/common/common-string.vala', '../engine/imap/api/imap-account.vala',