Skip to content

Commit 3de9787

Browse files
authoredFeb 28, 2025
Update object-cache.php
1 parent 8961ac4 commit 3de9787

File tree

1 file changed

+993
-615
lines changed

1 file changed

+993
-615
lines changed
 

‎object-cache.php

+993-615
Original file line numberDiff line numberDiff line change
@@ -2,836 +2,1214 @@
22
/*
33
Plugin Name: Object Cache
44
Plugin URI: https://www.littlebizzy.com/plugins/object-cache
5-
Description: Drop-in persistent object cache for WordPress based on Redis in-memory storage that supports Predis, clusters, and WP-CLI (forked from PressJitsu).
6-
Version: 1.2.3
5+
Description: Memcached backend for the WP Object Cache.
6+
Version: 2.0.0
77
Author: LittleBizzy
88
Author URI: https://www.littlebizzy.com
99
License: GPLv3
1010
License URI: https://www.gnu.org/licenses/gpl-3.0.html
11-
Upstream Plugin Name: Pressjitsu Redis Object Cache
12-
Upstream Plugin URI: https://github.com/pressjitsu/pj-object-cache-red
13-
Upstream Author: Eric Mann + Erick Hitter + Pressjitsu, Inc.
14-
Upstream Author URI: https://pressjitsu.com
15-
PBP Version: N/A
16-
WC requires at least: 3.3
17-
WC tested up to: 3.8
18-
Prefix: OBJCHE
11+
Upstream Plugin Name: Memcached
12+
Upstream Plugin URI: https://github.com/Automattic/wp-memcached
13+
Upstream Author: Ryan Boren, Denis de Bernardy, Matt Martz, Andy Skelton
14+
Upstream Author URI: https://wordpress.org
15+
Upstream Version: 4.0.0
1916
*/
2017

21-
declare(strict_types=1);
18+
// Users with setups where multiple installs share a common wp-config.php or $table_prefix
19+
// can use this to guarantee uniqueness for the keys generated by this object cache
20+
if ( ! defined( 'WP_CACHE_KEY_SALT' ) ) {
21+
define( 'WP_CACHE_KEY_SALT', '' );
22+
}
23+
24+
function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
25+
global $wp_object_cache;
2226

23-
// check if OBJECT_CACHE is disabled in wp-config.php
24-
if ( defined( 'OBJECT_CACHE' ) && !OBJECT_CACHE ) {
25-
return;
27+
return $wp_object_cache->add( $key, $data, $group, $expire );
2628
}
2729

