PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.6
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.6
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / includes / object-cache.php

object-cache.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.0.6, at includes/object-cache.php

749 lines 25.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * XSPEED_OBJECT_CACHE_DROPIN
4 *
5 * xSpeed's self-contained persistent object cache drop-in.
6 *
7 * Supports Redis (phpredis extension, with a graceful no-op fall-through when
8 * absent) and Memcached. Implements the full WordPress object-cache API as a
9 * global WP_Object_Cache class + wp_cache_* functions.
10 *
11 * Design principles:
12 * - NEVER fatal the site. If the backend can't be reached, we degrade to a
13 * non-persistent in-request array cache. A misconfigured Redis must never
14 * take a site down — that's why connection_timeout defaults low.
15 * - Read config from constants written by xSpeed into wp-config.php
16 * (XSPEED_OC_*), falling back to the widely-used WP_REDIS_* conventions so
17 * existing setups keep working.
18 * - WP-compliant: groups, global groups, multisite blog-id prefixing,
19 * add/get/set/delete/incr/decr/replace, flush, get_multiple, add_multiple.
20 *
21 * This file is copied to wp-content/object-cache.php by xSpeed when the user
22 * clicks "Enable Object Cache". It is loaded by WordPress very early
23 * (wp-settings.php), before most of core — so it must be self-sufficient.
24 *
25 * @package XSpeed
26 */
27
28 defined( 'ABSPATH' ) || exit;
29
30 // -----------------------------------------------------------------------------
31 // Config resolution. Prefer xSpeed's own XSPEED_OC_* constants; fall back to the
32 // de-facto WP_REDIS_* / $memcached_servers conventions so we interoperate.
33 // -----------------------------------------------------------------------------
34 if ( ! function_exists( 'xspeed_oc_config' ) ) {
35 /**
36 * Resolve a single config value from constants with sane defaults.
37 */
38 function xspeed_oc_config( $key, $default ) {
39 $map = array(
40 'backend' => array( 'XSPEED_OC_BACKEND' ),
41 'host' => array( 'XSPEED_OC_HOST', 'WP_REDIS_HOST' ),
42 'port' => array( 'XSPEED_OC_PORT', 'WP_REDIS_PORT' ),
43 'password' => array( 'XSPEED_OC_PASSWORD', 'WP_REDIS_PASSWORD' ),
44 'database' => array( 'XSPEED_OC_DATABASE', 'WP_REDIS_DATABASE' ),
45 'timeout' => array( 'XSPEED_OC_TIMEOUT', 'WP_REDIS_TIMEOUT' ),
46 'salt' => array( 'XSPEED_OC_SALT', 'WP_CACHE_KEY_SALT' ),
47 'persist' => array( 'XSPEED_OC_PERSISTENT', 'WP_REDIS_PERSISTENT' ),
48 );
49 if ( isset( $map[ $key ] ) ) {
50 foreach ( $map[ $key ] as $const ) {
51 if ( defined( $const ) ) {
52 return constant( $const );
53 }
54 }
55 }
56 return $default;
57 }
58 }
59
60 // -----------------------------------------------------------------------------
61 // WordPress object-cache API surface. Thin wrappers over the global instance.
62 // -----------------------------------------------------------------------------
63 if ( ! function_exists( 'wp_cache_init' ) ) {
64
65 function wp_cache_init() {
66 $GLOBALS['wp_object_cache'] = new XSpeed_Object_Cache();
67 }
68
69 function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
70 return $GLOBALS['wp_object_cache']->add( $key, $data, $group, (int) $expire );
71 }
72
73 function wp_cache_add_multiple( array $data, $group = '', $expire = 0 ) {
74 $out = array();
75 foreach ( $data as $key => $value ) {
76 $out[ $key ] = wp_cache_add( $key, $value, $group, $expire );
77 }
78 return $out;
79 }
80
81 function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
82 return $GLOBALS['wp_object_cache']->replace( $key, $data, $group, (int) $expire );
83 }
84
85 function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
86 return $GLOBALS['wp_object_cache']->set( $key, $data, $group, (int) $expire );
87 }
88
89 function wp_cache_set_multiple( array $data, $group = '', $expire = 0 ) {
90 $out = array();
91 foreach ( $data as $key => $value ) {
92 $out[ $key ] = wp_cache_set( $key, $value, $group, $expire );
93 }
94 return $out;
95 }
96
97 function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
98 return $GLOBALS['wp_object_cache']->get( $key, $group, $force, $found );
99 }
100
101 function wp_cache_get_multiple( $keys, $group = '', $force = false ) {
102 return $GLOBALS['wp_object_cache']->get_multiple( $keys, $group, $force );
103 }
104
105 function wp_cache_delete( $key, $group = '' ) {
106 return $GLOBALS['wp_object_cache']->delete( $key, $group );
107 }
108
109 function wp_cache_delete_multiple( array $keys, $group = '' ) {
110 $out = array();
111 foreach ( $keys as $key ) {
112 $out[ $key ] = wp_cache_delete( $key, $group );
113 }
114 return $out;
115 }
116
117 function wp_cache_incr( $key, $offset = 1, $group = '' ) {
118 return $GLOBALS['wp_object_cache']->incr( $key, (int) $offset, $group );
119 }
120
121 function wp_cache_decr( $key, $offset = 1, $group = '' ) {
122 return $GLOBALS['wp_object_cache']->decr( $key, (int) $offset, $group );
123 }
124
125 function wp_cache_flush() {
126 return $GLOBALS['wp_object_cache']->flush();
127 }
128
129 function wp_cache_flush_runtime() {
130 return $GLOBALS['wp_object_cache']->flush_runtime();
131 }
132
133 function wp_cache_flush_group( $group ) {
134 return $GLOBALS['wp_object_cache']->flush_group( $group );
135 }
136
137 function wp_cache_supports( $feature ) {
138 return in_array( $feature, array( 'get_multiple', 'set_multiple', 'add_multiple', 'delete_multiple', 'flush_runtime', 'flush_group' ), true );
139 }
140
141 function wp_cache_close() {
142 return $GLOBALS['wp_object_cache']->close();
143 }
144
145 function wp_cache_add_global_groups( $groups ) {
146 $GLOBALS['wp_object_cache']->add_global_groups( $groups );
147 }
148
149 function wp_cache_add_non_persistent_groups( $groups ) {
150 $GLOBALS['wp_object_cache']->add_non_persistent_groups( $groups );
151 }
152
153 function wp_cache_switch_to_blog( $blog_id ) {
154 $GLOBALS['wp_object_cache']->switch_to_blog( (int) $blog_id );
155 }
156
157 function wp_cache_reset() {
158 // Deprecated in core; kept for back-compat.
159 return $GLOBALS['wp_object_cache']->flush_runtime();
160 }
161 }
162
163 // -----------------------------------------------------------------------------
164 // The cache implementation.
165 // -----------------------------------------------------------------------------
166 if ( ! class_exists( 'XSpeed_Object_Cache' ) ) {
167
168 class XSpeed_Object_Cache {
169
170 /** @var array In-request cache (always populated; also the fallback store). */
171 private $cache = array();
172
173 /** @var \Redis|\Memcached|null Persistent backend handle, or null when degraded. */
174 private $conn = null;
175
176 /** @var string redis|memcached */
177 private $backend = 'redis';
178
179 /** @var string Concrete client driving a Redis backend: phpredis|builtin. */
180 private $client = 'phpredis';
181
182 /** @var bool True once a persistent backend is connected. */
183 private $persistent = false;
184
185 /**
186 * @var bool True when the drop-in is active but could NOT connect a
187 * persistent backend, so it's silently serving a non-persistent
188 * in-request cache. Surfaced so the dashboard can report "degraded"
189 * instead of implying object caching is healthy. (FBS-82210)
190 */
191 public $degraded = false;
192
193 /** @var string Key salt / prefix. */
194 private $salt = '';
195
196 /** @var int Current blog id (multisite prefixing). */
197 private $blog_prefix = 0;
198
199 /** @var bool */
200 private $multisite = false;
201
202 /** @var array<string,bool> Groups shared across the whole network. */
203 private $global_groups = array();
204
205 /** @var array<string,bool> Groups that must never hit the persistent store. */
206 private $non_persistent_groups = array();
207
208 /** @var int Cache hits this request. */
209 public $cache_hits = 0;
210
211 /** @var int Cache misses this request. */
212 public $cache_misses = 0;
213
214 public function __construct() {
215 $this->multisite = function_exists( 'is_multisite' ) && is_multisite();
216 $this->blog_prefix = $this->multisite ? (int) get_current_blog_id() : 0;
217 $this->salt = (string) xspeed_oc_config( 'salt', '' );
218 $this->backend = strtolower( (string) xspeed_oc_config( 'backend', 'redis' ) );
219
220 // Core groups that must never be served from the shared
221 // persistent backend, or a stale copy can resurrect data WP
222 // just wrote. The critical one is `options`: WordPress keeps
223 // `active_plugins` inside the autoloaded `alloptions` blob, so
224 // a stale persistent `alloptions` makes a just-deactivated
225 // plugin read as still active (the write succeeds, the
226 // verify-read sees the old blob) — plugins then "can't be
227 // deactivated". These groups load once per request from the DB
228 // (still fast), kept per-request only. Same default set Redis
229 // Object Cache and friends ship.
230 $this->add_non_persistent_groups(
231 array( 'options', 'site-options', 'comment', 'counts', 'plugins', 'themes' )
232 );
233
234 $this->connect();
235 }
236
237 // --- Connection -----------------------------------------------------
238
239 private function connect() {
240 $timeout = (float) xspeed_oc_config( 'timeout', 1 );
241 try {
242 if ( 'memcached' === $this->backend ) {
243 $host = (string) xspeed_oc_config( 'host', '127.0.0.1' );
244 $port = (int) xspeed_oc_config( 'port', 11211 );
245
246 if ( class_exists( 'Memcached' ) ) {
247 // ext/memcached (preferred).
248 $this->client = 'ext-memcached';
249 $mc = new Memcached();
250 $mc->addServer( $host, $port );
251 $mc->setOption( Memcached::OPT_CONNECT_TIMEOUT, (int) ( $timeout * 1000 ) );
252 $stats = @$mc->getStats();
253 if ( is_array( $stats ) && ! empty( $stats ) ) {
254 $this->conn = $mc;
255 $this->persistent = true;
256 }
257 } elseif ( $this->load_builtin_memcached() ) {
258 // xSpeed's own pure-PHP Memcached client.
259 $this->client = 'builtin-memcached';
260 $mc = new \XSpeed\Memcached_Client( $host, $port, $timeout );
261 if ( $mc->connect() && false !== $mc->version() ) {
262 $this->conn = $mc;
263 $this->persistent = true;
264 }
265 }
266 } else {
267 $this->backend = 'redis';
268 $host = (string) xspeed_oc_config( 'host', '127.0.0.1' );
269 $port = (int) xspeed_oc_config( 'port', 6379 );
270 $pass = (string) xspeed_oc_config( 'password', '' );
271 $db = (int) xspeed_oc_config( 'database', 0 );
272 $persist = (bool) xspeed_oc_config( 'persist', false );
273
274 if ( class_exists( 'Redis' ) ) {
275 // phpredis extension (preferred).
276 $this->client = 'phpredis';
277 $redis = new Redis();
278 $ok = $persist
279 ? @$redis->pconnect( $host, $port, $timeout )
280 : @$redis->connect( $host, $port, $timeout );
281 if ( $ok ) {
282 if ( '' !== $pass ) {
283 @$redis->auth( $pass );
284 }
285 if ( $db > 0 ) {
286 @$redis->select( $db );
287 }
288 if ( '+PONG' === @$redis->ping() || true === @$redis->ping() ) {
289 $this->conn = $redis;
290 $this->persistent = true;
291 }
292 }
293 } elseif ( $this->load_builtin_client() ) {
294 // xSpeed's own pure-PHP client (no extension, no library).
295 $this->client = 'builtin';
296 $rc = new \XSpeed\Redis_Client( $host, $port, (float) $timeout, $persist );
297 if ( $rc->connect() ) {
298 if ( '' !== $pass ) {
299 $rc->auth( $pass );
300 }
301 if ( $db > 0 ) {
302 $rc->select( $db );
303 }
304 $pong = $rc->ping();
305 if ( is_string( $pong ) && false !== stripos( $pong, 'PONG' ) ) {
306 $this->conn = $rc;
307 $this->persistent = true;
308 }
309 }
310 }
311 }
312 } catch ( \Throwable $e ) {
313 // Any failure → stay in non-persistent mode. Never fatal.
314 $this->conn = null;
315 $this->persistent = false;
316 }
317
318 // Connecting to an unreachable/unresolvable backend (e.g.
319 // `Redis::pconnect()` → "getaddrinfo for redis failed", or
320 // `stream_socket_client()` in our builtin clients) emits a PHP
321 // warning. We `@`-suppress those above and degrade gracefully to a
322 // non-persistent cache — but the warning still lingers in
323 // `error_get_last()`. WP reads that at `admin_body_class` time and
324 // tags every admin page `php-error`, which renders an empty banner
325 // above the admin menu even though nothing is actually broken.
326 //
327 // Clear it so a degraded-but-handled backend doesn't masquerade as
328 // a site error — but ONLY when the lingering error is OUR connect
329 // warning. We never blindly wipe the slot: matching on the
330 // originating file (this drop-in, or our bundled socket clients)
331 // guarantees we can't swallow an unrelated warning that happened to
332 // land in error_get_last() first. This does NOT touch the error
333 // LOG — if WP_DEBUG_LOG is on, PHP already wrote the warning to
334 // debug.log before this runs, and the explicit "NOT persisting"
335 // diagnostic below is the signal meant for humans.
336 if ( function_exists( 'error_clear_last' ) ) {
337 $last = error_get_last();
338 if ( is_array( $last ) && isset( $last['file'] ) ) {
339 $file = $last['file'];
340 if ( __FILE__ === $file
341 || false !== strpos( $file, 'class-redis-client.php' )
342 || false !== strpos( $file, 'class-memcached-client.php' )
343 ) {
344 error_clear_last();
345 }
346 }
347 }
348
349 // The drop-in is installed (we're running), so if we didn't manage
350 // to connect a persistent backend, object caching is effectively
351 // doing nothing — writes succeed but evaporate at request end.
352 // Flag it so detect()/the dashboard can report "degraded" instead
353 // of a false-healthy state, and log once per request so the failure
354 // is diagnosable rather than silent. (FBS-82210)
355 if ( ! $this->persistent ) {
356 $this->degraded = true;
357 $should_log = function_exists( 'apply_filters' )
358 ? apply_filters( 'xspeed_object_cache_log_degraded', true )
359 : true;
360 // Diagnostic only, and only when debug logging is on — keeps
361 // the production error log quiet (Plugin Check flags an
362 // unconditional error_log()).
363 if ( $should_log && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
364 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- WP_DEBUG-gated degraded-state diagnostic.
365 error_log( sprintf(
366 '[xSpeed] Object cache drop-in active but NOT persisting: could not connect a %s backend (client: %s). Serving a non-persistent in-request cache. Check the backend host/port and that the extension OR xSpeed\'s bundled client is loadable.',
367 $this->backend,
368 $this->client
369 ) );
370 }
371 }
372 }
373
374 /**
375 * Whether a persistent backend is actually connected. False means the
376 * drop-in is degraded (non-persistent) — see $this->degraded.
377 */
378 public function is_persistent() {
379 return (bool) $this->persistent;
380 }
381
382 /** Concrete client in use: phpredis|builtin|ext-memcached|builtin-memcached|''. */
383 public function client_name() {
384 return $this->persistent ? (string) $this->client : '';
385 }
386
387 /**
388 * Load xSpeed's own Redis_Client on demand. The drop-in runs before
389 * the plugin's autoloader, so we require the class file directly from
390 * the plugin. Returns true once \XSpeed\Redis_Client is available.
391 */
392 private function load_builtin_client() {
393 return $this->load_builtin( '\\XSpeed\\Redis_Client', 'class-redis-client.php' );
394 }
395
396 /**
397 * Robustly locate + require one of xSpeed's bundled, extension-free
398 * clients. This is the linchpin of the "no extension to install"
399 * promise: on a host without phpredis/ext-memcached, the drop-in MUST
400 * be able to load this file or it silently degrades to a non-persistent
401 * cache (writes return true but never reach the backend). (FBS-82210)
402 *
403 * The original implementation only tried WP_PLUGIN_DIR — which fails
404 * when the plugin dir is symlinked, when WP_PLUGIN_DIR points somewhere
405 * unexpected, or when the constant isn't defined yet at drop-in load
406 * time. We add a __DIR__-relative candidate: the drop-in lives in
407 * wp-content/, and the plugin sits at wp-content/plugins/xspeed/includes/,
408 * so we can resolve the client relative to our own location regardless
409 * of how the plugin is mounted. realpath() also resolves symlinks.
410 *
411 * @param string $class Fully-qualified class name to check for.
412 * @param string $filename Client file under the plugin's includes/ dir.
413 * @return bool True once the class is available.
414 */
415 private function load_builtin( $class, $filename ) {
416 if ( class_exists( $class ) ) {
417 return true;
418 }
419
420 $candidates = array();
421 if ( defined( 'WP_PLUGIN_DIR' ) ) {
422 $candidates[] = WP_PLUGIN_DIR . '/xspeed/includes/' . $filename;
423 }
424 if ( defined( 'WP_CONTENT_DIR' ) ) {
425 $candidates[] = WP_CONTENT_DIR . '/plugins/xspeed/includes/' . $filename;
426 $candidates[] = WP_CONTENT_DIR . '/mu-plugins/xspeed/includes/' . $filename;
427 }
428 // __DIR__-relative: this file is wp-content/object-cache.php, so the
429 // plugin is a sibling under plugins/xspeed/ — survives symlinks and
430 // odd WP_PLUGIN_DIR values the candidates above don't.
431 $candidates[] = __DIR__ . '/plugins/xspeed/includes/' . $filename;
432
433 foreach ( $candidates as $path ) {
434 if ( ! $path ) {
435 continue;
436 }
437 $real = @realpath( $path );
438 $path = false !== $real ? $real : $path;
439 if ( file_exists( $path ) ) {
440 require_once $path;
441 if ( class_exists( $class ) ) {
442 return true;
443 }
444 }
445 }
446
447 return class_exists( $class );
448 }
449
450 // --- Key helpers ----------------------------------------------------
451
452 private function group( $group ) {
453 return '' === (string) $group ? 'default' : (string) $group;
454 }
455
456 private function full_key( $key, $group ) {
457 $group = $this->group( $group );
458 $prefix = isset( $this->global_groups[ $group ] ) ? 0 : $this->blog_prefix;
459 return $this->salt . ':' . $prefix . ':' . $group . ':' . $key;
460 }
461
462 private function is_persistent_group( $group ) {
463 return $this->persistent && ! isset( $this->non_persistent_groups[ $this->group( $group ) ] );
464 }
465
466 // --- Core ops -------------------------------------------------------
467
468 public function add( $key, $data, $group = 'default', $expire = 0 ) {
469 if ( wp_suspend_cache_addition() ) {
470 return false;
471 }
472 $id = $this->full_key( $key, $group );
473 // Present in THIS request's runtime cache → already added.
474 if ( isset( $this->cache[ $id ] ) ) {
475 return false;
476 }
477
478 // For persistent groups, add() must fail if the key exists in the
479 // BACKEND too — not just this request's runtime array. Use the
480 // backend's atomic add (Redis SET NX / memcached add) so two
481 // processes racing to add the same key behave correctly and the
482 // existing value is never clobbered. Falling back to the runtime
483 // check alone (the old behaviour) let process B overwrite a key
484 // process A had already stored. (FBS-82111 Bug 2)
485 if ( $this->is_persistent_group( $group ) && $this->conn ) {
486 try {
487 if ( is_object( $data ) ) {
488 $data = clone $data;
489 }
490 $payload = maybe_serialize( $data );
491 $stored = $this->conn->add( $id, $payload, (int) $expire );
492 if ( ! $stored ) {
493 return false; // key already exists in the backend.
494 }
495 $this->cache[ $id ] = $data;
496 return true;
497 } catch ( \Throwable $e ) {
498 // Backend hiccup — fall through to the runtime-only path so
499 // add() still works against the in-request array cache.
500 }
501 }
502
503 return $this->set( $key, $data, $group, $expire );
504 }
505
506 public function replace( $key, $data, $group = 'default', $expire = 0 ) {
507 $id = $this->full_key( $key, $group );
508 if ( ! isset( $this->cache[ $id ] ) && false === $this->get( $key, $group ) ) {
509 return false;
510 }
511 return $this->set( $key, $data, $group, $expire );
512 }
513
514 public function set( $key, $data, $group = 'default', $expire = 0 ) {
515 $id = $this->full_key( $key, $group );
516 if ( is_object( $data ) ) {
517 $data = clone $data;
518 }
519 $this->cache[ $id ] = $data;
520
521 if ( $this->is_persistent_group( $group ) ) {
522 try {
523 $payload = maybe_serialize( $data );
524 if ( 'redis' === $this->backend ) {
525 return $expire > 0
526 ? (bool) $this->conn->setex( $id, (int) $expire, $payload )
527 : (bool) $this->conn->set( $id, $payload );
528 }
529 return (bool) $this->conn->set( $id, $payload, (int) $expire );
530 } catch ( \Throwable $e ) {
531 return true; // runtime cache still set
532 }
533 }
534 return true;
535 }
536
537 public function get( $key, $group = 'default', $force = false, &$found = null ) {
538 $id = $this->full_key( $key, $group );
539
540 if ( ! $force && isset( $this->cache[ $id ] ) ) {
541 $found = true;
542 ++$this->cache_hits;
543 $val = $this->cache[ $id ];
544 return is_object( $val ) ? clone $val : $val;
545 }
546
547 if ( $this->is_persistent_group( $group ) ) {
548 try {
549 $raw = $this->conn->get( $id );
550 if ( false !== $raw && null !== $raw ) {
551 $val = maybe_unserialize( $raw );
552 $this->cache[ $id ] = $val;
553 $found = true;
554 ++$this->cache_hits;
555 return is_object( $val ) ? clone $val : $val;
556 }
557 } catch ( \Throwable $e ) {
558 // fall through to miss
559 }
560 }
561
562 $found = false;
563 ++$this->cache_misses;
564 return false;
565 }
566
567 public function get_multiple( $keys, $group = 'default', $force = false ) {
568 $out = array();
569 foreach ( (array) $keys as $key ) {
570 $out[ $key ] = $this->get( $key, $group, $force );
571 }
572 return $out;
573 }
574
575 public function delete( $key, $group = 'default' ) {
576 $id = $this->full_key( $key, $group );
577 unset( $this->cache[ $id ] );
578 if ( $this->is_persistent_group( $group ) ) {
579 try {
580 return (bool) $this->backend_delete( $id );
581 } catch ( \Throwable $e ) {
582 return true;
583 }
584 }
585 return true;
586 }
587
588 public function incr( $key, $offset = 1, $group = 'default' ) {
589 $id = $this->full_key( $key, $group );
590 $offset = max( 0, (int) $offset );
591 if ( $this->is_persistent_group( $group ) ) {
592 try {
593 $new = $this->backend_incr( $id, $offset );
594 if ( false !== $new ) {
595 $this->cache[ $id ] = (int) $new;
596 return (int) $new;
597 }
598 } catch ( \Throwable $e ) {
599 // fall through
600 }
601 }
602 $val = isset( $this->cache[ $id ] ) ? (int) $this->cache[ $id ] : 0;
603 $val = max( 0, $val + $offset );
604 $this->cache[ $id ] = $val;
605 return $val;
606 }
607
608 public function decr( $key, $offset = 1, $group = 'default' ) {
609 $id = $this->full_key( $key, $group );
610 $offset = max( 0, (int) $offset );
611 if ( $this->is_persistent_group( $group ) ) {
612 try {
613 $new = $this->backend_decr( $id, $offset );
614 if ( false !== $new ) {
615 $new = max( 0, (int) $new );
616 $this->cache[ $id ] = $new;
617 return $new;
618 }
619 } catch ( \Throwable $e ) {
620 // fall through
621 }
622 }
623 $val = isset( $this->cache[ $id ] ) ? (int) $this->cache[ $id ] : 0;
624 $val = max( 0, $val - $offset );
625 $this->cache[ $id ] = $val;
626 return $val;
627 }
628
629 public function flush() {
630 $this->cache = array();
631 if ( $this->persistent ) {
632 try {
633 return (bool) $this->backend_flush( );
634 } catch ( \Throwable $e ) {
635 return false;
636 }
637 }
638 return true;
639 }
640
641 // --- Backend dispatch ------------------------------------------
642 // Normalises method-name differences across the four client kinds:
643 // phpredis + our Redis_Client (redis backend), ext/memcached + our
644 // Memcached_Client (memcached backend).
645
646 private function backend_delete( $id ) {
647 if ( 'redis' === $this->backend ) {
648 return $this->conn->del( $id );
649 }
650 return $this->conn->delete( $id );
651 }
652
653 private function backend_incr( $id, $offset ) {
654 if ( 'redis' === $this->backend ) {
655 return $this->conn->incrBy( $id, $offset );
656 }
657 return 'builtin-memcached' === $this->client
658 ? $this->conn->incr( $id, $offset )
659 : $this->conn->increment( $id, $offset );
660 }
661
662 private function backend_decr( $id, $offset ) {
663 if ( 'redis' === $this->backend ) {
664 return $this->conn->decrBy( $id, $offset );
665 }
666 return 'builtin-memcached' === $this->client
667 ? $this->conn->decr( $id, $offset )
668 : $this->conn->decrement( $id, $offset );
669 }
670
671 private function backend_flush() {
672 if ( 'redis' === $this->backend ) {
673 return $this->conn->flushDB();
674 }
675 return 'builtin-memcached' === $this->client
676 ? $this->conn->flush_all()
677 : $this->conn->flush();
678 }
679
680 /**
681 * Load xSpeed's own Memcached_Client (pure-PHP) on demand, the same
682 * way load_builtin_client() loads the Redis one.
683 */
684 private function load_builtin_memcached() {
685 return $this->load_builtin( '\\XSpeed\\Memcached_Client', 'class-memcached-client.php' );
686 }
687
688 public function flush_runtime() {
689 $this->cache = array();
690 return true;
691 }
692
693 public function flush_group( $group ) {
694 // Without key tagging we can't selectively flush a group on the
695 // persistent store cheaply; clear the runtime copy for the group.
696 $needle = $this->full_key( '', $group );
697 foreach ( array_keys( $this->cache ) as $id ) {
698 if ( 0 === strpos( $id, $needle ) ) {
699 unset( $this->cache[ $id ] );
700 }
701 }
702 return true;
703 }
704
705 public function close() {
706 if ( $this->persistent && $this->conn ) {
707 try {
708 if ( 'redis' === $this->backend ) {
709 $this->conn->close();
710 } else {
711 $this->conn->quit();
712 }
713 } catch ( \Throwable $e ) {
714 // ignore
715 }
716 }
717 return true;
718 }
719
720 // --- Group config ---------------------------------------------------
721
722 public function add_global_groups( $groups ) {
723 foreach ( (array) $groups as $g ) {
724 $this->global_groups[ $g ] = true;
725 }
726 }
727
728 public function add_non_persistent_groups( $groups ) {
729 foreach ( (array) $groups as $g ) {
730 $this->non_persistent_groups[ $g ] = true;
731 }
732 }
733
734 public function switch_to_blog( $blog_id ) {
735 $this->blog_prefix = $this->multisite ? (int) $blog_id : 0;
736 }
737
738 /** @return array{backend:string,persistent:bool,hits:int,misses:int} */
739 public function stats() {
740 return array(
741 'backend' => $this->backend,
742 'persistent' => $this->persistent,
743 'hits' => $this->cache_hits,
744 'misses' => $this->cache_misses,
745 );
746 }
747 }
748 }
749