PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.4
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.4
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 1.2.0 All 28 releases
xspeed / includes / object-cache.php

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

700 lines 22.7 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 $this->connect();
221 }
222
223 // --- Connection -----------------------------------------------------
224
225 private function connect() {
226 $timeout = (float) xspeed_oc_config( 'timeout', 1 );
227 try {
228 if ( 'memcached' === $this->backend ) {
229 $host = (string) xspeed_oc_config( 'host', '127.0.0.1' );
230 $port = (int) xspeed_oc_config( 'port', 11211 );
231
232 if ( class_exists( 'Memcached' ) ) {
233 // ext/memcached (preferred).
234 $this->client = 'ext-memcached';
235 $mc = new Memcached();
236 $mc->addServer( $host, $port );
237 $mc->setOption( Memcached::OPT_CONNECT_TIMEOUT, (int) ( $timeout * 1000 ) );
238 $stats = @$mc->getStats();
239 if ( is_array( $stats ) && ! empty( $stats ) ) {
240 $this->conn = $mc;
241 $this->persistent = true;
242 }
243 } elseif ( $this->load_builtin_memcached() ) {
244 // xSpeed's own pure-PHP Memcached client.
245 $this->client = 'builtin-memcached';
246 $mc = new \XSpeed\Memcached_Client( $host, $port, $timeout );
247 if ( $mc->connect() && false !== $mc->version() ) {
248 $this->conn = $mc;
249 $this->persistent = true;
250 }
251 }
252 } else {
253 $this->backend = 'redis';
254 $host = (string) xspeed_oc_config( 'host', '127.0.0.1' );
255 $port = (int) xspeed_oc_config( 'port', 6379 );
256 $pass = (string) xspeed_oc_config( 'password', '' );
257 $db = (int) xspeed_oc_config( 'database', 0 );
258 $persist = (bool) xspeed_oc_config( 'persist', false );
259
260 if ( class_exists( 'Redis' ) ) {
261 // phpredis extension (preferred).
262 $this->client = 'phpredis';
263 $redis = new Redis();
264 $ok = $persist
265 ? @$redis->pconnect( $host, $port, $timeout )
266 : @$redis->connect( $host, $port, $timeout );
267 if ( $ok ) {
268 if ( '' !== $pass ) {
269 @$redis->auth( $pass );
270 }
271 if ( $db > 0 ) {
272 @$redis->select( $db );
273 }
274 if ( '+PONG' === @$redis->ping() || true === @$redis->ping() ) {
275 $this->conn = $redis;
276 $this->persistent = true;
277 }
278 }
279 } elseif ( $this->load_builtin_client() ) {
280 // xSpeed's own pure-PHP client (no extension, no library).
281 $this->client = 'builtin';
282 $rc = new \XSpeed\Redis_Client( $host, $port, (float) $timeout, $persist );
283 if ( $rc->connect() ) {
284 if ( '' !== $pass ) {
285 $rc->auth( $pass );
286 }
287 if ( $db > 0 ) {
288 $rc->select( $db );
289 }
290 $pong = $rc->ping();
291 if ( is_string( $pong ) && false !== stripos( $pong, 'PONG' ) ) {
292 $this->conn = $rc;
293 $this->persistent = true;
294 }
295 }
296 }
297 }
298 } catch ( \Throwable $e ) {
299 // Any failure → stay in non-persistent mode. Never fatal.
300 $this->conn = null;
301 $this->persistent = false;
302 }
303
304 // The drop-in is installed (we're running), so if we didn't manage
305 // to connect a persistent backend, object caching is effectively
306 // doing nothing — writes succeed but evaporate at request end.
307 // Flag it so detect()/the dashboard can report "degraded" instead
308 // of a false-healthy state, and log once per request so the failure
309 // is diagnosable rather than silent. (FBS-82210)
310 if ( ! $this->persistent ) {
311 $this->degraded = true;
312 $should_log = function_exists( 'apply_filters' )
313 ? apply_filters( 'xspeed_object_cache_log_degraded', true )
314 : true;
315 if ( $should_log ) {
316 error_log( sprintf(
317 '[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.',
318 $this->backend,
319 $this->client
320 ) );
321 }
322 }
323 }
324
325 /**
326 * Whether a persistent backend is actually connected. False means the
327 * drop-in is degraded (non-persistent) — see $this->degraded.
328 */
329 public function is_persistent() {
330 return (bool) $this->persistent;
331 }
332
333 /** Concrete client in use: phpredis|builtin|ext-memcached|builtin-memcached|''. */
334 public function client_name() {
335 return $this->persistent ? (string) $this->client : '';
336 }
337
338 /**
339 * Load xSpeed's own Redis_Client on demand. The drop-in runs before
340 * the plugin's autoloader, so we require the class file directly from
341 * the plugin. Returns true once \XSpeed\Redis_Client is available.
342 */
343 private function load_builtin_client() {
344 return $this->load_builtin( '\\XSpeed\\Redis_Client', 'class-redis-client.php' );
345 }
346
347 /**
348 * Robustly locate + require one of xSpeed's bundled, extension-free
349 * clients. This is the linchpin of the "no extension to install"
350 * promise: on a host without phpredis/ext-memcached, the drop-in MUST
351 * be able to load this file or it silently degrades to a non-persistent
352 * cache (writes return true but never reach the backend). (FBS-82210)
353 *
354 * The original implementation only tried WP_PLUGIN_DIR — which fails
355 * when the plugin dir is symlinked, when WP_PLUGIN_DIR points somewhere
356 * unexpected, or when the constant isn't defined yet at drop-in load
357 * time. We add a __DIR__-relative candidate: the drop-in lives in
358 * wp-content/, and the plugin sits at wp-content/plugins/xspeed/includes/,
359 * so we can resolve the client relative to our own location regardless
360 * of how the plugin is mounted. realpath() also resolves symlinks.
361 *
362 * @param string $class Fully-qualified class name to check for.
363 * @param string $filename Client file under the plugin's includes/ dir.
364 * @return bool True once the class is available.
365 */
366 private function load_builtin( $class, $filename ) {
367 if ( class_exists( $class ) ) {
368 return true;
369 }
370
371 $candidates = array();
372 if ( defined( 'WP_PLUGIN_DIR' ) ) {
373 $candidates[] = WP_PLUGIN_DIR . '/xspeed/includes/' . $filename;
374 }
375 if ( defined( 'WP_CONTENT_DIR' ) ) {
376 $candidates[] = WP_CONTENT_DIR . '/plugins/xspeed/includes/' . $filename;
377 $candidates[] = WP_CONTENT_DIR . '/mu-plugins/xspeed/includes/' . $filename;
378 }
379 // __DIR__-relative: this file is wp-content/object-cache.php, so the
380 // plugin is a sibling under plugins/xspeed/ — survives symlinks and
381 // odd WP_PLUGIN_DIR values the candidates above don't.
382 $candidates[] = __DIR__ . '/plugins/xspeed/includes/' . $filename;
383
384 foreach ( $candidates as $path ) {
385 if ( ! $path ) {
386 continue;
387 }
388 $real = @realpath( $path );
389 $path = false !== $real ? $real : $path;
390 if ( file_exists( $path ) ) {
391 require_once $path;
392 if ( class_exists( $class ) ) {
393 return true;
394 }
395 }
396 }
397
398 return class_exists( $class );
399 }
400
401 // --- Key helpers ----------------------------------------------------
402
403 private function group( $group ) {
404 return '' === (string) $group ? 'default' : (string) $group;
405 }
406
407 private function full_key( $key, $group ) {
408 $group = $this->group( $group );
409 $prefix = isset( $this->global_groups[ $group ] ) ? 0 : $this->blog_prefix;
410 return $this->salt . ':' . $prefix . ':' . $group . ':' . $key;
411 }
412
413 private function is_persistent_group( $group ) {
414 return $this->persistent && ! isset( $this->non_persistent_groups[ $this->group( $group ) ] );
415 }
416
417 // --- Core ops -------------------------------------------------------
418
419 public function add( $key, $data, $group = 'default', $expire = 0 ) {
420 if ( wp_suspend_cache_addition() ) {
421 return false;
422 }
423 $id = $this->full_key( $key, $group );
424 // Present in THIS request's runtime cache → already added.
425 if ( isset( $this->cache[ $id ] ) ) {
426 return false;
427 }
428
429 // For persistent groups, add() must fail if the key exists in the
430 // BACKEND too — not just this request's runtime array. Use the
431 // backend's atomic add (Redis SET NX / memcached add) so two
432 // processes racing to add the same key behave correctly and the
433 // existing value is never clobbered. Falling back to the runtime
434 // check alone (the old behaviour) let process B overwrite a key
435 // process A had already stored. (FBS-82111 Bug 2)
436 if ( $this->is_persistent_group( $group ) && $this->conn ) {
437 try {
438 if ( is_object( $data ) ) {
439 $data = clone $data;
440 }
441 $payload = maybe_serialize( $data );
442 $stored = $this->conn->add( $id, $payload, (int) $expire );
443 if ( ! $stored ) {
444 return false; // key already exists in the backend.
445 }
446 $this->cache[ $id ] = $data;
447 return true;
448 } catch ( \Throwable $e ) {
449 // Backend hiccup — fall through to the runtime-only path so
450 // add() still works against the in-request array cache.
451 }
452 }
453
454 return $this->set( $key, $data, $group, $expire );
455 }
456
457 public function replace( $key, $data, $group = 'default', $expire = 0 ) {
458 $id = $this->full_key( $key, $group );
459 if ( ! isset( $this->cache[ $id ] ) && false === $this->get( $key, $group ) ) {
460 return false;
461 }
462 return $this->set( $key, $data, $group, $expire );
463 }
464
465 public function set( $key, $data, $group = 'default', $expire = 0 ) {
466 $id = $this->full_key( $key, $group );
467 if ( is_object( $data ) ) {
468 $data = clone $data;
469 }
470 $this->cache[ $id ] = $data;
471
472 if ( $this->is_persistent_group( $group ) ) {
473 try {
474 $payload = maybe_serialize( $data );
475 if ( 'redis' === $this->backend ) {
476 return $expire > 0
477 ? (bool) $this->conn->setex( $id, (int) $expire, $payload )
478 : (bool) $this->conn->set( $id, $payload );
479 }
480 return (bool) $this->conn->set( $id, $payload, (int) $expire );
481 } catch ( \Throwable $e ) {
482 return true; // runtime cache still set
483 }
484 }
485 return true;
486 }
487
488 public function get( $key, $group = 'default', $force = false, &$found = null ) {
489 $id = $this->full_key( $key, $group );
490
491 if ( ! $force && isset( $this->cache[ $id ] ) ) {
492 $found = true;
493 ++$this->cache_hits;
494 $val = $this->cache[ $id ];
495 return is_object( $val ) ? clone $val : $val;
496 }
497
498 if ( $this->is_persistent_group( $group ) ) {
499 try {
500 $raw = $this->conn->get( $id );
501 if ( false !== $raw && null !== $raw ) {
502 $val = maybe_unserialize( $raw );
503 $this->cache[ $id ] = $val;
504 $found = true;
505 ++$this->cache_hits;
506 return is_object( $val ) ? clone $val : $val;
507 }
508 } catch ( \Throwable $e ) {
509 // fall through to miss
510 }
511 }
512
513 $found = false;
514 ++$this->cache_misses;
515 return false;
516 }
517
518 public function get_multiple( $keys, $group = 'default', $force = false ) {
519 $out = array();
520 foreach ( (array) $keys as $key ) {
521 $out[ $key ] = $this->get( $key, $group, $force );
522 }
523 return $out;
524 }
525
526 public function delete( $key, $group = 'default' ) {
527 $id = $this->full_key( $key, $group );
528 unset( $this->cache[ $id ] );
529 if ( $this->is_persistent_group( $group ) ) {
530 try {
531 return (bool) $this->backend_delete( $id );
532 } catch ( \Throwable $e ) {
533 return true;
534 }
535 }
536 return true;
537 }
538
539 public function incr( $key, $offset = 1, $group = 'default' ) {
540 $id = $this->full_key( $key, $group );
541 $offset = max( 0, (int) $offset );
542 if ( $this->is_persistent_group( $group ) ) {
543 try {
544 $new = $this->backend_incr( $id, $offset );
545 if ( false !== $new ) {
546 $this->cache[ $id ] = (int) $new;
547 return (int) $new;
548 }
549 } catch ( \Throwable $e ) {
550 // fall through
551 }
552 }
553 $val = isset( $this->cache[ $id ] ) ? (int) $this->cache[ $id ] : 0;
554 $val = max( 0, $val + $offset );
555 $this->cache[ $id ] = $val;
556 return $val;
557 }
558
559 public function decr( $key, $offset = 1, $group = 'default' ) {
560 $id = $this->full_key( $key, $group );
561 $offset = max( 0, (int) $offset );
562 if ( $this->is_persistent_group( $group ) ) {
563 try {
564 $new = $this->backend_decr( $id, $offset );
565 if ( false !== $new ) {
566 $new = max( 0, (int) $new );
567 $this->cache[ $id ] = $new;
568 return $new;
569 }
570 } catch ( \Throwable $e ) {
571 // fall through
572 }
573 }
574 $val = isset( $this->cache[ $id ] ) ? (int) $this->cache[ $id ] : 0;
575 $val = max( 0, $val - $offset );
576 $this->cache[ $id ] = $val;
577 return $val;
578 }
579
580 public function flush() {
581 $this->cache = array();
582 if ( $this->persistent ) {
583 try {
584 return (bool) $this->backend_flush( );
585 } catch ( \Throwable $e ) {
586 return false;
587 }
588 }
589 return true;
590 }
591
592 // --- Backend dispatch ------------------------------------------
593 // Normalises method-name differences across the four client kinds:
594 // phpredis + our Redis_Client (redis backend), ext/memcached + our
595 // Memcached_Client (memcached backend).
596
597 private function backend_delete( $id ) {
598 if ( 'redis' === $this->backend ) {
599 return $this->conn->del( $id );
600 }
601 return $this->conn->delete( $id );
602 }
603
604 private function backend_incr( $id, $offset ) {
605 if ( 'redis' === $this->backend ) {
606 return $this->conn->incrBy( $id, $offset );
607 }
608 return 'builtin-memcached' === $this->client
609 ? $this->conn->incr( $id, $offset )
610 : $this->conn->increment( $id, $offset );
611 }
612
613 private function backend_decr( $id, $offset ) {
614 if ( 'redis' === $this->backend ) {
615 return $this->conn->decrBy( $id, $offset );
616 }
617 return 'builtin-memcached' === $this->client
618 ? $this->conn->decr( $id, $offset )
619 : $this->conn->decrement( $id, $offset );
620 }
621
622 private function backend_flush() {
623 if ( 'redis' === $this->backend ) {
624 return $this->conn->flushDB();
625 }
626 return 'builtin-memcached' === $this->client
627 ? $this->conn->flush_all()
628 : $this->conn->flush();
629 }
630
631 /**
632 * Load xSpeed's own Memcached_Client (pure-PHP) on demand, the same
633 * way load_builtin_client() loads the Redis one.
634 */
635 private function load_builtin_memcached() {
636 return $this->load_builtin( '\\XSpeed\\Memcached_Client', 'class-memcached-client.php' );
637 }
638
639 public function flush_runtime() {
640 $this->cache = array();
641 return true;
642 }
643
644 public function flush_group( $group ) {
645 // Without key tagging we can't selectively flush a group on the
646 // persistent store cheaply; clear the runtime copy for the group.
647 $needle = $this->full_key( '', $group );
648 foreach ( array_keys( $this->cache ) as $id ) {
649 if ( 0 === strpos( $id, $needle ) ) {
650 unset( $this->cache[ $id ] );
651 }
652 }
653 return true;
654 }
655
656 public function close() {
657 if ( $this->persistent && $this->conn ) {
658 try {
659 if ( 'redis' === $this->backend ) {
660 $this->conn->close();
661 } else {
662 $this->conn->quit();
663 }
664 } catch ( \Throwable $e ) {
665 // ignore
666 }
667 }
668 return true;
669 }
670
671 // --- Group config ---------------------------------------------------
672
673 public function add_global_groups( $groups ) {
674 foreach ( (array) $groups as $g ) {
675 $this->global_groups[ $g ] = true;
676 }
677 }
678
679 public function add_non_persistent_groups( $groups ) {
680 foreach ( (array) $groups as $g ) {
681 $this->non_persistent_groups[ $g ] = true;
682 }
683 }
684
685 public function switch_to_blog( $blog_id ) {
686 $this->blog_prefix = $this->multisite ? (int) $blog_id : 0;
687 }
688
689 /** @return array{backend:string,persistent:bool,hits:int,misses:int} */
690 public function stats() {
691 return array(
692 'backend' => $this->backend,
693 'persistent' => $this->persistent,
694 'hits' => $this->cache_hits,
695 'misses' => $this->cache_misses,
696 );
697 }
698 }
699 }
700