PluginProbe
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score / trunk
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score vtrunk
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 / memcache-object-cache.php

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

1,212 lines 32.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Upstream: https://github.com/poweredcache/wp-memcached
4 * Upstream version: 92d60df2dabac6a70b863a54d95602a0424fe391
5 */
6 defined( 'ABSPATH' ) || exit;
7
8 // We need Memcache to continue
9 if ( class_exists( 'Memcache' ) ):
10
11
12 // Users with setups where multiple installs share a common wp-config.php or $table_prefix
13 // can use this to guarantee uniqueness for the keys generated by this object cache
14 if ( ! defined( 'WP_CACHE_KEY_SALT' ) ) {
15 define( 'WP_CACHE_KEY_SALT', '' );
16 }
17
18 function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
19 global $wp_object_cache;
20
21 return $wp_object_cache->add( $key, $data, $group, $expire );
22 }
23
24 function wp_cache_add_multiple( array $data, $group = '', $expire = 0 ) {
25 global $wp_object_cache;
26
27 return $wp_object_cache->add_multiple( $data, $group, $expire );
28 }
29
30 function wp_cache_incr( $key, $n = 1, $group = '' ) {
31 global $wp_object_cache;
32
33 return $wp_object_cache->incr( $key, $n, $group );
34 }
35
36 function wp_cache_decr( $key, $n = 1, $group = '' ) {
37 global $wp_object_cache;
38
39 return $wp_object_cache->decr( $key, $n, $group );
40 }
41
42 function wp_cache_close() {
43 global $wp_object_cache;
44
45 return $wp_object_cache->close();
46 }
47
48 function wp_cache_delete( $key, $group = '' ) {
49 global $wp_object_cache;
50
51 return $wp_object_cache->delete( $key, $group );
52 }
53
54 function wp_cache_delete_multiple( array $keys, $group = '' ) {
55 global $wp_object_cache;
56
57 return $wp_object_cache->delete_multiple( $keys, $group );
58 }
59
60 function wp_cache_flush() {
61 global $wp_object_cache;
62
63 return $wp_object_cache->flush();
64 }
65
66 function wp_cache_flush_runtime() {
67 global $wp_object_cache;
68
69 return $wp_object_cache->flush_runtime();
70 }
71
72 function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
73 global $wp_object_cache;
74
75 if ( function_exists( 'apply_filters' ) ) {
76 $value = apply_filters( 'pre_wp_cache_get', false, $key, $group, $force );
77 if ( false !== $value ) {
78 $found = true;
79 return $value;
80 }
81 }
82
83 return $wp_object_cache->get( $key, $group, $force, $found );
84 }
85
86 function wp_cache_get_multiple( $keys, $group = '', $force = false ) {
87 global $wp_object_cache;
88
89 return $wp_object_cache->get_multiple( $keys, $group, $force );
90 }
91
92 /**
93 * Retrieve multiple cache entries
94 *
95 * @param array $groups Array of arrays, of groups and keys to retrieve
96 * @return mixed
97 */
98 function wp_cache_get_multi( $groups ) {
99 global $wp_object_cache;
100
101 return $wp_object_cache->get_multi( $groups );
102 }
103
104 function wp_cache_init() {
105 global $wp_object_cache;
106
107 $wp_object_cache = new WP_Object_Cache();
108 }
109
110 function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
111 global $wp_object_cache;
112
113 return $wp_object_cache->replace( $key, $data, $group, $expire );
114 }
115
116 function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
117 global $wp_object_cache;
118
119 if ( defined( 'WP_INSTALLING' ) === false ) {
120 return $wp_object_cache->set( $key, $data, $group, $expire );
121 } else {
122 return $wp_object_cache->delete( $key, $group );
123 }
124 }
125
126 function wp_cache_set_multiple( array $data, $group = '', $expire = 0 ) {
127 global $wp_object_cache;
128
129 return $wp_object_cache->set_multiple( $data, $group, $expire );
130 }
131
132 function wp_cache_switch_to_blog( $blog_id ) {
133 global $wp_object_cache;
134
135 return $wp_object_cache->switch_to_blog( $blog_id );
136 }
137
138 function wp_cache_add_global_groups( $groups ) {
139 global $wp_object_cache;
140
141 $wp_object_cache->add_global_groups( $groups );
142 }
143
144 function wp_cache_add_non_persistent_groups( $groups ) {
145 global $wp_object_cache;
146
147 $wp_object_cache->add_non_persistent_groups( $groups );
148 }
149
150 /**
151 * Determines whether the object cache implementation supports a particular feature.
152 *
153 * @param string $feature Name of the feature to check for. Possible values include:
154 * 'add_multiple', 'set_multiple', 'get_multiple', 'delete_multiple',
155 * 'flush_runtime', 'flush_group'.
156 * @return bool True if the feature is supported, false otherwise.
157 */
158 function wp_cache_supports( $feature ) {
159 switch ( $feature ) {
160 case 'get_multiple':
161 case 'flush_runtime':
162 return true;
163
164 default:
165 return false;
166 }
167 }
168
169 class WP_Object_Cache {
170 var $global_groups = array( 'WP_Object_Cache_global' );
171
172 var $no_mc_groups = array();
173
174 var $cache = array();
175 /** @var Memcache[] */
176 var $mc = array();
177 var $default_mcs = array();
178 var $stats = array(
179 'get' => 0,
180 'get_local' => 0,
181 'get_multi' => 0,
182 'set' => 0,
183 'set_local' => 0,
184 'add' => 0,
185 'delete' => 0,
186 'delete_local' => 0,
187 'slow-ops' => 0,
188 );
189 var $group_ops = array();
190 var $cache_hits = 0;
191 var $cache_misses = 0;
192 var $global_prefix = '';
193 var $blog_prefix = '';
194 var $key_salt = '';
195
196 var $flush_group = 'WP_Object_Cache';
197 var $global_flush_group = 'WP_Object_Cache_global';
198 var $flush_key = "flush_number_v4";
199 var $old_flush_key = "flush_number";
200 var $flush_number = array();
201 var $global_flush_number = null;
202
203 var $cache_enabled = true;
204 var $default_expiration = 0;
205 var $max_expiration = 2592000; // 30 days
206
207 var $stats_callback = null;
208
209 var $connection_errors = array();
210
211 var $time_start = 0;
212 var $time_total = 0;
213 var $size_total = 0;
214 var $slow_op_microseconds = 0.005; // 5 ms
215
216 function add( $id, $data, $group = 'default', $expire = 0 ) {
217 $key = $this->key( $id, $group );
218
219 if ( is_object( $data ) ) {
220 $data = clone $data;
221 }
222
223 if ( in_array( $group, $this->no_mc_groups ) ) {
224 if ( ! isset( $this->cache[ $key ] ) ) {
225 $this->cache[ $key ] = [
226 'value' => $data,
227 'found' => false,
228 ];
229
230 return true;
231 }
232
233 return false;
234 }
235
236 if ( isset( $this->cache[ $key ][ 'value' ] ) && false !== $this->cache[ $key ][ 'value' ] ) {
237 return false;
238 }
239
240 $mc = $this->get_mc( $group );
241
242 $expire = intval( $expire );
243 if ( 0 === $expire || $expire > $this->max_expiration ) {
244 $expire = $this->default_expiration;
245 }
246
247 $size = $this->get_data_size( $data );
248 $this->timer_start();
249 $result = $mc->add( $key, $data, false, $expire );
250 $elapsed = $this->timer_stop();
251
252 $comment = '';
253 if ( isset( $this->cache[ $key ] ) ) {
254 $comment .= ' [lc already]';
255 }
256 if ( false === $result ) {
257 $comment .= ' [mc already]';
258 }
259
260 $this->group_ops_stats( 'add', $key, $group, $size, $elapsed, $comment );
261
262 if ( false !== $result ) {
263 $this->cache[ $key ] = [
264 'value' => $data,
265 'found' => true,
266 ];
267 } else if ( false === $result && true === isset( $this->cache[$key][ 'value' ] ) && false === $this->cache[$key][ 'value' ] ) {
268 /*
269 * Here we unset local cache if remote add failed and local cache value is equal to `false` in order
270 * to update the local cache anytime we get a new information from remote server. This way, the next
271 * cache get will go to remote server and will fetch recent data.
272 */
273 unset( $this->cache[$key] );
274 }
275
276 return $result;
277 }
278
279 public function add_multiple( array $data, $group = '', $expire = 0 ) {
280 $values = array();
281
282 foreach ( $data as $key => $value ) {
283 $values[ $key ] = $this->add( $key, $value, $group, $expire );
284 }
285
286 return $values;
287 }
288
289 function add_global_groups( $groups ) {
290 if ( ! is_array( $groups ) ) {
291 $groups = (array) $groups;
292 }
293
294 $this->global_groups = array_merge( $this->global_groups, $groups );
295 $this->global_groups = array_unique( $this->global_groups );
296 }
297
298 function add_non_persistent_groups( $groups ) {
299 if ( ! is_array( $groups ) ) {
300 $groups = (array) $groups;
301 }
302
303 $this->no_mc_groups = array_merge( $this->no_mc_groups, $groups );
304 $this->no_mc_groups = array_unique( $this->no_mc_groups );
305 }
306
307 function incr( $id, $n = 1, $group = 'default' ) {
308 $key = $this->key( $id, $group );
309 $mc = $this->get_mc( $group );
310
311 $incremented = $mc->increment( $key, $n );
312
313 $this->cache[ $key ] = [
314 'value' => $incremented,
315 'found' => false !== $incremented,
316 ];
317
318 return $this->cache[ $key ][ 'value' ];
319 }
320
321 function decr( $id, $n = 1, $group = 'default' ) {
322 $key = $this->key( $id, $group );
323 $mc = $this->get_mc( $group );
324
325 $decremented = $mc->decrement( $key, $n );
326 $this->cache[ $key ] = [
327 'value' => $decremented,
328 'found' => false !== $decremented,
329 ];
330
331 return $this->cache[ $key ][ 'value' ];
332 }
333
334 function close() {
335 foreach ( $this->mc as $bucket => $mc ) {
336 $mc->close();
337 }
338 }
339
340 function delete( $id, $group = 'default' ) {
341 $key = $this->key( $id, $group );
342
343 if ( in_array( $group, $this->no_mc_groups ) ) {
344 unset( $this->cache[ $key ] );
345
346 return true;
347 }
348
349 $mc = $this->get_mc( $group );
350
351 $this->timer_start();
352 $result = $mc->delete( $key );
353 $elapsed = $this->timer_stop();
354
355 $this->group_ops_stats( 'delete', $key, $group, null, $elapsed );
356
357 if ( false !== $result ) {
358 unset( $this->cache[ $key ] );
359 }
360
361 return $result;
362 }
363
364 public function delete_multiple( array $keys, $group = '' ) {
365 $values = array();
366
367 foreach ( $keys as $key ) {
368 $values[ $key ] = $this->delete( $key, $group );
369 }
370
371 return $values;
372 }
373
374 // Gets number from all default servers, replicating if needed
375 function get_max_flush_number( $group ) {
376 $key = $this->key( $this->flush_key, $group );
377
378 $values = array();
379 $size = 19; // size of microsecond timestamp serialized
380 foreach ( $this->default_mcs as $i => $mc ) {
381 $flags = false;
382 $this->timer_start();
383 $values[ $i ] = $mc->get( $key, $flags );
384 $elapsed = $this->timer_stop();
385
386 if ( empty( $values[ $i ] ) ) {
387 $this->group_ops_stats( 'get_flush_number', $key, $group, null, $elapsed, 'not_in_memcache' );
388 } else {
389 $this->group_ops_stats( 'get_flush_number', $key, $group, $size, $elapsed, 'memcache' );
390 }
391 }
392
393 $max = max( $values );
394
395 if ( ! $max > 0 ) {
396 return false;
397 }
398
399 // Replicate to servers not having the max.
400 $expire = 0;
401 foreach ( $this->default_mcs as $i => $mc ) {
402 if ( $values[ $i ] < $max ) {
403 $this->timer_start();
404 $mc->set( $key, $max, false, $expire );
405 $elapsed = $this->timer_stop();
406 $this->group_ops_stats( 'set_flush_number', $key, $group, $size, $elapsed, 'replication_repair' );
407 }
408 }
409
410 return $max;
411 }
412
413 function set_flush_number( $value, $group ) {
414 $key = $this->key( $this->flush_key, $group );
415 $expire = 0;
416 $size = 19;
417 foreach ( $this->default_mcs as $i => $mc ) {
418 $this->timer_start();
419 $mc->set( $key, $value, false, $expire );
420 $elapsed = $this->timer_stop();
421 $this->group_ops_stats( 'set_flush_number', $key, $group, $size, $elapsed, 'replication' );
422 }
423 }
424
425 function get_flush_number( $group ) {
426 $flush_number = $this->get_max_flush_number( $group );
427 // What if there is no v4 flush number?
428 if ( empty( $flush_number ) ) {
429 // Look for the v3 flush number key.
430 $flush_number = intval( $this->get( $this->old_flush_key, $group ) );
431 if ( !empty( $flush_number ) ) {
432 // Found v3 flush number. Upgrade to v4 with replication.
433 $this->set_flush_number( $flush_number, $group );
434 // Delete v3 key so we can't later restore it and find stale keys.
435 } else {
436 // No flush number found anywhere. Make a new one. This flushes the cache.
437 $flush_number = $this->new_flush_number();
438 $this->set_flush_number( $flush_number, $group );
439 }
440 }
441
442 return $flush_number;
443 }
444
445 function get_global_flush_number() {
446 if ( ! isset( $this->global_flush_number ) ) {
447 $this->global_flush_number = $this->get_flush_number( $this->global_flush_group );
448 }
449 return $this->global_flush_number;
450 }
451
452 function get_blog_flush_number() {
453 if ( ! isset( $this->flush_number[ $this->blog_prefix ] ) ) {
454 $this->flush_number[ $this->blog_prefix ] = $this->get_flush_number( $this->flush_group );
455 }
456 return $this->flush_number[ $this->blog_prefix ];
457 }
458
459 function flush_runtime() {
460 $this->cache = array();
461 $this->group_ops = array();
462
463 return true;
464 }
465
466 function flush() {
467 // Do not use the memcached flush method. It acts on an
468 // entire memcached server, affecting all sites.
469 // Flush is also unusable in some setups, e.g. twemproxy.
470 // Instead, rotate the key prefix for the current site.
471 // Global keys are rotated when flushing on any network's
472 // main site.
473 $this->cache = array();
474
475 $flush_number = $this->new_flush_number();
476
477 $this->rotate_site_keys( $flush_number );
478
479 if ( function_exists( 'is_main_site' ) && is_main_site() ) {
480 $this->rotate_global_keys( $flush_number );
481 }
482
483 return true;
484 }
485
486 function rotate_site_keys( $flush_number = null ) {
487 if ( is_null( $flush_number ) ) {
488 $flush_number = $this->new_flush_number();
489 }
490
491 $this->set_flush_number( $flush_number, $this->flush_group );
492
493 $this->flush_number[ $this->blog_prefix ] = $flush_number;
494 }
495
496 function rotate_global_keys( $flush_number = null ) {
497 if ( is_null( $flush_number ) ) {
498 $flush_number = $this->new_flush_number();
499 }
500
501 $this->set_flush_number( $flush_number, $this->global_flush_group );
502
503 $this->global_flush_number = $flush_number;
504 }
505
506 function new_flush_number() {
507 return intval( microtime( true ) * 1e6 );
508 }
509
510 function get( $id, $group = 'default', $force = false, &$found = null ) {
511 $key = $this->key( $id, $group );
512 $mc = $this->get_mc( $group );
513 $found = true;
514
515 if ( isset( $this->cache[ $key ] ) && ( ! $force || in_array( $group, $this->no_mc_groups ) ) ) {
516 if ( isset( $this->cache[ $key ][ 'value' ] ) && is_object( $this->cache[ $key ][ 'value' ] ) ) {
517 $value = clone $this->cache[ $key ][ 'value' ];
518 } else {
519 $value = $this->cache[ $key ][ 'value' ];
520 }
521 $found = $this->cache[ $key ][ 'found' ];
522
523 $this->group_ops_stats( 'get_local', $key, $group, null, null, 'local' );
524 } else if ( in_array( $group, $this->no_mc_groups ) ) {
525 $this->cache[ $key ] = [
526 'value' => $value = false,
527 'found' => false,
528 ];
529
530 $found = false;
531
532 $this->group_ops_stats( 'get_local', $key, $group, null, null, 'not_in_local' );
533 } else {
534 $flags = false;
535 $this->timer_start();
536 $value = $mc->get( $key, $flags );
537 $elapsed = $this->timer_stop();
538
539 // Value will be unchanged if the key doesn't exist.
540 if ( false === $flags ) {
541 $found = false;
542 $value = false;
543 } elseif ( false === $value && ( $flags & 0xFF01 ) === 0x01 ) {
544 /*
545 * The lowest byte is used for flags.
546 * 0x01 means the value is serialized (MMC_SERIALIZED).
547 * The second lowest indicates data type: 0 is string, 1 is bool, 3 is long, 7 is double.
548 * `null` is serialized into a string, thus $flags is 0x0001
549 * Empty string will correspond to $flags = 0x0000 (not serialized).
550 * For `false` or `true` $flags will be 0x0100
551 *
552 * See:
553 * - https://github.com/websupport-sk/pecl-memcache/blob/2a5de3c5d9c0bd0acbcf7e6e0b7570f15f89f55b/php7/memcache_pool.h#L61-L76 - PHP 7.x
554 * - https://github.com/websupport-sk/pecl-memcache/blob/ccf702b14b18fce18a1863e115a7b4c964df952e/src/memcache_pool.h#L57-L76 - PHP 8.x
555 *
556 * In PHP 8, they changed the way memcache_get() handles `null`:
557 * https://github.com/websupport-sk/pecl-memcache/blob/ccf702b14b18fce18a1863e115a7b4c964df952e/src/memcache.c#L2175-L2177
558 *
559 * If the return value is `null`, it is silently converted to `false`. We can only rely upon $flags to find out whether `false` is real.
560 */
561 $value = null;
562 }
563
564 $this->cache[ $key ] = [
565 'value' => $value,
566 'found' => $found,
567 ];
568
569 if ( is_null( $value ) || $value === false ) {
570 $this->group_ops_stats( 'get', $key, $group, null, $elapsed, 'not_in_memcache' );
571 } else if ( 'checkthedatabaseplease' === $value ) {
572 $this->group_ops_stats( 'get', $key, $group, null, $elapsed, 'checkthedatabaseplease' );
573 } else {
574 $size = $this->get_data_size( $value );
575 $this->group_ops_stats( 'get', $key, $group, $size, $elapsed, 'memcache' );
576 }
577 }
578
579 if ( 'checkthedatabaseplease' === $value ) {
580 unset( $this->cache[ $key ] );
581
582 $found = false;
583 $value = false;
584 }
585
586 return $value;
587 }
588
589 function get_multi( $groups ) {
590 /*
591 format: $get['group-name'] = array( 'key1', 'key2' );
592 */
593 $return = array();
594 $return_cache = array(
595 'value' => false,
596 'found' => false,
597 );
598
599 foreach ( $groups as $group => $ids ) {
600 $mc = $this->get_mc( $group );
601 $keys = array();
602 $this->timer_start();
603
604 foreach ( $ids as $id ) {
605 $key = $this->key( $id, $group );
606 $keys[] = $key;
607
608 if ( isset( $this->cache[ $key ] ) ) {
609 if ( is_object( $this->cache[ $key ][ 'value'] ) ) {
610 $return[ $key ] = clone $this->cache[ $key ][ 'value'];
611 $return_cache[ $key ] = [
612 'value' => clone $this->cache[ $key ][ 'value'],
613 'found' => $this->cache[ $key ][ 'found'],
614 ];
615 } else {
616 $return[ $key ] = $this->cache[ $key ][ 'value'];
617 $return_cache[ $key ] = [
618 'value' => $this->cache[ $key ][ 'value' ],
619 'found' => $this->cache[ $key ][ 'found' ],
620 ];
621 }
622
623 continue;
624 } else if ( in_array( $group, $this->no_mc_groups ) ) {
625 $return[ $key ] = false;
626 $return_cache[ $key ] = [
627 'value' => false,
628 'found' => false,
629 ];
630
631 continue;
632 } else {
633 $fresh_get = $mc->get( $key );
634 $return[ $key ] = $fresh_get;
635 $return_cache[ $key ] = [
636 'value' => $fresh_get,
637 'found' => false !== $fresh_get,
638 ];
639 }
640 }
641
642 $elapsed = $this->timer_stop();
643 $this->group_ops_stats( 'get_multi', $keys, $group, null, $elapsed );
644 }
645
646 $this->cache = array_merge( $this->cache, $return_cache );
647
648 return $return;
649 }
650
651 public function get_multiple( $keys, $group = 'default', $force = false ) {
652 $mc = $this->get_mc( $group );
653
654 $no_mc = in_array( $group, $this->no_mc_groups, true );
655
656 $uncached_keys = array();
657 $return = array();
658 $return_cache = array();
659
660 foreach ( $keys as $id ) {
661 $key = $this->key( $id, $group );
662
663 if ( isset( $this->cache[ $key ] ) && ( ! $force || $no_mc ) ) {
664 $value = $this->cache[ $key ]['found'] ? $this->cache[ $key ]['value'] : false;
665 $return[ $id ] = is_object( $value ) ? clone $value : $value;
666 } else if ( $no_mc ) {
667 $return[ $id ] = false;
668 $return_cache[ $key ] = [
669 'value' => false,
670 'found' => false,
671 ];
672 } else {
673 $uncached_keys[ $id ] = $key;
674 }
675 }
676
677 if ( $uncached_keys ) {
678 $this->timer_start();
679 $uncached_keys_list = array_values( $uncached_keys );
680 $values = $mc->get( $uncached_keys_list );
681 $elapsed = $this->timer_stop();
682
683 $this->group_ops_stats( 'get_multiple', $uncached_keys_list, $group, null, $elapsed );
684
685 foreach ( $uncached_keys as $id => $key ) {
686 $found = array_key_exists( $key, $values );
687 $value = $found ? $values[ $key ] : false;
688
689 $return[ $id ] = $value;
690 $return_cache[ $key ] = [
691 'value' => $value,
692 'found' => $found,
693 ];
694 }
695 }
696
697 $this->cache = array_merge( $this->cache, $return_cache );
698
699 return $return;
700 }
701
702 function flush_prefix( $group ) {
703 if ( $group === $this->flush_group || $group === $this->global_flush_group ) {
704 // Never flush the flush numbers.
705 $number = '_';
706 } elseif ( false !== array_search( $group, $this->global_groups ) ) {
707 $number = $this->get_global_flush_number();
708 } else {
709 $number = $this->get_blog_flush_number();
710 }
711 return $number . ':';
712 }
713
714 function key( $key, $group ) {
715 if ( empty( $group ) ) {
716 $group = 'default';
717 }
718
719 $prefix = $this->key_salt;
720
721 $prefix .= $this->flush_prefix( $group );
722
723 if ( false !== array_search( $group, $this->global_groups ) ) {
724 $prefix .= $this->global_prefix;
725 } else {
726 $prefix .= $this->blog_prefix;
727 }
728
729 return preg_replace( '/\s+/', '', "$prefix:$group:$key" );
730 }
731
732 function replace( $id, $data, $group = 'default', $expire = 0 ) {
733 $key = $this->key( $id, $group );
734 $expire = intval( $expire );
735 if ( 0 === $expire || $expire > $this->max_expiration ) {
736 $expire = $this->default_expiration;
737 }
738 $mc = $this->get_mc( $group );
739
740 if ( is_object( $data ) ) {
741 $data = clone $data;
742 }
743
744 $size = $this->get_data_size( $data );
745 $this->timer_start();
746 $result = $mc->replace( $key, $data, false, $expire );
747 $elapsed = $this->timer_stop();
748 $this->group_ops_stats( 'replace', $key, $group, $size, $elapsed );
749
750 if ( false !== $result ) {
751 $this->cache[ $key ] = [
752 'value' => $data,
753 'found' => true,
754 ];
755 }
756
757 return $result;
758 }
759
760 function set( $id, $data, $group = 'default', $expire = 0 ) {
761 $key = $this->key( $id, $group );
762
763 if ( isset( $this->cache[ $key ] ) && ( 'checkthedatabaseplease' === $this->cache[ $key ][ 'value' ] ) ) {
764 return false;
765 }
766
767 if ( is_object( $data ) ) {
768 $data = clone $data;
769 }
770
771 $this->cache[ $key ] = [
772 'value' => $data,
773 'found' => false, // Set to false as not technically found in memcache at this point.
774 ];
775
776 if ( in_array( $group, $this->no_mc_groups ) ) {
777 $this->group_ops_stats( 'set_local', $key, $group, null, null );
778
779 return true;
780 }
781
782 $expire = intval( $expire );
783 if ( 0 === $expire || $expire > $this->max_expiration ) {
784 $expire = $this->default_expiration;
785 }
786
787 $mc = $this->get_mc( $group );
788
789 $size = $this->get_data_size( $data );
790 $this->timer_start();
791 $result = $mc->set( $key, $data, false, $expire );
792 $elapsed = $this->timer_stop();
793 $this->group_ops_stats( 'set', $key, $group, $size, $elapsed );
794
795 // Update the found cache value with the result of the set in memcache.
796 $this->cache[ $key ][ 'found' ] = $result;
797
798 return $result;
799 }
800
801 public function set_multiple( array $data, $group = '', $expire = 0 ) {
802 $values = array();
803
804 foreach ( $data as $key => $value ) {
805 $values[ $key ] = $this->set( $key, $value, $group, $expire );
806 }
807
808 return $values;
809 }
810
811 function switch_to_blog( $blog_id ) {
812 global $table_prefix;
813
814 $blog_id = (int) $blog_id;
815
816 $this->blog_prefix = ( is_multisite() ? $blog_id : $table_prefix );
817 }
818
819 function colorize_debug_line( $line, $trailing_html = '' ) {
820 $colors = array(
821 'get' => 'green',
822 'get_local' => 'lightgreen',
823 'get_multi' => 'fuchsia',
824 'get_multiple' => 'navy',
825 'set' => 'purple',
826 'set_local' => 'orchid',
827 'add' => 'blue',
828 'delete' => 'red',
829 'delete_local' => 'tomato',
830 'slow-ops' => 'crimson',
831 );
832
833 $cmd = substr( $line, 0, strpos( $line, ' ' ) );
834
835 // Start off with a neutral default color...
836 $color_for_cmd = 'brown';
837 // And if the cmd has a specific color, use that instead
838 if ( isset( $colors[ $cmd ] ) ) {
839 $color_for_cmd = $colors[ $cmd ];
840 }
841
842 $cmd2 = "<span style='color:" . esc_attr( $color_for_cmd ) . "; font-weight: bold;'>" . esc_html( $cmd ) . "</span>";
843
844 return $cmd2 . esc_html( substr( $line, strlen( $cmd ) ) ) . "$trailing_html\n";
845 }
846
847 function js_toggle() {
848 echo "
849 <script>
850 function memcachedToggleVisibility( id, hidePrefix ) {
851 var element = document.getElementById( id );
852 if ( ! element ) {
853 return;
854 }
855
856 // Hide all element with `hidePrefix` if given. Used to display only one element at a time.
857 if ( hidePrefix ) {
858 var groupStats = document.querySelectorAll( '[id^=\"' + hidePrefix + '\"]' );
859 groupStats.forEach(
860 function ( element ) {
861 element.style.display = 'none';
862 }
863 );
864 }
865
866 // Toggle the one we clicked.
867 if ( 'none' === element.style.display ) {
868 element.style.display = 'block';
869 } else {
870 element.style.display = 'none';
871 }
872 }
873 </script>
874 ";
875 }
876
877 /**
878 * Returns the collected raw stats.
879 *
880 * @return array $stats
881 */
882 function get_stats() {
883 $stats = [];
884 $stats['totals'] = [
885 'query_time' => $this->time_total,
886 'size' => $this->size_total,
887 ];
888 $stats['operation_counts'] = $this->stats;
889 $stats['operations'] = [];
890 $stats['groups'] = [];
891 $stats['slow-ops'] = [];
892 $stats['slow-ops-groups'] = [];
893 foreach ( $this->group_ops as $cache_group => $dataset ) {
894 if ( empty( $cache_group ) ) {
895 $cache_group = 'default';
896 }
897
898 foreach ( $dataset as $data ) {
899 $operation = $data[0];
900 $op = [
901 'key' => $data[1],
902 'size' => $data[2],
903 'time' => $data[3],
904 'group' => $cache_group,
905 'result' => $data[4],
906 ];
907
908 if ( $cache_group === 'slow-ops' ) {
909 $key = 'slow-ops';
910 $groups_key = 'slow-ops-groups';
911 $op['group'] = $data[5];
912 $op['backtrace'] = $data[6];
913 } else {
914 $key = 'operations';
915 $groups_key = 'groups';
916 }
917
918 $stats[ $key ][ $operation ][] = $op;
919
920 if ( ! in_array( $op['group'], $stats[ $groups_key ] ) ) {
921 $stats[ $groups_key ][] = $op['group'];
922 }
923 }
924 }
925
926 return $stats;
927 }
928
929
930 function stats() {
931 $this->js_toggle();
932
933 echo '<h2><span>Total memcache query time:</span>' . number_format_i18n( sprintf( '%0.1f', $this->time_total * 1000 ), 1 ) . ' ms</h2>';
934 echo "\n";
935 echo '<h2><span>Total memcache size:</span>' . esc_html( size_format( $this->size_total, 2 ) ) . '</h2>';
936 echo "\n";
937
938 foreach ( $this->stats as $stat => $n ) {
939 if ( empty( $n ) ) {
940 continue;
941 }
942
943 echo '<h2>';
944 echo $this->colorize_debug_line( "$stat $n" );
945 echo '</h2>';
946 }
947
948 echo "<ul class='debug-menu-links' style='clear:left;font-size:14px;'>\n";
949 $groups = array_keys( $this->group_ops );
950 usort( $groups, 'strnatcasecmp' );
951
952 $active_group = $groups[0];
953 // Always show `slow-ops` first
954 if ( in_array( 'slow-ops', $groups ) ) {
955 $slow_ops_key = array_search( 'slow-ops', $groups );
956 $slow_ops = $groups[ $slow_ops_key ];
957 unset( $groups[ $slow_ops_key ] );
958 array_unshift( $groups, $slow_ops );
959 $active_group = 'slow-ops';
960 }
961
962 $total_ops = 0;
963 $group_titles = array();
964 foreach ( $groups as $group ) {
965 $group_name = $group;
966 if ( empty( $group_name ) ) {
967 $group_name = 'default';
968 }
969 $group_ops = count( $this->group_ops[ $group ] );
970 $group_size = size_format( array_sum( array_map( function ( $op ) { return $op[2]; }, $this->group_ops[ $group ] ) ), 2 );
971 $group_time = number_format_i18n( sprintf( '%0.1f', array_sum( array_map( function ( $op ) { return $op[3]; }, $this->group_ops[ $group ] ) ) * 1000 ), 1 );
972 $total_ops += $group_ops;
973 $group_title = "{$group_name} [$group_ops][$group_size][{$group_time} ms]";
974 $group_titles[ $group ] = $group_title;
975 echo "\t<li><a href='#' onclick='memcachedToggleVisibility( \"object-cache-stats-menu-target-" . esc_js( $group_name ) . "\", \"object-cache-stats-menu-target-\" );'>" . esc_html( $group_title ) . "</a></li>\n";
976 }
977 echo "</ul>\n";
978
979 echo "<div id='object-cache-stats-menu-targets'>\n";
980 foreach ( $groups as $group ) {
981 $group_name = $group;
982 if ( empty( $group_name ) ) {
983 $group_name = 'default';
984 }
985 $current = $active_group == $group ? 'style="display: block"' : 'style="display: none"';
986 echo "<div id='object-cache-stats-menu-target-" . esc_attr( $group_name ) . "' class='object-cache-stats-menu-target' $current>\n";
987 echo '<h3>' . esc_html( $group_titles[ $group ] ) . '</h3>' . "\n";
988 echo "<pre>\n";
989 foreach ( $this->group_ops[ $group ] as $index => $arr ) {
990 printf( '%3d ', $index );
991 echo $this->get_group_ops_line( $index, $arr );
992 }
993 echo "</pre>\n";
994 echo "</div>";
995 }
996
997 echo "</div>";
998 }
999
1000 function get_group_ops_line( $index, $arr ) {
1001 // operation
1002 $line = "{$arr[0]} ";
1003
1004 // key
1005 $json_encoded_key = json_encode( $arr[1] );
1006 $line .= $json_encoded_key . " ";
1007
1008 // comment
1009 if ( ! empty( $arr[4] ) ) {
1010 $line .= "{$arr[4]} ";
1011 }
1012
1013 // size
1014 if ( isset( $arr[2] ) ) {
1015 $line .= '(' . size_format( $arr[2], 2 ) . ') ';
1016 }
1017
1018 // time
1019 if ( isset( $arr[3] ) ) {
1020 $line .= '(' . number_format_i18n( sprintf( '%0.1f', $arr[3] * 1000 ), 1 ) . ' ms)';
1021 }
1022
1023 // backtrace
1024 $bt_link = '';
1025 if ( isset( $arr[6] ) ) {
1026 $key_hash = md5( $index . $json_encoded_key );
1027 $bt_link = " <small><a href='#' onclick='memcachedToggleVisibility( \"object-cache-stats-debug-$key_hash\" );'>Toggle Backtrace</a></small>";
1028 $bt_link .= "<pre id='object-cache-stats-debug-$key_hash' style='display:none'>" . esc_html( $arr[6] ) . "</pre>";
1029 }
1030
1031 return $this->colorize_debug_line( $line, $bt_link );
1032 }
1033
1034 /**
1035 * @param int|string $group
1036 * @return Memcache
1037 */
1038 function get_mc( $group ) {
1039 if ( isset( $this->mc[ $group ] ) ) {
1040 return $this->mc[ $group ];
1041 }
1042
1043 return $this->mc['default'];
1044 }
1045
1046 function failure_callback( $host, $port ) {
1047 $this->connection_errors[] = array(
1048 'host' => $host,
1049 'port' => $port,
1050 );
1051 }
1052
1053 function salt_keys( $key_salt ) {
1054 if ( strlen( $key_salt ) ) {
1055 $this->key_salt = $key_salt . ':';
1056 } else {
1057 $this->key_salt = '';
1058 }
1059 }
1060
1061 function __construct() {
1062 global $memcached_servers;
1063
1064 if ( isset( $memcached_servers ) ) {
1065 $buckets = $memcached_servers;
1066 } else {
1067 $buckets = array( '127.0.0.1:11211' );
1068 }
1069
1070 reset( $buckets );
1071
1072 if ( is_int( key( $buckets ) ) ) {
1073 $buckets = array( 'default' => $buckets );
1074 }
1075
1076 foreach ( $buckets as $bucket => $servers ) {
1077 $this->mc[ $bucket ] = new Memcache();
1078
1079 foreach ( $servers as $i => $server ) {
1080 if ( 'unix://' == substr( $server, 0, 7 ) ) {
1081 $node = $server;
1082 $port = 0;
1083 } else {
1084 if ( false === strpos( $server, ':' ) ) {
1085 $node = $server;
1086 $port = ini_get( 'memcache.default_port' );
1087 } else {
1088 list ( $node, $port ) = explode( ':', $server, 2 );
1089 }
1090
1091 $port = intval( $port );
1092
1093 if ( ! $port ) {
1094 $port = 11211;
1095 }
1096 }
1097
1098 $this->mc[ $bucket ]->addServer( $node, $port, true, 1, 1, 15, true, array( $this, 'failure_callback' ) );
1099 $this->mc[ $bucket ]->setCompressThreshold( 20000, 0.2 );
1100
1101 // Prepare individual connections to servers in default bucket for flush_number redundancy
1102 if ( 'default' === $bucket ) {
1103 $this->default_mcs[ $i ] = new Memcache();
1104 $this->default_mcs[ $i ]->addServer( $node, $port, true, 1, 1, 15, true, array( $this, 'failure_callback' ) );
1105 }
1106 }
1107 }
1108
1109 global $blog_id, $table_prefix;
1110
1111 $this->global_prefix = '';
1112 $this->blog_prefix = '';
1113
1114 if ( function_exists( 'is_multisite' ) ) {
1115 $this->global_prefix = ( is_multisite() || defined( 'CUSTOM_USER_TABLE' ) && defined( 'CUSTOM_USER_META_TABLE' ) ) ? '' : $table_prefix;
1116 $this->blog_prefix = ( is_multisite() ? $blog_id : $table_prefix );
1117 }
1118
1119 $this->salt_keys( WP_CACHE_KEY_SALT );
1120
1121 $this->cache_hits =& $this->stats['get'];
1122 $this->cache_misses =& $this->stats['add'];
1123 }
1124
1125 function increment_stat( $field, $num = 1 ) {
1126 if ( ! isset( $this->stats[ $field ] ) ) {
1127 $this->stats[ $field ] = $num;
1128 } else {
1129 $this->stats[ $field ] += $num;
1130 }
1131 }
1132
1133 function group_ops_stats( $op, $keys, $group, $size, $time, $comment = '' ) {
1134 $this->increment_stat( $op );
1135
1136 // we have no use of the local ops details for now
1137 if ( strpos( $op, '_local' ) ) {
1138 return;
1139 }
1140
1141 $this->size_total += $size;
1142
1143 $keys = $this->strip_memcached_keys( $keys );
1144
1145 // @codeCoverageIgnoreStart
1146 if ( $time > $this->slow_op_microseconds && 'get_multi' !== $op ) {
1147 $this->increment_stat( 'slow-ops' );
1148 $backtrace = null;
1149 if ( function_exists( 'wp_debug_backtrace_summary' ) ) {
1150 $backtrace = wp_debug_backtrace_summary();
1151 }
1152 $this->group_ops['slow-ops'][] = array( $op, $keys, $size, $time, $comment, $group, $backtrace );
1153 }
1154 // @codeCoverageIgnoreEnd
1155
1156 $this->group_ops[ $group ][] = array( $op, $keys, $size, $time, $comment );
1157 }
1158
1159 /**
1160 * Key format: key_salt:flush_number:table_prefix:key_name
1161 *
1162 * We want to strip the `key_salt:flush_number` part to not leak the memcached keys.
1163 * If `key_salt` is set we strip `'key_salt:flush_number`, otherwise just strip the `flush_number` part.
1164 */
1165 function strip_memcached_keys( $keys ) {
1166 if ( ! is_array( $keys ) ) {
1167 $keys = [ $keys ];
1168 }
1169
1170 foreach ( $keys as $key => $value ) {
1171 $offset = 0;
1172 if ( ! empty( $this->key_salt ) ) {
1173 $offset = strpos( $value, ':' ) + 1;
1174 }
1175
1176 $start = strpos( $value, ':', $offset );
1177 $keys[ $key ] = substr( $value, $start + 1 );
1178 }
1179
1180 if ( 1 === count( $keys ) ) {
1181 return $keys[0];
1182 }
1183
1184 return $keys;
1185 }
1186
1187 function timer_start() {
1188 $this->time_start = microtime( true );
1189
1190 return true;
1191 }
1192
1193 function timer_stop() {
1194 $time_total = microtime( true ) - $this->time_start;
1195 $this->time_total += $time_total;
1196
1197 return $time_total;
1198 }
1199
1200 function get_data_size( $data ) {
1201 if ( is_string( $data ) ) {
1202 return strlen( $data );
1203 }
1204
1205 $serialized = serialize( $data );
1206
1207 return strlen( $serialized );
1208 }
1209 }
1210
1211 endif;
1212