]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - classes/Memcached_DataObject.php
Fix incorrect translator documentation.
[quix0rs-gnu-social.git] / classes / Memcached_DataObject.php
1 <?php
2 /*
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2008, 2009, StatusNet, Inc.
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU Affero General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU Affero General Public License for more details.
15  *
16  * You should have received a copy of the GNU Affero General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
21
22 class Memcached_DataObject extends Safe_DataObject
23 {
24     /**
25      * Wrapper for DB_DataObject's static lookup using memcached
26      * as backing instead of an in-process cache array.
27      *
28      * @param string $cls classname of object type to load
29      * @param mixed $k key field name, or value for primary key
30      * @param mixed $v key field value, or leave out for primary key lookup
31      * @return mixed Memcached_DataObject subtype or false
32      */
33     function &staticGet($cls, $k, $v=null)
34     {
35         if (is_null($v)) {
36             $v = $k;
37             // XXX: HACK!
38             $i = new $cls;
39             $keys = $i->keys();
40             $k = $keys[0];
41             unset($i);
42         }
43         $i = Memcached_DataObject::getcached($cls, $k, $v);
44         if ($i === false) { // false == cache miss
45             $i = DB_DataObject::factory($cls);
46             if (empty($i)) {
47                 $i = false;
48                 return $i;
49             }
50             $result = $i->get($k, $v);
51             if ($result) {
52                 // Hit!
53                 $i->encache();
54             } else {
55                 // save the fact that no such row exists
56                 $c = self::memcache();
57                 if (!empty($c)) {
58                     $ck = self::cachekey($cls, $k, $v);
59                     $c->set($ck, null);
60                 }
61                 $i = false;
62             }
63         }
64         return $i;
65     }
66
67     /**
68      * Get multiple items from the database by key
69      *
70      * @param string  $cls       Class to fetch
71      * @param string  $keyCol    name of column for key
72      * @param array   $keyVals   key values to fetch
73      * @param boolean $skipNulls return only non-null results?
74      *
75      * @return array Array of objects, in order
76      */
77     function multiGet($cls, $keyCol, $keyVals, $skipNulls=true)
78     {
79         $result = self::pivotGet($cls, $keyCol, $keyVals);
80
81         $values = array_values($result);
82
83         if ($skipNulls) {
84             $tmp = array();
85             foreach ($values as $value) {
86                 if (!empty($value)) {
87                     $tmp[] = $value;
88                 }
89             }
90             $values = $tmp;
91         }
92
93         return new ArrayWrapper($values);
94     }
95
96     /**
97      * Get multiple items from the database by key
98      *
99      * @param string  $cls       Class to fetch
100      * @param string  $keyCol    name of column for key
101      * @param array   $keyVals   key values to fetch
102      * @param boolean $otherCols Other columns to hold fixed
103      *
104      * @return array Array mapping $keyVals to objects, or null if not found
105      */
106     static function pivotGet($cls, $keyCol, $keyVals, $otherCols = array())
107     {
108         $result = array_fill_keys($keyVals, null);
109
110         $toFetch = array();
111
112         foreach ($keyVals as $keyVal) {
113
114             $kv = array_merge($otherCols, array($keyCol => $keyVal));
115
116             $i = self::multicache($cls, $kv);
117
118             if ($i !== false) {
119                 $result[$keyVal] = $i;
120             } else if (!empty($keyVal)) {
121                 $toFetch[] = $keyVal;
122             }
123         }
124
125         if (count($toFetch) > 0) {
126             $i = DB_DataObject::factory($cls);
127             if (empty($i)) {
128                 // TRANS: Exception thrown when a class (%s) could not be instantiated.
129                 throw new Exception(sprintf(_('Cannot instantiate class %s.'),$cls));
130             }
131             foreach ($otherCols as $otherKeyCol => $otherKeyVal) {
132                 $i->$otherKeyCol = $otherKeyVal;
133             }
134             $i->whereAddIn($keyCol, $toFetch, $i->columnType($keyCol));
135             if ($i->find()) {
136                 while ($i->fetch()) {
137                     $copy = clone($i);
138                     $copy->encache();
139                     $result[$i->$keyCol] = $copy;
140                 }
141             }
142
143             // Save state of DB misses
144
145             foreach ($toFetch as $keyVal) {
146                 if (empty($result[$keyVal])) {
147                     $kv = array_merge($otherCols, array($keyCol => $keyVal));
148                     // save the fact that no such row exists
149                     $c = self::memcache();
150                     if (!empty($c)) {
151                         $ck = self::multicacheKey($cls, $kv);
152                         $c->set($ck, null);
153                     }
154                 }
155             }
156         }
157
158         return $result;
159     }
160
161     function listGet($cls, $keyCol, $keyVals)
162     {
163         $result = array_fill_keys($keyVals, array());
164
165         $toFetch = array();
166
167         foreach ($keyVals as $keyVal) {
168             $l = self::cacheGet(sprintf("%s:list:%s:%s", $cls, $keyCol, $keyVal));
169             if ($l !== false) {
170                 $result[$keyVal] = $l;
171             } else {
172                 $toFetch[] = $keyVal;
173             }
174         }
175
176         if (count($toFetch) > 0) {
177             $i = DB_DataObject::factory($cls);
178             if (empty($i)) {
179                 // TRANS: Exception thrown when a class (%s) could not be instantiated.
180                 throw new Exception(sprintf(_('Cannot instantiate class %s.'),$cls));
181             }
182             $i->whereAddIn($keyCol, $toFetch, $i->columnType($keyCol));
183             if ($i->find()) {
184                 while ($i->fetch()) {
185                     $copy = clone($i);
186                     $copy->encache();
187                     $result[$i->$keyCol][] = $copy;
188                 }
189             }
190             foreach ($toFetch as $keyVal)
191             {
192                 self::cacheSet(sprintf("%s:list:%s:%s", $cls, $keyCol, $keyVal),
193                                $result[$keyVal]);
194             }
195         }
196
197         return $result;
198     }
199
200     function columnType($columnName)
201     {
202         $keys = $this->table();
203         if (!array_key_exists($columnName, $keys)) {
204             throw new Exception('Unknown key column ' . $columnName . ' in ' . join(',', array_keys($keys)));
205         }
206
207         $def = $keys[$columnName];
208
209         if ($def & DB_DATAOBJECT_INT) {
210             return 'integer';
211         } else {
212             return 'string';
213         }
214     }
215
216     /**
217      * @todo FIXME: Should this return false on lookup fail to match staticGet?
218      */
219     function pkeyGet($cls, $kv)
220     {
221         $i = Memcached_DataObject::multicache($cls, $kv);
222         if ($i !== false) { // false == cache miss
223             return $i;
224         } else {
225             $i = DB_DataObject::factory($cls);
226             if (empty($i) || PEAR::isError($i)) {
227                 return false;
228             }
229             foreach ($kv as $k => $v) {
230                 if (is_null($v)) {
231                     // XXX: possible SQL injection...? Don't
232                     // pass keys from the browser, eh.
233                     $i->whereAdd("$k is null");
234                 } else {
235                     $i->$k = $v;
236                 }
237             }
238             if ($i->find(true)) {
239                 $i->encache();
240             } else {
241                 $i = null;
242                 $c = self::memcache();
243                 if (!empty($c)) {
244                     $ck = self::multicacheKey($cls, $kv);
245                     $c->set($ck, null);
246                 }
247             }
248             return $i;
249         }
250     }
251
252     function insert()
253     {
254         $result = parent::insert();
255         if ($result) {
256             $this->fixupTimestamps();
257             $this->encache(); // in case of cached negative lookups
258         }
259         return $result;
260     }
261
262     function update($orig=null)
263     {
264         if (is_object($orig) && $orig instanceof Memcached_DataObject) {
265             $orig->decache(); # might be different keys
266         }
267         $result = parent::update($orig);
268         if ($result) {
269             $this->fixupTimestamps();
270             $this->encache();
271         }
272         return $result;
273     }
274
275     function delete()
276     {
277         $this->decache(); # while we still have the values!
278         return parent::delete();
279     }
280
281     static function memcache() {
282         return Cache::instance();
283     }
284
285     static function cacheKey($cls, $k, $v) {
286         if (is_object($cls) || is_object($k) || (is_object($v) && !($v instanceof DB_DataObject_Cast))) {
287             $e = new Exception();
288             common_log(LOG_ERR, __METHOD__ . ' object in param: ' .
289                 str_replace("\n", " ", $e->getTraceAsString()));
290         }
291         $vstr = self::valueString($v);
292         return Cache::key(strtolower($cls).':'.$k.':'.$vstr);
293     }
294
295     static function getcached($cls, $k, $v) {
296         $c = Memcached_DataObject::memcache();
297         if (!$c) {
298             return false;
299         } else {
300             $obj = $c->get(Memcached_DataObject::cacheKey($cls, $k, $v));
301             if (0 == strcasecmp($cls, 'User')) {
302                 // Special case for User
303                 if (is_object($obj) && is_object($obj->id)) {
304                     common_log(LOG_ERR, "User " . $obj->nickname . " was cached with User as ID; deleting");
305                     $c->delete(Memcached_DataObject::cacheKey($cls, $k, $v));
306                     return false;
307                 }
308             }
309             return $obj;
310         }
311     }
312
313     function keyTypes()
314     {
315         // ini-based classes return number-indexed arrays. handbuilt
316         // classes return column => keytype. Make this uniform.
317
318         $keys = $this->keys();
319
320         $keyskeys = array_keys($keys);
321
322         if (is_string($keyskeys[0])) {
323             return $keys;
324         }
325
326         global $_DB_DATAOBJECT;
327         if (!isset($_DB_DATAOBJECT['INI'][$this->_database][$this->__table."__keys"])) {
328             $this->databaseStructure();
329
330         }
331         return $_DB_DATAOBJECT['INI'][$this->_database][$this->__table."__keys"];
332     }
333
334     function encache()
335     {
336         $c = $this->memcache();
337
338         if (!$c) {
339             return false;
340         } else if ($this->tableName() == 'user' && is_object($this->id)) {
341             // Special case for User bug
342             $e = new Exception();
343             common_log(LOG_ERR, __METHOD__ . ' caching user with User object as ID ' .
344                        str_replace("\n", " ", $e->getTraceAsString()));
345             return false;
346         } else {
347             $keys = $this->_allCacheKeys();
348
349             foreach ($keys as $key) {
350                 $c->set($key, $this);
351             }
352         }
353     }
354
355     function decache()
356     {
357         $c = $this->memcache();
358
359         if (!$c) {
360             return false;
361         }
362
363         $keys = $this->_allCacheKeys();
364
365         foreach ($keys as $key) {
366             $c->delete($key, $this);
367         }
368     }
369
370     function _allCacheKeys()
371     {
372         $ckeys = array();
373
374         $types = $this->keyTypes();
375         ksort($types);
376
377         $pkey = array();
378         $pval = array();
379
380         foreach ($types as $key => $type) {
381
382             assert(!empty($key));
383
384             if ($type == 'U') {
385                 if (empty($this->$key)) {
386                     continue;
387                 }
388                 $ckeys[] = $this->cacheKey($this->tableName(), $key, self::valueString($this->$key));
389             } else if ($type == 'K' || $type == 'N') {
390                 $pkey[] = $key;
391                 $pval[] = self::valueString($this->$key);
392             } else {
393                 // Low level exception. No need for i18n as discussed with Brion.
394                 throw new Exception("Unknown key type $key => $type for " . $this->tableName());
395             }
396         }
397
398         assert(count($pkey) > 0);
399
400         // XXX: should work for both compound and scalar pkeys
401         $pvals = implode(',', $pval);
402         $pkeys = implode(',', $pkey);
403
404         $ckeys[] = $this->cacheKey($this->tableName(), $pkeys, $pvals);
405
406         return $ckeys;
407     }
408
409     function multicache($cls, $kv)
410     {
411         ksort($kv);
412         $c = self::memcache();
413         if (!$c) {
414             return false;
415         } else {
416             return $c->get(self::multicacheKey($cls, $kv));
417         }
418     }
419
420     static function multicacheKey($cls, $kv)
421     {
422         ksort($kv);
423         $pkeys = implode(',', array_keys($kv));
424         $pvals = implode(',', array_values($kv));
425         return self::cacheKey($cls, $pkeys, $pvals);
426     }
427
428     function getSearchEngine($table)
429     {
430         require_once INSTALLDIR.'/lib/search_engines.php';
431
432         if (Event::handle('GetSearchEngine', array($this, $table, &$search_engine))) {
433             if ('mysql' === common_config('db', 'type')) {
434                 $type = common_config('search', 'type');
435                 if ($type == 'like') {
436                     $search_engine = new MySQLLikeSearch($this, $table);
437                 } else if ($type == 'fulltext') {
438                     $search_engine = new MySQLSearch($this, $table);
439                 } else {
440                     // Low level exception. No need for i18n as discussed with Brion.
441                     throw new ServerException('Unknown search type: ' . $type);
442                 }
443             } else {
444                 $search_engine = new PGSearch($this, $table);
445             }
446         }
447
448         return $search_engine;
449     }
450
451     static function cachedQuery($cls, $qry, $expiry=3600)
452     {
453         $c = Memcached_DataObject::memcache();
454         if (!$c) {
455             $inst = new $cls();
456             $inst->query($qry);
457             return $inst;
458         }
459         $key_part = Cache::keyize($cls).':'.md5($qry);
460         $ckey = Cache::key($key_part);
461         $stored = $c->get($ckey);
462
463         if ($stored !== false) {
464             return new ArrayWrapper($stored);
465         }
466
467         $inst = new $cls();
468         $inst->query($qry);
469         $cached = array();
470         while ($inst->fetch()) {
471             $cached[] = clone($inst);
472         }
473         $inst->free();
474         $c->set($ckey, $cached, Cache::COMPRESSED, $expiry);
475         return new ArrayWrapper($cached);
476     }
477
478     /**
479      * sends query to database - this is the private one that must work
480      *   - internal functions use this rather than $this->query()
481      *
482      * Overridden to do logging.
483      *
484      * @param  string  $string
485      * @access private
486      * @return mixed none or PEAR_Error
487      */
488     function _query($string)
489     {
490         if (common_config('db', 'annotate_queries')) {
491             $string = $this->annotateQuery($string);
492         }
493
494         $start = microtime(true);
495         $fail = false;
496         $result = null;
497         if (Event::handle('StartDBQuery', array($this, $string, &$result))) {
498             common_perf_counter('query', $string);
499             try {
500                 $result = parent::_query($string);
501             } catch (Exception $e) {
502                 $fail = $e;
503             }
504             Event::handle('EndDBQuery', array($this, $string, &$result));
505         }
506         $delta = microtime(true) - $start;
507
508         $limit = common_config('db', 'log_slow_queries');
509         if (($limit > 0 && $delta >= $limit) || common_config('db', 'log_queries')) {
510             $clean = $this->sanitizeQuery($string);
511             if ($fail) {
512                 $msg = sprintf("FAILED DB query (%0.3fs): %s - %s", $delta, $fail->getMessage(), $clean);
513             } else {
514                 $msg = sprintf("DB query (%0.3fs): %s", $delta, $clean);
515             }
516             common_log(LOG_DEBUG, $msg);
517         }
518
519         if ($fail) {
520             throw $fail;
521         }
522         return $result;
523     }
524
525     /**
526      * Find the first caller in the stack trace that's not a
527      * low-level database function and add a comment to the
528      * query string. This should then be visible in process lists
529      * and slow query logs, to help identify problem areas.
530      *
531      * Also marks whether this was a web GET/POST or which daemon
532      * was running it.
533      *
534      * @param string $string SQL query string
535      * @return string SQL query string, with a comment in it
536      */
537     function annotateQuery($string)
538     {
539         $ignore = array('annotateQuery',
540                         '_query',
541                         'query',
542                         'get',
543                         'insert',
544                         'delete',
545                         'update',
546                         'find');
547         $ignoreStatic = array('staticGet',
548                               'pkeyGet',
549                               'cachedQuery');
550         $here = get_class($this); // if we get confused
551         $bt = debug_backtrace();
552
553         // Find the first caller that's not us?
554         foreach ($bt as $frame) {
555             $func = $frame['function'];
556             if (isset($frame['type']) && $frame['type'] == '::') {
557                 if (in_array($func, $ignoreStatic)) {
558                     continue;
559                 }
560                 $here = $frame['class'] . '::' . $func;
561                 break;
562             } else if (isset($frame['type']) && $frame['type'] == '->') {
563                 if ($frame['object'] === $this && in_array($func, $ignore)) {
564                     continue;
565                 }
566                 if (in_array($func, $ignoreStatic)) {
567                     continue; // @todo FIXME: This shouldn't be needed?
568                 }
569                 $here = get_class($frame['object']) . '->' . $func;
570                 break;
571             }
572             $here = $func;
573             break;
574         }
575
576         if (php_sapi_name() == 'cli') {
577             $context = basename($_SERVER['PHP_SELF']);
578         } else {
579             $context = $_SERVER['REQUEST_METHOD'];
580         }
581
582         // Slip the comment in after the first command,
583         // or DB_DataObject gets confused about handling inserts and such.
584         $parts = explode(' ', $string, 2);
585         $parts[0] .= " /* $context $here */";
586         return implode(' ', $parts);
587     }
588
589     // Sanitize a query for logging
590     // @fixme don't trim spaces in string literals
591     function sanitizeQuery($string)
592     {
593         $string = preg_replace('/\s+/', ' ', $string);
594         $string = trim($string);
595         return $string;
596     }
597
598     // We overload so that 'SET NAMES "utf8"' is called for
599     // each connection
600
601     function _connect()
602     {
603         global $_DB_DATAOBJECT;
604
605         $sum = $this->_getDbDsnMD5();
606
607         if (!empty($_DB_DATAOBJECT['CONNECTIONS'][$sum]) &&
608             !PEAR::isError($_DB_DATAOBJECT['CONNECTIONS'][$sum])) {
609             $exists = true;
610         } else {
611             $exists = false;
612        }
613
614         // @fixme horrible evil hack!
615         //
616         // In multisite configuration we don't want to keep around a separate
617         // connection for every database; we could end up with thousands of
618         // connections open per thread. In an ideal world we might keep
619         // a connection per server and select different databases, but that'd
620         // be reliant on having the same db username/pass as well.
621         //
622         // MySQL connections are cheap enough we're going to try just
623         // closing out the old connection and reopening when we encounter
624         // a new DSN.
625         //
626         // WARNING WARNING if we end up actually using multiple DBs at a time
627         // we'll need some fancier logic here.
628         if (!$exists && !empty($_DB_DATAOBJECT['CONNECTIONS']) && php_sapi_name() == 'cli') {
629             foreach ($_DB_DATAOBJECT['CONNECTIONS'] as $index => $conn) {
630                 if (!empty($conn)) {
631                     $conn->disconnect();
632                 }
633                 unset($_DB_DATAOBJECT['CONNECTIONS'][$index]);
634             }
635         }
636
637         $result = parent::_connect();
638
639         if ($result && !$exists) {
640             $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
641             if (common_config('db', 'type') == 'mysql' &&
642                 common_config('db', 'utf8')) {
643                 $conn = $DB->connection;
644                 if (!empty($conn)) {
645                     if ($DB instanceof DB_mysqli) {
646                         mysqli_set_charset($conn, 'utf8');
647                     } else if ($DB instanceof DB_mysql) {
648                         mysql_set_charset('utf8', $conn);
649                     }
650                 }
651             }
652             // Needed to make timestamp values usefully comparable.
653             if (common_config('db', 'type') == 'mysql') {
654                 parent::_query("set time_zone='+0:00'");
655             }
656         }
657
658         return $result;
659     }
660
661     // XXX: largely cadged from DB_DataObject
662
663     function _getDbDsnMD5()
664     {
665         if ($this->_database_dsn_md5) {
666             return $this->_database_dsn_md5;
667         }
668
669         $dsn = $this->_getDbDsn();
670
671         if (is_string($dsn)) {
672             $sum = md5($dsn);
673         } else {
674             /// support array based dsn's
675             $sum = md5(serialize($dsn));
676         }
677
678         return $sum;
679     }
680
681     function _getDbDsn()
682     {
683         global $_DB_DATAOBJECT;
684
685         if (empty($_DB_DATAOBJECT['CONFIG'])) {
686             DB_DataObject::_loadConfig();
687         }
688
689         $options = &$_DB_DATAOBJECT['CONFIG'];
690
691         // if the databse dsn dis defined in the object..
692
693         $dsn = isset($this->_database_dsn) ? $this->_database_dsn : null;
694
695         if (!$dsn) {
696
697             if (!$this->_database) {
698                 $this->_database = isset($options["table_{$this->__table}"]) ? $options["table_{$this->__table}"] : null;
699             }
700
701             if ($this->_database && !empty($options["database_{$this->_database}"]))  {
702                 $dsn = $options["database_{$this->_database}"];
703             } else if (!empty($options['database'])) {
704                 $dsn = $options['database'];
705             }
706         }
707
708         if (!$dsn) {
709             // TRANS: Exception thrown when database name or Data Source Name could not be found.
710             throw new Exception(_('No database name or DSN found anywhere.'));
711         }
712
713         return $dsn;
714     }
715
716     static function blow()
717     {
718         $c = self::memcache();
719
720         if (empty($c)) {
721             return false;
722         }
723
724         $args = func_get_args();
725
726         $format = array_shift($args);
727
728         $keyPart = vsprintf($format, $args);
729
730         $cacheKey = Cache::key($keyPart);
731
732         return $c->delete($cacheKey);
733     }
734
735     function fixupTimestamps()
736     {
737         // Fake up timestamp columns
738         $columns = $this->table();
739         foreach ($columns as $name => $type) {
740             if ($type & DB_DATAOBJECT_MYSQLTIMESTAMP) {
741                 $this->$name = common_sql_now();
742             }
743         }
744     }
745
746     function debugDump()
747     {
748         common_debug("debugDump: " . common_log_objstring($this));
749     }
750
751     function raiseError($message, $type = null, $behaviour = null)
752     {
753         $id = get_class($this);
754         if (!empty($this->id)) {
755             $id .= ':' . $this->id;
756         }
757         if ($message instanceof PEAR_Error) {
758             $message = $message->getMessage();
759         }
760         // Low level exception. No need for i18n as discussed with Brion.
761         throw new ServerException("[$id] DB_DataObject error [$type]: $message");
762     }
763
764     static function cacheGet($keyPart)
765     {
766         $c = self::memcache();
767
768         if (empty($c)) {
769             return false;
770         }
771
772         $cacheKey = Cache::key($keyPart);
773
774         return $c->get($cacheKey);
775     }
776
777     static function cacheSet($keyPart, $value, $flag=null, $expiry=null)
778     {
779         $c = self::memcache();
780
781         if (empty($c)) {
782             return false;
783         }
784
785         $cacheKey = Cache::key($keyPart);
786
787         return $c->set($cacheKey, $value, $flag, $expiry);
788     }
789
790     static function valueString($v)
791     {
792         $vstr = null;
793         if (is_object($v) && $v instanceof DB_DataObject_Cast) {
794             switch ($v->type) {
795             case 'date':
796                 $vstr = $v->year . '-' . $v->month . '-' . $v->day;
797                 break;
798             case 'blob':
799             case 'string':
800             case 'sql':
801             case 'datetime':
802             case 'time':
803                 // Low level exception. No need for i18n as discussed with Brion.
804                 throw new ServerException("Unhandled DB_DataObject_Cast type passed as cacheKey value: '$v->type'");
805                 break;
806             default:
807                 // Low level exception. No need for i18n as discussed with Brion.
808                 throw new ServerException("Unknown DB_DataObject_Cast type passed as cacheKey value: '$v->type'");
809                 break;
810             }
811         } else {
812             $vstr = strval($v);
813         }
814         return $vstr;
815     }
816 }