thomascube
2012-01-18 7fe3811c65a7c63154f03610e289a6d196f3ae2e
commit | author | age
5cf5ee 1 <?php
A 2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/rcube_cache.php                                       |
6  |                                                                       |
7  | This file is part of the Roundcube Webmail client                     |
8  | Copyright (C) 2011, The Roundcube Dev Team                            |
9  | Copyright (C) 2011, Kolab Systems AG                                  |
7fe381 10  |                                                                       |
T 11  | Licensed under the GNU General Public License version 3 or            |
12  | any later version with exceptions for skins & plugins.                |
13  | See the README file for a full license statement.                     |
5cf5ee 14  |                                                                       |
A 15  | PURPOSE:                                                              |
16  |   Caching engine                                                      |
17  |                                                                       |
18  +-----------------------------------------------------------------------+
19  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
20  | Author: Aleksander Machniak <alec@alec.pl>                            |
21  +-----------------------------------------------------------------------+
22
23  $Id$
24
25 */
26
27
28 /**
29  * Interface class for accessing Roundcube cache
30  *
31  * @package    Cache
32  * @author     Thomas Bruederli <roundcube@gmail.com>
33  * @author     Aleksander Machniak <alec@alec.pl>
feb378 34  * @version    1.1
5cf5ee 35  */
A 36 class rcube_cache
37 {
38     /**
39      * Instance of rcube_mdb2 or Memcache class
40      *
41      * @var rcube_mdb2/Memcache
42      */
43     private $db;
44     private $type;
45     private $userid;
46     private $prefix;
7ad8e2 47     private $ttl;
c9f4e9 48     private $packed;
b9e42e 49     private $index;
5cf5ee 50     private $cache         = array();
A 51     private $cache_keys    = array();
52     private $cache_changes = array();
53     private $cache_sums    = array();
54
55
56     /**
57      * Object constructor.
58      *
8edb3d 59      * @param string $type   Engine type ('db' or 'memcache' or 'apc')
5cf5ee 60      * @param int    $userid User identifier
A 61      * @param string $prefix Key name prefix
7ad8e2 62      * @param int    $ttl    Expiration time of memcache/apc items in seconds (max.2592000)
c9f4e9 63      * @param bool   $packed Enables/disabled data serialization.
A 64      *                       It's possible to disable data serialization if you're sure
65      *                       stored data will be always a safe string
5cf5ee 66      */
c9f4e9 67     function __construct($type, $userid, $prefix='', $ttl=0, $packed=true)
5cf5ee 68     {
A 69         $rcmail = rcmail::get_instance();
8edb3d 70         $type   = strtolower($type);
7ad8e2 71
8edb3d 72         if ($type == 'memcache') {
5cf5ee 73             $this->type = 'memcache';
A 74             $this->db   = $rcmail->get_memcache();
8edb3d 75         }
A 76         else if ($type == 'apc') {
77             $this->type = 'apc';
78             $this->db   = function_exists('apc_exists'); // APC 3.1.4 required
5cf5ee 79         }
A 80         else {
81             $this->type = 'db';
82             $this->db   = $rcmail->get_dbh();
83         }
84
c9f4e9 85         $this->userid    = (int) $userid;
A 86         $this->ttl       = (int) $ttl;
87         $this->packed    = $packed;
88         $this->prefix    = $prefix;
5cf5ee 89     }
A 90
91
92     /**
93      * Returns cached value.
94      *
b9e42e 95      * @param string $key Cache key name
5cf5ee 96      *
A 97      * @return mixed Cached value
98      */
99     function get($key)
100     {
b9e42e 101         if (!array_key_exists($key, $this->cache)) {
A 102             return $this->read_record($key);
103         }
5cf5ee 104
A 105         return $this->cache[$key];
106     }
107
108
109     /**
110      * Sets (add/update) value in cache.
111      *
b9e42e 112      * @param string $key  Cache key name
A 113      * @param mixed  $data Cache data
5cf5ee 114      */
A 115     function set($key, $data)
116     {
117         $this->cache[$key]         = $data;
118         $this->cache_changed       = true;
119         $this->cache_changes[$key] = true;
c9f4e9 120     }
A 121
122
123     /**
124      * Returns cached value without storing it in internal memory.
125      *
126      * @param string $key Cache key name
127      *
128      * @return mixed Cached value
129      */
130     function read($key)
131     {
132         if (array_key_exists($key, $this->cache)) {
133             return $this->cache[$key];
134         }
135
136         return $this->read_record($key, true);
137     }
138
139
140     /**
141      * Sets (add/update) value in cache and immediately saves
142      * it in the backend, no internal memory will be used.
143      *
144      * @param string $key  Cache key name
145      * @param mixed  $data Cache data
146      *
147      * @param boolean True on success, False on failure
148      */
149     function write($key, $data)
150     {
151         return $this->write_record($key, $this->packed ? serialize($data) : $data);
5cf5ee 152     }
A 153
154
155     /**
156      * Clears the cache.
157      *
ccc059 158      * @param string  $key         Cache key name or pattern
A 159      * @param boolean $prefix_mode Enable it to clear all keys starting
160      *                             with prefix specified in $key
5cf5ee 161      */
b9e42e 162     function remove($key=null, $prefix_mode=false)
5cf5ee 163     {
ccc059 164         // Remove all keys
5cf5ee 165         if ($key === null) {
A 166             $this->cache         = array();
167             $this->cache_changed = false;
168             $this->cache_changes = array();
ccc059 169             $this->cache_keys    = array();
5cf5ee 170         }
ccc059 171         // Remove keys by name prefix
b9e42e 172         else if ($prefix_mode) {
5cf5ee 173             foreach (array_keys($this->cache) as $k) {
ccc059 174                 if (strpos($k, $key) === 0) {
A 175                     $this->cache[$k] = null;
5cf5ee 176                     $this->cache_changes[$k] = false;
ccc059 177                     unset($this->cache_keys[$k]);
5cf5ee 178                 }
A 179             }
b9e42e 180         }
A 181         // Remove one key by name
182         else {
183             $this->cache[$key] = null;
184             $this->cache_changes[$key] = false;
185             unset($this->cache_keys[$key]);
5cf5ee 186         }
A 187
b9e42e 188         // Remove record(s) from the backend
A 189         $this->remove_record($key, $prefix_mode);
5cf5ee 190     }
A 191
192
193     /**
feb378 194      * Remove cache records older than ttl
T 195      */
196     function expunge()
197     {
198         if ($this->type == 'db' && $this->db) {
199             $this->db->query(
200                 "DELETE FROM ".get_table_name('cache').
201                 " WHERE user_id = ?".
202                 " AND cache_key LIKE ?".
203                 " AND " . $this->db->unixtimestamp('created')." < ?",
204                 $this->userid,
205                 $this->prefix.'.%',
206                 time() - $this->ttl);
207         }
208     }
209
210
211     /**
5cf5ee 212      * Writes the cache back to the DB.
A 213      */
214     function close()
215     {
216         if (!$this->cache_changed) {
217             return;
8edb3d 218         }
A 219
ccc059 220         foreach ($this->cache as $key => $data) {
A 221             // The key has been used
222             if ($this->cache_changes[$key]) {
223                 // Make sure we're not going to write unchanged data
224                 // by comparing current md5 sum with the sum calculated on DB read
c9f4e9 225                 $data = $this->packed ? serialize($data) : $data;
ccc059 226
A 227                 if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) {
228                     $this->write_record($key, $data);
229                 }
230             }
231         }
232
b9e42e 233         $this->write_index();
ccc059 234     }
A 235
236
237     /**
b9e42e 238      * Reads cache entry.
ccc059 239      *
c9f4e9 240      * @param string  $key     Cache key name
A 241      * @param boolean $nostore Enable to skip in-memory store
b9e42e 242      *
A 243      * @return mixed Cached value
244      */
c9f4e9 245     private function read_record($key, $nostore=false)
b9e42e 246     {
A 247         if (!$this->db) {
248             return null;
249         }
250
03079a 251         if ($this->type != 'db') {
A 252             if ($this->type == 'memcache') {
253                 $data = $this->db->get($this->ckey($key));
c9f4e9 254             }
03079a 255             else if ($this->type == 'apc') {
A 256                 $data = apc_fetch($this->ckey($key));
257             }
c9f4e9 258
03079a 259             if ($data) {
A 260                 $md5sum = md5($data);
261                 $data   = $this->packed ? unserialize($data) : $data;
262
263                 if ($nostore) {
264                     return $data;
265                 }
266
267                 $this->cache_sums[$key] = $md5sum;
268                 $this->cache[$key]      = $data;
269             }
130cdc 270             else {
03079a 271                 $this->cache[$key] = null;
A 272             }
b9e42e 273         }
03079a 274         else {
b9e42e 275             $sql_result = $this->db->limitquery(
A 276                 "SELECT cache_id, data, cache_key".
277                 " FROM ".get_table_name('cache').
278                 " WHERE user_id = ?".
279                 " AND cache_key = ?".
280                 // for better performance we allow more records for one key
281                 // get the newer one
282                 " ORDER BY created DESC",
283                 0, 1, $this->userid, $this->prefix.'.'.$key);
284
285             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
286                 $key = substr($sql_arr['cache_key'], strlen($this->prefix)+1);
287                 $md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
c9f4e9 288                 if ($sql_arr['data']) {
A 289                     $data = $this->packed ? unserialize($sql_arr['data']) : $sql_arr['data'];
290                 }
291
292                 if ($nostore) {
293                     return $data;
294                 }
295
b9e42e 296                 $this->cache[$key]      = $data;
A 297                 $this->cache_sums[$key] = $md5sum;
298                 $this->cache_keys[$key] = $sql_arr['cache_id'];
299             }
130cdc 300             else {
03079a 301                 $this->cache[$key] = null;
A 302             }
b9e42e 303         }
A 304
305         return $this->cache[$key];
306     }
307
308
309     /**
310      * Writes single cache record into DB.
311      *
312      * @param string $key  Cache key name
313      * @param mxied  $data Serialized cache data 
c9f4e9 314      *
A 315      * @param boolean True on success, False on failure
ccc059 316      */
A 317     private function write_record($key, $data)
318     {
319         if (!$this->db) {
320             return false;
5cf5ee 321         }
A 322
b9e42e 323         if ($this->type == 'memcache' || $this->type == 'apc') {
A 324             return $this->add_record($this->ckey($key), $data);
325         }
326
327         $key_exists = $this->cache_keys[$key];
328         $key        = $this->prefix . '.' . $key;
329
330         // Remove NULL rows (here we don't need to check if the record exist)
331         if ($data == 'N;') {
332             $this->db->query(
333                 "DELETE FROM ".get_table_name('cache').
334                 " WHERE user_id = ?".
335                 " AND cache_key = ?",
336                 $this->userid, $key);
337
338             return true;
339         }
340
5cf5ee 341         // update existing cache record
b9e42e 342         if ($key_exists) {
c9f4e9 343             $result = $this->db->query(
5cf5ee 344                 "UPDATE ".get_table_name('cache').
A 345                 " SET created = ". $this->db->now().", data = ?".
346                 " WHERE user_id = ?".
347                 " AND cache_key = ?",
b9e42e 348                 $data, $this->userid, $key);
5cf5ee 349         }
A 350         // add new cache record
351         else {
b9e42e 352             // for better performance we allow more records for one key
A 353             // so, no need to check if record exist (see rcube_cache::read_record())
c9f4e9 354             $result = $this->db->query(
5cf5ee 355                 "INSERT INTO ".get_table_name('cache').
A 356                 " (created, user_id, cache_key, data)".
357                 " VALUES (".$this->db->now().", ?, ?, ?)",
b9e42e 358                 $this->userid, $key, $data);
A 359         }
360
c9f4e9 361         return $this->db->affected_rows($result);
b9e42e 362     }
A 363
364
365     /**
366      * Deletes the cache record(s).
367      *
368      * @param string  $key         Cache key name or pattern
369      * @param boolean $prefix_mode Enable it to clear all keys starting
370      *                             with prefix specified in $key
371      *
372      */
373     private function remove_record($key=null, $prefix_mode=false)
374     {
375         if (!$this->db) {
376             return;
377         }
378
379         if ($this->type != 'db') {
380             $this->load_index();
381
382             // Remove all keys
383             if ($key === null) {
384                 foreach ($this->index as $key) {
385                     $this->delete_record($key, false);
386                 }
387                 $this->index = array();
388             }
389             // Remove keys by name prefix
390             else if ($prefix_mode) {
391                 foreach ($this->index as $k) {
392                     if (strpos($k, $key) === 0) {
393                         $this->delete_record($k);
394                     }
395                 }
396             }
397             // Remove one key by name
398             else {
399                 $this->delete_record($key);
400             }
401
402             return;
403         }
404
405         // Remove all keys (in specified cache)
406         if ($key === null) {
407             $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.%');
408         }
409         // Remove keys by name prefix
410         else if ($prefix_mode) {
411             $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
412         }
413         // Remove one key by name
414         else {
415             $where = " AND cache_key = " . $this->db->quote($this->prefix.'.'.$key);
416         }
417
418         $this->db->query(
419             "DELETE FROM ".get_table_name('cache').
420             " WHERE user_id = ?" . $where,
421             $this->userid);
422     }
423
424
425     /**
426      * Adds entry into memcache/apc DB.
c9f4e9 427      *
A 428      * @param string  $key   Cache key name
429      * @param mxied   $data  Serialized cache data
430      * @param bollean $index Enables immediate index update
431      *
432      * @param boolean True on success, False on failure
b9e42e 433      */
c9f4e9 434     private function add_record($key, $data, $index=false)
b9e42e 435     {
A 436         if ($this->type == 'memcache') {
7ad8e2 437             $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
b9e42e 438             if (!$result)
7ad8e2 439                 $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
b9e42e 440         }
c9f4e9 441         else if ($this->type == 'apc') {
b9e42e 442             if (apc_exists($key))
A 443                 apc_delete($key);
c9f4e9 444             $result = apc_store($key, $data, $this->ttl);
5cf5ee 445         }
c9f4e9 446
A 447         // Update index
448         if ($index && $result) {
449             $this->load_index();
450
451             if (array_search($key, $this->index) === false) {
452                 $this->index[] = $key;
453                 $data = serialize($this->index);
454                 $this->add_record($this->ikey(), $data);
455             }
456         }
457
458         return $result;
5cf5ee 459     }
A 460
b5f836 461
A 462     /**
b9e42e 463      * Deletes entry from memcache/apc DB.
A 464      */
9d195d 465     private function delete_record($key, $index=true)
b9e42e 466     {
A 467         if ($this->type == 'memcache')
468             $this->db->delete($this->ckey($key));
469         else
470             apc_delete($this->ckey($key));
471
472         if ($index) {
473             if (($idx = array_search($key, $this->index)) !== false) {
474                 unset($this->index[$idx]);
475             }
476         }
477     }
478
479
480     /**
481      * Writes the index entry into memcache/apc DB.
482      */
483     private function write_index()
484     {
485         if (!$this->db) {
486             return;
487         }
488
489         if ($this->type == 'db') {
490             return;
491         }
492
493         $this->load_index();
494
495         // Make sure index contains new keys
496         foreach ($this->cache as $key => $value) {
497             if ($value !== null) {
498                 if (array_search($key, $this->index) === false) {
499                     $this->index[] = $key;
500                 }
501             }
502         }
503
504         $data = serialize($this->index);
505         $this->add_record($this->ikey(), $data);
506     }
507
508
509     /**
510      * Gets the index entry from memcache/apc DB.
511      */
512     private function load_index()
513     {
514         if (!$this->db) {
515             return;
516         }
517
518         if ($this->index !== null) {
519             return;
520         }
521
522         $index_key = $this->ikey();
523         if ($this->type == 'memcache') {
524             $data = $this->db->get($index_key);
525         }
526         else if ($this->type == 'apc') {
527             $data = apc_fetch($index_key);
528         }
529
530         $this->index = $data ? unserialize($data) : array();
531     }
532
533
534     /**
535      * Creates per-user cache key name (for memcache and apc)
536      *
537      * @param string $key Cache key name
b5f836 538      *
ccc059 539      * @return string Cache key
b5f836 540      */
b9e42e 541     private function ckey($key)
b5f836 542     {
b9e42e 543         return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key);
A 544     }
545
546
547     /**
548      * Creates per-user index cache key name (for memcache and apc)
549      *
550      * @return string Cache key
551      */
552     private function ikey()
553     {
554         // This way each cache will have its own index
555         return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX');
b5f836 556     }
5cf5ee 557 }