Mercurial > hg > rc2
comparison program/lib/Roundcube/rcube_cache_shared.php @ 0:4681f974d28b
vanilla 1.3.3 distro, I hope
author | Charlie Root |
---|---|
date | Thu, 04 Jan 2018 15:52:31 -0500 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4681f974d28b |
---|---|
1 <?php | |
2 | |
3 /** | |
4 +-----------------------------------------------------------------------+ | |
5 | This file is part of the Roundcube Webmail client | | |
6 | Copyright (C) 2011-2013, The Roundcube Dev Team | | |
7 | Copyright (C) 2011-2013, Kolab Systems AG | | |
8 | | | |
9 | Licensed under the GNU General Public License version 3 or | | |
10 | any later version with exceptions for skins & plugins. | | |
11 | See the README file for a full license statement. | | |
12 | | | |
13 | PURPOSE: | | |
14 | Shared (cross-user) caching engine | | |
15 +-----------------------------------------------------------------------+ | |
16 | Author: Thomas Bruederli <roundcube@gmail.com> | | |
17 | Author: Aleksander Machniak <alec@alec.pl> | | |
18 +-----------------------------------------------------------------------+ | |
19 */ | |
20 | |
21 /** | |
22 * Interface class for accessing Roundcube shared cache | |
23 * | |
24 * @package Framework | |
25 * @subpackage Cache | |
26 * @author Thomas Bruederli <roundcube@gmail.com> | |
27 * @author Aleksander Machniak <alec@alec.pl> | |
28 */ | |
29 class rcube_cache_shared | |
30 { | |
31 /** | |
32 * Instance of database handler | |
33 * | |
34 * @var rcube_db|Memcache|bool | |
35 */ | |
36 private $db; | |
37 private $type; | |
38 private $prefix; | |
39 private $ttl; | |
40 private $packed; | |
41 private $index; | |
42 private $table; | |
43 private $debug; | |
44 private $index_changed = false; | |
45 private $cache = array(); | |
46 private $cache_changes = array(); | |
47 private $cache_sums = array(); | |
48 private $max_packet = -1; | |
49 | |
50 | |
51 /** | |
52 * Object constructor. | |
53 * | |
54 * @param string $type Engine type ('db' or 'memcache' or 'apc') | |
55 * @param string $prefix Key name prefix | |
56 * @param string $ttl Expiration time of memcache/apc items | |
57 * @param bool $packed Enables/disabled data serialization. | |
58 * It's possible to disable data serialization if you're sure | |
59 * stored data will be always a safe string | |
60 */ | |
61 function __construct($type, $prefix='', $ttl=0, $packed=true) | |
62 { | |
63 $rcube = rcube::get_instance(); | |
64 $type = strtolower($type); | |
65 | |
66 if ($type == 'memcache') { | |
67 $this->type = 'memcache'; | |
68 $this->db = $rcube->get_memcache(); | |
69 $this->debug = $rcube->config->get('memcache_debug'); | |
70 } | |
71 else if ($type == 'apc') { | |
72 $this->type = 'apc'; | |
73 $this->db = function_exists('apc_exists'); // APC 3.1.4 required | |
74 $this->debug = $rcube->config->get('apc_debug'); | |
75 } | |
76 else { | |
77 $this->type = 'db'; | |
78 $this->db = $rcube->get_dbh(); | |
79 $this->table = $this->db->table_name('cache_shared', true); | |
80 } | |
81 | |
82 // convert ttl string to seconds | |
83 $ttl = get_offset_sec($ttl); | |
84 if ($ttl > 2592000) $ttl = 2592000; | |
85 | |
86 $this->ttl = $ttl; | |
87 $this->packed = $packed; | |
88 $this->prefix = $prefix; | |
89 } | |
90 | |
91 /** | |
92 * Returns cached value. | |
93 * | |
94 * @param string $key Cache key name | |
95 * | |
96 * @return mixed Cached value | |
97 */ | |
98 function get($key) | |
99 { | |
100 if (!array_key_exists($key, $this->cache)) { | |
101 return $this->read_record($key); | |
102 } | |
103 | |
104 return $this->cache[$key]; | |
105 } | |
106 | |
107 /** | |
108 * Sets (add/update) value in cache. | |
109 * | |
110 * @param string $key Cache key name | |
111 * @param mixed $data Cache data | |
112 */ | |
113 function set($key, $data) | |
114 { | |
115 $this->cache[$key] = $data; | |
116 $this->cache_changes[$key] = true; | |
117 } | |
118 | |
119 /** | |
120 * Returns cached value without storing it in internal memory. | |
121 * | |
122 * @param string $key Cache key name | |
123 * | |
124 * @return mixed Cached value | |
125 */ | |
126 function read($key) | |
127 { | |
128 if (array_key_exists($key, $this->cache)) { | |
129 return $this->cache[$key]; | |
130 } | |
131 | |
132 return $this->read_record($key, true); | |
133 } | |
134 | |
135 /** | |
136 * Sets (add/update) value in cache and immediately saves | |
137 * it in the backend, no internal memory will be used. | |
138 * | |
139 * @param string $key Cache key name | |
140 * @param mixed $data Cache data | |
141 * | |
142 * @param boolean True on success, False on failure | |
143 */ | |
144 function write($key, $data) | |
145 { | |
146 return $this->write_record($key, $this->serialize($data)); | |
147 } | |
148 | |
149 /** | |
150 * Clears the cache. | |
151 * | |
152 * @param string $key Cache key name or pattern | |
153 * @param boolean $prefix_mode Enable it to clear all keys starting | |
154 * with prefix specified in $key | |
155 */ | |
156 function remove($key=null, $prefix_mode=false) | |
157 { | |
158 // Remove all keys | |
159 if ($key === null) { | |
160 $this->cache = array(); | |
161 $this->cache_changes = array(); | |
162 $this->cache_sums = array(); | |
163 } | |
164 // Remove keys by name prefix | |
165 else if ($prefix_mode) { | |
166 foreach (array_keys($this->cache) as $k) { | |
167 if (strpos($k, $key) === 0) { | |
168 $this->cache[$k] = null; | |
169 $this->cache_changes[$k] = false; | |
170 unset($this->cache_sums[$k]); | |
171 } | |
172 } | |
173 } | |
174 // Remove one key by name | |
175 else { | |
176 $this->cache[$key] = null; | |
177 $this->cache_changes[$key] = false; | |
178 unset($this->cache_sums[$key]); | |
179 } | |
180 | |
181 // Remove record(s) from the backend | |
182 $this->remove_record($key, $prefix_mode); | |
183 } | |
184 | |
185 /** | |
186 * Remove cache records older than ttl | |
187 */ | |
188 function expunge() | |
189 { | |
190 if ($this->type == 'db' && $this->db && $this->ttl) { | |
191 $this->db->query( | |
192 "DELETE FROM {$this->table}" | |
193 . " WHERE `cache_key` LIKE ?" | |
194 . " AND `expires` < " . $this->db->now(), | |
195 $this->prefix . '.%'); | |
196 } | |
197 } | |
198 | |
199 /** | |
200 * Remove expired records of all caches | |
201 */ | |
202 static function gc() | |
203 { | |
204 $rcube = rcube::get_instance(); | |
205 $db = $rcube->get_dbh(); | |
206 | |
207 $db->query("DELETE FROM " . $db->table_name('cache_shared', true) . " WHERE `expires` < " . $db->now()); | |
208 } | |
209 | |
210 /** | |
211 * Writes the cache back to the DB. | |
212 */ | |
213 function close() | |
214 { | |
215 foreach ($this->cache as $key => $data) { | |
216 // The key has been used | |
217 if ($this->cache_changes[$key]) { | |
218 // Make sure we're not going to write unchanged data | |
219 // by comparing current md5 sum with the sum calculated on DB read | |
220 $data = $this->serialize($data); | |
221 | |
222 if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) { | |
223 $this->write_record($key, $data); | |
224 } | |
225 } | |
226 } | |
227 | |
228 if ($this->index_changed) { | |
229 $this->write_index(); | |
230 } | |
231 | |
232 // reset internal cache index, thanks to this we can force index reload | |
233 $this->index = null; | |
234 $this->index_changed = false; | |
235 $this->cache = array(); | |
236 $this->cache_sums = array(); | |
237 $this->cache_changes = array(); | |
238 } | |
239 | |
240 /** | |
241 * Reads cache entry. | |
242 * | |
243 * @param string $key Cache key name | |
244 * @param boolean $nostore Enable to skip in-memory store | |
245 * | |
246 * @return mixed Cached value | |
247 */ | |
248 private function read_record($key, $nostore=false) | |
249 { | |
250 if (!$this->db) { | |
251 return null; | |
252 } | |
253 | |
254 if ($this->type != 'db') { | |
255 $this->load_index(); | |
256 | |
257 // Consistency check (#1490390) | |
258 if (!in_array($key, $this->index)) { | |
259 // we always check if the key exist in the index | |
260 // to have data in consistent state. Keeping the index consistent | |
261 // is needed for keys delete operation when we delete all keys or by prefix. | |
262 } | |
263 else { | |
264 $ckey = $this->ckey($key); | |
265 | |
266 if ($this->type == 'memcache') { | |
267 $data = $this->db->get($ckey); | |
268 } | |
269 else if ($this->type == 'apc') { | |
270 $data = apc_fetch($ckey); | |
271 } | |
272 | |
273 if ($this->debug) { | |
274 $this->debug('get', $ckey, $data); | |
275 } | |
276 } | |
277 | |
278 if ($data !== false) { | |
279 $md5sum = md5($data); | |
280 $data = $this->unserialize($data); | |
281 | |
282 if ($nostore) { | |
283 return $data; | |
284 } | |
285 | |
286 $this->cache_sums[$key] = $md5sum; | |
287 $this->cache[$key] = $data; | |
288 } | |
289 else { | |
290 $this->cache[$key] = null; | |
291 } | |
292 } | |
293 else { | |
294 $sql_result = $this->db->query( | |
295 "SELECT `data`, `cache_key` FROM {$this->table}" | |
296 . " WHERE `cache_key` = ?", | |
297 $this->prefix . '.' . $key); | |
298 | |
299 if ($sql_arr = $this->db->fetch_assoc($sql_result)) { | |
300 if (strlen($sql_arr['data']) > 0) { | |
301 $md5sum = md5($sql_arr['data']); | |
302 $data = $this->unserialize($sql_arr['data']); | |
303 } | |
304 | |
305 $this->db->reset(); | |
306 | |
307 if ($nostore) { | |
308 return $data; | |
309 } | |
310 | |
311 $this->cache[$key] = $data; | |
312 $this->cache_sums[$key] = $md5sum; | |
313 } | |
314 else { | |
315 $this->cache[$key] = null; | |
316 } | |
317 } | |
318 | |
319 return $this->cache[$key]; | |
320 } | |
321 | |
322 /** | |
323 * Writes single cache record into DB. | |
324 * | |
325 * @param string $key Cache key name | |
326 * @param mixed $data Serialized cache data | |
327 * | |
328 * @param boolean True on success, False on failure | |
329 */ | |
330 private function write_record($key, $data) | |
331 { | |
332 if (!$this->db) { | |
333 return false; | |
334 } | |
335 | |
336 // don't attempt to write too big data sets | |
337 if (strlen($data) > $this->max_packet_size()) { | |
338 trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write " . strlen($data) . " bytes", E_USER_WARNING); | |
339 return false; | |
340 } | |
341 | |
342 if ($this->type == 'memcache' || $this->type == 'apc') { | |
343 $result = $this->add_record($this->ckey($key), $data); | |
344 | |
345 // make sure index will be updated | |
346 if ($result) { | |
347 if (!array_key_exists($key, $this->cache_sums)) { | |
348 $this->cache_sums[$key] = true; | |
349 } | |
350 | |
351 $this->load_index(); | |
352 | |
353 if (!$this->index_changed && !in_array($key, $this->index)) { | |
354 $this->index_changed = true; | |
355 } | |
356 } | |
357 | |
358 return $result; | |
359 } | |
360 | |
361 $db_key = $this->prefix . '.' . $key; | |
362 | |
363 // Remove NULL rows (here we don't need to check if the record exist) | |
364 if ($data == 'N;') { | |
365 $result = $this->db->query("DELETE FROM {$this->table} WHERE `cache_key` = ?", $db_key); | |
366 | |
367 return !$this->db->is_error($result); | |
368 } | |
369 | |
370 $key_exists = array_key_exists($key, $this->cache_sums); | |
371 $expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL'; | |
372 | |
373 if (!$key_exists) { | |
374 // Try INSERT temporarily ignoring "duplicate key" errors | |
375 $this->db->set_option('ignore_key_errors', true); | |
376 | |
377 $result = $this->db->query( | |
378 "INSERT INTO {$this->table} (`expires`, `cache_key`, `data`)" | |
379 . " VALUES ($expires, ?, ?)", | |
380 $db_key, $data); | |
381 | |
382 $this->db->set_option('ignore_key_errors', false); | |
383 } | |
384 | |
385 // otherwise try UPDATE | |
386 if (!isset($result) || !($count = $this->db->affected_rows($result))) { | |
387 $result = $this->db->query( | |
388 "UPDATE {$this->table} SET `expires` = $expires, `data` = ?" | |
389 . " WHERE `cache_key` = ?", | |
390 $data, $db_key); | |
391 | |
392 $count = $this->db->affected_rows($result); | |
393 } | |
394 | |
395 return $count > 0; | |
396 } | |
397 | |
398 /** | |
399 * Deletes the cache record(s). | |
400 * | |
401 * @param string $key Cache key name or pattern | |
402 * @param boolean $prefix_mode Enable it to clear all keys starting | |
403 * with prefix specified in $key | |
404 */ | |
405 private function remove_record($key=null, $prefix_mode=false) | |
406 { | |
407 if (!$this->db) { | |
408 return; | |
409 } | |
410 | |
411 if ($this->type != 'db') { | |
412 $this->load_index(); | |
413 | |
414 // Remove all keys | |
415 if ($key === null) { | |
416 foreach ($this->index as $key) { | |
417 $this->delete_record($this->ckey($key)); | |
418 } | |
419 | |
420 $this->index = array(); | |
421 } | |
422 // Remove keys by name prefix | |
423 else if ($prefix_mode) { | |
424 foreach ($this->index as $idx => $k) { | |
425 if (strpos($k, $key) === 0) { | |
426 $this->delete_record($this->ckey($k)); | |
427 unset($this->index[$idx]); | |
428 } | |
429 } | |
430 } | |
431 // Remove one key by name | |
432 else { | |
433 $this->delete_record($this->ckey($key)); | |
434 if (($idx = array_search($key, $this->index)) !== false) { | |
435 unset($this->index[$idx]); | |
436 } | |
437 } | |
438 | |
439 $this->index_changed = true; | |
440 | |
441 return; | |
442 } | |
443 | |
444 // Remove all keys (in specified cache) | |
445 if ($key === null) { | |
446 $where = " WHERE `cache_key` LIKE " . $this->db->quote($this->prefix.'.%'); | |
447 } | |
448 // Remove keys by name prefix | |
449 else if ($prefix_mode) { | |
450 $where = " WHERE `cache_key` LIKE " . $this->db->quote($this->prefix.'.'.$key.'%'); | |
451 } | |
452 // Remove one key by name | |
453 else { | |
454 $where = " WHERE `cache_key` = " . $this->db->quote($this->prefix.'.'.$key); | |
455 } | |
456 | |
457 $this->db->query("DELETE FROM " . $this->table . $where); | |
458 } | |
459 | |
460 /** | |
461 * Adds entry into memcache/apc DB. | |
462 * | |
463 * @param string $key Cache internal key name | |
464 * @param mixed $data Serialized cache data | |
465 * | |
466 * @param boolean True on success, False on failure | |
467 */ | |
468 private function add_record($key, $data) | |
469 { | |
470 if ($this->type == 'memcache') { | |
471 $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl); | |
472 | |
473 if (!$result) { | |
474 $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl); | |
475 } | |
476 } | |
477 else if ($this->type == 'apc') { | |
478 if (apc_exists($key)) { | |
479 apc_delete($key); | |
480 } | |
481 | |
482 $result = apc_store($key, $data, $this->ttl); | |
483 } | |
484 | |
485 if ($this->debug) { | |
486 $this->debug('set', $key, $data, $result); | |
487 } | |
488 | |
489 return $result; | |
490 } | |
491 | |
492 /** | |
493 * Deletes entry from memcache/apc DB. | |
494 * | |
495 * @param string $key Cache internal key name | |
496 * | |
497 * @param boolean True on success, False on failure | |
498 */ | |
499 private function delete_record($key) | |
500 { | |
501 if ($this->type == 'memcache') { | |
502 // #1488592: use 2nd argument | |
503 $result = $this->db->delete($key, 0); | |
504 } | |
505 else { | |
506 $result = apc_delete($key); | |
507 } | |
508 | |
509 if ($this->debug) { | |
510 $this->debug('delete', $key, null, $result); | |
511 } | |
512 | |
513 return $result; | |
514 } | |
515 | |
516 /** | |
517 * Writes the index entry into memcache/apc DB. | |
518 */ | |
519 private function write_index() | |
520 { | |
521 if (!$this->db || $this->type == 'db') { | |
522 return; | |
523 } | |
524 | |
525 $this->load_index(); | |
526 | |
527 // Make sure index contains new keys | |
528 foreach ($this->cache as $key => $value) { | |
529 if ($value !== null && !in_array($key, $this->index)) { | |
530 $this->index[] = $key; | |
531 } | |
532 } | |
533 | |
534 // new keys added using self::write() | |
535 foreach ($this->cache_sums as $key => $value) { | |
536 if ($value === true && !in_array($key, $this->index)) { | |
537 $this->index[] = $key; | |
538 } | |
539 } | |
540 | |
541 $data = serialize($this->index); | |
542 $this->add_record($this->ikey(), $data); | |
543 } | |
544 | |
545 /** | |
546 * Gets the index entry from memcache/apc DB. | |
547 */ | |
548 private function load_index() | |
549 { | |
550 if (!$this->db || $this->type == 'db') { | |
551 return; | |
552 } | |
553 | |
554 if ($this->index !== null) { | |
555 return; | |
556 } | |
557 | |
558 $index_key = $this->ikey(); | |
559 | |
560 if ($this->type == 'memcache') { | |
561 $data = $this->db->get($index_key); | |
562 } | |
563 else if ($this->type == 'apc') { | |
564 $data = apc_fetch($index_key); | |
565 } | |
566 | |
567 if ($this->debug) { | |
568 $this->debug('get', $index_key, $data); | |
569 } | |
570 | |
571 $this->index = $data ? unserialize($data) : array(); | |
572 } | |
573 | |
574 /** | |
575 * Creates cache key name (for memcache and apc) | |
576 * | |
577 * @param string $key Cache key name | |
578 * | |
579 * @return string Cache key | |
580 */ | |
581 private function ckey($key) | |
582 { | |
583 return $this->prefix . ':' . $key; | |
584 } | |
585 | |
586 /** | |
587 * Creates index cache key name (for memcache and apc) | |
588 * | |
589 * @return string Cache key | |
590 */ | |
591 private function ikey() | |
592 { | |
593 // This way each cache will have its own index | |
594 return $this->prefix . 'INDEX'; | |
595 } | |
596 | |
597 /** | |
598 * Serializes data for storing | |
599 */ | |
600 private function serialize($data) | |
601 { | |
602 if ($this->type == 'db') { | |
603 return $this->db->encode($data, $this->packed); | |
604 } | |
605 | |
606 return $this->packed ? serialize($data) : $data; | |
607 } | |
608 | |
609 /** | |
610 * Unserializes serialized data | |
611 */ | |
612 private function unserialize($data) | |
613 { | |
614 if ($this->type == 'db') { | |
615 return $this->db->decode($data, $this->packed); | |
616 } | |
617 | |
618 return $this->packed ? @unserialize($data) : $data; | |
619 } | |
620 | |
621 /** | |
622 * Determine the maximum size for cache data to be written | |
623 */ | |
624 private function max_packet_size() | |
625 { | |
626 if ($this->max_packet < 0) { | |
627 $this->max_packet = 2097152; // default/max is 2 MB | |
628 | |
629 if ($this->type == 'db') { | |
630 if ($value = $this->db->get_variable('max_allowed_packet', $this->max_packet)) { | |
631 $this->max_packet = $value; | |
632 } | |
633 $this->max_packet -= 2000; | |
634 } | |
635 else { | |
636 $max_packet = rcube::get_instance()->config->get($this->type . '_max_allowed_packet'); | |
637 $this->max_packet = parse_bytes($max_packet) ?: $this->max_packet; | |
638 } | |
639 } | |
640 | |
641 return $this->max_packet; | |
642 } | |
643 | |
644 /** | |
645 * Write memcache/apc debug info to the log | |
646 */ | |
647 private function debug($type, $key, $data = null, $result = null) | |
648 { | |
649 $line = strtoupper($type) . ' ' . $key; | |
650 | |
651 if ($data !== null) { | |
652 $line .= ' ' . ($this->packed ? $data : serialize($data)); | |
653 } | |
654 | |
655 rcube::debug($this->type, $line, $result); | |
656 } | |
657 } |