[geary/wip/713150-conversations: 6/9] ConversationTable, MessageConversationTable garbage collection



commit e744e1461772d2061906717c38b28c81654a37f9
Author: Jim Nelson <jim yorba org>
Date:   Mon Mar 2 16:35:16 2015 -0800

    ConversationTable, MessageConversationTable garbage collection

 src/client/application/geary-args.vala             |    4 +
 src/client/application/geary-controller.vala       |   10 ++-
 src/engine/api/geary-account.vala                  |   27 +++++-
 src/engine/imap-db/imap-db-account.vala            |    6 +-
 src/engine/imap-db/imap-db-database.vala           |   14 ++-
 src/engine/imap-db/imap-db-gc.vala                 |  105 +++++++++++++++++++-
 .../imap-engine/imap-engine-generic-account.vala   |   15 ++-
 7 files changed, 166 insertions(+), 15 deletions(-)
---
diff --git a/src/client/application/geary-args.vala b/src/client/application/geary-args.vala
index 8ba4d2a..d3bf529 100644
--- a/src/client/application/geary-args.vala
+++ b/src/client/application/geary-args.vala
@@ -24,6 +24,8 @@ private const OptionEntry[] options = {
     { "log-folder-normalization", 0, 0, OptionArg.NONE, ref log_folder_normalization, N_("Log folder 
normalization"), null },
     { "inspector", 'i', 0, OptionArg.NONE, ref inspector, N_("Allow inspection of WebView"), null },
     { "revoke-certs", 0, 0, OptionArg.NONE, ref revoke_certs, N_("Revoke all server certificates with TLS 
warnings"), null },
+    { "force-cleanup", 0, OptionFlags.HIDDEN, OptionArg.NONE, ref force_cleanup, N_("Force a mail database 
cleanup (not recommended)"), null },
+    { "force-rebuild", 0, OptionFlags.HIDDEN, OptionArg.NONE, ref force_rebuild, N_("Force a mail database 
rebuild (not recommended)"), null },
     { "version", 'V', 0, OptionArg.NONE, ref version, N_("Display program version"), null },
     { null }
 };
@@ -40,6 +42,8 @@ public bool log_sql = false;
 public bool log_folder_normalization = false;
 public bool inspector = false;
 public bool revoke_certs = false;
+public bool force_cleanup = false;
+public bool force_rebuild = false;
 public bool version = false;
 
 public bool parse(string[] args) {
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index c60574c..4d354da 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -100,6 +100,7 @@ public class GearyController : Geary.BaseObject {
         = new Gee.HashMap<Geary.Account, Geary.App.EmailStore>();
     private Gee.HashMap<Geary.Account, Geary.Folder> inboxes
         = new Gee.HashMap<Geary.Account, Geary.Folder>();
+    private Geary.Account.OpenFlag account_flags = Geary.Account.OpenFlag.NONE;
     private Geary.Folder? current_folder = null;
     private Cancellable cancellable_folder = new Cancellable();
     private Cancellable cancellable_search = new Cancellable();
@@ -232,6 +233,13 @@ public class GearyController : Geary.BaseObject {
         libnotify = new Libnotify(new_messages_monitor);
         libnotify.invoked.connect(on_libnotify_invoked);
         
+        // Geary.Account flags
+        if (Args.force_cleanup)
+            account_flags |= Geary.Account.OpenFlag.FORCE_DB_CLEANUP;
+        
+        if (Args.force_rebuild)
+            account_flags |= Geary.Account.OpenFlag.FORCE_DB_REBUILD;
+        
         // This is fired after the accounts are ready.
         Geary.Engine.instance.opened.connect(on_engine_opened);
         
@@ -1058,7 +1066,7 @@ public class GearyController : Geary.BaseObject {
         do {
             try {
                 account.set_data(PROP_ATTEMPT_OPEN_ACCOUNT, true);
-                yield account.open_async(cancellable);
+                yield account.open_async(account_flags, cancellable);
                 retry = false;
             } catch (Error open_err) {
                 debug("Unable to open account %s: %s", account.to_string(), open_err.message);
diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala
index b5f950d..9c1287a 100644
--- a/src/engine/api/geary-account.vala
+++ b/src/engine/api/geary-account.vala
@@ -21,6 +21,31 @@
  */
 
 public abstract class Geary.Account : BaseObject {
+    /**
+     * Flags for specific options when opening an { link Account}.
+     *
+     * @see open_async
+     */
+    [Flags]
+    public enum OpenFlag {
+        NONE = 0,
+        /**
+         * Tell the local database / disk store to rebuild.
+         *
+         * This should be used sparingly or only if directed by the user.  The local database
+         * should perform this automatically when it detects it's necessary.
+         */
+        FORCE_DB_REBUILD,
+        /**
+         * Tell the local database / disk store to cleanup.
+         *
+         *
+         * This should be used sparingly or only if directed by the user.  The local database
+         * should perform this automatically when it detects it's necessary.
+         */
+        FORCE_DB_CLEANUP
+    }
+    
     public enum Problem {
         RECV_EMAIL_LOGIN_FAILED,
         SEND_EMAIL_LOGIN_FAILED,
@@ -227,7 +252,7 @@ public abstract class Geary.Account : BaseObject {
      * @throws EngineError.VERSION if the local store was created or updated for a different
      *         version of Geary.
      */
-    public abstract async void open_async(Cancellable? cancellable = null) throws Error;
+    public abstract async void open_async(OpenFlag open_flags, Cancellable? cancellable = null) throws Error;
     
     /**
      * Closes the { link Account}, which makes most its operations unavailable.
diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala
index e638c06..56133b1 100644
--- a/src/engine/imap-db/imap-db-account.vala
+++ b/src/engine/imap-db/imap-db-account.vala
@@ -71,8 +71,8 @@ private class Geary.ImapDB.Account : BaseObject {
         attachments_dir = ImapDB.Attachment.get_attachments_dir(user_data_dir);
     }
     
-    public async void open_async(File user_data_dir, File schema_dir, Cancellable? cancellable)
-        throws Error {
+    public async void open_async(Database.ImplFlag impl_flags, File user_data_dir, File schema_dir,
+        Cancellable? cancellable) throws Error {
         if (db != null)
             throw new EngineError.ALREADY_OPEN("IMAP database already open");
         
@@ -82,7 +82,7 @@ private class Geary.ImapDB.Account : BaseObject {
         try {
             yield db.open_async(
                 Db.DatabaseFlags.CREATE_DIRECTORY | Db.DatabaseFlags.CREATE_FILE | 
Db.DatabaseFlags.CHECK_CORRUPTION,
-                cancellable);
+                impl_flags, cancellable);
         } catch (Error err) {
             warning("Unable to open database: %s", err.message);
             
diff --git a/src/engine/imap-db/imap-db-database.vala b/src/engine/imap-db/imap-db-database.vala
index 90c2fc8..549d0be 100644
--- a/src/engine/imap-db/imap-db-database.vala
+++ b/src/engine/imap-db/imap-db-database.vala
@@ -10,6 +10,13 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
     private const string DB_FILENAME = "geary.db";
     private const int OPEN_PUMP_EVENT_LOOP_MSEC = 100;
     
+    [Flags]
+    public enum ImplFlag {
+        NONE = 0,
+        FORCE_REAP,
+        FORCE_VACUUM
+    }
+    
     private ProgressMonitor upgrade_monitor;
     private ProgressMonitor vacuum_monitor;
     private string account_owner_email;
@@ -35,7 +42,8 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
      * This should only be done from the main thread, as it is designed to pump the event loop
      * while the database is being opened and updated.
      */
-    public async void open_async(Db.DatabaseFlags flags, Cancellable? cancellable) throws Error {
+    public async void open_async(Db.DatabaseFlags flags, ImplFlag impl_flags, Cancellable? cancellable)
+        throws Error {
         open_background(flags, on_prepare_database_connection, pump_event_loop,
             OPEN_PUMP_EVENT_LOOP_MSEC, cancellable);
         
@@ -52,7 +60,7 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
         
         // VACUUM needs to execute in the foreground with the user given a busy prompt (and cannot
         // be run at the same time as REAP)
-        if ((recommended & GC.RecommendedOperation.VACUUM) != 0) {
+        if ((recommended & GC.RecommendedOperation.VACUUM) != 0 || (impl_flags & ImplFlag.FORCE_VACUUM) != 
0) {
             if (!vacuum_monitor.is_in_progress)
                 vacuum_monitor.notify_start();
             
@@ -69,7 +77,7 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
         }
         
         // REAP can run in the background while the application is executing
-        if ((recommended & GC.RecommendedOperation.REAP) != 0) {
+        if ((recommended & GC.RecommendedOperation.REAP) != 0 || (impl_flags & ImplFlag.FORCE_REAP) != 0) {
             // run in the background and allow application to continue running
             gc.reap_async.begin(gc_cancellable, on_reap_async_completed);
         }
diff --git a/src/engine/imap-db/imap-db-gc.vala b/src/engine/imap-db/imap-db-gc.vala
index bba994f..1050a22 100644
--- a/src/engine/imap-db/imap-db-gc.vala
+++ b/src/engine/imap-db/imap-db-gc.vala
@@ -316,7 +316,7 @@ private class Geary.ImapDB.GC {
         // at a time, deleting it from the message table and subsidiary tables.  Although slow, we
         // do want this to be a background task that doesn't interrupt the user.  This approach
         // also means gc can be interrupted at any time (i.e. the user exits the application)
-        // without leaving the database in an incoherent state.  gc can be resumed even if'
+        // without leaving the database in an incoherent state.  gc can be resumed even if
         // interrupted.
         //
         
@@ -340,7 +340,40 @@ private class Geary.ImapDB.GC {
                 debug("[%s] Reaped %d messages", to_string(), count);
         }
         
-        message("[%s] Reaped completed: %d messages", to_string(), count);
+        message("[%s] Reap completed: %d messages reaped", to_string(), count);
+        
+        //
+        // Delete orphaned conversations (which shouldn't happen, but it's a simple check to
+        // perform)
+        //
+        
+        count = 0;
+        yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
+            Db.Statement stmt = cx.prepare("""
+                SELECT id
+                FROM ConversationTable
+                WHERE NOT EXISTS (
+                    SELECT conversation_id
+                    FROM MessageConversationTable
+                    WHERE MessageConversationTable.conversation_id = ConversationTable.id
+                )
+            """);
+            
+            for (Db.Result result = stmt.exec(cancellable); !result.finished; result.next(cancellable)) {
+                stmt = cx.prepare("""
+                    DELETE FROM ConversationTable
+                    WHERE id = ?
+                """);
+                stmt.bind_rowid(0, result.rowid_at(0));
+                
+                stmt.exec(cancellable);
+                count++;
+            }
+            
+            return Db.TransactionOutcome.COMMIT;
+        }, cancellable);
+        
+        message("[%s] Reap orphan conversations completed: %d conversations reaped", to_string(), count);
         
         //
         // Now reap on-disk attachment files marked for deletion.  Since they're added to the
@@ -363,7 +396,8 @@ private class Geary.ImapDB.GC {
                 debug("[%s] Reaped %d attachment files", to_string(), count);
         }
         
-        message("[%s] Completed: Reaped %d attachment files", to_string(), count);
+        message("[%s] Reap orphan attachment files completed: Reaped %d attachment files", to_string(),
+            count);
         
         //
         // To be sure everything's clean, delete any empty directories in the attachment dir tree,
@@ -462,6 +496,71 @@ private class Geary.ImapDB.GC {
             stmt.exec(cancellable);
             
             //
+            // See which conversation the message is linked to (if any)
+            //
+            
+            stmt = cx.prepare("""
+                SELECT conversation_id
+                FROM MessageConversationTable
+                WHERE message_id = ?
+            """);
+            stmt.bind_rowid(0, message_id);
+            
+            result = stmt.exec(cancellable);
+            int64 conversation_id = !result.finished ? result.rowid_at(0) : Db.INVALID_ROWID;
+            
+            //
+            // Unlink from message-conversation table
+            //
+            // Don't delete, as MessageConversationTable tracks *all* Message-IDs seen in emails'
+            // REFERENCES (Message-ID, In-Reply-To, References).  These rows are only deleted when
+            // the conversation itself is deleted, meaning all related emails are gone.
+            //
+            
+            stmt = cx.prepare("""
+                UPDATE MessageConversationTable
+                SET message_id = ?
+                WHERE message_id = ?
+            """);
+            stmt.bind_null(0);
+            stmt.bind_rowid(1, message_id);
+            
+            stmt.exec(cancellable);
+            
+            //
+            // Look for orphaned conversations, where all Message-IDs linked to it are no longer
+            // referenced in the MessageTable
+            //
+            
+            if (conversation_id != Db.INVALID_ROWID) {
+                stmt = cx.prepare("""
+                    SELECT COUNT(*)
+                    FROM MessageConversationTable
+                    WHERE conversation_id = ? AND message_id IS NOT NULL
+                """);
+                stmt.bind_rowid(0, conversation_id);
+                
+                result = stmt.exec(cancellable);
+                if (result.finished || result.int_at(0) == 0) {
+                    stmt = cx.prepare("""
+                        DELETE FROM MessageConversationTable
+                        WHERE conversation_id = ?
+                    """);
+                    stmt.bind_rowid(0, conversation_id);
+                    
+                    stmt.exec(cancellable);
+                    
+                    stmt = cx.prepare("""
+                        DELETE FROM ConversationTable
+                        WHERE id = ?
+                    """);
+                    stmt.bind_rowid(0, conversation_id);
+                    
+                    stmt.exec(cancellable);
+                }
+            }
+            
+            //
             // Delete from message table
             //
             
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala 
b/src/engine/imap-engine/imap-engine-generic-account.vala
index 8372b0c..3980400 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -99,7 +99,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
             throw new EngineError.OPEN_REQUIRED("Account %s not opened", to_string());
     }
     
-    public override async void open_async(Cancellable? cancellable = null) throws Error {
+    public override async void open_async(Account.OpenFlag open_flags, Cancellable? cancellable = null) 
throws Error {
         if (open)
             throw new EngineError.ALREADY_OPEN("Account %s already opened", to_string());
         
@@ -107,7 +107,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
         
         Error? throw_err = null;
         try {
-            yield internal_open_async(cancellable);
+            yield internal_open_async(open_flags, cancellable);
         } catch (Error err) {
             throw_err = err;
         }
@@ -118,9 +118,16 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
             throw throw_err;
     }
     
-    private async void internal_open_async(Cancellable? cancellable) throws Error {
+    private async void internal_open_async(Account.OpenFlag open_flags, Cancellable? cancellable) throws 
Error {
+        // Translate Account flags to ImapDB.Database flags
+        ImapDB.Database.ImplFlag db_flags = ImapDB.Database.ImplFlag.NONE;
+        if ((open_flags & Account.OpenFlag.FORCE_DB_REBUILD) != 0)
+            db_flags |= ImapDB.Database.ImplFlag.FORCE_VACUUM;
+        if ((open_flags & Account.OpenFlag.FORCE_DB_CLEANUP) != 0)
+            db_flags |= ImapDB.Database.ImplFlag.FORCE_REAP;
+        
         try {
-            yield local.open_async(information.settings_dir, Engine.instance.resource_dir.get_child("sql"),
+            yield local.open_async(db_flags, information.settings_dir, 
Engine.instance.resource_dir.get_child("sql"),
                 cancellable);
         } catch (Error err) {
             // convert database-open errors


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]