PluginProbe
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score / 1.2.8
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score v1.2.8
trunk 1.0 1.0.1 1.1 1.1.1 1.1.2 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 2.0 2.0.1 2.0.2 2.0.3 2.0.4 2.1 2.1.1 2.1.2 2.2 2.2.1 All 69 releases
powered-cache / includes / dropins / redis-object-cache.php

redis-object-cache.php in Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score 1.2.8, at includes/dropins/redis-object-cache.php

1,251 lines 36.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // WP Redis
4 // This file needs to be symlinked or copied to wp-content/object-cache.php
5
6 # Users with setups where multiple installs share a common wp-config.php or $table_prefix
7 # can use this to guarantee uniqueness for the keys generated by this object cache
8 if ( ! defined( 'WP_CACHE_KEY_SALT' ) ) {
9 define( 'WP_CACHE_KEY_SALT', DB_NAME );
10 }
11
12 if ( ! defined( 'WP_REDIS_OBJECT_CACHE' ) ) {
13 define( 'WP_REDIS_OBJECT_CACHE', true );
14 }
15
16 if ( ! defined( 'WP_REDIS_USE_CACHE_GROUPS' ) ) {
17 define( 'WP_REDIS_USE_CACHE_GROUPS', false );
18 }
19
20 /**
21 * Adds data to the cache, if the cache key doesn't already exist.
22 *
23 * @uses $wp_object_cache Object Cache Class
24 * @see WP_Object_Cache::add()
25 *
26 * @param int|string $key The cache key to use for retrieval later
27 * @param mixed $data The data to add to the cache store
28 * @param string $group The group to add the cache to
29 * @param int $expire When the cache data should be expired
30 * @return bool False if cache key and group already exist, true on success
31 */
32 function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
33 global $wp_object_cache;
34
35 return $wp_object_cache->add( $key, $data, $group, (int) $expire );
36 }
37
38 /**
39 * Closes the cache.
40 *
41 * This function has ceased to do anything since WordPress 2.5. The
42 * functionality was removed along with the rest of the persistent cache. This
43 * does not mean that plugins can't implement this function when they need to
44 * make sure that the cache is cleaned up after WordPress no longer needs it.
45 *
46 * @return bool Always returns True
47 */
48 function wp_cache_close() {
49 return true;
50 }
51
52 /**
53 * Decrement numeric cache item's value
54 *
55 * @uses $wp_object_cache Object Cache Class
56 * @see WP_Object_Cache::decr()
57 *
58 * @param int|string $key The cache key to increment
59 * @param int $offset The amount by which to decrement the item's value. Default is 1.
60 * @param string $group The group the key is in.
61 * @return false|int False on failure, the item's new value on success.
62 */
63 function wp_cache_decr( $key, $offset = 1, $group = '' ) {
64 global $wp_object_cache;
65
66 return $wp_object_cache->decr( $key, $offset, $group );
67 }
68
69 /**
70 * Removes the cache contents matching key and group.
71 *
72 * @uses $wp_object_cache Object Cache Class
73 * @see WP_Object_Cache::delete()
74 *
75 * @param int|string $key What the contents in the cache are called
76 * @param string $group Where the cache contents are grouped
77 * @return bool True on successful removal, false on failure
78 */
79 function wp_cache_delete( $key, $group = '' ) {
80 global $wp_object_cache;
81
82 return $wp_object_cache->delete( $key, $group );
83 }
84
85 /**
86 * Removes cache contents for a given group.
87 *
88 * @uses $wp_object_cache Object Cache Class
89 * @see WP_Object_Cache::delete_group()
90 *
91 * @param string $group Where the cache contents are grouped
92 * @return bool True on successful removal, false on failure
93 */
94 function wp_cache_delete_group( $group ) {
95 global $wp_object_cache;
96 return $wp_object_cache->delete_group( $group );
97 }
98
99
100 /**
101 * Removes all cache items.
102 *
103 * @uses $wp_object_cache Object Cache Class
104 * @see WP_Object_Cache::flush()
105 *
106 * @return bool False on failure, true on success
107 */
108 function wp_cache_flush() {
109 global $wp_object_cache;
110
111 return $wp_object_cache->flush();
112 }
113
114 /**
115 * Retrieves the cache contents from the cache by key and group.
116 *
117 * @uses $wp_object_cache Object Cache Class
118 * @see WP_Object_Cache::get()
119 *
120 * @param int|string $key What the contents in the cache are called
121 * @param string $group Where the cache contents are grouped
122 * @param bool $force Whether to force an update of the local cache from the persistent cache (default is false)
123 * @param &bool $found Whether key was found in the cache. Disambiguates a return of false, a storable value.
124 * @return bool|mixed False on failure to retrieve contents or the cache
125 * contents on success
126 */
127 function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
128 global $wp_object_cache;
129
130 return $wp_object_cache->get( $key, $group, $force, $found );
131 }
132
133 /**
134 * Increment numeric cache item's value
135 *
136 * @uses $wp_object_cache Object Cache Class
137 * @see WP_Object_Cache::incr()
138 *
139 * @param int|string $key The cache key to increment
140 * @param int $offset The amount by which to increment the item's value. Default is 1.
141 * @param string $group The group the key is in.
142 * @return false|int False on failure, the item's new value on success.
143 */
144 function wp_cache_incr( $key, $offset = 1, $group = '' ) {
145 global $wp_object_cache;
146
147 return $wp_object_cache->incr( $key, $offset, $group );
148 }
149
150 /**
151 * Sets up Object Cache Global and assigns it.
152 *
153 * @global WP_Object_Cache $wp_object_cache WordPress Object Cache
154 */
155 function wp_cache_init() {
156 $GLOBALS['wp_object_cache'] = new WP_Object_Cache();
157 }
158
159 /**
160 * Replaces the contents of the cache with new data.
161 *
162 * @uses $wp_object_cache Object Cache Class
163 * @see WP_Object_Cache::replace()
164 *
165 * @param int|string $key What to call the contents in the cache
166 * @param mixed $data The contents to store in the cache
167 * @param string $group Where to group the cache contents
168 * @param int $expire When to expire the cache contents
169 * @return bool False if not exists, true if contents were replaced
170 */
171 function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
172 global $wp_object_cache;
173
174 return $wp_object_cache->replace( $key, $data, $group, (int) $expire );
175 }
176
177 /**
178 * Saves the data to the cache.
179 *
180 * @uses $wp_object_cache Object Cache Class
181 * @see WP_Object_Cache::set()
182 *
183 * @param int|string $key What to call the contents in the cache
184 * @param mixed $data The contents to store in the cache
185 * @param string $group Where to group the cache contents
186 * @param int $expire When to expire the cache contents
187 * @return bool False on failure, true on success
188 */
189 function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
190 global $wp_object_cache;
191
192 return $wp_object_cache->set( $key, $data, $group, (int) $expire );
193 }
194
195 /**
196 * Switch the interal blog id.
197 *
198 * This changes the blog id used to create keys in blog specific groups.
199 *
200 * @param int $blog_id Blog ID
201 */
202 function wp_cache_switch_to_blog( $blog_id ) {
203 global $wp_object_cache;
204
205 return $wp_object_cache->switch_to_blog( $blog_id );
206 }
207
208 /**
209 * Adds a group or set of groups to the list of global groups.
210 *
211 * @param string|array $groups A group or an array of groups to add
212 */
213 function wp_cache_add_global_groups( $groups ) {
214 global $wp_object_cache;
215
216 return $wp_object_cache->add_global_groups( $groups );
217 }
218
219 /**
220 * Adds a group or set of groups to the list of non-persistent groups.
221 *
222 * @param string|array $groups A group or an array of groups to add
223 */
224 function wp_cache_add_non_persistent_groups( $groups ) {
225 global $wp_object_cache;
226
227 $wp_object_cache->add_non_persistent_groups( $groups );
228 }
229
230 /**
231 * Adds a group or set of groups to the list of groups that use Redis hashes.
232 *
233 * @param string|array $groups A group or an array of groups to add.
234 */
235 function wp_cache_add_redis_hash_groups( $groups ) {
236 global $wp_object_cache;
237
238 $wp_object_cache->add_redis_hash_groups( $groups );
239 }
240
241 /**
242 * Reset internal cache keys and structures. If the cache backend uses global
243 * blog or site IDs as part of its cache keys, this function instructs the
244 * backend to reset those keys and perform any cleanup since blog or site IDs
245 * have changed since cache init.
246 *
247 * This function is deprecated. Use wp_cache_switch_to_blog() instead of this
248 * function when preparing the cache for a blog switch. For clearing the cache
249 * during unit tests, consider using wp_cache_init(). wp_cache_init() is not
250 * recommended outside of unit tests as the performance penality for using it is
251 * high.
252 *
253 * @deprecated 3.5.0
254 */
255 function wp_cache_reset() {
256 _deprecated_function( __FUNCTION__, '3.5' );
257
258 global $wp_object_cache;
259
260 return $wp_object_cache->reset();
261 }
262
263 /**
264 * WordPress Object Cache
265 *
266 * The WordPress Object Cache is used to save on trips to the database. The
267 * Object Cache stores all of the cache data to memory and makes the cache
268 * contents available by using a key, which is used to name and later retrieve
269 * the cache contents.
270 *
271 * The Object Cache can be replaced by other caching mechanisms by placing files
272 * in the wp-content folder which is looked at in wp-settings. If that file
273 * exists, then this file will not be included.
274 */
275 class WP_Object_Cache {
276
277 /**
278 * Holds the cached objects
279 *
280 * @var array
281 * @access private
282 */
283 var $cache = array();
284
285 /**
286 * The amount of times the cache data was already stored in the cache.
287 *
288 * @access private
289 * @var int
290 */
291 var $cache_hits = 0;
292
293 /**
294 * Amount of times the cache did not have the request in cache
295 *
296 * @var int
297 * @access public
298 */
299 var $cache_misses = 0;
300
301 /**
302 * The amount of times a request was made to Redis
303 *
304 * @access private
305 * @var int
306 */
307 var $redis_calls = array();
308
309 /**
310 * List of global groups
311 *
312 * @var array
313 * @access protected
314 */
315 var $global_groups = array();
316
317 /**
318 * List of non-persistent groups
319 *
320 * @var array
321 * @access protected
322 */
323 var $non_persistent_groups = array();
324
325 /**
326 * List of groups which use Redis hashes.
327 *
328 * @var array
329 * @access protected
330 */
331 var $redis_hash_groups = array();
332
333 /**
334 * The blog prefix to prepend to keys in non-global groups.
335 *
336 * @var int
337 * @access private
338 */
339 var $blog_prefix;
340
341 /**
342 * Whether or not Redis is connected
343 *
344 * @var bool
345 * @access private
346 */
347 var $is_redis_connected = false;
348
349 /**
350 * Whether or not the object cache thinks Redis needs a flush
351 *
352 * @var bool
353 * @access private
354 */
355 var $do_redis_failback_flush = false;
356
357 /**
358 * The last triggered error
359 */
360 var $last_triggered_error = '';
361
362 /**
363 * Whether or not to use true cache groups, instead of flattening.
364 *
365 * @var bool
366 * @access private
367 */
368 const USE_GROUPS = WP_REDIS_USE_CACHE_GROUPS;
369
370 /**
371 * Adds data to the cache if it doesn't already exist.
372 *
373 * @uses WP_Object_Cache::_exists Checks to see if the cache already has data.
374 * @uses WP_Object_Cache::set Sets the data after the checking the cache
375 * contents existence.
376 *
377 * @param int|string $key What to call the contents in the cache
378 * @param mixed $data The contents to store in the cache
379 * @param string $group Where to group the cache contents
380 * @param int $expire When to expire the cache contents
381 * @return bool False if cache key and group already exist, true on success
382 */
383 public function add( $key, $data, $group = 'default', $expire = 0 ) {
384
385 if ( empty( $group ) ) {
386 $group = 'default';
387 }
388
389 if ( function_exists( 'wp_suspend_cache_addition' ) && wp_suspend_cache_addition() ) {
390 return false;
391 }
392
393 if ( $this->_exists( $key, $group ) ) {
394 return false;
395 }
396
397 return $this->set( $key, $data, $group, (int) $expire );
398 }
399
400 /**
401 * Sets the list of global groups.
402 *
403 * @param array $groups List of groups that are global.
404 */
405 public function add_global_groups( $groups ) {
406 $groups = (array) $groups;
407
408 $groups = array_fill_keys( $groups, true );
409 $this->global_groups = array_merge( $this->global_groups, $groups );
410 }
411
412 /**
413 * Sets the list of non-persistent groups.
414 *
415 * @param array $groups List of groups that are non-persistent.
416 */
417 public function add_non_persistent_groups( $groups ) {
418 $groups = (array) $groups;
419
420 $groups = array_fill_keys( $groups, true );
421 $this->non_persistent_groups = array_merge( $this->non_persistent_groups, $groups );
422 }
423
424 /**
425 * Sets the list of groups that use Redis hashes.
426 *
427 * @param array $groups List of groups that use Redis hashes.
428 */
429 public function add_redis_hash_groups( $groups ) {
430 $groups = (array) $groups;
431
432 $groups = array_fill_keys( $groups, true );
433 $this->redis_hash_groups = array_merge( $this->redis_hash_groups, $groups );
434 }
435
436 /**
437 * Decrement numeric cache item's value
438 *
439 * @param int|string $key The cache key to increment
440 * @param int $offset The amount by which to decrement the item's value. Default is 1.
441 * @param string $group The group the key is in.
442 * @return false|int False on failure, the item's new value on success.
443 */
444 public function decr( $key, $offset = 1, $group = 'default' ) {
445
446 if ( empty( $group ) ) {
447 $group = 'default';
448 }
449
450 // The key needs to exist in order to be decremented
451 if ( ! $this->_exists( $key, $group ) ) {
452 return false;
453 }
454
455 $offset = (int) $offset;
456
457 # If this isn't a persistant group, we have to sort this out ourselves, grumble grumble
458 if ( ! $this->_should_persist( $group ) ) {
459 $existing = $this->_get_internal( $key, $group );
460 if ( empty( $existing ) || ! is_numeric( $existing ) ) {
461 $existing = 0;
462 } else {
463 $existing -= $offset;
464 }
465 if ( $existing < 0 ) {
466 $existing = 0;
467 }
468 $this->_set_internal( $key, $group, $existing );
469 return $existing;
470 }
471
472 if ( $this->_should_use_redis_hashes( $group ) ) {
473 $redis_safe_group = $this->_key( '', $group );
474 $result = $this->_call_redis( 'hIncrBy', $redis_safe_group, $key, -$offset, $group );
475 if ( $result < 0 ) {
476 $result = 0;
477 $this->_call_redis( 'hSet', $redis_safe_group, $key, $result );
478 }
479 } else {
480 $id = $this->_key( $key, $group );
481 $result = $this->_call_redis( 'decrBy', $id, $offset );
482 if ( $result < 0 ) {
483 $result = 0;
484 $this->_call_redis( 'set', $id, $result );
485 }
486 }
487
488 if ( is_int( $result ) ) {
489 $this->_set_internal( $key, $group, $result );
490 }
491 return $result;
492 }
493
494 /**
495 * Remove the contents of the cache key in the group
496 *
497 * If the cache key does not exist in the group and $force parameter is set
498 * to false, then nothing will happen. The $force parameter is set to false
499 * by default.
500 *
501 * @param int|string $key What the contents in the cache are called
502 * @param string $group Where the cache contents are grouped
503 * @param bool $force Optional. Whether to force the unsetting of the cache
504 * key in the group
505 * @return bool False if the contents weren't deleted and true on success
506 */
507 public function delete( $key, $group = 'default', $force = false ) {
508
509 if ( empty( $group ) ) {
510 $group = 'default';
511 }
512
513 if ( ! $force && ! $this->_exists( $key, $group ) ) {
514 return false;
515 }
516
517 if ( $this->_should_persist( $group ) ) {
518 if ( $this->_should_use_redis_hashes( $group ) ) {
519 $redis_safe_group = $this->_key( '', $group );
520 $result = $this->_call_redis( 'hDel', $redis_safe_group, $key );
521 } else {
522 $id = $this->_key( $key, $group );
523 $result = $this->_call_redis( 'del', $id );
524 }
525 if ( 1 !== $result ) {
526 return false;
527 }
528 }
529
530 $this->_unset_internal( $key, $group );
531 return true;
532 }
533
534 /**
535 * Remove the contents of all cache keys in the group.
536 *
537 * @param string $group Where the cache contents are grouped.
538 * @return boolean True on success, false on failure.
539 */
540 public function delete_group( $group ) {
541 if ( ! $this->_should_use_redis_hashes( $group ) ) {
542 return false;
543 }
544
545 $multisite_safe_group = $this->multisite && ! isset( $this->global_groups[ $group ] ) ? $this->blog_prefix . $group : $group;
546 $redis_safe_group = $this->_key( '', $group );
547 if ( $this->_should_persist( $group ) ) {
548 $result = $this->_call_redis( 'del', $redis_safe_group );
549 if ( 1 !== $result ) {
550 return false;
551 }
552 } elseif ( ! $this->_should_persist( $group ) && ! isset( $this->cache[ $multisite_safe_group ] ) ) {
553 return false;
554 }
555 unset( $this->cache[ $multisite_safe_group ] );
556 return true;
557 }
558
559 /**
560 * Clears the object cache of all data.
561 *
562 * By default, this will flush the session cache as well as Redis, but we
563 * can leave the redis cache intact if we want. This is helpful when, for
564 * instance, you're running a batch process and want to clear the session
565 * store to reduce the memory footprint, but you don't want to have to
566 * re-fetch all the values from the database.
567 *
568 * @param bool $redis Should we flush redis as well as the session cache?
569 * @return bool Always returns true
570 */
571 public function flush( $redis = true ) {
572 $this->cache = array();
573 if ( $redis ) {
574 $this->_call_redis( 'flushAll' );
575 }
576
577 return true;
578 }
579
580 /**
581 * Retrieves the cache contents, if it exists
582 *
583 * The contents will be first attempted to be retrieved by searching by the
584 * key in the cache group. If the cache is hit (success) then the contents
585 * are returned.
586 *
587 * On failure, the number of cache misses will be incremented.
588 *
589 * @param int|string $key What the contents in the cache are called
590 * @param string $group Where the cache contents are grouped
591 * @param string $force Whether to force a refetch rather than relying on the local cache (default is false)
592 * @param bool $found Optional. Whether the key was found in the cache. Disambiguates a return of false, a storable value. Passed by reference. Default null.
593 * @return bool|mixed False on failure to retrieve contents or the cache
594 * contents on success
595 */
596 public function get( $key, $group = 'default', $force = false, &$found = null ) {
597
598 if ( empty( $group ) ) {
599 $group = 'default';
600 }
601
602 // Key is set internally, so we can use this value
603 if ( $this->_isset_internal( $key, $group ) && ! $force ) {
604 $this->cache_hits += 1;
605 $found = true;
606 return $this->_get_internal( $key, $group );
607 }
608
609 // Not a persistent group, so don't try Redis if the value doesn't exist
610 // internally
611 if ( ! $this->_should_persist( $group ) ) {
612 $this->cache_misses += 1;
613 $found = false;
614 return false;
615 }
616
617 if ( $this->_should_use_redis_hashes( $group ) ) {
618 $redis_safe_group = $this->_key( '', $group );
619 $value = $this->_call_redis( 'hGet', $redis_safe_group, $key );
620 } else {
621 $id = $this->_key( $key, $group );
622 $value = $this->_call_redis( 'get', $id );
623 }
624
625 // PhpRedis returns `false` when the key doesn't exist
626 if ( false === $value ) {
627 $this->cache_misses += 1;
628 $found = false;
629 return false;
630 }
631
632 // All non-numeric values are serialized
633 if ( ! is_numeric( $value ) ) {
634 $value = unserialize( $value );
635 }
636
637 $this->_set_internal( $key, $group, $value );
638 $this->cache_hits += 1;
639 $found = true;
640 return $value;
641 }
642
643 /**
644 * Increment numeric cache item's value
645 *
646 * @param int|string $key The cache key to increment
647 * @param int $offset The amount by which to increment the item's value. Default is 1.
648 * @param string $group The group the key is in.
649 * @return false|int False on failure, the item's new value on success.
650 */
651 public function incr( $key, $offset = 1, $group = 'default' ) {
652
653 if ( empty( $group ) ) {
654 $group = 'default';
655 }
656
657 // The key needs to exist in order to be incremented
658 if ( ! $this->_exists( $key, $group ) ) {
659 return false;
660 }
661
662 $offset = (int) $offset;
663
664 # If this isn't a persistant group, we have to sort this out ourselves, grumble grumble
665 if ( ! $this->_should_persist( $group ) ) {
666 $existing = $this->_get_internal( $key, $group );
667 if ( empty( $existing ) || ! is_numeric( $existing ) ) {
668 $existing = 1;
669 } else {
670 $existing += $offset;
671 }
672 if ( $existing < 0 ) {
673 $existing = 0;
674 }
675 $this->_set_internal( $key, $group, $existing );
676 return $existing;
677 }
678
679 if ( $this->_should_use_redis_hashes( $group ) ) {
680 $redis_safe_group = $this->_key( '', $group );
681 $result = $this->_call_redis( 'hIncrBy', $redis_safe_group, $key, $offset, $group );
682 if ( $result < 0 ) {
683 $result = 0;
684 $this->_call_redis( 'hSet', $redis_safe_group, $key, $result );
685 }
686 } else {
687 $id = $this->_key( $key, $group );
688 $result = $this->_call_redis( 'incrBy', $id, $offset );
689 if ( $result < 0 ) {
690 $result = 0;
691 $this->_call_redis( 'set', $id, $result );
692 }
693 }
694
695 if ( is_int( $result ) ) {
696 $this->_set_internal( $key, $group, $result );
697 }
698 return $result;
699 }
700
701 /**
702 * Replace the contents in the cache, if contents already exist
703 * @see WP_Object_Cache::set()
704 *
705 * @param int|string $key What to call the contents in the cache
706 * @param mixed $data The contents to store in the cache
707 * @param string $group Where to group the cache contents
708 * @param int $expire When to expire the cache contents
709 * @return bool False if not exists, true if contents were replaced
710 */
711 public function replace( $key, $data, $group = 'default', $expire = 0 ) {
712
713 if ( empty( $group ) ) {
714 $group = 'default';
715 }
716
717 if ( ! $this->_exists( $key, $group ) ) {
718 return false;
719 }
720
721 return $this->set( $key, $data, $group, (int) $expire );
722 }
723
724 /**
725 * Reset keys
726 *
727 * @deprecated 3.5.0
728 */
729 public function reset() {
730 _deprecated_function( __FUNCTION__, '3.5', 'switch_to_blog()' );
731 }
732
733 /**
734 * Sets the data contents into the cache
735 *
736 * The cache contents is grouped by the $group parameter followed by the
737 * $key. This allows for duplicate ids in unique groups. Therefore, naming of
738 * the group should be used with care and should follow normal function
739 * naming guidelines outside of core WordPress usage.
740 *
741 * The $expire parameter is not used, because the cache will automatically
742 * expire for each time a page is accessed and PHP finishes. The method is
743 * more for cache plugins which use files.
744 *
745 * @param int|string $key What to call the contents in the cache
746 * @param mixed $data The contents to store in the cache
747 * @param string $group Where to group the cache contents
748 * @param int $expire TTL for the data, in seconds
749 * @return bool Always returns true
750 */
751 public function set( $key, $data, $group = 'default', $expire = 0 ) {
752
753 if ( empty( $group ) ) {
754 $group = 'default';
755 }
756
757 if ( is_object( $data ) ) {
758 $data = clone $data;
759 }
760
761 $this->_set_internal( $key, $group, $data );
762
763 if ( ! $this->_should_persist( $group ) ) {
764 return true;
765 }
766
767 # If this is an integer, store it as such. Otherwise, serialize it.
768 if ( ! is_numeric( $data ) || intval( $data ) !== $data ) {
769 $data = serialize( $data );
770 }
771
772 // Redis doesn't support expire on hash group keys
773 if ( $this->_should_use_redis_hashes( $group ) ) {
774 $redis_safe_group = $this->_key( '', $group );
775 $this->_call_redis( 'hSet', $redis_safe_group, $key, $data );
776 return true;
777 }
778
779 $id = $this->_key( $key, $group );
780 if ( empty( $expire ) ) {
781 $this->_call_redis( 'set', $id, $data );
782 } else {
783 $this->_call_redis( 'setex', $id, $expire, $data );
784 }
785 return true;
786 }
787
788 /**
789 * Echoes the stats of the caching.
790 *
791 * Gives the cache hits, and cache misses. Also prints every cached group,
792 * key and the data.
793 */
794 public function stats() {
795 $total_redis_calls = 0;
796 foreach ( $this->redis_calls as $method => $calls ) {
797 $total_redis_calls += $calls;
798 }
799 $out = array();
800 $out[] = '<p>';
801 $out[] = '<strong>Cache Hits:</strong>' . (int) $this->cache_hits . '<br />';
802 $out[] = '<strong>Cache Misses:</strong>' . (int) $this->cache_misses . '<br />';
803 $out[] = '<strong>Redis Calls:</strong>' . (int) $total_redis_calls . ':<br />';
804 foreach ( $this->redis_calls as $method => $calls ) {
805 $out[] = ' - ' . esc_html( $method ) . ': ' . (int) $calls . '<br />';
806 }
807 $out[] = '</p>';
808 $out[] = '<ul>';
809 foreach ( $this->cache as $group => $cache ) {
810 $out[] = '<li><strong>Group:</strong> ' . esc_html( $group ) . ' - ( ' . number_format( strlen( serialize( $cache ) ) / 1024, 2 ) . 'k )</li>';
811 }
812 $out[] = '</ul>';
813 // @codingStandardsIgnoreStart
814 echo implode( PHP_EOL, $out );
815 // @codingStandardsIgnoreEnd
816 }
817
818 /**
819 * Switch the interal blog id.
820 *
821 * This changes the blog id used to create keys in blog specific groups.
822 *
823 * @param int $blog_id Blog ID
824 */
825 public function switch_to_blog( $blog_id ) {
826 $blog_id = (int) $blog_id;
827 $this->blog_prefix = $this->multisite ? $blog_id . ':' : '';
828 }
829
830 /**
831 * Utility function to determine whether a key exists in the cache.
832 *
833 * @access protected
834 */
835 protected function _exists( $key, $group ) {
836 if ( $this->_isset_internal( $key, $group ) ) {
837 return true;
838 }
839
840 if ( ! $this->_should_persist( $group ) ) {
841 return false;
842 }
843
844 if ( $this->_should_use_redis_hashes( $group ) ) {
845 $redis_safe_group = $this->_key( '', $group );
846 return $this->_call_redis( 'hExists', $redis_safe_group, $key );
847 }
848 $id = $this->_key( $key, $group );
849 return $this->_call_redis( 'exists', $id );
850 }
851
852 /**
853 * Check whether there's a value in the internal object cache.
854 *
855 * @param string $key
856 * @param string $group
857 * @return boolean
858 */
859 protected function _isset_internal( $key, $group ) {
860 if ( $this->_should_use_redis_hashes( $group ) ) {
861 $multisite_safe_group = $this->multisite && ! isset( $this->global_groups[ $group ] ) ? $this->blog_prefix . $group : $group;
862 return isset( $this->cache[ $multisite_safe_group ][ $key ] );
863 } else {
864 $key = $this->_key( $key, $group );
865 return isset( $this->cache[ $key ] );
866 }
867 }
868
869 /**
870 * Get a value from the internal object cache
871 *
872 * @param string $key
873 * @param string $group
874 * @return mixed
875 */
876 protected function _get_internal( $key, $group ) {
877 $value = null;
878 if ( $this->_should_use_redis_hashes( $group ) ) {
879 $multisite_safe_group = $this->multisite && ! isset( $this->global_groups[ $group ] ) ? $this->blog_prefix . $group : $group;
880 if ( isset( $this->cache[ $multisite_safe_group ][ $key ] ) ) {
881 $value = $this->cache[ $multisite_safe_group ][ $key ];
882 }
883 } else {
884 $key = $this->_key( $key, $group );
885 if ( isset( $this->cache[ $key ] ) ) {
886 $value = $this->cache[ $key ];
887 }
888 }
889 if ( is_object( $value ) ) {
890 return clone $value;
891 }
892 return $value;
893 }
894
895 /**
896 * Set a value to the internal object cache
897 *
898 * @param string $key
899 * @param string $group
900 * @param mixed $value
901 */
902 protected function _set_internal( $key, $group, $value ) {
903 // Redis converts null to an empty string
904 if ( is_null( $value ) ) {
905 $value = '';
906 }
907 if ( $this->_should_use_redis_hashes( $group ) ) {
908 $multisite_safe_group = $this->multisite && ! isset( $this->global_groups[ $group ] ) ? $this->blog_prefix . $group : $group;
909 if ( ! isset( $this->cache[ $multisite_safe_group ] ) ) {
910 $this->cache[ $multisite_safe_group ] = array();
911 }
912 $this->cache[ $multisite_safe_group ][ $key ] = $value;
913 } else {
914 $key = $this->_key( $key, $group );
915 $this->cache[ $key ] = $value;
916 }
917 }
918
919 /**
920 * Unset a value from the internal object cache
921 *
922 * @param string $key
923 * @param string $group
924 */
925 protected function _unset_internal( $key, $group ) {
926 if ( $this->_should_use_redis_hashes( $group ) ) {
927 $multisite_safe_group = $this->multisite && ! isset( $this->global_groups[ $group ] ) ? $this->blog_prefix . $group : $group;
928 if ( isset( $this->cache[ $multisite_safe_group ][ $key ] ) ) {
929 unset( $this->cache[ $multisite_safe_group ][ $key ] );
930 }
931 } else {
932 $key = $this->_key( $key, $group );
933 if ( isset( $this->cache[ $key ] ) ) {
934 unset( $this->cache[ $key ] );
935 }
936 }
937 }
938
939 /**
940 * Utility function to generate the redis key for a given key and group.
941 *
942 * @param string $key The cache key.
943 * @param string $group The cache group.
944 * @return string A properly prefixed redis cache key.
945 */
946 protected function _key( $key = '', $group = 'default' ) {
947 if ( empty( $group ) ) {
948 $group = 'default';
949 }
950
951 if ( ! empty( $this->global_groups[ $group ] ) ) {
952 $prefix = $this->global_prefix;
953 } else {
954 $prefix = $this->blog_prefix;
955 }
956
957 return preg_replace( '/\s+/', '', WP_CACHE_KEY_SALT . "$prefix$group:$key" );
958 }
959
960 /**
961 * Does this group use persistent storage?
962 *
963 * @param string $group Cache group.
964 * @return bool true if the group is persistent, false if not.
965 */
966 protected function _should_persist( $group ) {
967 return empty( $this->non_persistent_groups[ $group ] );
968 }
969
970 /**
971 * Should this group use Redis hashes?
972 *
973 * @param string $group Cache group.
974 * @return bool True if the group should use Redis hashes, false if not.
975 */
976 protected function _should_use_redis_hashes( $group ) {
977 if ( self::USE_GROUPS || ! empty( $this->redis_hash_groups[ $group ] ) ) {
978 return true;
979 }
980 return false;
981 }
982
983 /**
984 * Wrapper method for connecting to Redis, which lets us retry the connection
985 */
986 protected function _connect_redis() {
987 global $redis_server;
988
989 if ( ! class_exists( 'Redis' ) ) {
990 $this->is_redis_connected = false;
991 $this->missing_redis_message = 'Warning! PHPRedis module is unavailable, which is required by WP Redis object cache.';
992 return $this->is_redis_connected;
993 }
994
995 if ( empty( $redis_server ) ) {
996 # Attempt to automatically load Pantheon's Redis config from the env.
997 if ( isset( $_SERVER['CACHE_HOST'] ) ) {
998 $redis_server = array(
999 'host' => $_SERVER['CACHE_HOST'],
1000 'port' => $_SERVER['CACHE_PORT'],
1001 'auth' => $_SERVER['CACHE_PASSWORD'],
1002 );
1003 } else {
1004 $redis_server = array(
1005 'host' => '127.0.0.1',
1006 'port' => 6379,
1007 );
1008 }
1009 }
1010
1011 $this->redis = new Redis;
1012
1013 if ( file_exists( $redis_server['host'] ) && 'socket' === filetype( $redis_server['host'] ) ) { //unix socket connection
1014 //port must be null or socket won't connect
1015 $port = null;
1016 } else { //tcp connection
1017 $port = ! empty( $redis_server['port'] ) ? $redis_server['port'] : 6379;
1018 }
1019
1020 try {
1021 $this->redis->connect( $redis_server['host'], $port, 1, null, 100 ); # 1s timeout, 100ms delay between reconnections
1022 } catch ( RedisException $e ) {
1023 // PhpRedis throws an Exception when it fails a server call.
1024 // To prevent WordPress from fataling, we catch the Exception.
1025 try {
1026 $this->last_triggered_error = 'WP Redis: ' . $e->getMessage();
1027 // Be friendly to developers debugging production servers by triggering an error
1028 // @codingStandardsIgnoreStart
1029 trigger_error( $this->last_triggered_error, E_USER_WARNING );
1030 // @codingStandardsIgnoreEnd
1031 } catch ( PHPUnit_Framework_Error_Warning $e ) {
1032 // PHPUnit throws an Exception when `trigger_error()` is called.
1033 // To ensure our tests (which expect Exceptions to be caught) continue to run,
1034 // we catch the PHPUnit exception and inspect the RedisException message
1035 }
1036 }
1037
1038 foreach ( array( 'auth' => 'auth', 'database' => 'select' ) as $k => $method ) {
1039 if ( ! isset( $redis_server[ $k ] ) ) {
1040 continue;
1041 }
1042 try {
1043 $this->redis->$method( $redis_server[ $k ] );
1044 } catch ( RedisException $e ) {
1045 // PhpRedis throws an Exception when it fails a server call.
1046 // To prevent WordPress from fataling, we catch the Exception.
1047 try {
1048 $this->last_triggered_error = 'WP Redis: ' . $e->getMessage();
1049 // Be friendly to developers debugging production servers by triggering an error
1050 // @codingStandardsIgnoreStart
1051 trigger_error( $this->last_triggered_error, E_USER_WARNING );
1052 // @codingStandardsIgnoreEnd
1053 } catch ( PHPUnit_Framework_Error_Warning $e ) {
1054 // PHPUnit throws an Exception when `trigger_error()` is called.
1055 // To ensure our tests (which expect Exceptions to be caught) continue to run,
1056 // we catch the PHPUnit exception and inspect the RedisException message
1057 }
1058 }
1059 }
1060 $this->is_redis_connected = $this->redis->isConnected();
1061 if ( ! $this->is_redis_connected ) {
1062 $this->missing_redis_message = 'Warning! WP Redis object cache cannot connect to Redis server.';
1063 }
1064 return $this->is_redis_connected;
1065 }
1066
1067 /**
1068 * Wrapper method for calls to Redis, which fails gracefully when Redis is unavailable
1069 *
1070 * @param string $method
1071 * @param mixed $args
1072 * @return mixed
1073 */
1074 protected function _call_redis( $method ) {
1075 global $wpdb;
1076
1077 $arguments = func_get_args();
1078 array_shift( $arguments ); // ignore $method
1079
1080 // $group is intended for the failback, and isn't passed to the Redis callback
1081 if ( 'hIncrBy' === $method ) {
1082 $group = array_pop( $arguments );
1083 }
1084
1085 if ( $this->is_redis_connected ) {
1086 try {
1087 if ( ! isset( $this->redis_calls[ $method ] ) ) {
1088 $this->redis_calls[ $method ] = 0;
1089 }
1090 $this->redis_calls[ $method ]++;
1091 $retval = call_user_func_array( array( $this->redis, $method ), $arguments );
1092 return $retval;
1093 } catch ( RedisException $e ) {
1094 // PhpRedis throws an Exception when it fails a server call.
1095 // To prevent WordPress from fataling, we catch the Exception.
1096 $retry_exception_messages = array( 'socket error on read socket', 'Connection closed', 'Redis server went away' );
1097 $retry_exception_messages = apply_filters( 'wp_redis_retry_exception_messages', $retry_exception_messages );
1098 if ( in_array( $e->getMessage(), $retry_exception_messages, true ) ) {
1099 try {
1100 $this->last_triggered_error = 'WP Redis: ' . $e->getMessage();
1101 // Be friendly to developers debugging production servers by triggering an error
1102 // @codingStandardsIgnoreStart
1103 trigger_error( $this->last_triggered_error, E_USER_WARNING );
1104 // @codingStandardsIgnoreEnd
1105 } catch ( PHPUnit_Framework_Error_Warning $e ) {
1106 // PHPUnit throws an Exception when `trigger_error()` is called.
1107 // To ensure our tests (which expect Exceptions to be caught) continue to run,
1108 // we catch the PHPUnit exception and inspect the RedisException message
1109 }
1110 // Attempt to refresh the connection if it was successfully established once
1111 // $this->is_redis_connected will be set inside _connect_redis()
1112 if ( $this->_connect_redis() ) {
1113 return call_user_func_array( array( $this, '_call_redis' ), array_merge( array( $method ), $arguments ) );
1114 }
1115 // Fall through to fallback below
1116 } else {
1117 throw $e;
1118 }
1119 }
1120 }
1121
1122 if ( $this->is_redis_failback_flush_enabled() && ! $this->do_redis_failback_flush && ! empty( $wpdb ) ) {
1123 if ( $this->multisite ) {
1124 $table = $wpdb->sitemeta;
1125 $col1 = 'meta_key';
1126 $col2 = 'meta_value';
1127 } else {
1128 $table = $wpdb->options;
1129 $col1 = 'option_name';
1130 $col2 = 'option_value';
1131 }
1132 // @codingStandardsIgnoreStart
1133 $wpdb->query( "INSERT IGNORE INTO {$table} ({$col1},{$col2}) VALUES ('wp_redis_do_redis_failback_flush',1)" );
1134 // @codingStandardsIgnoreEnd
1135 $this->do_redis_failback_flush = true;
1136 }
1137
1138 // Mock expected behavior from Redis for these methods
1139 switch ( $method ) {
1140 case 'incr':
1141 case 'incrBy':
1142 $val = $this->cache[ $arguments[0] ];
1143 $offset = isset( $arguments[1] ) && 'incrBy' === $method ? $arguments[1] : 1;
1144 $val = $val + $offset;
1145 return $val;
1146 case 'hIncrBy':
1147 $val = $this->_get_internal( $arguments[1], $group );
1148 return $val + $arguments[2];
1149 case 'decrBy':
1150 case 'decr':
1151 $val = $this->cache[ $arguments[0] ];
1152 $offset = isset( $arguments[1] ) && 'decrBy' === $method ? $arguments[1] : 1;
1153 $val = $val - $offset;
1154 return $val;
1155 case 'del':
1156 case 'hDel':
1157 return 1;
1158 case 'flushAll':
1159 case 'IsConnected':
1160 case 'exists':
1161 case 'get':
1162 case 'hGet':
1163 return false;
1164 }
1165
1166 }
1167
1168 /**
1169 * Admin UI to let the end user know something about the Redis connection isn't working.
1170 */
1171 public function wp_action_admin_notices_warn_missing_redis() {
1172 if ( ! current_user_can( 'manage_options' ) || empty( $this->missing_redis_message ) ) {
1173 return;
1174 }
1175 echo '<div class="message error"><p>' . esc_html( $this->missing_redis_message ) . '</p></div>';
1176 }
1177
1178 /**
1179 * Whether or not wakeup flush is enabled
1180 *
1181 * @return bool
1182 */
1183 private function is_redis_failback_flush_enabled() {
1184 if ( defined( 'WP_INSTALLING' ) && WP_INSTALLING ) {
1185 return false;
1186 } elseif ( defined( 'WP_REDIS_DISABLE_FAILBACK_FLUSH' ) && WP_REDIS_DISABLE_FAILBACK_FLUSH ) {
1187 return false;
1188 }
1189 return true;
1190 }
1191
1192 /**
1193 * Sets up object properties; PHP 5 style constructor
1194 *
1195 * @return null|WP_Object_Cache If cache is disabled, returns null.
1196 */
1197 public function __construct() {
1198 global $blog_id, $table_prefix, $wpdb;
1199
1200 $this->multisite = is_multisite();
1201 $this->blog_prefix = $this->multisite ? $blog_id . ':' : '';
1202
1203 if ( ! $this->_connect_redis() && function_exists( 'add_action' ) ) {
1204 add_action( 'admin_notices', array( $this, 'wp_action_admin_notices_warn_missing_redis' ) );
1205 }
1206
1207 if ( $this->is_redis_failback_flush_enabled() && ! empty( $wpdb ) ) {
1208 if ( $this->multisite ) {
1209 $table = $wpdb->sitemeta;
1210 $col1 = 'meta_key';
1211 $col2 = 'meta_value';
1212 } else {
1213 $table = $wpdb->options;
1214 $col1 = 'option_name';
1215 $col2 = 'option_value';
1216 }
1217 // @codingStandardsIgnoreStart
1218 $this->do_redis_failback_flush = (bool) $wpdb->get_results( "SELECT {$col2} FROM {$table} WHERE {$col1}='wp_redis_do_redis_failback_flush'" );
1219 // @codingStandardsIgnoreEnd
1220 if ( $this->is_redis_connected && $this->do_redis_failback_flush ) {
1221 $ret = $this->_call_redis( 'flushAll' );
1222 if ( $ret ) {
1223 // @codingStandardsIgnoreStart
1224 $wpdb->query( "DELETE FROM {$table} WHERE {$col1}='wp_redis_do_redis_failback_flush'" );
1225 // @codingStandardsIgnoreEnd
1226 $this->do_redis_failback_flush = false;
1227 }
1228 }
1229 }
1230
1231 $this->global_prefix = ( $this->multisite || defined( 'CUSTOM_USER_TABLE' ) && defined( 'CUSTOM_USER_META_TABLE' ) ) ? '' : $table_prefix;
1232
1233 /**
1234 * @todo This should be moved to the PHP4 style constructor, PHP5
1235 * already calls __destruct()
1236 */
1237 register_shutdown_function( array( $this, '__destruct' ) );
1238 }
1239
1240 /**
1241 * Will save the object cache before object is completely destroyed.
1242 *
1243 * Called upon object destruction, which should be when PHP ends.
1244 *
1245 * @return bool True value. Won't be used by PHP
1246 */
1247 public function __destruct() {
1248 return true;
1249 }
1250 }
1251