├── README.md └── object-cache.php /README.md: -------------------------------------------------------------------------------- 1 | # KDK Memcached Object Cache (v1.1 / Feb 2021) 2 | Object cache driver for Memcached in WordPress. 3 | 4 | Based on Memcached Redux with corrections for handling Memcached connections and an option to have multiple Memcached backends (defined as an array in wp-config.php). 5 | 6 | 7 | ## Installation 8 | 9 | Download the repo from https://github.com/kodeka/kdk_memcached_object_cache/archive/master.zip and extract it. 10 | 11 | Upload the object-cache.php file to your WordPress site's /wp-content/ folder. 12 | 13 | Setup multiple Memcached backends by defining them in wp-config.php like so: 14 | 15 | Example: 16 | ``` 17 | # Place in wp-config.php & adjust accordingly 18 | function get_memcached_servers () { 19 | return array( 20 | '127.0.0.1:11211', 21 | '10.1.1.1:11211', 22 | 'production.cache.amazonaws.com:11211' 23 | ); 24 | } 25 | ``` 26 | 27 | Any WordPress caching plugin that offers in-memory object caching (e.g. [WP Rocket](https://wp-rocket.me/)) can now utilize this object cache driver to store cache objects in Memcached backends. 28 | 29 | Enjoy :) 30 | 31 | 32 | ## License & Credits 33 | 34 | Maintained by Kodeka OÜ. 35 | 36 | Big thanks to [Fotis Alexandrou](https://github.com/falexandrou) for originally improving [Memcached Redux](https://wordpress.org/plugins/memcached-redux/) (which uses code from Scott Taylor, Ryan Boren, Denis de Bernardy, Matt Martz, Mike Schroder, Mika Epstein). Silence is not golden m/f. 37 | 38 | Licensed under the GNU/GPL license (https://www.gnu.org/copyleft/gpl.html). 39 | 40 | Copyright (c) 2018 - 2021 Kodeka OÜ. All rights reserved. 41 | -------------------------------------------------------------------------------- /object-cache.php: -------------------------------------------------------------------------------- 1 | add($key, $data, $group, $expire); 22 | } 23 | 24 | function wp_cache_incr($key, $n = 1, $group = '') 25 | { 26 | global $wp_object_cache; 27 | 28 | return $wp_object_cache->incr($key, $n, $group); 29 | } 30 | 31 | function wp_cache_decr($key, $n = 1, $group = '') 32 | { 33 | global $wp_object_cache; 34 | 35 | return $wp_object_cache->decr($key, $n, $group); 36 | } 37 | 38 | function wp_cache_close() 39 | { 40 | global $wp_object_cache; 41 | 42 | return $wp_object_cache->close(); 43 | } 44 | 45 | function wp_cache_delete($key, $group = '') 46 | { 47 | global $wp_object_cache; 48 | 49 | return $wp_object_cache->delete($key, $group); 50 | } 51 | 52 | function wp_cache_flush() 53 | { 54 | global $wp_object_cache; 55 | 56 | return $wp_object_cache->flush(); 57 | } 58 | 59 | function wp_cache_get($key, $group = '', $force = false, &$found = null) 60 | { 61 | global $wp_object_cache; 62 | 63 | return $wp_object_cache->get($key, $group, $force, $found); 64 | } 65 | 66 | /** 67 | * $keys_and_groups = array( 68 | * array( 'key', 'group' ), 69 | * array( 'key', '' ), 70 | * array( 'key', 'group' ), 71 | * array( 'key' ) 72 | * ); 73 | * 74 | */ 75 | function wp_cache_get_multi($key_and_groups, $bucket = 'default') 76 | { 77 | global $wp_object_cache; 78 | 79 | return $wp_object_cache->get_multi($key_and_groups, $bucket); 80 | } 81 | 82 | /** 83 | * $items = array( 84 | * array( 'key', 'data', 'group' ), 85 | * array( 'key', 'data' ) 86 | * ); 87 | * 88 | */ 89 | function wp_cache_set_multi($items, $expire = 0, $group = 'default') 90 | { 91 | global $wp_object_cache; 92 | 93 | return $wp_object_cache->set_multi($items, $expire = 0, $group = 'default'); 94 | } 95 | 96 | function wp_cache_init() 97 | { 98 | global $wp_object_cache; 99 | 100 | $wp_object_cache = new WP_Object_Cache(); 101 | } 102 | 103 | function wp_cache_replace($key, $data, $group = '', $expire = 0) 104 | { 105 | global $wp_object_cache; 106 | 107 | return $wp_object_cache->replace($key, $data, $group, $expire); 108 | } 109 | 110 | function wp_cache_set($key, $data, $group = '', $expire = 0) 111 | { 112 | global $wp_object_cache; 113 | 114 | if (defined('WP_INSTALLING') == false) { 115 | return $wp_object_cache->set($key, $data, $group, $expire); 116 | } else { 117 | return $wp_object_cache->delete($key, $group); 118 | } 119 | } 120 | 121 | function wp_cache_add_global_groups($groups) 122 | { 123 | global $wp_object_cache; 124 | 125 | $wp_object_cache->add_global_groups($groups); 126 | } 127 | 128 | function wp_cache_add_non_persistent_groups($groups) 129 | { 130 | global $wp_object_cache; 131 | 132 | $wp_object_cache->add_non_persistent_groups($groups); 133 | } 134 | 135 | class WP_Object_Cache 136 | { 137 | public $global_groups = array(); 138 | public $no_mc_groups = array(); 139 | public $cache = array(); 140 | public $mc = array(); 141 | public $stats = array(); 142 | public $group_ops = array(); 143 | public $cache_enabled = true; 144 | public $default_expiration = 0; 145 | 146 | public function add($id, $data, $group = 'default', $expire = 0) 147 | { 148 | $key = $this->key($id, $group); 149 | 150 | if (is_object($data)) { 151 | $data = clone $data; 152 | } 153 | 154 | if (in_array($group, $this->no_mc_groups)) { 155 | $this->cache[$key] = $data; 156 | return true; 157 | } elseif (isset($this->cache[$key]) && $this->cache[$key] !== false) { 158 | return false; 159 | } 160 | 161 | $mc =& $this->get_mc($group); 162 | $expire = ($expire == 0) ? $this->default_expiration : $expire; 163 | $result = $mc->add($key, $data, $expire); 164 | 165 | if (false !== $result) { 166 | ++$this->stats['add']; 167 | $this->group_ops[$group][] = "add $id"; 168 | $this->cache[$key] = $data; 169 | } 170 | 171 | return $result; 172 | } 173 | 174 | public function __destruct() 175 | { 176 | $this->close(); 177 | foreach ($this->mc as $bucket => $mc) { 178 | $mc->resetServerList(); 179 | } 180 | } 181 | 182 | public function add_global_groups($groups) 183 | { 184 | if (! is_array($groups)) { 185 | $groups = (array) $groups; 186 | } 187 | 188 | $this->global_groups = array_merge($this->global_groups, $groups); 189 | $this->global_groups = array_unique($this->global_groups); 190 | } 191 | 192 | public function add_non_persistent_groups($groups) 193 | { 194 | if (! is_array($groups)) { 195 | $groups = (array) $groups; 196 | } 197 | 198 | $this->no_mc_groups = array_merge($this->no_mc_groups, $groups); 199 | $this->no_mc_groups = array_unique($this->no_mc_groups); 200 | } 201 | 202 | public function incr($id, $n = 1, $group = 'default') 203 | { 204 | $key = $this->key($id, $group); 205 | $mc =& $this->get_mc($group); 206 | $this->cache[ $key ] = $mc->increment($key, $n); 207 | return $this->cache[ $key ]; 208 | } 209 | 210 | public function decr($id, $n = 1, $group = 'default') 211 | { 212 | $key = $this->key($id, $group); 213 | $mc =& $this->get_mc($group); 214 | $this->cache[ $key ] = $mc->decrement($key, $n); 215 | return $this->cache[ $key ]; 216 | } 217 | 218 | public function close() 219 | { 220 | foreach ($this->mc as $bucket => $mc) { 221 | $mc->quit(); 222 | } 223 | 224 | return true; 225 | } 226 | 227 | public function delete($id, $group = 'default') 228 | { 229 | $key = $this->key($id, $group); 230 | 231 | if (in_array($group, $this->no_mc_groups)) { 232 | unset($this->cache[$key]); 233 | return true; 234 | } 235 | 236 | $mc =& $this->get_mc($group); 237 | 238 | $result = $mc->delete($key); 239 | 240 | if (false !== $result) { 241 | ++$this->stats['delete']; 242 | $this->group_ops[$group][] = "delete $id"; 243 | unset($this->cache[$key]); 244 | } 245 | 246 | return $result; 247 | } 248 | 249 | public function flush() 250 | { 251 | // Don't flush if multi-blog. 252 | if (function_exists('is_site_admin') || defined('CUSTOM_USER_TABLE') && defined('CUSTOM_USER_META_TABLE')) { 253 | return true; 254 | } 255 | 256 | $ret = true; 257 | foreach (array_keys($this->mc) as $group) { 258 | $ret &= $this->mc[$group]->flush(); 259 | } 260 | return $ret; 261 | } 262 | 263 | public function get($id, $group = 'default', $force = false, &$found = null) 264 | { 265 | $key = $this->key($id, $group); 266 | $mc =& $this->get_mc($group); 267 | $found = false; 268 | 269 | if (isset($this->cache[$key]) && (!$force || in_array($group, $this->no_mc_groups))) { 270 | $found = true; 271 | if (is_object($this->cache[$key])) { 272 | $value = clone $this->cache[$key]; 273 | } else { 274 | $value = $this->cache[$key]; 275 | } 276 | } elseif (in_array($group, $this->no_mc_groups)) { 277 | $this->cache[$key] = $value = false; 278 | } else { 279 | $value = $mc->get($key); 280 | if ($value === false || (is_integer($value) && -1 == $value)) { 281 | // $value = false; 282 | $found = $mc->getResultCode() !== Memcached::RES_NOTFOUND; 283 | } else { 284 | $found = true; 285 | } 286 | $this->cache[$key] = $value; 287 | } 288 | 289 | if ($found) { 290 | ++$this->stats['get']; 291 | $this->group_ops[$group][] = "get $id"; 292 | } else { 293 | ++$this->stats['miss']; 294 | } 295 | 296 | if ('checkthedatabaseplease' === $value) { 297 | unset($this->cache[$key]); 298 | $value = false; 299 | } 300 | 301 | return $value; 302 | } 303 | 304 | public function get_multi($keys, $group = 'default') 305 | { 306 | $return = array(); 307 | $gets = array(); 308 | foreach ($keys as $i => $values) { 309 | $mc =& $this->get_mc($group); 310 | $values = (array) $values; 311 | if (empty($values[1])) { 312 | $values[1] = 'default'; 313 | } 314 | 315 | list($id, $group) = (array) $values; 316 | $key = $this->key($id, $group); 317 | 318 | if (isset($this->cache[$key])) { 319 | if (is_object($this->cache[$key])) { 320 | $return[$key] = clone $this->cache[$key]; 321 | } else { 322 | $return[$key] = $this->cache[$key]; 323 | } 324 | } elseif (in_array($group, $this->no_mc_groups)) { 325 | $return[$key] = false; 326 | } else { 327 | $gets[$key] = $key; 328 | } 329 | } 330 | 331 | if (!empty($gets)) { 332 | $results = $mc->getMulti($gets, $null, Memcached::GET_PRESERVE_ORDER); 333 | $joined = array_combine(array_keys($gets), array_values($results)); 334 | $return = array_merge($return, $joined); 335 | } 336 | 337 | ++$this->stats['get_multi']; 338 | $this->group_ops[$group][] = "get_multi $id"; 339 | $this->cache = array_merge($this->cache, $return); 340 | return array_values($return); 341 | } 342 | 343 | public function key($key, $group) 344 | { 345 | if (empty($group)) { 346 | $group = 'default'; 347 | } 348 | 349 | if (false !== array_search($group, $this->global_groups)) { 350 | $prefix = $this->global_prefix; 351 | } else { 352 | $prefix = $this->blog_prefix; 353 | } 354 | 355 | return preg_replace('/\s+/', '', WP_CACHE_KEY_SALT . "$prefix$group:$key"); 356 | } 357 | 358 | public function replace($id, $data, $group = 'default', $expire = 0) 359 | { 360 | $key = $this->key($id, $group); 361 | $expire = ($expire == 0) ? $this->default_expiration : $expire; 362 | $mc =& $this->get_mc($group); 363 | 364 | if (is_object($data)) { 365 | $data = clone $data; 366 | } 367 | 368 | $result = $mc->replace($key, $data, $expire); 369 | if (false !== $result) { 370 | $this->cache[$key] = $data; 371 | } 372 | return $result; 373 | } 374 | 375 | public function set($id, $data, $group = 'default', $expire = 0) 376 | { 377 | $key = $this->key($id, $group); 378 | if (isset($this->cache[$key]) && ('checkthedatabaseplease' === $this->cache[$key])) { 379 | return false; 380 | } 381 | 382 | if (is_object($data)) { 383 | $data = clone $data; 384 | } 385 | 386 | $this->cache[$key] = $data; 387 | 388 | if (in_array($group, $this->no_mc_groups)) { 389 | return true; 390 | } 391 | 392 | $expire = ($expire == 0) ? $this->default_expiration : $expire; 393 | $mc =& $this->get_mc($group); 394 | $result = $mc->set($key, $data, $expire); 395 | 396 | return $result; 397 | } 398 | 399 | public function set_multi($items, $expire = 0, $group = 'default') 400 | { 401 | $sets = array(); 402 | $mc =& $this->get_mc($group); 403 | $expire = ($expire == 0) ? $this->default_expiration : $expire; 404 | 405 | foreach ($items as $i => $item) { 406 | if (empty($item[2])) { 407 | $item[2] = 'default'; 408 | } 409 | 410 | list($id, $data, $group) = $item; 411 | 412 | $key = $this->key($id, $group); 413 | if (isset($this->cache[$key]) && ('checkthedatabaseplease' === $this->cache[$key])) { 414 | continue; 415 | } 416 | 417 | if (is_object($data)) { 418 | $data = clone $data; 419 | } 420 | 421 | $this->cache[$key] = $data; 422 | 423 | if (in_array($group, $this->no_mc_groups)) { 424 | continue; 425 | } 426 | 427 | $sets[$key] = $data; 428 | } 429 | 430 | if (!empty($sets)) { 431 | $mc->setMulti($sets, $expire); 432 | } 433 | } 434 | 435 | public function colorize_debug_line($line) 436 | { 437 | $colors = array( 438 | 'get' => 'green', 439 | 'set' => 'purple', 440 | 'add' => 'blue', 441 | 'delete'=> 'red' 442 | ); 443 | 444 | $cmd = substr($line, 0, strpos($line, ' ')); 445 | 446 | $cmd2 = "$cmd"; 447 | 448 | return $cmd2 . substr($line, strlen($cmd)) . "\n"; 449 | } 450 | 451 | public function stats() 452 | { 453 | echo "

