├── composer.json └── object-cache.php /composer.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "humanmade/memcache-object-cache", 4 | "description": "Memcached backend for the WP Object Cache.", 5 | "homepage": "https://github.com/humanmade/memcache-object-cache", 6 | "keywords": [ 7 | "wordpress","object-cache" 8 | ], 9 | "license": "GPL-2.0+", 10 | "authors": [ 11 | { 12 | "name":"Human Made Limited", 13 | "email":"support@humanmade.co.uk", 14 | "homepage":"http://hmn.md/" 15 | } 16 | ], 17 | "type": "wordpress-muplugin", 18 | "require": { 19 | "composer/installers": "~1.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /object-cache.php: -------------------------------------------------------------------------------- 1 | add($key, $data, $group, $expire); 56 | } 57 | 58 | function wp_cache_incr($key, $n = 1, $group = '') { 59 | global $wp_object_cache; 60 | 61 | return $wp_object_cache->incr($key, $n, $group); 62 | } 63 | 64 | function wp_cache_decr($key, $n = 1, $group = '') { 65 | global $wp_object_cache; 66 | 67 | return $wp_object_cache->decr($key, $n, $group); 68 | } 69 | 70 | function wp_cache_close() { 71 | global $wp_object_cache; 72 | 73 | return $wp_object_cache->close(); 74 | } 75 | 76 | function wp_cache_delete($key, $group = '') { 77 | global $wp_object_cache; 78 | 79 | return $wp_object_cache->delete($key, $group); 80 | } 81 | 82 | function wp_cache_flush() { 83 | global $wp_object_cache; 84 | 85 | return $wp_object_cache->flush(); 86 | } 87 | 88 | function wp_cache_get($key, $group = '', $force = false) { 89 | global $wp_object_cache; 90 | 91 | return $wp_object_cache->get($key, $group, $force); 92 | } 93 | 94 | function wp_cache_get_multi($groups, $force = false) { 95 | global $wp_object_cache; 96 | 97 | return $wp_object_cache->get_multi($groups, $force); 98 | } 99 | 100 | function wp_cache_init() { 101 | global $wp_object_cache; 102 | 103 | $wp_object_cache = new WP_Object_Cache(); 104 | } 105 | 106 | function wp_cache_replace($key, $data, $group = '', $expire = 0) { 107 | global $wp_object_cache; 108 | 109 | return $wp_object_cache->replace($key, $data, $group, $expire); 110 | } 111 | 112 | function wp_cache_set($key, $data, $group = '', $expire = 0) { 113 | global $wp_object_cache; 114 | 115 | if ( defined( 'WP_INSTALLING' ) == false ) { 116 | return $wp_object_cache->set( $key, $data, $group, $expire ); 117 | } else { 118 | return $wp_object_cache->delete( $key, $group ); 119 | } 120 | } 121 | 122 | function wp_cache_switch_to_blog( $blog_id ) { 123 | global $wp_object_cache; 124 | 125 | return $wp_object_cache->switch_to_blog( $blog_id ); 126 | } 127 | 128 | function wp_cache_add_global_groups( $groups ) { 129 | global $wp_object_cache; 130 | 131 | $wp_object_cache->add_global_groups($groups); 132 | } 133 | 134 | function wp_cache_add_non_persistent_groups( $groups ) { 135 | global $wp_object_cache; 136 | 137 | $wp_object_cache->add_non_persistent_groups($groups); 138 | } 139 | 140 | class WP_Object_Cache { 141 | var $global_groups = array(); 142 | 143 | var $no_mc_groups = array(); 144 | 145 | var $cache = array(); 146 | var $mc = array(); 147 | var $stats = array(); 148 | var $group_ops = array(); 149 | 150 | var $cache_enabled = true; 151 | var $default_expiration = 0; 152 | 153 | public function get_alloptions() { 154 | // Check our internal cache, to avoid the more expensive get-multi 155 | $key = $this->key( 'alloptions', 'options' ); 156 | if ( isset( $this->cache[ $key ] ) ) { 157 | return $this->cache[ $key ]; 158 | } 159 | 160 | $keys = $this->get( 'alloptionskeys', 'options' ); 161 | if ( empty( $keys ) || ! is_array( $keys ) ) { 162 | return array(); 163 | } 164 | 165 | $data = $this->get_multi( array( 'options' => array_keys( $keys ) ) ); 166 | if ( empty( $data ) || empty( $data['options'] ) ) { 167 | return array(); 168 | } 169 | 170 | $this->cache[ $key ] = $data['options']; 171 | return $this->cache[ $key ]; 172 | } 173 | 174 | public function set_alloptions( $data ) { 175 | $internal_cache_key = $this->key( 'alloptions', 'options' ); 176 | $existing = $internal_cache = $this->get_alloptions(); 177 | 178 | $keys = $this->get( 'alloptionskeys', 'options' ); 179 | if ( empty( $keys ) || ! is_array( $keys ) ) { 180 | $keys = array(); 181 | } 182 | 183 | // While you could use array_diff here, it ends up being a bit more 184 | // complicated than just checking 185 | foreach ( $data as $key => $value ) { 186 | if ( isset( $existing[ $key ] ) && $existing[ $key ] === $value ) { 187 | continue; 188 | } 189 | 190 | if ( ! isset( $keys[ $key ] ) ) { 191 | $keys[ $key ] = true; 192 | } 193 | 194 | if ( ! $this->set( $key, $value, 'options' ) ) { 195 | return false; 196 | } 197 | 198 | $internal_cache[ $key ] = $value; 199 | } 200 | 201 | // Remove deleted elements 202 | foreach ( $existing as $key => $value ) { 203 | if ( isset( $data[ $key ] ) ) { 204 | continue; 205 | } 206 | 207 | if ( isset( $keys[ $key ] ) ) { 208 | unset( $keys[ $key ] ); 209 | } 210 | 211 | if ( ! $this->delete( $key, 'options' ) ) { 212 | return false; 213 | } 214 | 215 | unset( $internal_cache[ $key ] ); 216 | } 217 | 218 | if ( ! $this->set( 'alloptionskeys', $keys, 'options' ) ) { 219 | return false; 220 | } 221 | $this->cache[ $internal_cache_key ] = $internal_cache; 222 | 223 | return true; 224 | } 225 | 226 | public function delete_alloptions() { 227 | $key = $this->key( 'alloptions', 'options' ); 228 | $this->cache[ $key ] = array(); 229 | 230 | return $this->delete( 'alloptionskeys', 'options' ); 231 | } 232 | 233 | function add($id, $data, $group = 'default', $expire = 0) { 234 | if ( $id === 'alloptions' && $group === 'options' ) { 235 | return $this->set_alloptions( $data ); 236 | } 237 | 238 | $key = $this->key($id, $group); 239 | 240 | if ( is_object( $data ) ) { 241 | $data = clone $data; 242 | } 243 | 244 | if ( in_array($group, $this->no_mc_groups) ) { 245 | $this->cache[$key] = $data; 246 | return true; 247 | } elseif ( isset($this->cache[$key]) && $this->cache[$key] !== false ) { 248 | return false; 249 | } 250 | 251 | $mc =& $this->get_mc($group); 252 | $expire = ($expire == 0) ? $this->default_expiration : $expire; 253 | 254 | $time = microtime(true); 255 | $result = $mc->add($key, $data, false, $expire); 256 | $time_taken = microtime(true) - $time; 257 | 258 | if ( false !== $result ) { 259 | @ ++$this->stats['add']; 260 | $this->stats['add_time'] += $time_taken; 261 | $this->group_ops[$group][] = "add $id"; 262 | $this->cache[$key] = $data; 263 | } 264 | 265 | return $result; 266 | } 267 | 268 | function add_global_groups($groups) { 269 | if ( ! is_array( $groups ) ) { 270 | $groups = (array) $groups; 271 | } 272 | 273 | $this->global_groups = array_merge($this->global_groups, $groups); 274 | $this->global_groups = array_unique($this->global_groups); 275 | } 276 | 277 | function add_non_persistent_groups($groups) { 278 | if ( ! is_array( $groups ) ) { 279 | $groups = (array) $groups; 280 | } 281 | 282 | $this->no_mc_groups = array_merge($this->no_mc_groups, $groups); 283 | $this->no_mc_groups = array_unique($this->no_mc_groups); 284 | } 285 | 286 | function incr($id, $n = 1, $group = 'default' ) { 287 | $key = $this->key($id, $group); 288 | $mc =& $this->get_mc($group); 289 | $this->cache[ $key ] = $mc->increment( $key, $n ); 290 | return $this->cache[ $key ]; 291 | } 292 | 293 | function decr($id, $n = 1, $group = 'default' ) { 294 | $key = $this->key($id, $group); 295 | $mc =& $this->get_mc($group); 296 | $this->cache[ $key ] = $mc->decrement( $key, $n ); 297 | return $this->cache[ $key ]; 298 | } 299 | 300 | function close() { 301 | 302 | foreach ( $this->mc as $bucket => $mc ) { 303 | $mc->close(); 304 | } 305 | } 306 | 307 | function delete($id, $group = 'default') { 308 | if ( $id === 'alloptions' && $group === 'options' ) { 309 | return $this->delete_alloptions(); 310 | } 311 | 312 | $key = $this->key($id, $group); 313 | 314 | if ( in_array($group, $this->no_mc_groups) ) { 315 | unset($this->cache[$key]); 316 | return true; 317 | } 318 | 319 | $mc =& $this->get_mc($group); 320 | 321 | $time = microtime(true); 322 | $result = $mc->delete($key); 323 | $time_taken = microtime(true) - $time; 324 | 325 | @ ++$this->stats['delete']; 326 | $this->stats['delete_time'] += $time_taken; 327 | $this->group_ops[$group][] = "delete $id"; 328 | 329 | if ( false !== $result ) { 330 | unset( $this->cache[ $key ] ); 331 | } 332 | 333 | return $result; 334 | } 335 | 336 | function flush() { 337 | 338 | // Return true is flushing is disabled 339 | if ( ! WP_MEMCACHE_DISABLE_FLUSHING ) { 340 | return true; 341 | } 342 | 343 | // Did someone try and wipe our stats? >:( 344 | // This occurs during unit tests, where WP reaches in and resets the 345 | // stats array. 346 | if ( empty( $this->stats ) ) { 347 | $this->reset_stats(); 348 | } 349 | 350 | $ret = true; 351 | foreach ( array_keys( $this->mc ) as $group ) { 352 | $ret &= $this->mc[ $group ]->flush(); 353 | } 354 | return $ret; 355 | } 356 | 357 | /** 358 | * Flush the local (in-memory) object cache 359 | * 360 | * Forces all future requests to fetch from memcache. Can be used to 361 | * alleviate memory pressure in long-running requests. 362 | */ 363 | public function flush_local() { 364 | $this->cache = array(); 365 | $this->group_ops = array(); 366 | } 367 | 368 | protected function reset_stats() { 369 | $this->stats = array( 'get' => 0, 'get_time' => 0, 'add' => 0, 'add_time' => 0, 'delete' => 0, 'delete_time' => 0, 'set' => 0, 'set_time' => 0 ); 370 | } 371 | 372 | function get($id, $group = 'default', $force = false) { 373 | if ( $id === 'alloptions' && $group === 'options' ) { 374 | return $this->get_alloptions(); 375 | } 376 | 377 | $key = $this->key($id, $group); 378 | $mc =& $this->get_mc($group); 379 | 380 | if ( isset($this->cache[$key]) && ( !$force || in_array($group, $this->no_mc_groups) ) ) { 381 | if ( is_object( $this->cache[ $key ] ) ) { 382 | $value = clone $this->cache[ $key ]; 383 | } else { 384 | $value = $this->cache[ $key ]; 385 | } 386 | } else if ( in_array($group, $this->no_mc_groups) ) { 387 | $this->cache[$key] = $value = false; 388 | } else { 389 | 390 | $time = microtime(true); 391 | 392 | $value = $mc->get($key); 393 | if ( NULL === $value ) 394 | $value = false; 395 | 396 | $time_taken = microtime(true) - $time; 397 | 398 | $this->cache[$key] = $value; 399 | @ ++$this->stats['get']; 400 | $this->stats['get_time'] += $time_taken; 401 | $this->group_ops[$group][] = "get $id"; 402 | } 403 | 404 | if ( 'checkthedatabaseplease' === $value ) { 405 | unset( $this->cache[$key] ); 406 | $value = false; 407 | } 408 | 409 | return $value; 410 | } 411 | 412 | function get_multi( $groups ) { 413 | /* 414 | format: $get['group-name'] = array( 'key1', 'key2' ); 415 | */ 416 | $return = array(); 417 | $to_get = array(); 418 | 419 | foreach ( $groups as $group => $ids ) { 420 | $mc =& $this->get_mc( $group ); 421 | $return[ $group ] = array(); 422 | 423 | foreach ( $ids as $id ) { 424 | $key = $this->key( $id, $group ); 425 | if ( isset( $this->cache[ $key ] ) ) { 426 | if ( is_object( $this->cache[ $key ] ) ) { 427 | $return[ $group ][ $id ] = clone $this->cache[ $key ]; 428 | } else { 429 | $return[ $group ][ $id ] = $this->cache[ $key ]; 430 | } 431 | continue; 432 | } else if ( in_array( $group, $this->no_mc_groups ) ) { 433 | $return[ $group ][ $id ] = false; 434 | continue; 435 | } else { 436 | $to_get[ $key ] = array( $group, $id ); 437 | } 438 | } 439 | } 440 | 441 | if ( $to_get ) { 442 | $vals = $mc->get( array_keys( $to_get ) ); 443 | 444 | foreach ( $to_get as $key => $bits ) { 445 | if ( ! isset( $vals[ $key ] ) ) { 446 | continue; 447 | } 448 | 449 | list( $group, $id ) = $bits; 450 | 451 | $return[ $group ][ $id ] = $vals[ $key ]; 452 | $this->cache[ $key ] = $vals[ $key ]; 453 | } 454 | } 455 | 456 | @ ++$this->stats['get_multi']; 457 | $this->group_ops[$group][] = "get_multi $id"; 458 | return $return; 459 | } 460 | 461 | function key($key, $group) { 462 | if ( empty( $group ) ) { 463 | $group = 'default'; 464 | } 465 | 466 | if ( false !== array_search( $group, $this->global_groups ) ) { 467 | $prefix = $this->global_prefix; 468 | } else { 469 | $prefix = $this->blog_prefix; 470 | } 471 | 472 | return preg_replace('/\s+/', '', WP_CACHE_KEY_SALT . ":$prefix$group:$key"); 473 | } 474 | 475 | function replace( $id, $data, $group = 'default', $expire = 0 ) { 476 | $key = $this->key( $id, $group ); 477 | $expire = ( $expire == 0 ) ? $this->default_expiration : $expire; 478 | $mc =& $this->get_mc( $group ); 479 | 480 | if ( is_object( $data ) ) { 481 | $data = clone $data; 482 | } 483 | 484 | $result = $mc->replace( $key, $data, false, $expire ); 485 | if ( false !== $result ) { 486 | $this->cache[ $key ] = $data; 487 | } 488 | 489 | return $result; 490 | } 491 | 492 | function set($id, $data, $group = 'default', $expire = 0) { 493 | if ( $id === 'alloptions' && $group === 'options' ) { 494 | return $this->set_alloptions( $data ); 495 | } 496 | 497 | $key = $this->key($id, $group); 498 | if ( isset( $this->cache[ $key ] ) && ( 'checkthedatabaseplease' === $this->cache[ $key ] ) ) { 499 | return false; 500 | } 501 | 502 | if ( is_object( $data ) ) { 503 | $data = clone $data; 504 | } 505 | 506 | $this->cache[ $key ] = $data; 507 | 508 | if ( in_array( $group, $this->no_mc_groups ) ) { 509 | return true; 510 | } 511 | 512 | $expire = ($expire == 0) ? $this->default_expiration : $expire; 513 | $mc =& $this->get_mc($group); 514 | 515 | $time = microtime(true); 516 | 517 | /** 518 | * If the expiry exceeds 30 days, we have to sent the expire 519 | * as a unix timestamp. This is because PHP Memcache extension 520 | * uses this hueristic. 521 | * 522 | * To make this consistant, we always use absolute unix timestamps. 523 | */ 524 | if ( $expire ) { 525 | $expire += time(); 526 | } 527 | 528 | $result = $mc->set($key, $data, false, $expire); 529 | $time_taken = microtime(true) - $time; 530 | 531 | @ ++$this->stats['set']; 532 | $this->stats['set_time'] += $time_taken; 533 | $this->group_ops[$group][] = "set $id"; 534 | 535 | return $result; 536 | } 537 | 538 | function switch_to_blog( $blog_id ) { 539 | global $wpdb; 540 | $table_prefix = $wpdb->prefix; 541 | $blog_id = (int) $blog_id; 542 | $this->blog_prefix = ( is_multisite() ? $blog_id : $table_prefix ) . ':'; 543 | } 544 | 545 | function colorize_debug_line($line) { 546 | $colors = array( 547 | 'get' => 'green', 548 | 'set' => 'purple', 549 | 'add' => 'blue', 550 | 'delete' => 'red'); 551 | 552 | $cmd = substr($line, 0, strpos($line, ' ')); 553 | 554 | $cmd2 = "$cmd"; 555 | 556 | return $cmd2 . substr($line, strlen($cmd)) . "\n"; 557 | } 558 | 559 | function stats() { 560 | echo "

\n"; 561 | foreach ( $this->stats as $stat => $n ) { 562 | 563 | if ( ! $n ) { 564 | continue; 565 | } 566 | 567 | echo "$stat $n"; 568 | echo "
\n"; 569 | } 570 | echo "

\n"; 571 | echo "

Memcached:

"; 572 | foreach ( $this->group_ops as $group => $ops ) { 573 | if ( !isset($_GET['debug_queries']) && 500 < count($ops) ) { 574 | $ops = array_slice( $ops, 0, 500 ); 575 | echo "Too many to show! Show them anyway.\n"; 576 | } 577 | echo "

$group commands

"; 578 | echo "
\n";
579 | 			$lines = array();
580 | 			foreach ( $ops as $op ) {
581 | 				$lines[] = $this->colorize_debug_line($op);
582 | 			}
583 | 			print_r($lines);
584 | 			echo "
\n"; 585 | } 586 | } 587 | 588 | function &get_mc($group) { 589 | if ( isset( $this->mc[ $group ] ) ) { 590 | return $this->mc[ $group ]; 591 | } 592 | 593 | return $this->mc['default']; 594 | } 595 | 596 | function failure_callback( $host, $port ) { 597 | if ( !WP_MEMCACHE_DISABLE_LOGGING ) { 598 | error_log( "Memcache Connection failure for $host:$port\n" ); 599 | } 600 | } 601 | 602 | function __construct() { 603 | global $memcached_servers, $blog_id, $table_prefix; 604 | 605 | if ( isset( $memcached_servers ) ) { 606 | $buckets = $memcached_servers; 607 | } else { 608 | $buckets = array( '127.0.0.1:11211' ); 609 | } 610 | 611 | reset( $buckets ); 612 | if ( is_int( key( $buckets ) ) ) { 613 | $buckets = array( 'default' => $buckets ); 614 | } 615 | 616 | foreach ( $buckets as $bucket => $servers) { 617 | $this->mc[$bucket] = new Memcache(); 618 | foreach ( $servers as $server ) { 619 | list ( $node, $port ) = explode(':', $server); 620 | if ( ! $port ) { 621 | $port = ini_get( 'memcache.default_port' ); 622 | } 623 | $port = intval( $port ); 624 | if ( ! $port ) { 625 | $port = 11211; 626 | } 627 | $this->mc[$bucket]->addServer($node, $port, WP_MEMCACHE_PERSISTENT, WP_MEMCACHE_WEIGHT, WP_MEMCACHE_TIMEOUT, WP_MEMCACHE_RETRY, true, array($this, 'failure_callback')); 628 | $this->mc[$bucket]->setCompressThreshold(20000, 0.2); 629 | } 630 | } 631 | 632 | 633 | $this->global_prefix = ''; 634 | $this->blog_prefix = ''; 635 | if ( function_exists( 'is_multisite' ) ) { 636 | $this->global_prefix = ( is_multisite() || defined('CUSTOM_USER_TABLE') && defined('CUSTOM_USER_META_TABLE') ) ? '' : $table_prefix; 637 | $this->blog_prefix = ( is_multisite() ? $blog_id : $table_prefix ) . ':'; 638 | } 639 | 640 | $this->reset_stats(); 641 | $this->cache_hits =& $this->stats['get']; 642 | $this->cache_misses =& $this->stats['add']; 643 | } 644 | } 645 | --------------------------------------------------------------------------------