source: subversion/trunk/roundcubemail/program/include/rcube_cache.php @ 5305

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