From be4b5c2fe57fbf667e24d3042239e75f48a6bd78 Mon Sep 17 00:00:00 2001
From: Aleksander Machniak <alec@alec.pl>
Date: Wed, 05 Jun 2013 09:20:53 -0400
Subject: [PATCH] Fix "duplicate entry" errors on inserts to imap cache tables (#1489146)

---
 CHANGELOG                                  |    1 
 program/lib/Roundcube/rcube_db.php         |   26 ++++++++++---
 program/lib/Roundcube/rcube_imap_cache.php |   86 ++++++++++++++++++++++++++++++++++--------
 3 files changed, 90 insertions(+), 23 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 45d9dab..7713afe 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,7 @@
 CHANGELOG Roundcube Webmail
 ===========================
 
+- Fix "duplicate entry" errors on inserts to imap cache tables (#1489146)
 - Fix so bounces addresses in Sender headers are skipped on Reply-All (#1489011)
 - Fix bug where serialized strings were truncated in PDO::quote() (#1489142)
 - Improved handling of Reply-To/Bcc addresses of identity in compose form (#1489016)
diff --git a/program/lib/Roundcube/rcube_db.php b/program/lib/Roundcube/rcube_db.php
index 2d1e32e..69793b9 100644
--- a/program/lib/Roundcube/rcube_db.php
+++ b/program/lib/Roundcube/rcube_db.php
@@ -415,13 +415,16 @@
 
         if ($result === false) {
             $error = $this->dbh->errorInfo();
-            $this->db_error = true;
-            $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
 
-            rcube::raise_error(array('code' => 500, 'type' => 'db',
-                'line' => __LINE__, 'file' => __FILE__,
-                'message' => $this->db_error_msg . " (SQL Query: $query)"
-                ), true, false);
+            if (empty($this->options['ignore_key_errors']) || $error[0] != '23000') {
+                $this->db_error = true;
+                $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
+
+                rcube::raise_error(array('code' => 500, 'type' => 'db',
+                    'line' => __LINE__, 'file' => __FILE__,
+                    'message' => $this->db_error_msg . " (SQL Query: $query)"
+                    ), true, false);
+            }
         }
 
         $this->last_result = $result;
@@ -882,6 +885,17 @@
     }
 
     /**
+     * Set class option value
+     *
+     * @param string $name  Option name
+     * @param mixed  $value Option value
+     */
+    public function set_option($name, $value)
+    {
+        $this->options[$name] = $value;
+    }
+
+    /**
      * MDB2 DSN string parser
      *
      * @param string $sequence Secuence name
diff --git a/program/lib/Roundcube/rcube_imap_cache.php b/program/lib/Roundcube/rcube_imap_cache.php
index 71545f1..403137f 100644
--- a/program/lib/Roundcube/rcube_imap_cache.php
+++ b/program/lib/Roundcube/rcube_imap_cache.php
@@ -437,12 +437,28 @@
             }
         }
 
+        $this->db->set_option('ignore_key_errors', true);
+
         // insert new record
-        $this->db->query(
+        $res = $this->db->query(
             "INSERT INTO ".$this->db->table_name('cache_messages')
             ." (user_id, mailbox, uid, flags, changed, data)"
             ." VALUES (?, ?, ?, ?, ".$this->db->now().", ?)",
             $this->userid, $mailbox, (int) $message->uid, $flags, $msg);
+
+        // race-condition, insert failed so try update (#1489146)
+        // thanks to ignore_key_errors "duplicate row" errors will be ignored
+        if ($force && !$res && !$this->db->is_error($res)) {
+            $this->db->query(
+                "UPDATE ".$this->db->table_name('cache_messages')
+                ." SET flags = ?, data = ?, changed = ".$this->db->now()
+                ." WHERE user_id = ?"
+                    ." AND mailbox = ?"
+                    ." AND uid = ?",
+                $flags, $msg, $this->userid, $mailbox, (int) $message->uid);
+        }
+
+        $this->db->set_option('ignore_key_errors', false);
     }
 
 
@@ -714,20 +730,38 @@
         $data = implode('@', $data);
 
         if ($exists) {
-            $sql_result = $this->db->query(
+            $res = $this->db->query(
+                "UPDATE ".$this->db->table_name('cache_index')
+                ." SET data = ?, valid = 1, changed = ".$this->db->now()
+                ." WHERE user_id = ?"
+                    ." AND mailbox = ?",
+                $data, $this->userid, $mailbox);
+
+            if ($this->db->affected_rows($res)) {
+                return;
+            }
+        }
+
+        $this->db->set_option('ignore_key_errors', true);
+
+        $res = $this->db->query(
+            "INSERT INTO ".$this->db->table_name('cache_index')
+            ." (user_id, mailbox, data, valid, changed)"
+            ." VALUES (?, ?, ?, 1, ".$this->db->now().")",
+            $this->userid, $mailbox, $data);
+
+        // race-condition, insert failed so try update (#1489146)
+        // thanks to ignore_key_errors "duplicate row" errors will be ignored
+        if (!$exists && !$res && !$this->db->is_error($res)) {
+            $res = $this->db->query(
                 "UPDATE ".$this->db->table_name('cache_index')
                 ." SET data = ?, valid = 1, changed = ".$this->db->now()
                 ." WHERE user_id = ?"
                     ." AND mailbox = ?",
                 $data, $this->userid, $mailbox);
         }
-        else {
-            $sql_result = $this->db->query(
-                "INSERT INTO ".$this->db->table_name('cache_index')
-                ." (user_id, mailbox, data, valid, changed)"
-                ." VALUES (?, ?, ?, 1, ".$this->db->now().")",
-                $this->userid, $mailbox, $data);
-        }
+
+        $this->db->set_option('ignore_key_errors', false);
     }
 
 
@@ -745,20 +779,38 @@
         $data = implode('@', $data);
 
         if ($exists) {
-            $sql_result = $this->db->query(
+            $res = $this->db->query(
+                "UPDATE ".$this->db->table_name('cache_thread')
+                ." SET data = ?, changed = ".$this->db->now()
+                ." WHERE user_id = ?"
+                    ." AND mailbox = ?",
+                $data, $this->userid, $mailbox);
+
+            if ($this->db->affected_rows($res)) {
+                return;
+            }
+        }
+
+        $this->db->set_option('ignore_key_errors', true);
+
+        $res = $this->db->query(
+            "INSERT INTO ".$this->db->table_name('cache_thread')
+            ." (user_id, mailbox, data, changed)"
+            ." VALUES (?, ?, ?, ".$this->db->now().")",
+            $this->userid, $mailbox, $data);
+
+        // race-condition, insert failed so try update (#1489146)
+        // thanks to ignore_key_errors "duplicate row" errors will be ignored
+        if (!$exists && !$res && !$this->db->is_error($res)) {
+            $this->db->query(
                 "UPDATE ".$this->db->table_name('cache_thread')
                 ." SET data = ?, changed = ".$this->db->now()
                 ." WHERE user_id = ?"
                     ." AND mailbox = ?",
                 $data, $this->userid, $mailbox);
         }
-        else {
-            $sql_result = $this->db->query(
-                "INSERT INTO ".$this->db->table_name('cache_thread')
-                ." (user_id, mailbox, data, changed)"
-                ." VALUES (?, ?, ?, ".$this->db->now().")",
-                $this->userid, $mailbox, $data);
-        }
+
+        $this->db->set_option('ignore_key_errors', false);
     }
 
 

--
Gitblit v1.9.1