]> git.mxchange.org Git - quix0rs-gnu-social.git/blobdiff - classes/Memcached_DataObject.php
Merge commit 'mainline/0.9.x' into 0.9.x
[quix0rs-gnu-social.git] / classes / Memcached_DataObject.php
index 96f8d520bae94d28f7d684805317ad77e0403dd5..33645a3e8b6eeaf75246d69e87f70db41ab209a7 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 /*
- * Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, 2009, Control Yourself, Inc.
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-if (!defined('LACONICA')) { exit(1); }
-
-require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
 
 class Memcached_DataObject extends DB_DataObject
 {
+    /**
+     * Destructor to free global memory resources associated with
+     * this data object when it's unset or goes out of scope.
+     * DB_DataObject doesn't do this yet by itself.
+     */
+
+    function __destruct()
+    {
+        $this->free();
+        if (method_exists('DB_DataObject', '__destruct')) {
+            parent::__destruct();
+        }
+    }
+
+    /**
+     * Magic function called at serialize() time.
+     *
+     * We use this to drop a couple process-specific references
+     * from DB_DataObject which can cause trouble in future
+     * processes.
+     *
+     * @return array of variable names to include in serialization.
+     */
+    function __sleep()
+    {
+        $vars = array_keys(get_object_vars($this));
+        $skip = array('_DB_resultid', '_link_loaded');
+        return array_diff($vars, $skip);
+    }
+
+    /**
+     * Magic function called at unserialize() time.
+     *
+     * Clean out some process-specific variables which might
+     * be floating around from a previous process's cached
+     * objects.
+     *
+     * Old cached objects may still have them.
+     */
+    function __wakeup()
+    {
+        // Refers to global state info from a previous process.
+        // Clear this out so we don't accidentally break global
+        // state in *this* process.
+        $this->_DB_resultid = null;
+
+        // We don't have any local DBO refs, so clear these out.
+        $this->_link_loaded = false;
+    }
+
+    /**
+     * Wrapper for DB_DataObject's static lookup using memcached
+     * as backing instead of an in-process cache array.
+     *
+     * @param string $cls classname of object type to load
+     * @param mixed $k key field name, or value for primary key
+     * @param mixed $v key field value, or leave out for primary key lookup
+     * @return mixed Memcached_DataObject subtype or false
+     */
     function &staticGet($cls, $k, $v=null)
     {
         if (is_null($v)) {
@@ -33,41 +90,29 @@ class Memcached_DataObject extends DB_DataObject
             $k = $keys[0];
             unset($i);
         }
-        $i = self::getcached($cls, $k, $v);
+        $i = Memcached_DataObject::getcached($cls, $k, $v);
         if ($i) {
             return $i;
         } else {
-            $i = DB_DataObject::staticGet($cls, $k, $v);
-            if ($i) {
+            $i = DB_DataObject::factory($cls);
+            if (empty($i)) {
+                $i = false;
+                return $i;
+            }
+            $result = $i->get($k, $v);
+            if ($result) {
                 $i->encache();
+                return $i;
             } else {
-                self::cachenull($cls, $k, $v);
+                $i = false;
+                return $i;
             }
-            return $i;
-        }
-    }
-
-    function cachenull($cls, $k, $v)
-    {
-        $c = self::memcache();
-        if (empty($c)) {
-            return;
-        }
-        $c->set(self::cacheKey($cls, $k, $v), null);
-    }
-
-    function multicachenull($cls, $kv)
-    {
-        $c = self::memcache();
-        if (empty($c)) {
-            return;
         }
-        $c->set(self::multicachekey($cls, $kv), null);
     }
 
     function &pkeyGet($cls, $kv)
     {
-        $i = self::multicache($cls, $kv);
+        $i = Memcached_DataObject::multicache($cls, $kv);
         if ($i) {
             return $i;
         } else {
@@ -78,7 +123,6 @@ class Memcached_DataObject extends DB_DataObject
             if ($i->find(true)) {
                 $i->encache();
             } else {
-                self::multicachenull($cls, $kv);
                 $i = null;
             }
             return $i;
@@ -88,9 +132,6 @@ class Memcached_DataObject extends DB_DataObject
     function insert()
     {
         $result = parent::insert();
-        if ($result) {
-            $this->encache();
-        }
         return $result;
     }
 
@@ -117,15 +158,29 @@ class Memcached_DataObject extends DB_DataObject
     }
 
     static function cacheKey($cls, $k, $v) {
+        if (is_object($cls) || is_object($k) || is_object($v)) {
+            $e = new Exception();
+            common_log(LOG_ERR, __METHOD__ . ' object in param: ' .
+                str_replace("\n", " ", $e->getTraceAsString()));
+        }
         return common_cache_key(strtolower($cls).':'.$k.':'.$v);
     }
 
     static function getcached($cls, $k, $v) {
-        $c = self::memcache();
+        $c = Memcached_DataObject::memcache();
         if (!$c) {
             return false;
         } else {
-            return $c->get(self::cacheKey($cls, $k, $v));
+            $obj = $c->get(Memcached_DataObject::cacheKey($cls, $k, $v));
+            if (0 == strcasecmp($cls, 'User')) {
+                // Special case for User
+                if (is_object($obj) && is_object($obj->id)) {
+                    common_log(LOG_ERR, "User " . $obj->nickname . " was cached with User as ID; deleting");
+                    $c->delete(Memcached_DataObject::cacheKey($cls, $k, $v));
+                    return false;
+                }
+            }
+            return $obj;
         }
     }
 
@@ -144,6 +199,12 @@ class Memcached_DataObject extends DB_DataObject
         $c = $this->memcache();
         if (!$c) {
             return false;
+        } else if ($this->tableName() == 'user' && is_object($this->id)) {
+            // Special case for User bug
+            $e = new Exception();
+            common_log(LOG_ERR, __METHOD__ . ' caching user with User object as ID ' .
+                       str_replace("\n", " ", $e->getTraceAsString()));
+            return false;
         } else {
             $pkey = array();
             $pval = array();
@@ -192,56 +253,43 @@ class Memcached_DataObject extends DB_DataObject
 
     function multicache($cls, $kv)
     {
-        $c = self::memcache();
+        ksort($kv);
+        $c = Memcached_DataObject::memcache();
         if (!$c) {
             return false;
         } else {
-            return $c->get(self::multicachekey($cls, $kv));
+            $pkeys = implode(',', array_keys($kv));
+            $pvals = implode(',', array_values($kv));
+            return $c->get(Memcached_DataObject::cacheKey($cls, $pkeys, $pvals));
         }
     }
 
-    function multicachekey($cls, $kv)
-    {
-        ksort($kv);
-        $pkeys = implode(',', array_keys($kv));
-        $pvals = implode(',', array_values($kv));
-
-        return self::cacheKey($cls, $pkeys, $pvals);
-    }
-
     function getSearchEngine($table)
     {
         require_once INSTALLDIR.'/lib/search_engines.php';
         static $search_engine;
         if (!isset($search_engine)) {
-                $connected = false;
-                if (common_config('sphinx', 'enabled')) {
-                    $search_engine = new SphinxSearch($this, $table);
-                    $connected = $search_engine->is_connected();
-                }
-
-                // unable to connect to sphinx' search daemon
-                if (!$connected) {
-                    if ('mysql' === common_config('db', 'type')) {
-                        $type = common_config('search', 'type');
-                        if ($type == 'like') {
-                            $search_engine = new MySQLLikeSearch($this, $table);
-                        } else if ($type == 'fulltext') {
-                            $search_engine = new MySQLSearch($this, $table);
-                        } else {
-                            throw new ServerException('Unknown search type: ' . $type);
-                        }
+            if (Event::handle('GetSearchEngine', array($this, $table, &$search_engine))) {
+                if ('mysql' === common_config('db', 'type')) {
+                    $type = common_config('search', 'type');
+                    if ($type == 'like') {
+                        $search_engine = new MySQLLikeSearch($this, $table);
+                    } else if ($type == 'fulltext') {
+                        $search_engine = new MySQLSearch($this, $table);
                     } else {
-                        $search_engine = new PGSearch($this, $table);
+                        throw new ServerException('Unknown search type: ' . $type);
                     }
+                } else {
+                    $search_engine = new PGSearch($this, $table);
                 }
+            }
         }
         return $search_engine;
     }
 
     static function cachedQuery($cls, $qry, $expiry=3600)
     {
-        $c = self::memcache();
+        $c = Memcached_DataObject::memcache();
         if (!$c) {
             $inst = new $cls();
             $inst->query($qry);
@@ -265,16 +313,81 @@ class Memcached_DataObject extends DB_DataObject
         return new ArrayWrapper($cached);
     }
 
+    /**
+     * sends query to database - this is the private one that must work 
+     *   - internal functions use this rather than $this->query()
+     *
+     * Overridden to do logging.
+     *
+     * @param  string  $string
+     * @access private
+     * @return mixed none or PEAR_Error
+     */
+    function _query($string)
+    {
+        $start = microtime(true);
+        $result = parent::_query($string);
+        $delta = microtime(true) - $start;
+
+        $limit = common_config('db', 'log_slow_queries');
+        if (($limit > 0 && $delta >= $limit) || common_config('db', 'log_queries')) {
+            $clean = $this->sanitizeQuery($string);
+            common_log(LOG_DEBUG, sprintf("DB query (%0.3fs): %s", $delta, $clean));
+        }
+        return $result;
+    }
+
+    // Sanitize a query for logging
+    // @fixme don't trim spaces in string literals
+    function sanitizeQuery($string)
+    {
+        $string = preg_replace('/\s+/', ' ', $string);
+        $string = trim($string);
+        return $string;
+    }
+
     // We overload so that 'SET NAMES "utf8"' is called for
     // each connection
 
     function _connect()
     {
         global $_DB_DATAOBJECT;
-        $exists = !empty($this->_database_dsn_md5) &&
-          isset($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]);
+
+        $sum = $this->_getDbDsnMD5();
+
+        if (!empty($_DB_DATAOBJECT['CONNECTIONS'][$sum]) &&
+            !PEAR::isError($_DB_DATAOBJECT['CONNECTIONS'][$sum])) {
+            $exists = true;
+        } else {
+            $exists = false;
+       }
+
+        // @fixme horrible evil hack!
+        //
+        // In multisite configuration we don't want to keep around a separate
+        // connection for every database; we could end up with thousands of
+        // connections open per thread. In an ideal world we might keep
+        // a connection per server and select different databases, but that'd
+        // be reliant on having the same db username/pass as well.
+        //
+        // MySQL connections are cheap enough we're going to try just
+        // closing out the old connection and reopening when we encounter
+        // a new DSN.
+        //
+        // WARNING WARNING if we end up actually using multiple DBs at a time
+        // we'll need some fancier logic here.
+        if (!$exists && !empty($_DB_DATAOBJECT['CONNECTIONS'])) {
+            foreach ($_DB_DATAOBJECT['CONNECTIONS'] as $index => $conn) {
+                if (!empty($conn)) {
+                    $conn->disconnect();
+                }
+                unset($_DB_DATAOBJECT['CONNECTIONS'][$index]);
+            }
+        }
+
         $result = parent::_connect();
-        if (!$exists) {
+
+        if ($result && !$exists) {
             $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
             if (common_config('db', 'type') == 'mysql' &&
                 common_config('db', 'utf8')) {
@@ -288,7 +401,61 @@ class Memcached_DataObject extends DB_DataObject
                 }
             }
         }
+
         return $result;
     }
 
+    // XXX: largely cadged from DB_DataObject
+
+    function _getDbDsnMD5()
+    {
+        if ($this->_database_dsn_md5) {
+            return $this->_database_dsn_md5;
+        }
+
+        $dsn = $this->_getDbDsn();
+
+        if (is_string($dsn)) {
+            $sum = md5($dsn);
+        } else {
+            /// support array based dsn's
+            $sum = md5(serialize($dsn));
+        }
+
+        return $sum;
+    }
+
+    function _getDbDsn()
+    {
+        global $_DB_DATAOBJECT;
+
+        if (empty($_DB_DATAOBJECT['CONFIG'])) {
+            DB_DataObject::_loadConfig();
+        }
+
+        $options = &$_DB_DATAOBJECT['CONFIG'];
+
+        // if the databse dsn dis defined in the object..
+
+        $dsn = isset($this->_database_dsn) ? $this->_database_dsn : null;
+
+        if (!$dsn) {
+
+            if (!$this->_database) {
+                $this->_database = isset($options["table_{$this->__table}"]) ? $options["table_{$this->__table}"] : null;
+            }
+
+            if ($this->_database && !empty($options["database_{$this->_database}"]))  {
+                $dsn = $options["database_{$this->_database}"];
+            } else if (!empty($options['database'])) {
+                $dsn = $options['database'];
+            }
+        }
+
+        if (!$dsn) {
+            throw new Exception("No database name / dsn found anywhere");
+        }
+
+        return $dsn;
+    }
 }