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