alecpl
2011-05-21 bc8c2c57880523472b30f475d566a8133e2d2e20
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                                  |
10  | Licensed under the GNU GPL                                            |
11  |                                                                       |
12  | PURPOSE:                                                              |
13  |   Caching engine                                                      |
14  |                                                                       |
15  +-----------------------------------------------------------------------+
16  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17  | Author: Aleksander Machniak <alec@alec.pl>                            |
18  +-----------------------------------------------------------------------+
19
20  $Id$
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>
31  * @version    1.0
32  */
33 class rcube_cache
34 {
35     /**
36      * Instance of rcube_mdb2 or Memcache class
37      *
38      * @var rcube_mdb2/Memcache
39      */
40     private $db;
41     private $type;
42     private $userid;
43     private $prefix;
b9e42e 44     private $index;
5cf5ee 45     private $cache         = array();
A 46     private $cache_keys    = array();
47     private $cache_changes = array();
48     private $cache_sums    = array();
49
50
51     /**
52      * Object constructor.
53      *
8edb3d 54      * @param string $type   Engine type ('db' or 'memcache' or 'apc')
5cf5ee 55      * @param int    $userid User identifier
A 56      * @param string $prefix Key name prefix
57      */
58     function __construct($type, $userid, $prefix='')
59     {
60         $rcmail = rcmail::get_instance();
8edb3d 61         $type   = strtolower($type);
5cf5ee 62     
8edb3d 63         if ($type == 'memcache') {
5cf5ee 64             $this->type = 'memcache';
A 65             $this->db   = $rcmail->get_memcache();
8edb3d 66         }
A 67         else if ($type == 'apc') {
68             $this->type = 'apc';
69             $this->db   = function_exists('apc_exists'); // APC 3.1.4 required
5cf5ee 70         }
A 71         else {
72             $this->type = 'db';
73             $this->db   = $rcmail->get_dbh();
74         }
75
76         $this->userid = (int) $userid;
77         $this->prefix = $prefix;
78     }
79
80
81     /**
82      * Returns cached value.
83      *
b9e42e 84      * @param string $key Cache key name
5cf5ee 85      *
A 86      * @return mixed Cached value
87      */
88     function get($key)
89     {
b9e42e 90         if (!array_key_exists($key, $this->cache)) {
A 91             return $this->read_record($key);
92         }
5cf5ee 93
A 94         return $this->cache[$key];
95     }
96
97
98     /**
99      * Sets (add/update) value in cache.
100      *
b9e42e 101      * @param string $key  Cache key name
A 102      * @param mixed  $data Cache data
5cf5ee 103      */
A 104     function set($key, $data)
105     {
106         $this->cache[$key]         = $data;
107         $this->cache_changed       = true;
108         $this->cache_changes[$key] = true;
109     }
110
111
112     /**
113      * Clears the cache.
114      *
ccc059 115      * @param string  $key         Cache key name or pattern
A 116      * @param boolean $prefix_mode Enable it to clear all keys starting
117      *                             with prefix specified in $key
5cf5ee 118      */
b9e42e 119     function remove($key=null, $prefix_mode=false)
5cf5ee 120     {
ccc059 121         // Remove all keys
5cf5ee 122         if ($key === null) {
A 123             $this->cache         = array();
124             $this->cache_changed = false;
125             $this->cache_changes = array();
ccc059 126             $this->cache_keys    = array();
5cf5ee 127         }
ccc059 128         // Remove keys by name prefix
b9e42e 129         else if ($prefix_mode) {
5cf5ee 130             foreach (array_keys($this->cache) as $k) {
ccc059 131                 if (strpos($k, $key) === 0) {
A 132                     $this->cache[$k] = null;
5cf5ee 133                     $this->cache_changes[$k] = false;
ccc059 134                     unset($this->cache_keys[$k]);
5cf5ee 135                 }
A 136             }
b9e42e 137         }
A 138         // Remove one key by name
139         else {
140             $this->cache[$key] = null;
141             $this->cache_changes[$key] = false;
142             unset($this->cache_keys[$key]);
5cf5ee 143         }
A 144
b9e42e 145         // Remove record(s) from the backend
A 146         $this->remove_record($key, $prefix_mode);
5cf5ee 147     }
A 148
149
150     /**
151      * Writes the cache back to the DB.
152      */
153     function close()
154     {
155         if (!$this->cache_changed) {
156             return;
8edb3d 157         }
A 158
ccc059 159         foreach ($this->cache as $key => $data) {
A 160             // The key has been used
161             if ($this->cache_changes[$key]) {
162                 // Make sure we're not going to write unchanged data
163                 // by comparing current md5 sum with the sum calculated on DB read
164                 $data = serialize($data);
165
166                 if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) {
167                     $this->write_record($key, $data);
168                 }
169             }
170         }
171
b9e42e 172         $this->write_index();
ccc059 173     }
A 174
175
176     /**
b9e42e 177      * Reads cache entry.
ccc059 178      *
b9e42e 179      * @param string $key Cache key name
A 180      *
181      * @return mixed Cached value
182      * @access private
183      */
184     private function read_record($key)
185     {
186         if (!$this->db) {
187             return null;
188         }
189
190         if ($this->type == 'memcache') {
191             $data = $this->db->get($this->ckey($key));
192         }
193         else if ($this->type == 'apc') {
194             $data = apc_fetch($this->ckey($key));
195         }
196
197         if ($data) {
198             $this->cache_sums[$key] = md5($data);
199             $this->cache[$key]      = unserialize($data);
200         }
201         else if ($this->type == 'db') {
202             $sql_result = $this->db->limitquery(
203                 "SELECT cache_id, data, cache_key".
204                 " FROM ".get_table_name('cache').
205                 " WHERE user_id = ?".
206                 " AND cache_key = ?".
207                 // for better performance we allow more records for one key
208                 // get the newer one
209                 " ORDER BY created DESC",
210                 0, 1, $this->userid, $this->prefix.'.'.$key);
211
212             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
213                 $key = substr($sql_arr['cache_key'], strlen($this->prefix)+1);
214                 $md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
215                 $data   = $sql_arr['data'] ? unserialize($sql_arr['data']) : null;
216                 $this->cache[$key]      = $data;
217                 $this->cache_sums[$key] = $md5sum;
218                 $this->cache_keys[$key] = $sql_arr['cache_id'];
219             }
220         }
221
222         return $this->cache[$key];
223     }
224
225
226     /**
227      * Writes single cache record into DB.
228      *
229      * @param string $key  Cache key name
230      * @param mxied  $data Serialized cache data 
ccc059 231      * @access private
A 232      */
233     private function write_record($key, $data)
234     {
235         if (!$this->db) {
236             return false;
5cf5ee 237         }
A 238
b9e42e 239         if ($this->type == 'memcache' || $this->type == 'apc') {
A 240             return $this->add_record($this->ckey($key), $data);
241         }
242
243         $key_exists = $this->cache_keys[$key];
244         $key        = $this->prefix . '.' . $key;
245
246         // Remove NULL rows (here we don't need to check if the record exist)
247         if ($data == 'N;') {
248             $this->db->query(
249                 "DELETE FROM ".get_table_name('cache').
250                 " WHERE user_id = ?".
251                 " AND cache_key = ?",
252                 $this->userid, $key);
253
254             return true;
255         }
256
5cf5ee 257         // update existing cache record
b9e42e 258         if ($key_exists) {
5cf5ee 259             $this->db->query(
A 260                 "UPDATE ".get_table_name('cache').
261                 " SET created = ". $this->db->now().", data = ?".
262                 " WHERE user_id = ?".
263                 " AND cache_key = ?",
b9e42e 264                 $data, $this->userid, $key);
5cf5ee 265         }
A 266         // add new cache record
267         else {
b9e42e 268             // for better performance we allow more records for one key
A 269             // so, no need to check if record exist (see rcube_cache::read_record())
5cf5ee 270             $this->db->query(
A 271                 "INSERT INTO ".get_table_name('cache').
272                 " (created, user_id, cache_key, data)".
273                 " VALUES (".$this->db->now().", ?, ?, ?)",
b9e42e 274                 $this->userid, $key, $data);
A 275         }
276
277         return true;
278     }
279
280
281     /**
282      * Deletes the cache record(s).
283      *
284      * @param string  $key         Cache key name or pattern
285      * @param boolean $prefix_mode Enable it to clear all keys starting
286      *                             with prefix specified in $key
287      *
288      * @access private;
289      */
290     private function remove_record($key=null, $prefix_mode=false)
291     {
292         if (!$this->db) {
293             return;
294         }
295
296         if ($this->type != 'db') {
297             $this->load_index();
298
299             // Remove all keys
300             if ($key === null) {
301                 foreach ($this->index as $key) {
302                     $this->delete_record($key, false);
303                 }
304                 $this->index = array();
305             }
306             // Remove keys by name prefix
307             else if ($prefix_mode) {
308                 foreach ($this->index as $k) {
309                     if (strpos($k, $key) === 0) {
310                         $this->delete_record($k);
311                     }
312                 }
313             }
314             // Remove one key by name
315             else {
316                 $this->delete_record($key);
317             }
318
319             return;
320         }
321
322         // Remove all keys (in specified cache)
323         if ($key === null) {
324             $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.%');
325         }
326         // Remove keys by name prefix
327         else if ($prefix_mode) {
328             $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
329         }
330         // Remove one key by name
331         else {
332             $where = " AND cache_key = " . $this->db->quote($this->prefix.'.'.$key);
333         }
334
335         $this->db->query(
336             "DELETE FROM ".get_table_name('cache').
337             " WHERE user_id = ?" . $where,
338             $this->userid);
339     }
340
341
342     /**
343      * Adds entry into memcache/apc DB.
344      * @access private
345      */
346     private function add_record($key, $data)
347     {
348         if ($this->type == 'memcache') {
349             $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED);
350             if (!$result)
351                 $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED);
352             return $result;
353         }
354
355         if ($this->type == 'apc') {
356             if (apc_exists($key))
357                 apc_delete($key);
358             return apc_store($key, $data);
5cf5ee 359         }
A 360     }
361
b5f836 362
A 363     /**
b9e42e 364      * Deletes entry from memcache/apc DB.
A 365      * @access private
366      */
367     private function delete_record($index=true)
368     {
369         if ($this->type == 'memcache')
370             $this->db->delete($this->ckey($key));
371         else
372             apc_delete($this->ckey($key));
373
374         if ($index) {
375             if (($idx = array_search($key, $this->index)) !== false) {
376                 unset($this->index[$idx]);
377             }
378         }
379     }
380
381
382     /**
383      * Writes the index entry into memcache/apc DB.
384      * @access private
385      */
386     private function write_index()
387     {
388         if (!$this->db) {
389             return;
390         }
391
392         if ($this->type == 'db') {
393             return;
394         }
395
396         $this->load_index();
397
398         // Make sure index contains new keys
399         foreach ($this->cache as $key => $value) {
400             if ($value !== null) {
401                 if (array_search($key, $this->index) === false) {
402                     $this->index[] = $key;
403                 }
404             }
405         }
406
407         $data = serialize($this->index);
408         $this->add_record($this->ikey(), $data);
409     }
410
411
412     /**
413      * Gets the index entry from memcache/apc DB.
414      * @access private
415      */
416     private function load_index()
417     {
418         if (!$this->db) {
419             return;
420         }
421
422         if ($this->index !== null) {
423             return;
424         }
425
426         $index_key = $this->ikey();
427         if ($this->type == 'memcache') {
428             $data = $this->db->get($index_key);
429         }
430         else if ($this->type == 'apc') {
431             $data = apc_fetch($index_key);
432         }
433
434         $this->index = $data ? unserialize($data) : array();
435     }
436
437
438     /**
439      * Creates per-user cache key name (for memcache and apc)
440      *
441      * @param string $key Cache key name
b5f836 442      *
ccc059 443      * @return string Cache key
b5f836 444      * @access private
A 445      */
b9e42e 446     private function ckey($key)
b5f836 447     {
b9e42e 448         return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key);
A 449     }
450
451
452     /**
453      * Creates per-user index cache key name (for memcache and apc)
454      *
455      * @return string Cache key
456      * @access private
457      */
458     private function ikey()
459     {
460         // This way each cache will have its own index
461         return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX');
b5f836 462     }
5cf5ee 463 }