|
2 | 2 | /*
|
3 | 3 | Plugin Name: Object Cache
|
4 | 4 | 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 |
7 | 7 | Author: LittleBizzy
|
8 | 8 | Author URI: https://www.littlebizzy.com
|
9 | 9 | License: GPLv3
|
10 | 10 | 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 |
19 | 16 | */
|
20 | 17 |
|
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; |
22 | 26 |
|
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 ); |
26 | 28 | }
|
27 | 29 |
|
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; |
30 | 32 |
|
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 = '' ) { |
47 | 37 | 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 ); |
49 | 46 | }
|
50 | 47 |
|
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 |
| - */ |
61 | 48 | function wp_cache_close() {
|
62 |
| - return true; |
| 49 | + global $wp_object_cache; |
| 50 | + |
| 51 | + return $wp_object_cache->close(); |
63 | 52 | }
|
64 | 53 |
|
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 = '' ) { |
77 | 55 | global $wp_object_cache;
|
78 |
| - return $wp_object_cache->decr( $key, $offset, $group ); |
| 56 | + |
| 57 | + return $wp_object_cache->delete( $key, $group ); |
79 | 58 | }
|
80 | 59 |
|
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 = '' ) { |
93 | 61 | global $wp_object_cache;
|
94 |
| - return $wp_object_cache->delete( $key, $group, $time ); |
| 62 | + |
| 63 | + return $wp_object_cache->delete_multiple( $keys, $group ); |
95 | 64 | }
|
96 | 65 |
|
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() { |
107 | 67 | global $wp_object_cache;
|
108 |
| - return $wp_object_cache->flush( $delay ); |
| 68 | + |
| 69 | + return $wp_object_cache->flush(); |
109 | 70 | }
|
110 | 71 |
|
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() { |
124 | 73 | global $wp_object_cache;
|
125 |
| - return $wp_object_cache->get( $key, $group, $force ); |
| 74 | + |
| 75 | + return $wp_object_cache->flush_runtime(); |
126 | 76 | }
|
127 | 77 |
|
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 ) { |
144 | 79 | 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 ); |
146 | 90 | }
|
147 | 91 |
|
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 ) { |
160 | 93 | global $wp_object_cache;
|
161 |
| - return $wp_object_cache->incr( $key, $offset, $group ); |
| 94 | + |
| 95 | + return $wp_object_cache->get_multiple( $keys, $group, $force ); |
162 | 96 | }
|
163 | 97 |
|
164 | 98 | /**
|
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 |
168 | 100 | *
|
169 |
| - * @return void |
| 101 | + * @param array $groups Array of arrays, of groups and keys to retrieve |
| 102 | + * @return mixed |
170 | 103 | */
|
| 104 | +function wp_cache_get_multi( $groups ) { |
| 105 | + global $wp_object_cache; |
| 106 | + |
| 107 | + return $wp_object_cache->get_multi( $groups ); |
| 108 | +} |
| 109 | + |
171 | 110 | function wp_cache_init() {
|
172 | 111 | global $wp_object_cache;
|
| 112 | + |
173 | 113 | $wp_object_cache = new WP_Object_Cache();
|
174 | 114 | }
|
175 | 115 |
|
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 ) { |
192 | 117 | 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 ); |
194 | 120 | }
|
195 | 121 |
|
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 ) { |
211 | 123 | 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 | + } |
213 | 130 | }
|
214 | 131 |
|
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 ) { |
227 | 133 | 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 ); |
229 | 142 | }
|
230 | 143 |
|
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 |
| - */ |
240 | 144 | function wp_cache_add_global_groups( $groups ) {
|
241 | 145 | global $wp_object_cache;
|
| 146 | + |
242 | 147 | $wp_object_cache->add_global_groups( $groups );
|
243 | 148 | }
|
244 | 149 |
|
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 |
| - */ |
254 | 150 | function wp_cache_add_non_persistent_groups( $groups ) {
|
255 | 151 | global $wp_object_cache;
|
| 152 | + |
256 | 153 | $wp_object_cache->add_non_persistent_groups( $groups );
|
257 | 154 | }
|
258 | 155 |
|
| 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 | + |
259 | 175 | 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 | + } |
260 | 228 |
|
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 | + ]; |
267 | 235 |
|
268 |
| - /** |
269 |
| - * Track if Redis is available |
270 |
| - * |
271 |
| - * @var bool |
272 |
| - */ |
273 |
| - private $redis_connected = false; |
| 236 | + return true; |
| 237 | + } |
274 | 238 |
|
275 |
| - /** |
276 |
| - * Local cache |
277 |
| - * |
278 |
| - * @var array |
279 |
| - */ |
280 |
| - public $cache = array(); |
| 239 | + return false; |
| 240 | + } |
281 | 241 |
|
282 |
| - private $to_unserialize = array(); |
| 242 | + if ( isset( $this->cache[ $key ][ 'value' ] ) && false !== $this->cache[ $key ][ 'value' ] ) { |
| 243 | + return false; |
| 244 | + } |
283 | 245 |
|
284 |
| - public $to_preload = array(); |
| 246 | + $mc = $this->get_mc( $group ); |
285 | 247 |
|
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 | + } |
312 | 252 |
|
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(); |
319 | 257 |
|
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 | + } |
326 | 265 |
|
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 | + } |
333 | 281 |
|
334 |
| - /** |
335 |
| - * Track how many requests were found in cache |
336 |
| - * |
337 |
| - * @var int |
338 |
| - */ |
339 |
| - public $cache_hits = 0; |
| 282 | + return $result; |
| 283 | + } |
340 | 284 |
|
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(); |
347 | 287 |
|
348 |
| - private $multisite; |
| 288 | + foreach ( $data as $key => $value ) { |
| 289 | + $values[ $key ] = $this->add( $key, $value, $group, $expire ); |
| 290 | + } |
349 | 291 |
|
350 |
| - public $stats = array(); |
| 292 | + return $values; |
| 293 | + } |
351 | 294 |
|
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 | + } |
365 | 299 |
|
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; |
368 | 307 | }
|
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(); |
371 | 343 | }
|
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; |
374 | 353 | }
|
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 ] ); |
377 | 365 | }
|
378 | 366 |
|
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' ); |
383 | 396 | }
|
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 | + } |
387 | 404 |
|
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' ); |
390 | 413 | }
|
| 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 | + } |
391 | 430 |
|
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 ); |
394 | 445 | }
|
| 446 | + } |
| 447 | + |
| 448 | + return $flush_number; |
| 449 | + } |
395 | 450 |
|
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 ); |
401 | 454 | }
|
| 455 | + return $this->global_flush_number; |
| 456 | + } |
402 | 457 |
|
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 ); |
409 | 461 | }
|
| 462 | + return $this->flush_number[ $this->blog_prefix ]; |
| 463 | + } |
410 | 464 |
|
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(); |
414 | 468 |
|
415 |
| - $this->maybe_preload(); |
| 469 | + return true; |
416 | 470 | }
|
417 | 471 |
|
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 ); |
421 | 487 | }
|
422 | 488 |
|
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(); |
425 | 495 | }
|
426 | 496 |
|
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 ); |
431 | 498 |
|
432 |
| - $this->preload( $request_hash ); |
| 499 | + $this->flush_number[ $this->blog_prefix ] = $flush_number; |
| 500 | + } |
433 | 501 |
|
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(); |
436 | 505 | }
|
437 | 506 |
|
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; |
439 | 510 | }
|
440 | 511 |
|
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 ); |
446 | 514 | }
|
447 | 515 |
|
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; |
450 | 520 |
|
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' ]; |
454 | 526 | }
|
| 527 | + $found = $this->cache[ $key ][ 'found' ]; |
455 | 528 |
|
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; |
458 | 568 | }
|
459 | 569 |
|
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; |
462 | 590 | }
|
463 | 591 |
|
464 |
| - $this->set( $hash, $keys, 'pj-preload' ); |
| 592 | + return $value; |
465 | 593 | }
|
466 | 594 |
|
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; |
474 | 655 | }
|
475 | 656 |
|
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 | + } |
491 | 681 | }
|
492 | 682 |
|
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(); |
494 | 688 |
|
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 | + } |
497 | 701 | }
|
498 | 702 |
|
499 |
| - return $this->set( $_key, $value, $group, $expiration ); |
| 703 | + $this->cache = array_merge( $this->cache, $return_cache ); |
| 704 | + |
| 705 | + return $return; |
500 | 706 | }
|
501 | 707 |
|
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 | + } |
516 | 719 |
|
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; |
522 | 731 | } else {
|
523 |
| - if ( ! $this->redis->exists( $redis_key ) ) { |
524 |
| - return false; |
525 |
| - } |
| 732 | + $prefix .= $this->blog_prefix; |
526 | 733 | }
|
527 | 734 |
|
528 |
| - return $this->set( $_key, $value, $group, $expiration ); |
| 735 | + return preg_replace( '/\s+/', '', "$prefix:$group:$key" ); |
529 | 736 | }
|
530 | 737 |
|
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 ); |
540 | 745 |
|
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 ); |
545 | 784 |
|
546 |
| - unset( $this->cache[ $group ][ $key ] ); |
547 |
| - unset( $this->to_preload[ $group ][ $key ] ); |
548 |
| - unset( $this->to_unserialize[ $redis_key ] ); |
549 | 785 | return true;
|
550 | 786 | }
|
551 | 787 |
|
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 ); |
555 | 800 |
|
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; |
557 | 805 | }
|
558 | 806 |
|
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(); |
568 | 809 |
|
569 |
| - if ( $this->can_redis() ) { |
570 |
| - $this->redis->flushDb(); |
| 810 | + foreach ( $data as $key => $value ) { |
| 811 | + $values[ $key ] = $this->set( $key, $value, $group, $expire ); |
571 | 812 | }
|
572 | 813 |
|
573 |
| - return true; |
| 814 | + return $values; |
574 | 815 | }
|
575 | 816 |
|
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; |
587 | 819 |
|
588 |
| - $this->to_preload[ $group ][ $_key ] = true; |
| 820 | + $blog_id = (int) $blog_id; |
589 | 821 |
|
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 | + } |
592 | 824 |
|
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 | + ); |
595 | 838 |
|
596 |
| - if ( is_string( $value ) ) { |
597 |
| - $value = unserialize( $value ); |
598 |
| - } |
| 839 | + $cmd = substr( $line, 0, strpos( $line, ' ' ) ); |
599 | 840 |
|
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 | + } |
602 | 847 |
|
603 |
| - $this->cache_hits += 1; |
| 848 | + $cmd2 = "<span style='color:" . esc_attr( $color_for_cmd ) . "; font-weight: bold;'>" . esc_html( $cmd ) . "</span>"; |
604 | 849 |
|
605 |
| - return is_object( $value ) ? clone $value : $value; |
606 |
| - } |
| 850 | + return $cmd2 . esc_html( substr( $line, strlen( $cmd ) ) ) . "$trailing_html\n"; |
| 851 | + } |
607 | 852 |
|
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 | + } |
612 | 861 |
|
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 | + } |
615 | 871 |
|
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 | + } |
620 | 878 | }
|
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 | + "; |
626 | 881 | }
|
627 | 882 |
|
628 | 883 | /**
|
629 |
| - * Retrieve multiple values from cache. |
630 |
| - * |
631 |
| - * Gets multiple values from cache, including across multiple groups |
| 884 | + * Returns the collected raw stats. |
632 | 885 | *
|
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 |
637 | 887 | */
|
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 | + } |
642 | 923 |
|
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; |
647 | 925 |
|
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']; |
653 | 928 | }
|
| 929 | + } |
| 930 | + } |
| 931 | + |
| 932 | + return $stats; |
| 933 | + } |
654 | 934 |
|
| 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 ) ) { |
655 | 946 | continue;
|
656 | 947 | }
|
657 | 948 |
|
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 | + } |
661 | 953 |
|
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 | + } |
664 | 967 |
|
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"; |
669 | 984 |
|
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'; |
673 | 990 | }
|
| 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>"; |
674 | 1001 | }
|
675 | 1002 |
|
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]} "; |
679 | 1017 | }
|
680 | 1018 |
|
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 | + } |
684 | 1023 |
|
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 | + } |
695 | 1028 |
|
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>"; |
697 | 1035 | }
|
698 | 1036 |
|
699 |
| - return $cache; |
| 1037 | + return $this->colorize_debug_line( $line, $bt_link ); |
700 | 1038 | }
|
701 | 1039 |
|
702 | 1040 | /**
|
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 |
712 | 1043 | */
|
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 ]; |
718 | 1047 | }
|
719 | 1048 |
|
720 |
| - $this->cache[ $group ][ $key ] = $value; |
| 1049 | + return $this->mc['default']; |
| 1050 | + } |
721 | 1051 |
|
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 = ''; |
724 | 1064 | }
|
| 1065 | + } |
725 | 1066 |
|
726 |
| - $value = is_numeric( $value ) ? $value : serialize( $value ); |
| 1067 | + function __construct() { |
| 1068 | + global $memcached_servers; |
727 | 1069 |
|
728 |
| - // Save to Redis |
729 |
| - if ( $expiration ) { |
730 |
| - $this->redis->setex( $redis_key, $expiration, $value ); |
| 1070 | + if ( isset( $memcached_servers ) ) { |
| 1071 | + $buckets = $memcached_servers; |
731 | 1072 | } else {
|
732 |
| - $this->redis->set( $redis_key, $value ); |
| 1073 | + $buckets = array( '127.0.0.1:11211' ); |
733 | 1074 | }
|
734 | 1075 |
|
735 |
| - return true; |
736 |
| - } |
| 1076 | + reset( $buckets ); |
737 | 1077 |
|
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 ); |
748 | 1106 |
|
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 | + } |
753 | 1112 | }
|
| 1113 | + } |
754 | 1114 |
|
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 ); |
757 | 1123 | }
|
758 | 1124 |
|
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']; |
763 | 1129 | }
|
764 | 1130 |
|
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 | + } |
775 | 1137 | }
|
776 | 1138 |
|
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; |
792 | 1145 | }
|
793 | 1146 |
|
794 |
| - $local_key = $prefix . $key; |
795 |
| - return array( $local_key, WP_CACHE_KEY_SALT . "$prefix$group:$key" ); |
796 |
| - } |
| 1147 | + $this->size_total += $size; |
797 | 1148 |
|
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 ); |
806 | 1163 | }
|
807 | 1164 |
|
808 | 1165 | /**
|
809 |
| - * Sets the list of global groups. |
| 1166 | + * Key format: key_salt:flush_number:table_prefix:key_name |
810 | 1167 | *
|
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. |
812 | 1170 | */
|
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 | + } |
815 | 1175 |
|
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]; |
820 | 1188 | }
|
821 | 1189 |
|
822 |
| - $this->_global_groups = array_flip( $this->global_groups ); |
| 1190 | + return $keys; |
823 | 1191 | }
|
824 | 1192 |
|
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 ); |
832 | 1195 |
|
833 |
| - $this->no_redis_groups = array_unique( array_merge( $this->no_redis_groups, $groups ) ); |
| 1196 | + return true; |
834 | 1197 | }
|
835 |
| -} |
836 | 1198 |
|
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