\n"; 454 | foreach ($this->stats as $stat => $n) { 455 | echo "$stat $n"; 456 | echo "
\n"; 457 | } 458 | echo "

\n"; 459 | echo "

Memcached:

"; 460 | foreach ($this->group_ops as $group => $ops) { 461 | if (!isset($_GET['debug_queries']) && 500 < count($ops)) { 462 | $ops = array_slice($ops, 0, 500); 463 | echo "Too many to show! Show them anyway.\n"; 464 | } 465 | echo "

$group commands

"; 466 | echo "
\n";
467 |                 $lines = array();
468 |                 foreach ($ops as $op) {
469 |                     $lines[] = $this->colorize_debug_line($op);
470 |                 }
471 |                 print_r($lines);
472 |                 echo "
\n"; 473 | } 474 | 475 | if (!empty($this->debug) && $this->debug) { 476 | var_dump($this->memcache_debug); 477 | } 478 | } 479 | 480 | public function &get_mc($group) 481 | { 482 | if (isset($this->mc[$group])) { 483 | return $this->mc[$group]; 484 | } 485 | return $this->mc['default']; 486 | } 487 | 488 | public function __construct() 489 | { 490 | $this->stats = array( 491 | 'get' => 0, 492 | 'get_multi' => 0, 493 | 'add' => 0, 494 | 'set' => 0, 495 | 'delete' => 0, 496 | 'miss' => 0, 497 | ); 498 | 499 | $memcached_servers = function_exists('get_memcached_servers') ? get_memcached_servers() : null; 500 | 501 | if (isset($memcached_servers)) { 502 | $buckets = $memcached_servers; 503 | } else { 504 | $buckets = array( '127.0.0.1' ); 505 | } 506 | 507 | reset($buckets); 508 | if (is_int(key($buckets))) { 509 | $buckets = array( 'default' => $buckets ); 510 | } 511 | 512 | foreach ($buckets as $bucket => $servers) { 513 | $this->mc[$bucket] = new Memcached('wpcache'); 514 | $instances = array(); 515 | foreach ($servers as $server) { 516 | if (strpos($server, ':') !== false) { 517 | list($node, $port) = explode(':', $server); 518 | } else { 519 | $node = $server; 520 | $port = ini_get('memcache.default_port'); 521 | } 522 | $port = intval($port); 523 | if (!$port) { 524 | $port = 11211; 525 | } 526 | $instances[] = array( $node, $port, 1 ); 527 | } 528 | $this->mc[$bucket]->addServers($instances); 529 | } 530 | 531 | global $blog_id, $table_prefix; 532 | $this->global_prefix = ''; 533 | $this->blog_prefix = ''; 534 | if (function_exists('is_multisite')) { 535 | $this->global_prefix = (is_multisite() || defined('CUSTOM_USER_TABLE') && defined('CUSTOM_USER_META_TABLE')) ? '' : $table_prefix; 536 | $this->blog_prefix = (is_multisite() ? $blog_id : $table_prefix) . ':'; 537 | } 538 | 539 | $this->cache_hits =& $this->stats['get']; 540 | $this->cache_misses =& $this->stats['miss']; 541 | } 542 | } 543 | } else { 544 | // No Memcached 545 | if (function_exists('wp_using_ext_object_cache')) { 546 | // In 3.7+, we can handle this smoothly 547 | wp_using_ext_object_cache(false); 548 | } else { 549 | // In earlier versions, there isn't a clean bail-out method. 550 | wp_die('Memcached class not available.'); 551 | } 552 | } 553 | --------------------------------------------------------------------------------