28-
// check if 'Redis' class exists
29-
if( class_exists( 'Redis' ) ) :
30+
function wp_cache_add_multiple( array $data, $group = '', $expire = 0 ) {
31+
global $wp_object_cache;
3032

31-
/**
32-
* Adds a value to cache.
33-
*
34-
* If the specified key wp_cache_addalready exists, the value is not stored and the function
35-
* returns false.
36-
*
37-
* @param string $key The key under which to store the value.
38-
* @param mixed $value The value to store.
39-
* @param string $group The group value appended to the $key.
40-
* @param int $expiration The expiration time, defaults to 0.
41-
*
42-
* @global WP_Object_Cache $wp_object_cache
43-
*
44-
* @return bool Returns TRUE on success or FALSE on failure.
45-
*/
46-
function wp_cache_add( $key, $value, $group = 'default', $expiration = 0 ) {
33+
return $wp_object_cache->add_multiple( $data, $group, $expire );
34+
}
35+
36+
function wp_cache_incr( $key, $n = 1, $group = '' ) {
4737
global $wp_object_cache;
48-
return $wp_object_cache->add( $key, $value, $group, $expiration );
38+
39+
return $wp_object_cache->incr( $key, $n, $group );
40+
}
41+
42+
function wp_cache_decr( $key, $n = 1, $group = '' ) {
43+
global $wp_object_cache;
44+
45+
return $wp_object_cache->decr( $key, $n, $group );
4946
}
5047

51-
/**
52-
* Closes the cache.
53-
*
54-
* This function has ceased to do anything since WordPress 2.5. The
55-
* functionality was removed along with the rest of the persistent cache. This
56-
* does not mean that plugins can't implement this function when they need to
57-
* make sure that the cache is cleaned up after WordPress no longer needs it.
58-
*
59-
* @return bool Always returns True
60-
*/
6148
function wp_cache_close() {
62-
return true;
49+
global $wp_object_cache;
50+
51+
return $wp_object_cache->close();
6352
}
6453

65-
/**
66-
* Decrement a numeric item's value.
67-
*
68-
* @param string $key The key under which to store the value.
69-
* @param int $offset The amount by which to decrement the item's value.
70-
* @param string $group The group value appended to the $key.
71-
*
72-
* @global WP_Object_Cache $wp_object_cache
73-
*
74-
* @return int|bool Returns item's new value on success or FALSE on failure.
75-
*/
76-
function wp_cache_decr( $key, $offset = 1, $group = 'default' ) {
54+
function wp_cache_delete( $key, $group = '' ) {
7755
global $wp_object_cache;
78-
return $wp_object_cache->decr( $key, $offset, $group );
56+
57+
return $wp_object_cache->delete( $key, $group );
7958
}
8059

81-
/**
82-
* Remove the item from the cache.
83-
*
84-
* @param string $key The key under which to store the value.
85-
* @param string $group The group value appended to the $key.
86-
* @param int $time The amount of time the server will wait to delete the item in seconds.
87-
*
88-
* @global WP_Object_Cache $wp_object_cache
89-
*
90-
* @return bool Returns TRUE on success or FALSE on failure.
91-
*/
92-
function wp_cache_delete( $key, $group = 'default', $time = 0 ) {
60+
function wp_cache_delete_multiple( array $keys, $group = '' ) {
9361
global $wp_object_cache;
94-
return $wp_object_cache->delete( $key, $group, $time );
62+
63+
return $wp_object_cache->delete_multiple( $keys, $group );
9564
}
9665

97-
/**
98-
* Invalidate all items in the cache.
99-
*
100-
* @param int $delay Number of seconds to wait before invalidating the items.
101-
*
102-
* @global WP_Object_Cache $wp_object_cache
103-
*
104-
* @return bool Returns TRUE on success or FALSE on failure.
105-
*/
106-
function wp_cache_flush( $delay = 0 ) {
66+
function wp_cache_flush() {
10767
global $wp_object_cache;
108-
return $wp_object_cache->flush( $delay );
68+
69+
return $wp_object_cache->flush();
10970
}
11071

111-
/**
112-
* Retrieve object from cache.
113-
*
114-
* Gets an object from cache based on $key and $group.
115-
*
116-
* @param string $key The key under which to store the value.
117-
* @param string $group The group value appended to the $key.
118-
*
119-
* @global WP_Object_Cache $wp_object_cache
120-
*
121-
* @return bool|mixed Cached object value.
122-
*/
123-
function wp_cache_get( $key, $group = 'default', $force = false ) {
72+
function wp_cache_flush_runtime() {
12473
global $wp_object_cache;
125-
return $wp_object_cache->get( $key, $group, $force );
74+
75+
return $wp_object_cache->flush_runtime();
12676
}
12777

128-
/**
129-
* Retrieve multiple values from cache.
130-
*
131-
* Gets multiple values from cache, including across multiple groups
132-
*
133-
* Usage: array( 'group0' => array( 'key0', 'key1', 'key2', ), 'group1' => array( 'key0' ) )
134-
*
135-
* Mirrors the Memcached Object Cache plugin's argument and return-value formats
136-
*
137-
* @param array $groups Array of groups and keys to retrieve
138-
*
139-
* @global WP_Object_Cache $wp_object_cache
140-
*
141-
* @return bool|mixed Array of cached values, keys in the format $group:$key. Non-existent keys false
142-
*/
143-
function wp_cache_get_multi( $groups, $unserialize = true ) {
78+
function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
14479
global $wp_object_cache;
145-
return $wp_object_cache->get_multi( $groups, $unserialize );
80+
81+
if ( function_exists( 'apply_filters' ) ) {
82+
$value = apply_filters( 'pre_wp_cache_get', false, $key, $group, $force );
83+
if ( false !== $value ) {
84+
$found = true;
85+
return $value;
86+
}
87+
}
88+
89+
return $wp_object_cache->get( $key, $group, $force, $found );
14690
}
14791

148-
/**
149-
* Increment a numeric item's value.
150-
*
151-
* @param string $key The key under which to store the value.
152-
* @param int $offset The amount by which to increment the item's value.
153-
* @param string $group The group value appended to the $key.
154-
*
155-
* @global WP_Object_Cache $wp_object_cache
156-
*
157-
* @return int|bool Returns item's new value on success or FALSE on failure.
158-
*/
159-
function wp_cache_incr( $key, $offset = 1, $group = 'default' ) {
92+
function wp_cache_get_multiple( $keys, $group = '', $force = false ) {
16093
global $wp_object_cache;
161-
return $wp_object_cache->incr( $key, $offset, $group );
94+
95+
return $wp_object_cache->get_multiple( $keys, $group, $force );
16296
}
16397

16498
/**
165-
* Sets up Object Cache Global and assigns it.
166-
*
167-
* @global WP_Object_Cache $wp_object_cache WordPress Object Cache
99+
* Retrieve multiple cache entries
168100
*
169-
* @return void
101+
* @param array $groups Array of arrays, of groups and keys to retrieve
102+
* @return mixed
170103
*/
104+
function wp_cache_get_multi( $groups ) {
105+
global $wp_object_cache;
106+
107+
return $wp_object_cache->get_multi( $groups );
108+
}
109+
171110
function wp_cache_init() {
172111
global $wp_object_cache;
112+
173113
$wp_object_cache = new WP_Object_Cache();
174114
}
175115

176-
/**
177-
* Replaces a value in cache.
178-
*
179-
* This method is similar to "add"; however, is does not successfully set a value if
180-
* the object's key is not already set in cache.
181-
*
182-
* @param string $key The key under which to store the value.
183-
* @param mixed $value The value to store.
184-
* @param string $group The group value appended to the $key.
185-
* @param int $expiration The expiration time, defaults to 0.
186-
*
187-
* @global WP_Object_Cache $wp_object_cache
188-
*
189-
* @return bool Returns TRUE on success or FALSE on failure.
190-
*/
191-
function wp_cache_replace( $key, $value, $group = 'default', $expiration = 0 ) {
116+
function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
192117
global $wp_object_cache;
193-
return $wp_object_cache->replace( $key, $value, $group, $expiration );
118+
119+
return $wp_object_cache->replace( $key, $data, $group, $expire );
194120
}
195121

196-
/**
197-
* Sets a value in cache.
198-
*
199-
* The value is set whether or not this key already exists in Redis.
200-
*
201-
* @param string $key The key under which to store the value.
202-
* @param mixed $value The value to store.
203-
* @param string $group The group value appended to the $key.
204-
* @param int $expiration The expiration time, defaults to 0.
205-
*
206-
* @global WP_Object_Cache $wp_object_cache
207-
*
208-
* @return bool Returns TRUE on success or FALSE on failure.
209-
*/
210-
function wp_cache_set( $key, $value, $group = 'default', $expiration = 0 ) {
122+
function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
211123
global $wp_object_cache;
212-
return $wp_object_cache->set( $key, $value, $group, $expiration );
124+
125+
if ( defined( 'WP_INSTALLING' ) === false ) {
126+
return $wp_object_cache->set( $key, $data, $group, $expire );
127+
} else {
128+
return $wp_object_cache->delete( $key, $group );
129+
}
213130
}
214131

215-
/**
216-
* Switch the interal blog id.
217-
*
218-
* This changes the blog id used to create keys in blog specific groups.
219-
*
220-
* @param int $_blog_id Blog ID
221-
*
222-
* @global WP_Object_Cache $wp_object_cache
223-
*
224-
* @return bool
225-
*/
226-
function wp_cache_switch_to_blog( $_blog_id ) {
132+
function wp_cache_set_multiple( array $data, $group = '', $expire = 0 ) {
227133
global $wp_object_cache;
228-
return $wp_object_cache->switch_to_blog( $_blog_id );
134+
135+
return $wp_object_cache->set_multiple( $data, $group, $expire );
136+
}
137+
138+
function wp_cache_switch_to_blog( $blog_id ) {
139+
global $wp_object_cache;
140+
141+
return $wp_object_cache->switch_to_blog( $blog_id );
229142
}
230143

231-
/**
232-
* Adds a group or set of groups to the list of Redis groups.
233-
*
234-
* @param string|array $groups A group or an array of groups to add.
235-
*
236-
* @global WP_Object_Cache $wp_object_cache
237-
*
238-
* @return void
239-
*/
240144
function wp_cache_add_global_groups( $groups ) {
241145
global $wp_object_cache;
146+
242147
$wp_object_cache->add_global_groups( $groups );
243148
}
244149

245-
/**
246-
* Adds a group or set of groups to the list of non-Redis groups.
247-
*
248-
* @param string|array $groups A group or an array of groups to add.
249-
*
250-
* @global WP_Object_Cache $wp_object_cache
251-
*
252-
* @return void
253-
*/
254150
function wp_cache_add_non_persistent_groups( $groups ) {
255151
global $wp_object_cache;
152+
256153
$wp_object_cache->add_non_persistent_groups( $groups );
257154
}
258155

156+
/**
157+
* Determines whether the object cache implementation supports a particular feature.
158+
*
159+
* @param string $feature Name of the feature to check for. Possible values include:
160+
* 'add_multiple', 'set_multiple', 'get_multiple', 'delete_multiple',
161+
* 'flush_runtime', 'flush_group'.
162+
* @return bool True if the feature is supported, false otherwise.
163+
*/
164+
function wp_cache_supports( $feature ) {
165+
switch ( $feature ) {
166+
case 'get_multiple':
167+
case 'flush_runtime':
168+
return true;
169+
170+
default:
171+
return false;
172+
}
173+
}
174+
259175
class WP_Object_Cache {
176+
var $global_groups = array( 'WP_Object_Cache_global' );
177+
178+
var $no_mc_groups = array();
179+
180+
var $cache = array();
181+
/** @var Memcache[] */
182+
var $mc = array();
183+
var $default_mcs = array();
184+
var $stats = array(
185+
'get' => 0,
186+
'get_local' => 0,
187+
'get_multi' => 0,
188+
'set' => 0,
189+
'set_local' => 0,
190+
'add' => 0,
191+
'delete' => 0,
192+
'delete_local' => 0,
193+
'slow-ops' => 0,
194+
);
195+
var $group_ops = array();
196+
var $cache_hits = 0;
197+
var $cache_misses = 0;
198+
var $global_prefix = '';
199+
var $blog_prefix = '';
200+
var $key_salt = '';
201+
202+
var $flush_group = 'WP_Object_Cache';
203+
var $global_flush_group = 'WP_Object_Cache_global';
204+
var $flush_key = "flush_number_v4";
205+
var $old_flush_key = "flush_number";
206+
var $flush_number = array();
207+
var $global_flush_number = null;
208+
209+
var $cache_enabled = true;
210+
var $default_expiration = 0;
211+
var $max_expiration = 2592000; // 30 days
212+
213+
var $stats_callback = null;
214+
215+
var $connection_errors = array();
216+
217+
var $time_start = 0;
218+
var $time_total = 0;
219+
var $size_total = 0;
220+
var $slow_op_microseconds = 0.005; // 5 ms
221+
222+
function add( $id, $data, $group = 'default', $expire = 0 ) {
223+
$key = $this->key( $id, $group );
224+
225+
if ( is_object( $data ) ) {
226+
$data = clone $data;
227+
}
260228

261-
/**
262-
* Holds the Redis client.
263-
*
264-
* @var Redis
265-
*/
266-
private $redis;
229+
if ( in_array( $group, $this->no_mc_groups ) ) {
230+
if ( ! isset( $this->cache[ $key ] ) ) {
231+
$this->cache[ $key ] = [
232+
'value' => $data,
233+
'found' => false,
234+
];
267235

268-
/**
269-
* Track if Redis is available
270-
*
271-
* @var bool
272-
*/
273-
private $redis_connected = false;
236+
return true;
237+
}
274238

275-
/**
276-
* Local cache
277-
*
278-
* @var array
279-
*/
280-
public $cache = array();
239+
return false;
240+
}
281241

282-
private $to_unserialize = array();
242+
if ( isset( $this->cache[ $key ][ 'value' ] ) && false !== $this->cache[ $key ][ 'value' ] ) {
243+
return false;
244+
}
283245

284-
public $to_preload = array();
246+
$mc = $this->get_mc( $group );
285247

286-
/**
287-
* List of global groups.
288-
*
289-
* @var array
290-
*/
291-
public $global_groups = array(
292-
'blog-details',
293-
'blog-id-cache',
294-
'blog-lookup',
295-
'global-posts',
296-
'networks',
297-
'rss',
298-
'sites',
299-
'site-details',
300-
'site-lookup',
301-
'site-options',
302-
'site-transient',
303-
'users',
304-
'useremail',
305-
'userlogins',
306-
'usermeta',
307-
'user_meta',
308-
'userslugs',
309-
);
310-
311-
private $_global_groups;
248+
$expire = intval( $expire );
249+
if ( 0 === $expire || $expire > $this->max_expiration ) {
250+
$expire = $this->default_expiration;
251+
}
312252

313-
/**
314-
* List of groups not saved to Redis.
315-
*
316-
* @var array
317-
*/
318-
public $no_redis_groups = array( 'comment', 'counts' );
253+
$size = $this->get_data_size( $data );
254+
$this->timer_start();
255+
$result = $mc->add( $key, $data, false, $expire );
256+
$elapsed = $this->timer_stop();
319257

320-
/**
321-
* Prefix used for global groups.
322-
*
323-
* @var string
324-
*/
325-
public $global_prefix = '';
258+
$comment = '';
259+
if ( isset( $this->cache[ $key ] ) ) {
260+
$comment .= ' [lc already]';
261+
}
262+
if ( false === $result ) {
263+
$comment .= ' [mc already]';
264+
}
326265

327-
/**
328-
* Prefix used for non-global groups.
329-
*
330-
* @var string
331-
*/
332-
public $blog_prefix = '';
266+
$this->group_ops_stats( 'add', $key, $group, $size, $elapsed, $comment );
267+
268+
if ( false !== $result ) {
269+
$this->cache[ $key ] = [
270+
'value' => $data,
271+
'found' => true,
272+
];
273+
} else if ( false === $result && true === isset( $this->cache[$key][ 'value' ] ) && false === $this->cache[$key][ 'value' ] ) {
274+
/*
275+
* Here we unset local cache if remote add failed and local cache value is equal to `false` in order
276+
* to update the local cache anytime we get a new information from remote server. This way, the next
277+
* cache get will go to remote server and will fetch recent data.
278+
*/
279+
unset( $this->cache[$key] );
280+
}
333281

334-
/**
335-
* Track how many requests were found in cache
336-
*
337-
* @var int
338-
*/
339-
public $cache_hits = 0;
282+
return $result;
283+
}
340284

341-
/**
342-
* Track how may requests were not cached
343-
*
344-
* @var int
345-
*/
346-
public $cache_misses = 0;
285+
public function add_multiple( array $data, $group = '', $expire = 0 ) {
286+
$values = array();
347287

348-
private $multisite;
288+
foreach ( $data as $key => $value ) {
289+
$values[ $key ] = $this->add( $key, $value, $group, $expire );
290+
}
349291

350-
public $stats = array();
292+
return $values;
293+
}
351294

352-
/**
353-
* Instantiate the Redis class.
354-
*
355-
* Instantiates the Redis class.
356-
*
357-
* @param null $persistent_id To create an instance that persists between requests, use persistent_id to specify a unique ID for the instance.
358-
*/
359-
public function __construct( $redis_instance = null ) {
360-
// General Redis settings
361-
$redis = array(
362-
'host' => '127.0.0.1',
363-
'port' => 6379,
364-
);
295+
function add_global_groups( $groups ) {
296+
if ( ! is_array( $groups ) ) {
297+
$groups = (array) $groups;
298+
}
365299

366-
if ( defined( 'WP_REDIS_BACKEND_HOST' ) && WP_REDIS_BACKEND_HOST ) {
367-
$redis['host'] = WP_REDIS_BACKEND_HOST;
300+
$this->global_groups = array_merge( $this->global_groups, $groups );
301+
$this->global_groups = array_unique( $this->global_groups );
302+
}
303+
304+
function add_non_persistent_groups( $groups ) {
305+
if ( ! is_array( $groups ) ) {
306+
$groups = (array) $groups;
368307
}
369-
if ( defined( 'WP_REDIS_BACKEND_PORT' ) && WP_REDIS_BACKEND_PORT ) {
370-
$redis['port'] = WP_REDIS_BACKEND_PORT;
308+
309+
$this->no_mc_groups = array_merge( $this->no_mc_groups, $groups );
310+
$this->no_mc_groups = array_unique( $this->no_mc_groups );
311+
}
312+
313+
function incr( $id, $n = 1, $group = 'default' ) {
314+
$key = $this->key( $id, $group );
315+
$mc = $this->get_mc( $group );
316+
317+
$incremented = $mc->increment( $key, $n );
318+
319+
$this->cache[ $key ] = [
320+
'value' => $incremented,
321+
'found' => false !== $incremented,
322+
];
323+
324+
return $this->cache[ $key ][ 'value' ];
325+
}
326+
327+
function decr( $id, $n = 1, $group = 'default' ) {
328+
$key = $this->key( $id, $group );
329+
$mc = $this->get_mc( $group );
330+
331+
$decremented = $mc->decrement( $key, $n );
332+
$this->cache[ $key ] = [
333+
'value' => $decremented,
334+
'found' => false !== $decremented,
335+
];
336+
337+
return $this->cache[ $key ][ 'value' ];
338+
}
339+
340+
function close() {
341+
foreach ( $this->mc as $bucket => $mc ) {
342+
$mc->close();
371343
}
372-
if ( defined( 'WP_REDIS_BACKEND_AUTH' ) && WP_REDIS_BACKEND_AUTH ) {
373-
$redis['auth'] = WP_REDIS_BACKEND_AUTH;
344+
}
345+
346+
function delete( $id, $group = 'default' ) {
347+
$key = $this->key( $id, $group );
348+
349+
if ( in_array( $group, $this->no_mc_groups ) ) {
350+
unset( $this->cache[ $key ] );
351+
352+
return true;
374353
}
375-
if ( defined( 'WP_REDIS_BACKEND_DB' ) && WP_REDIS_BACKEND_DB ) {
376-
$redis['database'] = WP_REDIS_BACKEND_DB;
354+
355+
$mc = $this->get_mc( $group );
356+
357+
$this->timer_start();
358+
$result = $mc->delete( $key );
359+
$elapsed = $this->timer_stop();
360+
361+
$this->group_ops_stats( 'delete', $key, $group, null, $elapsed );
362+
363+
if ( false !== $result ) {
364+
unset( $this->cache[ $key ] );
377365
}
378366

379-
// Use Redis PECL library.
380-
try {
381-
if ( is_null( $redis_instance ) ) {
382-
$redis_instance = new Redis();
367+
return $result;
368+
}
369+
370+
public function delete_multiple( array $keys, $group = '' ) {
371+
$values = array();
372+
373+
foreach ( $keys as $key ) {
374+
$values[ $key ] = $this->delete( $key, $group );
375+
}
376+
377+
return $values;
378+
}
379+
380+
// Gets number from all default servers, replicating if needed
381+
function get_max_flush_number( $group ) {
382+
$key = $this->key( $this->flush_key, $group );
383+
384+
$values = array();
385+
$size = 19; // size of microsecond timestamp serialized
386+
foreach ( $this->default_mcs as $i => $mc ) {
387+
$flags = false;
388+
$this->timer_start();
389+
$values[ $i ] = $mc->get( $key, $flags );
390+
$elapsed = $this->timer_stop();
391+
392+
if ( empty( $values[ $i ] ) ) {
393+
$this->group_ops_stats( 'get_flush_number', $key, $group, null, $elapsed, 'not_in_memcache' );
394+
} else {
395+
$this->group_ops_stats( 'get_flush_number', $key, $group, $size, $elapsed, 'memcache' );
383396
}
384-
$this->redis = $redis_instance;
385-
$this->redis->connect( $redis['host'], $redis['port'] );
386-
$this->redis->setOption( Redis::OPT_SERIALIZER, (string) Redis::SERIALIZER_PHP );
397+
}
398+
399+
$max = max( $values );
400+
401+
if ( ! $max > 0 ) {
402+
return false;
403+
}
387404

388-
if ( isset( $redis['auth'] ) ) {
389-
$this->redis->auth( $redis['auth'] );
405+
// Replicate to servers not having the max.
406+
$expire = 0;
407+
foreach ( $this->default_mcs as $i => $mc ) {
408+
if ( $values[ $i ] < $max ) {
409+
$this->timer_start();
410+
$mc->set( $key, $max, false, $expire );
411+
$elapsed = $this->timer_stop();
412+
$this->group_ops_stats( 'set_flush_number', $key, $group, $size, $elapsed, 'replication_repair' );
390413
}
414+
}
415+
416+
return $max;
417+
}
418+
419+
function set_flush_number( $value, $group ) {
420+
$key = $this->key( $this->flush_key, $group );
421+
$expire = 0;
422+
$size = 19;
423+
foreach ( $this->default_mcs as $i => $mc ) {
424+
$this->timer_start();
425+
$mc->set( $key, $value, false, $expire );
426+
$elapsed = $this->timer_stop();
427+
$this->group_ops_stats( 'set_flush_number', $key, $group, $size, $elapsed, 'replication' );
428+
}
429+
}
391430

392-
if ( isset( $redis['database'] ) ) {
393-
$this->redis->select( $redis['database'] );
431+
function get_flush_number( $group ) {
432+
$flush_number = $this->get_max_flush_number( $group );
433+
// What if there is no v4 flush number?
434+
if ( empty( $flush_number ) ) {
435+
// Look for the v3 flush number key.
436+
$flush_number = intval( $this->get( $this->old_flush_key, $group ) );
437+
if ( !empty( $flush_number ) ) {
438+
// Found v3 flush number. Upgrade to v4 with replication.
439+
$this->set_flush_number( $flush_number, $group );
440+
// Delete v3 key so we can't later restore it and find stale keys.
441+
} else {
442+
// No flush number found anywhere. Make a new one. This flushes the cache.
443+
$flush_number = $this->new_flush_number();
444+
$this->set_flush_number( $flush_number, $group );
394445
}
446+
}
447+
448+
return $flush_number;
449+
}
395450

396-
$this->redis_connected = true;
397-
} catch ( RedisException $e ) {
398-
// When Redis is unavailable, fall back to the internal back by forcing all groups to be "no redis" groups
399-
$this->no_redis_groups = array_unique( array_merge( $this->no_redis_groups, $this->global_groups ) );
400-
$this->redis_connected = false;
451+
function get_global_flush_number() {
452+
if ( ! isset( $this->global_flush_number ) ) {
453+
$this->global_flush_number = $this->get_flush_number( $this->global_flush_group );
401454
}
455+
return $this->global_flush_number;
456+
}
402457

403-
/**
404-
* This approach is borrowed from Sivel and Boren. Use the salt for easy cache invalidation and for
405-
* multi single WP installs on the same server.
406-
*/
407-
if ( ! defined( 'WP_CACHE_KEY_SALT' ) ) {
408-
define( 'WP_CACHE_KEY_SALT', '' );
458+
function get_blog_flush_number() {
459+
if ( ! isset( $this->flush_number[ $this->blog_prefix ] ) ) {
460+
$this->flush_number[ $this->blog_prefix ] = $this->get_flush_number( $this->flush_group );
409461
}
462+
return $this->flush_number[ $this->blog_prefix ];
463+
}
410464

411-
$this->multisite = is_multisite();
412-
$this->blog_prefix = $this->multisite ? get_current_blog_id() . ':' : '';
413-
$this->_global_groups = array_flip( $this->global_groups );
465+
function flush_runtime() {
466+
$this->cache = array();
467+
$this->group_ops = array();
414468

415-
$this->maybe_preload();
469+
return true;
416470
}
417471

418-
public function maybe_preload() {
419-
if ( ! $this->can_redis() || empty( $_SERVER['REQUEST_URI'] ) ) {
420-
return;
472+
function flush() {
473+
// Do not use the memcached flush method. It acts on an
474+
// entire memcached server, affecting all sites.
475+
// Flush is also unusable in some setups, e.g. twemproxy.
476+
// Instead, rotate the key prefix for the current site.
477+
// Global keys are rotated when flushing on any network's
478+
// main site.
479+
$this->cache = array();
480+
481+
$flush_number = $this->new_flush_number();
482+
483+
$this->rotate_site_keys( $flush_number );
484+
485+
if ( function_exists( 'is_main_site' ) && is_main_site() ) {
486+
$this->rotate_global_keys( $flush_number );
421487
}
422488

423-
if ( defined( 'WP_CLI' ) && WP_CLI ) {
424-
return;
489+
return true;
490+
}
491+
492+
function rotate_site_keys( $flush_number = null ) {
493+
if ( is_null( $flush_number ) ) {
494+
$flush_number = $this->new_flush_number();
425495
}
426496

427-
$request_hash = md5( json_encode( array(
428-
$_SERVER['HTTP_HOST'],
429-
$_SERVER['REQUEST_URI'],
430-
) ) );
497+
$this->set_flush_number( $flush_number, $this->flush_group );
431498

432-
$this->preload( $request_hash );
499+
$this->flush_number[ $this->blog_prefix ] = $flush_number;
500+
}
433501

434-
if ( defined( 'DOING_TESTS' ) && DOING_TESTS ) {
435-
return $request_hash;
502+
function rotate_global_keys( $flush_number = null ) {
503+
if ( is_null( $flush_number ) ) {
504+
$flush_number = $this->new_flush_number();
436505
}
437506

438-
register_shutdown_function( array( $this, 'save_preloads' ), $request_hash );
507+
$this->set_flush_number( $flush_number, $this->global_flush_group );
508+
509+
$this->global_flush_number = $flush_number;
439510
}
440511

441-
public function preload( $hash ) {
442-
$keys = $this->get( $hash, 'pj-preload' );
443-
if ( is_array( $keys ) ) {
444-
$this->get_multi( $keys, false );
445-
}
512+
function new_flush_number() {
513+
return intval( microtime( true ) * 1e6 );
446514
}
447515

448-
public function save_preloads( $hash ) {
449-
$keys = array();
516+
function get( $id, $group = 'default', $force = false, &$found = null ) {
517+
$key = $this->key( $id, $group );
518+
$mc = $this->get_mc( $group );
519+
$found = true;
450520

451-
foreach ( $this->to_preload as $group => $_keys ) {
452-
if ( $group === 'pj-preload' ) {
453-
continue;
521+
if ( isset( $this->cache[ $key ] ) && ( ! $force || in_array( $group, $this->no_mc_groups ) ) ) {
522+
if ( isset( $this->cache[ $key ][ 'value' ] ) && is_object( $this->cache[ $key ][ 'value' ] ) ) {
523+
$value = clone $this->cache[ $key ][ 'value' ];
524+
} else {
525+
$value = $this->cache[ $key ][ 'value' ];
454526
}
527+
$found = $this->cache[ $key ][ 'found' ];
455528

456-
if ( in_array( $group, $this->no_redis_groups ) ) {
457-
continue;
529+
$this->group_ops_stats( 'get_local', $key, $group, null, null, 'local' );
530+
} else if ( in_array( $group, $this->no_mc_groups ) ) {
531+
$this->cache[ $key ] = [
532+
'value' => $value = false,
533+
'found' => false,
534+
];
535+
536+
$found = false;
537+
538+
$this->group_ops_stats( 'get_local', $key, $group, null, null, 'not_in_local' );
539+
} else {
540+
$flags = false;
541+
$this->timer_start();
542+
$value = $mc->get( $key, $flags );
543+
$elapsed = $this->timer_stop();
544+
545+
// Value will be unchanged if the key doesn't exist.
546+
if ( false === $flags ) {
547+
$found = false;
548+
$value = false;
549+
} elseif ( false === $value && ( $flags & 0xFF01 ) === 0x01 ) {
550+
/*
551+
* The lowest byte is used for flags.
552+
* 0x01 means the value is serialized (MMC_SERIALIZED).
553+
* The second lowest indicates data type: 0 is string, 1 is bool, 3 is long, 7 is double.
554+
* `null` is serialized into a string, thus $flags is 0x0001
555+
* Empty string will correspond to $flags = 0x0000 (not serialized).
556+
* For `false` or `true` $flags will be 0x0100
557+
*
558+
* See:
559+
* - https://github.com/websupport-sk/pecl-memcache/blob/2a5de3c5d9c0bd0acbcf7e6e0b7570f15f89f55b/php7/memcache_pool.h#L61-L76 - PHP 7.x
560+
* - https://github.com/websupport-sk/pecl-memcache/blob/ccf702b14b18fce18a1863e115a7b4c964df952e/src/memcache_pool.h#L57-L76 - PHP 8.x
561+
*
562+
* In PHP 8, they changed the way memcache_get() handles `null`:
563+
* https://github.com/websupport-sk/pecl-memcache/blob/ccf702b14b18fce18a1863e115a7b4c964df952e/src/memcache.c#L2175-L2177
564+
*
565+
* 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.
566+
*/
567+
$value = null;
458568
}
459569

460-
$_keys = array_keys( $_keys );
461-
$keys[ $group ] = $_keys;
570+
$this->cache[ $key ] = [
571+
'value' => $value,
572+
'found' => $found,
573+
];
574+
575+
if ( is_null( $value ) || $value === false ) {
576+
$this->group_ops_stats( 'get', $key, $group, null, $elapsed, 'not_in_memcache' );
577+
} else if ( 'checkthedatabaseplease' === $value ) {
578+
$this->group_ops_stats( 'get', $key, $group, null, $elapsed, 'checkthedatabaseplease' );
579+
} else {
580+
$size = $this->get_data_size( $value );
581+
$this->group_ops_stats( 'get', $key, $group, $size, $elapsed, 'memcache' );
582+
}
583+
}
584+
585+
if ( 'checkthedatabaseplease' === $value ) {
586+
unset( $this->cache[ $key ] );
587+
588+
$found = false;
589+
$value = false;
462590
}
463591

464-
$this->set( $hash, $keys, 'pj-preload' );
592+
return $value;
465593
}
466594

467-
/**
468-
* Is Redis available?
469-
*
470-
* @return bool
471-
*/
472-
protected function can_redis() {
473-
return $this->redis_connected;
595+
function get_multi( $groups ) {
596+
/*
597+
format: $get['group-name'] = array( 'key1', 'key2' );
598+
*/
599+
$return = array();
600+
$return_cache = array(
601+
'value' => false,
602+
'found' => false,
603+
);
604+
605+
foreach ( $groups as $group => $ids ) {
606+
$mc = $this->get_mc( $group );
607+
$keys = array();
608+
$this->timer_start();
609+
610+
foreach ( $ids as $id ) {
611+
$key = $this->key( $id, $group );
612+
$keys[] = $key;
613+
614+
if ( isset( $this->cache[ $key ] ) ) {
615+
if ( is_object( $this->cache[ $key ][ 'value'] ) ) {
616+
$return[ $key ] = clone $this->cache[ $key ][ 'value'];
617+
$return_cache[ $key ] = [
618+
'value' => clone $this->cache[ $key ][ 'value'],
619+
'found' => $this->cache[ $key ][ 'found'],
620+
];
621+
} else {
622+
$return[ $key ] = $this->cache[ $key ][ 'value'];
623+
$return_cache[ $key ] = [
624+
'value' => $this->cache[ $key ][ 'value' ],
625+
'found' => $this->cache[ $key ][ 'found' ],
626+
];
627+
}
628+
629+
continue;
630+
} else if ( in_array( $group, $this->no_mc_groups ) ) {
631+
$return[ $key ] = false;
632+
$return_cache[ $key ] = [
633+
'value' => false,
634+
'found' => false,
635+
];
636+
637+
continue;
638+
} else {
639+
$fresh_get = $mc->get( $key );
640+
$return[ $key ] = $fresh_get;
641+
$return_cache[ $key ] = [
642+
'value' => $fresh_get,
643+
'found' => false !== $fresh_get,
644+
];
645+
}
646+
}
647+
648+
$elapsed = $this->timer_stop();
649+
$this->group_ops_stats( 'get_multi', $keys, $group, null, $elapsed );
650+
}
651+
652+
$this->cache = array_merge( $this->cache, $return_cache );
653+
654+
return $return;
474655
}
475656

476-
/**
477-
* Adds a value to cache.
478-
*
479-
* If the specified key already exists, the value is not stored and the function
480-
* returns false.
481-
*
482-
* @param string $key The key under which to store the value.
483-
* @param mixed $value The value to store.
484-
* @param string $group The group value appended to the $key.
485-
* @param int $expiration The expiration time, defaults to 0.
486-
* @return bool Returns TRUE on success or FALSE on failure.
487-
*/
488-
public function add( $_key, $value, $group, $expiration = 0 ) {
489-
if ( wp_suspend_cache_addition() ) {
490-
return false;
657+
public function get_multiple( $keys, $group = 'default', $force = false ) {
658+
$mc = $this->get_mc( $group );
659+
660+
$no_mc = in_array( $group, $this->no_mc_groups, true );
661+
662+
$uncached_keys = array();
663+
$return = array();
664+
$return_cache = array();
665+
666+
foreach ( $keys as $id ) {
667+
$key = $this->key( $id, $group );
668+
669+
if ( isset( $this->cache[ $key ] ) && ( ! $force || $no_mc ) ) {
670+
$value = $this->cache[ $key ]['found'] ? $this->cache[ $key ]['value'] : false;
671+
$return[ $id ] = is_object( $value ) ? clone $value : $value;
672+
} else if ( $no_mc ) {
673+
$return[ $id ] = false;
674+
$return_cache[ $key ] = [
675+
'value' => false,
676+
'found' => false,
677+
];
678+
} else {
679+
$uncached_keys[ $id ] = $key;
680+
}
491681
}
492682

493-
list( $key, $redis_key ) = $this->build_key( $_key, $group );
683+
if ( $uncached_keys ) {
684+
$this->timer_start();
685+
$uncached_keys_list = array_values( $uncached_keys );
686+
$values = $mc->get( $uncached_keys_list );
687+
$elapsed = $this->timer_stop();
494688

495-
if ( isset( $this->cache[ $group ], $this->cache[ $group ][ $key ] ) && false !== $this->cache[ $group ][ $key ] ) {
496-
return false;
689+
$this->group_ops_stats( 'get_multiple', $uncached_keys_list, $group, null, $elapsed );
690+
691+
foreach ( $uncached_keys as $id => $key ) {
692+
$found = array_key_exists( $key, $values );
693+
$value = $found ? $values[ $key ] : false;
694+
695+
$return[ $id ] = $value;
696+
$return_cache[ $key ] = [
697+
'value' => $value,
698+
'found' => $found,
699+
];
700+
}
497701
}
498702

499-
return $this->set( $_key, $value, $group, $expiration );
703+
$this->cache = array_merge( $this->cache, $return_cache );
704+
705+
return $return;
500706
}
501707

502-
/**
503-
* Replace a value in the cache.
504-
*
505-
* If the specified key doesn't exist, the value is not stored and the function
506-
* returns false.
507-
*
508-
* @param string $key The key under which to store the value.
509-
* @param mixed $value The value to store.
510-
* @param string $group The group value appended to the $key.
511-
* @param int $expiration The expiration time, defaults to 0.
512-
* @return bool Returns TRUE on success or FALSE on failure.
513-
*/
514-
public function replace( $_key, $value, $group, $expiration = 0 ) {
515-
list( $key, $redis_key ) = $this->build_key( $_key, $group );
708+
function flush_prefix( $group ) {
709+
if ( $group === $this->flush_group || $group === $this->global_flush_group ) {
710+
// Never flush the flush numbers.
711+
$number = '_';
712+
} elseif ( false !== array_search( $group, $this->global_groups ) ) {
713+
$number = $this->get_global_flush_number();
714+
} else {
715+
$number = $this->get_blog_flush_number();
716+
}
717+
return $number . ':';
718+
}
516719

517-
// If group is a non-Redis group, save to internal cache, not Redis
518-
if ( in_array( $group, $this->no_redis_groups ) || ! $this->can_redis() ) {
519-
if ( ! isset( $this->cache[ $group ], $this->cache[ $group ][ $key ] ) ) {
520-
return false;
521-
}
720+
function key( $key, $group ) {
721+
if ( empty( $group ) ) {
722+
$group = 'default';
723+
}
724+
725+
$prefix = $this->key_salt;
726+
727+
$prefix .= $this->flush_prefix( $group );
728+
729+
if ( false !== array_search( $group, $this->global_groups ) ) {
730+
$prefix .= $this->global_prefix;
522731
} else {
523-
if ( ! $this->redis->exists( $redis_key ) ) {
524-
return false;
525-
}
732+
$prefix .= $this->blog_prefix;
526733
}
527734

528-
return $this->set( $_key, $value, $group, $expiration );
735+
return preg_replace( '/\s+/', '', "$prefix:$group:$key" );
529736
}
530737

531-
/**
532-
* Remove the item from the cache.
533-
*
534-
* @param string $key The key under which to store the value.
535-
* @param string $group The group value appended to the $key.
536-
* @return bool Returns TRUE on success or FALSE on failure.
537-
*/
538-
public function delete( $_key, $group ) {
539-
list( $key, $redis_key ) = $this->build_key( $_key, $group );
738+
function replace( $id, $data, $group = 'default', $expire = 0 ) {
739+
$key = $this->key( $id, $group );
740+
$expire = intval( $expire );
741+
if ( 0 === $expire || $expire > $this->max_expiration ) {
742+
$expire = $this->default_expiration;
743+
}
744+
$mc = $this->get_mc( $group );
540745

541-
if ( in_array( $group, $this->no_redis_groups ) || ! $this->can_redis() ) {
542-
if ( ! isset( $this->cache[ $group ], $this->cache[ $group ][ $key ] ) ) {
543-
return false;
544-
}
746+
if ( is_object( $data ) ) {
747+
$data = clone $data;
748+
}
749+
750+
$size = $this->get_data_size( $data );
751+
$this->timer_start();
752+
$result = $mc->replace( $key, $data, false, $expire );
753+
$elapsed = $this->timer_stop();
754+
$this->group_ops_stats( 'replace', $key, $group, $size, $elapsed );
755+
756+
if ( false !== $result ) {
757+
$this->cache[ $key ] = [
758+
'value' => $data,
759+
'found' => true,
760+
];
761+
}
762+
763+
return $result;
764+
}
765+
766+
function set( $id, $data, $group = 'default', $expire = 0 ) {
767+
$key = $this->key( $id, $group );
768+
769+
if ( isset( $this->cache[ $key ] ) && ( 'checkthedatabaseplease' === $this->cache[ $key ][ 'value' ] ) ) {
770+
return false;
771+
}
772+
773+
if ( is_object( $data ) ) {
774+
$data = clone $data;
775+
}
776+
777+
$this->cache[ $key ] = [
778+
'value' => $data,
779+
'found' => false, // Set to false as not technically found in memcache at this point.
780+
];
781+
782+
if ( in_array( $group, $this->no_mc_groups ) ) {
783+
$this->group_ops_stats( 'set_local', $key, $group, null, null );
545784

546-
unset( $this->cache[ $group ][ $key ] );
547-
unset( $this->to_preload[ $group ][ $key ] );
548-
unset( $this->to_unserialize[ $redis_key ] );
549785
return true;
550786
}
551787

552-
unset( $this->cache[ $group ][ $key ] );
553-
unset( $this->to_preload[ $group ][ $key ] );
554-
unset( $this->to_unserialize[ $redis_key ] );
788+
$expire = intval( $expire );
789+
if ( 0 === $expire || $expire > $this->max_expiration ) {
790+
$expire = $this->default_expiration;
791+
}
792+
793+
$mc = $this->get_mc( $group );
794+
795+
$size = $this->get_data_size( $data );
796+
$this->timer_start();
797+
$result = $mc->set( $key, $data, false, $expire );
798+
$elapsed = $this->timer_stop();
799+
$this->group_ops_stats( 'set', $key, $group, $size, $elapsed );
555800

556-
return (bool) $this->redis->del( $redis_key );
801+
// Update the found cache value with the result of the set in memcache.
802+
$this->cache[ $key ][ 'found' ] = $result;
803+
804+
return $result;
557805
}
558806

559-
/**
560-
* Invalidate all items in the cache.
561-
*
562-
* @return bool
563-
*/
564-
public function flush() {
565-
$this->cache = array();
566-
$this->to_preload = array();
567-
$this->to_unserialize = array();
807+
public function set_multiple( array $data, $group = '', $expire = 0 ) {
808+
$values = array();
568809

569-
if ( $this->can_redis() ) {
570-
$this->redis->flushDb();
810+
foreach ( $data as $key => $value ) {
811+
$values[ $key ] = $this->set( $key, $value, $group, $expire );
571812
}
572813

573-
return true;
814+
return $values;
574815
}
575816

576-
/**
577-
* Retrieve object from cache.
578-
*
579-
* Gets an object from cache based on $key and $group.
580-
*
581-
* @param string $key The key under which to store the value.
582-
* @param string $group The group value appended to the $key.
583-
* @return bool|mixed Cached object value.
584-
*/
585-
public function get( $_key, $group = 'default', $force = false ) {
586-
list( $key, $redis_key ) = $this->build_key( $_key, $group );
817+
function switch_to_blog( $blog_id ) {
818+
global $table_prefix;
587819

588-
$this->to_preload[ $group ][ $_key ] = true;
820+
$blog_id = (int) $blog_id;
589821

590-
if ( ! $force && isset( $this->cache[ $group ][ $key ] ) ) {
591-
$value = $this->cache[ $group ][ $key ];
822+
$this->blog_prefix = ( is_multisite() ? $blog_id : $table_prefix );
823+
}
592824

593-
if ( isset( $this->to_unserialize[ $redis_key ] ) ) {
594-
unset( $this->to_unserialize[ $redis_key ] );
825+
function colorize_debug_line( $line, $trailing_html = '' ) {
826+
$colors = array(
827+
'get' => 'green',
828+
'get_local' => 'lightgreen',
829+
'get_multi' => 'fuchsia',
830+
'get_multiple' => 'navy',
831+
'set' => 'purple',
832+
'set_local' => 'orchid',
833+
'add' => 'blue',
834+
'delete' => 'red',
835+
'delete_local' => 'tomato',
836+
'slow-ops' => 'crimson',
837+
);
595838

596-
if ( is_string( $value ) ) {
597-
$value = unserialize( $value );
598-
}
839+
$cmd = substr( $line, 0, strpos( $line, ' ' ) );
599840

600-
$this->cache[ $group ][ $key ] = $value;
601-
}
841+
// Start off with a neutral default color...
842+
$color_for_cmd = 'brown';
843+
// And if the cmd has a specific color, use that instead
844+
if ( isset( $colors[ $cmd ] ) ) {
845+
$color_for_cmd = $colors[ $cmd ];
846+
}
602847

603-
$this->cache_hits += 1;
848+
$cmd2 = "<span style='color:" . esc_attr( $color_for_cmd ) . "; font-weight: bold;'>" . esc_html( $cmd ) . "</span>";
604849

605-
return is_object( $value ) ? clone $value : $value;
606-
}
850+
return $cmd2 . esc_html( substr( $line, strlen( $cmd ) ) ) . "$trailing_html\n";
851+
}
607852

608-
if ( in_array( $group, $this->no_redis_groups ) || ! $this->can_redis() ) {
609-
$this->cache_misses += 1;
610-
return false;
611-
}
853+
function js_toggle() {
854+
echo "
855+
<script>
856+
function memcachedToggleVisibility( id, hidePrefix ) {
857+
var element = document.getElementById( id );
858+
if ( ! element ) {
859+
return;
860+
}
612861
613-
// Fetch from Redis
614-
$value = $this->redis->get( $redis_key );
862+
// Hide all element with `hidePrefix` if given. Used to display only one element at a time.
863+
if ( hidePrefix ) {
864+
var groupStats = document.querySelectorAll( '[id^=\"' + hidePrefix + '\"]' );
865+
groupStats.forEach(
866+
function ( element ) {
867+
element.style.display = 'none';
868+
}
869+
);
870+
}
615871
616-
if ( ! is_string( $value ) ) {
617-
$this->cache[ $group ][ $key ] = false;
618-
$this->cache_misses += 1;
619-
return false;
872+
// Toggle the one we clicked.
873+
if ( 'none' === element.style.display ) {
874+
element.style.display = 'block';
875+
} else {
876+
element.style.display = 'none';
877+
}
620878
}
621-
622-
$value = is_numeric( $value ) ? $value : unserialize( $value );
623-
$this->cache[ $group ][ $key ] = $value;
624-
$this->cache_hits += 1;
625-
return $value;
879+
</script>
880+
";
626881
}
627882

628883
/**
629-
* Retrieve multiple values from cache.
630-
*
631-
* Gets multiple values from cache, including across multiple groups
884+
* Returns the collected raw stats.
632885
*
633-
* Usage: array( 'group0' => array( 'key0', 'key1', 'key2', ), 'group1' => array( 'key0' ) )
634-
*
635-
* @param array $groups Array of groups and keys to retrieve
636-
* @return bool|mixed Array of cached values, keys in the format $group:$key. Non-existent keys null.
886+
* @return array $stats
637887
*/
638-
public function get_multi( $groups, $unserialize = true ) {
639-
if ( empty( $groups ) || ! is_array( $groups ) ) {
640-
return false;
641-
}
888+
function get_stats() {
889+
$stats = [];
890+
$stats['totals'] = [
891+
'query_time' => $this->time_total,
892+
'size' => $this->size_total,
893+
];
894+
$stats['operation_counts'] = $this->stats;
895+
$stats['operations'] = [];
896+
$stats['groups'] = [];
897+
$stats['slow-ops'] = [];
898+
$stats['slow-ops-groups'] = [];
899+
foreach ( $this->group_ops as $cache_group => $dataset ) {
900+
if ( empty( $cache_group ) ) {
901+
$cache_group = 'default';
902+
}
903+
904+
foreach ( $dataset as $data ) {
905+
$operation = $data[0];
906+
$op = [
907+
'key' => $data[1],
908+
'size' => $data[2],
909+
'time' => $data[3],
910+
'group' => $cache_group,
911+
'result' => $data[4],
912+
];
913+
914+
if ( $cache_group === 'slow-ops' ) {
915+
$key = 'slow-ops';
916+
$groups_key = 'slow-ops-groups';
917+
$op['group'] = $data[5];
918+
$op['backtrace'] = $data[6];
919+
} else {
920+
$key = 'operations';
921+
$groups_key = 'groups';
922+
}
642923

643-
// Retrieve requested caches and reformat results to mimic Memcached Object Cache's output
644-
$cache = array();
645-
$fetch_keys = array();
646-
$map = array();
924+
$stats[ $key ][ $operation ][] = $op;
647925

648-
foreach ( $groups as $group => $keys ) {
649-
if ( in_array( $group, $this->no_redis_groups ) || ! $this->can_redis() ) {
650-
foreach ( $keys as $_key ) {
651-
list( $key, $redis_key ) = $this->build_key( $_key, $group );
652-
$cache[ $group ][ $key ] = $this->get( $_key, $group );
926+
if ( ! in_array( $op['group'], $stats[ $groups_key ] ) ) {
927+
$stats[ $groups_key ][] = $op['group'];
653928
}
929+
}
930+
}
931+
932+
return $stats;
933+
}
654934

935+
936+
function stats() {
937+
$this->js_toggle();
938+
939+
echo '<h2><span>Total memcache query time:</span>' . number_format_i18n( sprintf( '%0.1f', $this->time_total * 1000 ), 1 ) . ' ms</h2>';
940+
echo "\n";
941+
echo '<h2><span>Total memcache size:</span>' . esc_html( size_format( $this->size_total, 2 ) ) . '</h2>';
942+
echo "\n";
943+
944+
foreach ( $this->stats as $stat => $n ) {
945+
if ( empty( $n ) ) {
655946
continue;
656947
}
657948

658-
if ( empty( $cache[ $group ] ) ) {
659-
$cache[ $group ] = array();
660-
}
949+
echo '<h2>';
950+
echo $this->colorize_debug_line( "$stat $n" );
951+
echo '</h2>';
952+
}
661953

662-
foreach ( $keys as $_key ) {
663-
list( $key, $redis_key ) = $this->build_key( $_key, $group );
954+
echo "<ul class='debug-menu-links' style='clear:left;font-size:14px;'>\n";
955+
$groups = array_keys( $this->group_ops );
956+
usort( $groups, 'strnatcasecmp' );
957+
958+
$active_group = $groups[0];
959+
// Always show `slow-ops` first
960+
if ( in_array( 'slow-ops', $groups ) ) {
961+
$slow_ops_key = array_search( 'slow-ops', $groups );
962+
$slow_ops = $groups[ $slow_ops_key ];
963+
unset( $groups[ $slow_ops_key ] );
964+
array_unshift( $groups, $slow_ops );
965+
$active_group = 'slow-ops';
966+
}
664967

665-
if ( isset( $this->cache[ $group ][ $key ] ) ) {
666-
$cache[ $group ][ $key ] = $this->cache[ $group ][ $key ];
667-
continue;
668-
}
968+
$total_ops = 0;
969+
$group_titles = array();
970+
foreach ( $groups as $group ) {
971+
$group_name = $group;
972+
if ( empty( $group_name ) ) {
973+
$group_name = 'default';
974+
}
975+
$group_ops = count( $this->group_ops[ $group ] );
976+
$group_size = size_format( array_sum( array_map( function ( $op ) { return $op[2]; }, $this->group_ops[ $group ] ) ), 2 );
977+
$group_time = number_format_i18n( sprintf( '%0.1f', array_sum( array_map( function ( $op ) { return $op[3]; }, $this->group_ops[ $group ] ) ) * 1000 ), 1 );
978+
$total_ops += $group_ops;
979+
$group_title = "{$group_name} [$group_ops][$group_size][{$group_time} ms]";
980+
$group_titles[ $group ] = $group_title;
981+
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";
982+
}
983+
echo "</ul>\n";
669984

670-
// Fetch these from Redis
671-
$map[ $redis_key ] = array( $group, $key );
672-
$fetch_keys[] = $redis_key;
985+
echo "<div id='object-cache-stats-menu-targets'>\n";
986+
foreach ( $groups as $group ) {
987+
$group_name = $group;
988+
if ( empty( $group_name ) ) {
989+
$group_name = 'default';
673990
}
991+
$current = $active_group == $group ? 'style="display: block"' : 'style="display: none"';
992+
echo "<div id='object-cache-stats-menu-target-" . esc_attr( $group_name ) . "' class='object-cache-stats-menu-target' $current>\n";
993+
echo '<h3>' . esc_html( $group_titles[ $group ] ) . '</h3>' . "\n";
994+
echo "<pre>\n";
995+
foreach ( $this->group_ops[ $group ] as $index => $arr ) {
996+
printf( '%3d ', $index );
997+
echo $this->get_group_ops_line( $index, $arr );
998+
}
999+
echo "</pre>\n";
1000+
echo "</div>";
6741001
}
6751002

676-
// Nothing else to fetch
677-
if ( empty( $fetch_keys ) ) {
678-
return $cache;
1003+
echo "</div>";
1004+
}
1005+
1006+
function get_group_ops_line( $index, $arr ) {
1007+
// operation
1008+
$line = "{$arr[0]} ";
1009+
1010+
// key
1011+
$json_encoded_key = json_encode( $arr[1] );
1012+
$line .= $json_encoded_key . " ";
1013+
1014+
// comment
1015+
if ( ! empty( $arr[4] ) ) {
1016+
$line .= "{$arr[4]} ";
6791017
}
6801018

681-
$results = $this->redis->mget( $fetch_keys );
682-
foreach( array_combine( $fetch_keys, $results ) as $redis_key => $value ) {
683-
list( $group, $key ) = $map[ $redis_key ];
1019+
// size
1020+
if ( isset( $arr[2] ) ) {
1021+
$line .= '(' . size_format( $arr[2], 2 ) . ') ';
1022+
}
6841023

685-
if ( is_string( $value ) ) {
686-
if ( ! $unserialize && ! is_numeric( $value ) ) {
687-
$this->to_unserialize[ $redis_key ] = true;
688-
} elseif ( $unserialize ) {
689-
$this->to_preload[ $group ][ $key ] = true;
690-
$value = is_numeric( $value ) ? $value : unserialize( $value );
691-
}
692-
} else {
693-
$value = false;
694-
}
1024+
// time
1025+
if ( isset( $arr[3] ) ) {
1026+
$line .= '(' . number_format_i18n( sprintf( '%0.1f', $arr[3] * 1000 ), 1 ) . ' ms)';
1027+
}
6951028

696-
$this->cache[ $group ][ $key ] = $cache[ $group ][ $key ] = $value;
1029+
// backtrace
1030+
$bt_link = '';
1031+
if ( isset( $arr[6] ) ) {
1032+
$key_hash = md5( $index . $json_encoded_key );
1033+
$bt_link = " <small><a href='#' onclick='memcachedToggleVisibility( \"object-cache-stats-debug-$key_hash\" );'>Toggle Backtrace</a></small>";
1034+
$bt_link .= "<pre id='object-cache-stats-debug-$key_hash' style='display:none'>" . esc_html( $arr[6] ) . "</pre>";
6971035
}
6981036

699-
return $cache;
1037+
return $this->colorize_debug_line( $line, $bt_link );
7001038
}
7011039

7021040
/**
703-
* Sets a value in cache.
704-
*
705-
* The value is set whether or not this key already exists in Redis.
706-
*
707-
* @param string $key The key under which to store the value.
708-
* @param mixed $value The value to store.
709-
* @param string $group The group value appended to the $key.
710-
* @param int $expiration The expiration time, defaults to 0.
711-
* @return bool Returns TRUE on success or FALSE on failure.
1041+
* @param int|string $group
1042+
* @return Memcache
7121043
*/
713-
public function set( $_key, $value, $group = 'default', $expiration = 0 ) {
714-
list( $key, $redis_key ) = $this->build_key( $_key, $group );
715-
716-
if ( is_object( $value ) ) {
717-
$value = clone $value;
1044+
function get_mc( $group ) {
1045+
if ( isset( $this->mc[ $group ] ) ) {
1046+
return $this->mc[ $group ];
7181047
}
7191048

720-
$this->cache[ $group ][ $key ] = $value;
1049+
return $this->mc['default'];
1050+
}
7211051

722-
if ( in_array( $group, $this->no_redis_groups ) || ! $this->can_redis() ) {
723-
return true;
1052+
function failure_callback( $host, $port ) {
1053+
$this->connection_errors[] = array(
1054+
'host' => $host,
1055+
'port' => $port,
1056+
);
1057+
}
1058+
1059+
function salt_keys( $key_salt ) {
1060+
if ( strlen( $key_salt ) ) {
1061+
$this->key_salt = $key_salt . ':';
1062+
} else {
1063+
$this->key_salt = '';
7241064
}
1065+
}
7251066

726-
$value = is_numeric( $value ) ? $value : serialize( $value );
1067+
function __construct() {
1068+
global $memcached_servers;
7271069

728-
// Save to Redis
729-
if ( $expiration ) {
730-
$this->redis->setex( $redis_key, $expiration, $value );
1070+
if ( isset( $memcached_servers ) ) {
1071+
$buckets = $memcached_servers;
7311072
} else {
732-
$this->redis->set( $redis_key, $value );
1073+
$buckets = array( '127.0.0.1:11211' );
7331074
}
7341075

735-
return true;
736-
}
1076+
reset( $buckets );
7371077

738-
/**
739-
* Increment a Redis counter by the amount specified
740-
*
741-
* @param string $key
742-
* @param int $offset
743-
* @param string $group
744-
* @return bool
745-
*/
746-
public function incr( $_key, $offset = 1, $group ) {
747-
list( $key, $redis_key ) = $this->build_key( $_key, $group );
1078+
if ( is_int( key( $buckets ) ) ) {
1079+
$buckets = array( 'default' => $buckets );
1080+
}
1081+
1082+
foreach ( $buckets as $bucket => $servers ) {
1083+
$this->mc[ $bucket ] = new Memcache();
1084+
1085+
foreach ( $servers as $i => $server ) {
1086+
if ( 'unix://' == substr( $server, 0, 7 ) ) {
1087+
$node = $server;
1088+
$port = 0;
1089+
} else {
1090+
if ( false === strpos( $server, ':' ) ) {
1091+
$node = $server;
1092+
$port = ini_get( 'memcache.default_port' );
1093+
} else {
1094+
list ( $node, $port ) = explode( ':', $server, 2 );
1095+
}
1096+
1097+
$port = intval( $port );
1098+
1099+
if ( ! $port ) {
1100+
$port = 11211;
1101+
}
1102+
}
1103+
1104+
$this->mc[ $bucket ]->addServer( $node, $port, true, 1, 1, 15, true, array( $this, 'failure_callback' ) );
1105+
$this->mc[ $bucket ]->setCompressThreshold( 20000, 0.2 );
7481106

749-
if ( in_array( $group, $this->no_redis_groups ) || ! $this->can_redis() ) {
750-
// Consistent with the Redis behavior (start from 0 if not exists)
751-
if ( ! isset( $this->cache[ $group ][ $key ] ) ) {
752-
$this->cache[ $group ][ $key ] = 0;
1107+
// Prepare individual connections to servers in default bucket for flush_number redundancy
1108+
if ( 'default' === $bucket ) {
1109+
$this->default_mcs[ $i ] = new Memcache();
1110+
$this->default_mcs[ $i ]->addServer( $node, $port, true, 1, 1, 15, true, array( $this, 'failure_callback' ) );
1111+
}
7531112
}
1113+
}
7541114

755-
$this->cache[ $group ][ $key ] += $offset;
756-
return true;
1115+
global $blog_id, $table_prefix;
1116+
1117+
$this->global_prefix = '';
1118+
$this->blog_prefix = '';
1119+
1120+
if ( function_exists( 'is_multisite' ) ) {
1121+
$this->global_prefix = ( is_multisite() || defined( 'CUSTOM_USER_TABLE' ) && defined( 'CUSTOM_USER_META_TABLE' ) ) ? '' : $table_prefix;
1122+
$this->blog_prefix = ( is_multisite() ? $blog_id : $table_prefix );
7571123
}
7581124

759-
// Save to Redis
760-
$value = $this->redis->incrBy( $redis_key, $offset );
761-
$this->cache[ $group ][ $key ] = $value;
762-
return $value;
1125+
$this->salt_keys( WP_CACHE_KEY_SALT );
1126+
1127+
$this->cache_hits =& $this->stats['get'];
1128+
$this->cache_misses =& $this->stats['add'];
7631129
}
7641130

765-
/**
766-
* Decrement a Redis counter by the amount specified
767-
*
768-
* @param string $key
769-
* @param int $offset
770-
* @param string $group
771-
* @return bool
772-
*/
773-
public function decr( $key, $offset = 1, $group = 'default' ) {
774-
return $this->incr( $key, $offset * -1, $group );
1131+
function increment_stat( $field, $num = 1 ) {
1132+
if ( ! isset( $this->stats[ $field ] ) ) {
1133+
$this->stats[ $field ] = $num;
1134+
} else {
1135+
$this->stats[ $field ] += $num;
1136+
}
7751137
}
7761138

777-
/**
778-
* Builds a key for the cached object using the blog_id, key, and group values.
779-
*
780-
* @author Ryan Boren This function is inspired by the original WP Memcached Object cache.
781-
* @link http://wordpress.org/extend/plugins/memcached/
782-
*
783-
* @param string $key The key under which to store the value.
784-
* @param string $group The group value appended to the $key.
785-
*
786-
* @return array
787-
*/
788-
public function build_key( $key, $group = 'default' ) {
789-
$prefix = '';
790-
if ( ! isset( $this->_global_groups[ $group ] ) ) {
791-
$prefix = $this->blog_prefix;
1139+
function group_ops_stats( $op, $keys, $group, $size, $time, $comment = '' ) {
1140+
$this->increment_stat( $op );
1141+
1142+
// we have no use of the local ops details for now
1143+
if ( strpos( $op, '_local' ) ) {
1144+
return;
7921145
}
7931146

794-
$local_key = $prefix . $key;
795-
return array( $local_key, WP_CACHE_KEY_SALT . "$prefix$group:$key" );
796-
}
1147+
$this->size_total += $size;
7971148

798-
/**
799-
* In multisite, switch blog prefix when switching blogs
800-
*
801-
* @param int $_blog_id
802-
* @return bool
803-
*/
804-
public function switch_to_blog( $blog_id ) {
805-
$this->blog_prefix = $this->multisite ? $blog_id . ':' : '';
1149+
$keys = $this->strip_memcached_keys( $keys );
1150+
1151+
// @codeCoverageIgnoreStart
1152+
if ( $time > $this->slow_op_microseconds && 'get_multi' !== $op ) {
1153+
$this->increment_stat( 'slow-ops' );
1154+
$backtrace = null;
1155+
if ( function_exists( 'wp_debug_backtrace_summary' ) ) {
1156+
$backtrace = wp_debug_backtrace_summary();
1157+
}
1158+
$this->group_ops['slow-ops'][] = array( $op, $keys, $size, $time, $comment, $group, $backtrace );
1159+
}
1160+
// @codeCoverageIgnoreEnd
1161+
1162+
$this->group_ops[ $group ][] = array( $op, $keys, $size, $time, $comment );
8061163
}
8071164

8081165
/**
809-
* Sets the list of global groups.
1166+
* Key format: key_salt:flush_number:table_prefix:key_name
8101167
*
811-
* @param array $groups List of groups that are global.
1168+
* We want to strip the `key_salt:flush_number` part to not leak the memcached keys.
1169+
* If `key_salt` is set we strip `'key_salt:flush_number`, otherwise just strip the `flush_number` part.
8121170
*/
813-
public function add_global_groups( $groups ) {
814-
$groups = (array) $groups;
1171+
function strip_memcached_keys( $keys ) {
1172+
if ( ! is_array( $keys ) ) {
1173+
$keys = [ $keys ];
1174+
}
8151175

816-
if ( $this->can_redis() ) {
817-
$this->global_groups = array_unique( array_merge( $this->global_groups, $groups ) );
818-
} else {
819-
$this->no_redis_groups = array_unique( array_merge( $this->no_redis_groups, $groups ) );
1176+
foreach ( $keys as $key => $value ) {
1177+
$offset = 0;
1178+
if ( ! empty( $this->key_salt ) ) {
1179+
$offset = strpos( $value, ':' ) + 1;
1180+
}
1181+
1182+
$start = strpos( $value, ':', $offset );
1183+
$keys[ $key ] = substr( $value, $start + 1 );
1184+
}
1185+
1186+
if ( 1 === count( $keys ) ) {
1187+
return $keys[0];
8201188
}
8211189

822-
$this->_global_groups = array_flip( $this->global_groups );
1190+
return $keys;
8231191
}
8241192

825-
/**
826-
* Sets the list of groups not to be cached by Redis.
827-
*
828-
* @param array $groups List of groups that are to be ignored.
829-
*/
830-
public function add_non_persistent_groups( $groups ) {
831-
$groups = (array) $groups;
1193+
function timer_start() {
1194+
$this->time_start = microtime( true );
8321195

833-
$this->no_redis_groups = array_unique( array_merge( $this->no_redis_groups, $groups ) );
1196+
return true;
8341197
}
835-
}
8361198

837-
endif; // if 'Redis' class exists
1199+
function timer_stop() {
1200+
$time_total = microtime( true ) - $this->time_start;
1201+
$this->time_total += $time_total;
1202+
1203+
return $time_total;
1204+
}
1205+
1206+
function get_data_size( $data ) {
1207+
if ( is_string( $data ) ) {
1208+
return strlen( $data );
1209+
}
1210+
1211+
$serialized = serialize( $data );
1212+
1213+
return strlen( $serialized );
1214+
}
1215+
}

0 commit comments

Comments
 (0)
Please sign in to comment.