├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── composer.json ├── object-cache.php ├── phpunit.xml.dist └── tests ├── bin └── install-wp-tests.sh ├── bootstrap.php ├── ms-mock.php ├── redis-spy.php └── test-object-cache.php /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | composer.lock 3 | vendor 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | 4 | language: php 5 | 6 | notifications: 7 | email: 8 | on_success: never 9 | on_failure: change 10 | 11 | service: 12 | - redis-server 13 | 14 | branches: 15 | only: 16 | - master 17 | 18 | cache: 19 | directories: 20 | - $HOME/.composer/cache 21 | 22 | matrix: 23 | include: 24 | - php: 7.2 25 | env: WP_VERSION=latest 26 | - php: 7.0 27 | env: WP_VERSION=latest 28 | - php: 5.6 29 | env: WP_VERSION=latest 30 | 31 | before_script: 32 | - export PATH="$HOME/.composer/vendor/bin:$PATH" 33 | - echo "extension = redis.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 34 | - | 35 | if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then 36 | phpenv config-rm xdebug.ini 37 | else 38 | echo "xdebug.ini does not exist" 39 | fi 40 | - | 41 | if [[ ! -z "$WP_VERSION" ]] ; then 42 | bash tests/bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION 43 | composer global require "phpunit/phpunit=4.8.*|5.7.*" 44 | fi 45 | 46 | script: 47 | - | 48 | if [[ ! -z "$WP_VERSION" ]] ; then 49 | phpunit --no-coverage 50 | fi 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 1. Any fix PR should initially contain two commits: one with unit tests that fail, one with a fix. 2 | 1. Discussion in a PR may yield further fixes ontop. We'll squeeze them when merging. 3 | 1. Any feature PR should first be discussed in an Issue. 4 | 1. Any one commit should address one specific problem, do not clutter a commit with unrelated fixes or features. 5 | 1. Complete file changes are not going to be reviewed at all in 99% of cases. 6 | 1. In merge conflict cases we'll ask you to rebase off of a specific branch and force push once more. 7 | 1. We value your time as much as we value ours. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | A highly efficient, predictive and unit tested WordPress object cache backend that implements all available methods using the Redis PECL library. 4 | 5 | # Why is this fork better? 6 | 7 | - Preloads known cache keys via a single `mget()` call with lazy unserialization 8 | - Further microoptimized routines makes this the **fastest** Redis object cache implementation out there 9 | - Unit-tested with 100% effective target coverage 10 | 11 | For more information check out https://pressjitsu.com/blog/redis-object-cache-wordpress/ 12 | 13 | ## Authors 14 | 15 | * Pressjitsu, Inc. 16 | * Gennady Kovshenin 17 | * Eric Mann 18 | * Erick Hitter 19 | 20 | ## Installation 21 | 1. Install and configure Redis. There is a good tutorial [here](https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-debian-9). 22 | 2. Install the [Redis PECL module](http://pecl.php.net/package/redis) or compile from [source](https://github.com/phpredis/phpredis). 23 | 3. Add `object-cache.php` to the wp-content directory. It is a drop-in file, not a plugin, so it belongs in the wp-content directory, not the plugins directory. 24 | 4. By default, the script will connect to Redis at 127.0.0.1:6379. See the *Connecting to Redis* section for further options. 25 | 26 | ### Connecting to Redis ### 27 | 28 | By default, the plugin uses `127.0.0.1` and `6379` as the default host and port when creating a new client instance; the default database of `0` is also used. Three constants are provided to override these default values. 29 | 30 | Specify `WP_REDIS_BACKEND_HOST`, `WP_REDIS_BACKEND_PORT`, and `WP_REDIS_BACKEND_DB` to set the necessary, non-default connection values for your Redis instance. 31 | 32 | ### Prefixing Cache Keys ### 33 | 34 | The constant `WP_CACHE_KEY_SALT` is provided to add a prefix to all cache keys used by the plugin. If running two single instances of WordPress from the same Redis instance, this constant could be used to avoid overlap in cache keys. Note that special handling is not needed for WordPress Multisite. 35 | 36 | ## Support 37 | 38 | Support for this plugin can be had over at support@pressjitsu.com 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pressjitsu/pj-object-cache-red", 3 | "type": "wordpress-plugin", 4 | "description": "The fastest known WordPress Redis-based Object Cache.", 5 | "keywords": ["object cache", "redis", "performance"], 6 | "homepage": "https://pressjitsu.com/blog/redis-object-cache-wordpress/", 7 | "license": "GPL-3.0+", 8 | 9 | "require": { 10 | "php": ">=5.3", 11 | "ext-json": "*", 12 | "ext-redis": "*" 13 | }, 14 | 15 | "require-dev": { 16 | "phpunit/phpunit": ">4.0 <7" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /object-cache.php: -------------------------------------------------------------------------------- 1 | add( $key, $value, $group, $expiration ); 38 | } 39 | 40 | /** 41 | * Closes the cache. 42 | * 43 | * This function has ceased to do anything since WordPress 2.5. The 44 | * functionality was removed along with the rest of the persistent cache. This 45 | * does not mean that plugins can't implement this function when they need to 46 | * make sure that the cache is cleaned up after WordPress no longer needs it. 47 | * 48 | * @return bool Always returns True 49 | */ 50 | function wp_cache_close() { 51 | return true; 52 | } 53 | 54 | /** 55 | * Decrement a numeric item's value. 56 | * 57 | * @param string $key The key under which to store the value. 58 | * @param int $offset The amount by which to decrement the item's value. 59 | * @param string $group The group value appended to the $key. 60 | * 61 | * @global WP_Object_Cache $wp_object_cache 62 | * 63 | * @return int|bool Returns item's new value on success or FALSE on failure. 64 | */ 65 | function wp_cache_decr( $key, $offset = 1, $group = 'default' ) { 66 | global $wp_object_cache; 67 | return $wp_object_cache->decr( $key, $offset, $group ); 68 | } 69 | 70 | /** 71 | * Remove the item from the cache. 72 | * 73 | * @param string $key The key under which to store the value. 74 | * @param string $group The group value appended to the $key. 75 | * @param int $time The amount of time the server will wait to delete the item in seconds. 76 | * 77 | * @global WP_Object_Cache $wp_object_cache 78 | * 79 | * @return bool Returns TRUE on success or FALSE on failure. 80 | */ 81 | function wp_cache_delete( $key, $group = 'default', $time = 0 ) { 82 | global $wp_object_cache; 83 | return $wp_object_cache->delete( $key, $group, $time ); 84 | } 85 | 86 | /** 87 | * Invalidate all items in the cache. 88 | * 89 | * @param int $delay Number of seconds to wait before invalidating the items. 90 | * 91 | * @global WP_Object_Cache $wp_object_cache 92 | * 93 | * @return bool Returns TRUE on success or FALSE on failure. 94 | */ 95 | function wp_cache_flush( $delay = 0 ) { 96 | global $wp_object_cache; 97 | return $wp_object_cache->flush( $delay ); 98 | } 99 | 100 | /** 101 | * Retrieve object from cache. 102 | * 103 | * Gets an object from cache based on $key and $group. 104 | * 105 | * @param string $key The key under which to store the value. 106 | * @param string $group The group value appended to the $key. 107 | * 108 | * @global WP_Object_Cache $wp_object_cache 109 | * 110 | * @return bool|mixed Cached object value. 111 | */ 112 | function wp_cache_get( $key, $group = 'default', $force = false, &$found = null ) { 113 | global $wp_object_cache; 114 | return $wp_object_cache->get( $key, $group, $force, $found ); 115 | } 116 | 117 | /** 118 | * Retrieve multiple values from cache. 119 | * 120 | * Gets multiple values from cache, including across multiple groups 121 | * 122 | * Usage: array( 'group0' => array( 'key0', 'key1', 'key2', ), 'group1' => array( 'key0' ) ) 123 | * 124 | * Mirrors the Memcached Object Cache plugin's argument and return-value formats 125 | * 126 | * @param array $groups Array of groups and keys to retrieve 127 | * 128 | * @global WP_Object_Cache $wp_object_cache 129 | * 130 | * @return bool|mixed Array of cached values, keys in the format $group:$key. Non-existent keys false 131 | */ 132 | function wp_cache_get_multi( $groups, $unserialize = true ) { 133 | global $wp_object_cache; 134 | return $wp_object_cache->get_multi( $groups, $unserialize ); 135 | } 136 | 137 | /** 138 | * Increment a numeric item's value. 139 | * 140 | * @param string $key The key under which to store the value. 141 | * @param int $offset The amount by which to increment the item's value. 142 | * @param string $group The group value appended to the $key. 143 | * 144 | * @global WP_Object_Cache $wp_object_cache 145 | * 146 | * @return int|bool Returns item's new value on success or FALSE on failure. 147 | */ 148 | function wp_cache_incr( $key, $offset = 1, $group = 'default' ) { 149 | global $wp_object_cache; 150 | return $wp_object_cache->incr( $key, $offset, $group ); 151 | } 152 | 153 | /** 154 | * Sets up Object Cache Global and assigns it. 155 | * 156 | * @global WP_Object_Cache $wp_object_cache WordPress Object Cache 157 | * 158 | * @return void 159 | */ 160 | function wp_cache_init() { 161 | global $wp_object_cache; 162 | $wp_object_cache = new WP_Object_Cache(); 163 | } 164 | 165 | /** 166 | * Replaces a value in cache. 167 | * 168 | * This method is similar to "add"; however, is does not successfully set a value if 169 | * the object's key is not already set in cache. 170 | * 171 | * @param string $key The key under which to store the value. 172 | * @param mixed $value The value to store. 173 | * @param string $group The group value appended to the $key. 174 | * @param int $expiration The expiration time, defaults to 0. 175 | * 176 | * @global WP_Object_Cache $wp_object_cache 177 | * 178 | * @return bool Returns TRUE on success or FALSE on failure. 179 | */ 180 | function wp_cache_replace( $key, $value, $group = 'default', $expiration = 0 ) { 181 | global $wp_object_cache; 182 | return $wp_object_cache->replace( $key, $value, $group, $expiration ); 183 | } 184 | 185 | /** 186 | * Sets a value in cache. 187 | * 188 | * The value is set whether or not this key already exists in Redis. 189 | * 190 | * @param string $key The key under which to store the value. 191 | * @param mixed $value The value to store. 192 | * @param string $group The group value appended to the $key. 193 | * @param int $expiration The expiration time, defaults to 0. 194 | * 195 | * @global WP_Object_Cache $wp_object_cache 196 | * 197 | * @return bool Returns TRUE on success or FALSE on failure. 198 | */ 199 | function wp_cache_set( $key, $value, $group = 'default', $expiration = 0 ) { 200 | global $wp_object_cache; 201 | return $wp_object_cache->set( $key, $value, $group, $expiration ); 202 | } 203 | 204 | /** 205 | * Switch the interal blog id. 206 | * 207 | * This changes the blog id used to create keys in blog specific groups. 208 | * 209 | * @param int $_blog_id Blog ID 210 | * 211 | * @global WP_Object_Cache $wp_object_cache 212 | * 213 | * @return bool 214 | */ 215 | function wp_cache_switch_to_blog( $_blog_id ) { 216 | global $wp_object_cache; 217 | return $wp_object_cache->switch_to_blog( $_blog_id ); 218 | } 219 | 220 | /** 221 | * Adds a group or set of groups to the list of Redis groups. 222 | * 223 | * @param string|array $groups A group or an array of groups to add. 224 | * 225 | * @global WP_Object_Cache $wp_object_cache 226 | * 227 | * @return void 228 | */ 229 | function wp_cache_add_global_groups( $groups ) { 230 | global $wp_object_cache; 231 | $wp_object_cache->add_global_groups( $groups ); 232 | } 233 | 234 | /** 235 | * Adds a group or set of groups to the list of non-Redis groups. 236 | * 237 | * @param string|array $groups A group or an array of groups to add. 238 | * 239 | * @global WP_Object_Cache $wp_object_cache 240 | * 241 | * @return void 242 | */ 243 | function wp_cache_add_non_persistent_groups( $groups ) { 244 | global $wp_object_cache; 245 | $wp_object_cache->add_non_persistent_groups( $groups ); 246 | } 247 | 248 | class WP_Object_Cache { 249 | 250 | /** 251 | * Holds the Redis client. 252 | * 253 | * @var Redis 254 | */ 255 | private $redis; 256 | 257 | /** 258 | * Track if Redis is available 259 | * 260 | * @var bool 261 | */ 262 | private $redis_connected = false; 263 | 264 | /** 265 | * Local cache 266 | * 267 | * @var array 268 | */ 269 | public $cache = array(); 270 | 271 | private $to_unserialize = array(); 272 | 273 | public $to_preload = array(); 274 | 275 | /** 276 | * List of global groups. 277 | * 278 | * @var array 279 | */ 280 | public $global_groups = array( 'users', 'userlogins', 'usermeta', 'site-options', 'site-lookup', 'blog-lookup', 'blog-details', 'rss' ); 281 | 282 | private $_global_groups; 283 | 284 | /** 285 | * List of groups not saved to Redis. 286 | * 287 | * @var array 288 | */ 289 | public $no_redis_groups = array( 'comment', 'counts' ); 290 | 291 | /** 292 | * Prefix used for global groups. 293 | * 294 | * @var string 295 | */ 296 | public $global_prefix = ''; 297 | 298 | /** 299 | * Prefix used for non-global groups. 300 | * 301 | * @var string 302 | */ 303 | public $blog_prefix = ''; 304 | 305 | /** 306 | * Track how many requests were found in cache 307 | * 308 | * @var int 309 | */ 310 | public $cache_hits = 0; 311 | 312 | /** 313 | * Track how may requests were not cached 314 | * 315 | * @var int 316 | */ 317 | public $cache_misses = 0; 318 | 319 | private $multisite; 320 | 321 | public $stats = array(); 322 | 323 | /** 324 | * Instantiate the Redis class. 325 | * 326 | * Instantiates the Redis class. 327 | * 328 | * @param null $persistent_id To create an instance that persists between requests, use persistent_id to specify a unique ID for the instance. 329 | */ 330 | public function __construct( $redis_instance = null ) { 331 | // General Redis settings 332 | $redis = array( 333 | 'host' => '127.0.0.1', 334 | 'port' => 6379, 335 | ); 336 | 337 | if ( defined( 'WP_REDIS_BACKEND_HOST' ) && WP_REDIS_BACKEND_HOST ) { 338 | $redis['host'] = WP_REDIS_BACKEND_HOST; 339 | } 340 | if ( defined( 'WP_REDIS_BACKEND_PORT' ) && WP_REDIS_BACKEND_PORT ) { 341 | $redis['port'] = WP_REDIS_BACKEND_PORT; 342 | } 343 | if ( defined( 'WP_REDIS_BACKEND_AUTH' ) && WP_REDIS_BACKEND_AUTH ) { 344 | $redis['auth'] = WP_REDIS_BACKEND_AUTH; 345 | } 346 | if ( defined( 'WP_REDIS_BACKEND_DB' ) && WP_REDIS_BACKEND_DB ) { 347 | $redis['database'] = WP_REDIS_BACKEND_DB; 348 | } 349 | 350 | // Use Redis PECL library. 351 | try { 352 | if ( is_null( $redis_instance ) ) { 353 | $redis_instance = new Redis(); 354 | } 355 | $this->redis = $redis_instance; 356 | $this->redis->connect( $redis['host'], $redis['port'] ); 357 | $this->redis->setOption( Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE ); 358 | 359 | if ( isset( $redis['auth'] ) ) { 360 | $this->redis->auth( $redis['auth'] ); 361 | } 362 | 363 | if ( isset( $redis['database'] ) ) { 364 | $this->redis->select( $redis['database'] ); 365 | } 366 | 367 | $this->redis_connected = true; 368 | } catch ( RedisException $e ) { 369 | // When Redis is unavailable, fall back to the internal back by forcing all groups to be "no redis" groups 370 | $this->no_redis_groups = array_unique( array_merge( $this->no_redis_groups, $this->global_groups ) ); 371 | $this->redis_connected = false; 372 | } 373 | 374 | /** 375 | * This approach is borrowed from Sivel and Boren. Use the salt for easy cache invalidation and for 376 | * multi single WP installs on the same server. 377 | */ 378 | if ( ! defined( 'WP_CACHE_KEY_SALT' ) ) { 379 | define( 'WP_CACHE_KEY_SALT', '' ); 380 | } 381 | 382 | $this->multisite = is_multisite(); 383 | $this->blog_prefix = $this->multisite ? get_current_blog_id() . ':' : ''; 384 | $this->_global_groups = array_flip( $this->global_groups ); 385 | 386 | $this->maybe_preload(); 387 | } 388 | 389 | public function maybe_preload() { 390 | if ( ! $this->can_redis() || empty( $_SERVER['REQUEST_URI'] ) ) { 391 | return; 392 | } 393 | 394 | if ( defined( 'WP_CLI' ) && WP_CLI ) { 395 | return; 396 | } 397 | 398 | $request_hash = md5( json_encode( array( 399 | $_SERVER['HTTP_HOST'], 400 | $_SERVER['REQUEST_URI'], 401 | ) ) ); 402 | 403 | $this->preload( $request_hash ); 404 | 405 | if ( defined( 'DOING_TESTS' ) && DOING_TESTS ) { 406 | return $request_hash; 407 | } 408 | 409 | register_shutdown_function( array( $this, 'save_preloads' ), $request_hash ); 410 | } 411 | 412 | public function preload( $hash ) { 413 | $keys = $this->get( $hash, 'pj-preload' ); 414 | if ( is_array( $keys ) ) { 415 | $this->get_multi( $keys, false ); 416 | } 417 | } 418 | 419 | public function save_preloads( $hash ) { 420 | $keys = array(); 421 | 422 | foreach ( $this->to_preload as $group => $_keys ) { 423 | if ( $group === 'pj-preload' ) { 424 | continue; 425 | } 426 | 427 | if ( in_array( $group, $this->no_redis_groups ) ) { 428 | continue; 429 | } 430 | 431 | $_keys = array_keys( $_keys ); 432 | $keys[ $group ] = $_keys; 433 | } 434 | 435 | $this->set( $hash, $keys, 'pj-preload' ); 436 | } 437 | 438 | /** 439 | * Is Redis available? 440 | * 441 | * @return bool 442 | */ 443 | protected function can_redis() { 444 | return $this->redis_connected; 445 | } 446 | 447 | /** 448 | * Adds a value to cache. 449 | * 450 | * If the specified key already exists, the value is not stored and the function 451 | * returns false. 452 | * 453 | * @param string $key The key under which to store the value. 454 | * @param mixed $value The value to store. 455 | * @param string $group The group value appended to the $key. 456 | * @param int $expiration The expiration time, defaults to 0. 457 | * @return bool Returns TRUE on success or FALSE on failure. 458 | */ 459 | public function add( $_key, $value, $group, $expiration = 0 ) { 460 | if ( wp_suspend_cache_addition() ) { 461 | return false; 462 | } 463 | 464 | list( $key, $redis_key ) = $this->build_key( $_key, $group ); 465 | 466 | if ( isset( $this->cache[ $group ], $this->cache[ $group ][ $key ] ) && false !== $this->cache[ $group ][ $key ] ) { 467 | return false; 468 | } 469 | 470 | return $this->set( $_key, $value, $group, $expiration ); 471 | } 472 | 473 | /** 474 | * Replace a value in the cache. 475 | * 476 | * If the specified key doesn't exist, the value is not stored and the function 477 | * returns false. 478 | * 479 | * @param string $key The key under which to store the value. 480 | * @param mixed $value The value to store. 481 | * @param string $group The group value appended to the $key. 482 | * @param int $expiration The expiration time, defaults to 0. 483 | * @return bool Returns TRUE on success or FALSE on failure. 484 | */ 485 | public function replace( $_key, $value, $group, $expiration = 0 ) { 486 | list( $key, $redis_key ) = $this->build_key( $_key, $group ); 487 | 488 | // If group is a non-Redis group, save to internal cache, not Redis 489 | if ( in_array( $group, $this->no_redis_groups ) || ! $this->can_redis() ) { 490 | if ( ! isset( $this->cache[ $group ], $this->cache[ $group ][ $key ] ) ) { 491 | return false; 492 | } 493 | } else { 494 | if ( ! $this->redis->exists( $redis_key ) ) { 495 | return false; 496 | } 497 | } 498 | 499 | return $this->set( $_key, $value, $group, $expiration ); 500 | } 501 | 502 | /** 503 | * Remove the item from the cache. 504 | * 505 | * @param string $key The key under which to store the value. 506 | * @param string $group The group value appended to the $key. 507 | * @return bool Returns TRUE on success or FALSE on failure. 508 | */ 509 | public function delete( $_key, $group ) { 510 | list( $key, $redis_key ) = $this->build_key( $_key, $group ); 511 | 512 | if ( in_array( $group, $this->no_redis_groups ) || ! $this->can_redis() ) { 513 | if ( ! isset( $this->cache[ $group ], $this->cache[ $group ][ $key ] ) ) { 514 | return false; 515 | } 516 | 517 | unset( $this->cache[ $group ][ $key ] ); 518 | unset( $this->to_preload[ $group ][ $key ] ); 519 | unset( $this->to_unserialize[ $redis_key ] ); 520 | return true; 521 | } 522 | 523 | unset( $this->cache[ $group ][ $key ] ); 524 | unset( $this->to_preload[ $group ][ $key ] ); 525 | unset( $this->to_unserialize[ $redis_key ] ); 526 | 527 | return (bool) $this->redis->del( $redis_key ); 528 | } 529 | 530 | /** 531 | * Invalidate all items in the cache. 532 | * 533 | * @return bool 534 | */ 535 | public function flush() { 536 | $this->cache = array(); 537 | $this->to_preload = array(); 538 | $this->to_unserialize = array(); 539 | 540 | if ( $this->can_redis() ) { 541 | $this->redis->flushDb(); 542 | } 543 | 544 | return true; 545 | } 546 | 547 | /** 548 | * Retrieve object from cache. 549 | * 550 | * Gets an object from cache based on $key and $group. 551 | * 552 | * @param string $key The key under which to store the value. 553 | * @param string $group The group value appended to the $key. 554 | * @return bool|mixed Cached object value. 555 | */ 556 | public function get( $_key, $group = 'default', $force = false, &$found = null ) { 557 | list( $key, $redis_key ) = $this->build_key( $_key, $group ); 558 | 559 | $this->to_preload[ $group ][ $_key ] = true; 560 | 561 | if ( ! $force && isset( $this->cache[ $group ][ $key ] ) ) { 562 | $value = $this->cache[ $group ][ $key ]; 563 | 564 | if ( isset( $this->to_unserialize[ $redis_key ] ) ) { 565 | unset( $this->to_unserialize[ $redis_key ] ); 566 | $value = unserialize( $value ); 567 | $this->cache[ $group ][ $key ] = $value; 568 | } 569 | 570 | $found = true; 571 | $this->cache_hits += 1; 572 | 573 | return is_object( $value ) ? clone $value : $value; 574 | } 575 | 576 | if ( in_array( $group, $this->no_redis_groups ) || ! $this->can_redis() ) { 577 | $found = false; 578 | $this->cache_misses += 1; 579 | return false; 580 | } 581 | 582 | // Fetch from Redis 583 | $value = $this->redis->get( $redis_key ); 584 | 585 | if ( ! is_string( $value ) ) { 586 | $found = false; 587 | $this->cache[ $group ][ $key ] = false; 588 | $this->cache_misses += 1; 589 | return false; 590 | } 591 | 592 | $found = true; 593 | 594 | $value = is_numeric( $value ) ? $value : unserialize( $value ); 595 | $this->cache[ $group ][ $key ] = $value; 596 | $this->cache_hits += 1; 597 | return $value; 598 | } 599 | 600 | /** 601 | * Retrieve multiple values from cache. 602 | * 603 | * Gets multiple values from cache, including across multiple groups 604 | * 605 | * Usage: array( 'group0' => array( 'key0', 'key1', 'key2', ), 'group1' => array( 'key0' ) ) 606 | * 607 | * @param array $groups Array of groups and keys to retrieve 608 | * @return bool|mixed Array of cached values, keys in the format $group:$key. Non-existent keys null. 609 | */ 610 | public function get_multi( $groups, $unserialize = true ) { 611 | if ( empty( $groups ) || ! is_array( $groups ) ) { 612 | return false; 613 | } 614 | 615 | // Retrieve requested caches and reformat results to mimic Memcached Object Cache's output 616 | $cache = array(); 617 | $fetch_keys = array(); 618 | $map = array(); 619 | 620 | foreach ( $groups as $group => $keys ) { 621 | if ( in_array( $group, $this->no_redis_groups ) || ! $this->can_redis() ) { 622 | foreach ( $keys as $_key ) { 623 | list( $key, $redis_key ) = $this->build_key( $_key, $group ); 624 | $cache[ $group ][ $key ] = $this->get( $_key, $group ); 625 | } 626 | 627 | continue; 628 | } 629 | 630 | if ( empty( $cache[ $group ] ) ) { 631 | $cache[ $group ] = array(); 632 | } 633 | 634 | foreach ( $keys as $_key ) { 635 | list( $key, $redis_key ) = $this->build_key( $_key, $group ); 636 | 637 | if ( isset( $this->cache[ $group ][ $key ] ) ) { 638 | $cache[ $group ][ $key ] = $this->cache[ $group ][ $key ]; 639 | continue; 640 | } 641 | 642 | // Fetch these from Redis 643 | $map[ $redis_key ] = array( $group, $key ); 644 | $fetch_keys[] = $redis_key; 645 | } 646 | } 647 | 648 | // Nothing else to fetch 649 | if ( empty( $fetch_keys ) ) { 650 | return $cache; 651 | } 652 | 653 | $results = $this->redis->mget( $fetch_keys ); 654 | foreach( array_combine( $fetch_keys, $results ) as $redis_key => $value ) { 655 | list( $group, $key ) = $map[ $redis_key ]; 656 | 657 | if ( is_string( $value ) ) { 658 | if ( ! $unserialize && ! is_numeric( $value ) ) { 659 | $this->to_unserialize[ $redis_key ] = true; 660 | } elseif ( $unserialize ) { 661 | $this->to_preload[ $group ][ $key ] = true; 662 | $value = is_numeric( $value ) ? $value : unserialize( $value ); 663 | } 664 | } else { 665 | $value = false; 666 | } 667 | 668 | $this->cache[ $group ][ $key ] = $cache[ $group ][ $key ] = $value; 669 | } 670 | 671 | return $cache; 672 | } 673 | 674 | /** 675 | * Sets a value in cache. 676 | * 677 | * The value is set whether or not this key already exists in Redis. 678 | * 679 | * @param string $key The key under which to store the value. 680 | * @param mixed $value The value to store. 681 | * @param string $group The group value appended to the $key. 682 | * @param int $expiration The expiration time, defaults to 0. 683 | * @return bool Returns TRUE on success or FALSE on failure. 684 | */ 685 | public function set( $_key, $value, $group = 'default', $expiration = 0 ) { 686 | list( $key, $redis_key ) = $this->build_key( $_key, $group ); 687 | 688 | if ( is_object( $value ) ) { 689 | $value = clone $value; 690 | } 691 | 692 | $this->cache[ $group ][ $key ] = $value; 693 | 694 | if ( in_array( $group, $this->no_redis_groups ) || ! $this->can_redis() ) { 695 | return true; 696 | } 697 | 698 | $value = is_numeric( $value ) ? $value : serialize( $value ); 699 | 700 | // Save to Redis 701 | if ( $expiration ) { 702 | $this->redis->setex( $redis_key, $expiration, $value ); 703 | } else { 704 | $this->redis->set( $redis_key, $value ); 705 | } 706 | 707 | return true; 708 | } 709 | 710 | /** 711 | * Increment a Redis counter by the amount specified 712 | * 713 | * @param string $key 714 | * @param int $offset 715 | * @param string $group 716 | * @return bool 717 | */ 718 | public function incr( $_key, $offset = 1, $group ) { 719 | list( $key, $redis_key ) = $this->build_key( $_key, $group ); 720 | 721 | if ( in_array( $group, $this->no_redis_groups ) || ! $this->can_redis() ) { 722 | // Consistent with the Redis behavior (start from 0 if not exists) 723 | if ( ! isset( $this->cache[ $group ][ $key ] ) ) { 724 | $this->cache[ $group ][ $key ] = 0; 725 | } 726 | 727 | $this->cache[ $group ][ $key ] += $offset; 728 | return true; 729 | } 730 | 731 | // Save to Redis 732 | $value = $this->redis->incrBy( $redis_key, $offset ); 733 | $this->cache[ $group ][ $key ] = $value; 734 | return $value; 735 | } 736 | 737 | /** 738 | * Decrement a Redis counter by the amount specified 739 | * 740 | * @param string $key 741 | * @param int $offset 742 | * @param string $group 743 | * @return bool 744 | */ 745 | public function decr( $key, $offset = 1, $group = 'default' ) { 746 | return $this->incr( $key, $offset * -1, $group ); 747 | } 748 | 749 | /** 750 | * Builds a key for the cached object using the blog_id, key, and group values. 751 | * 752 | * @author Ryan Boren This function is inspired by the original WP Memcached Object cache. 753 | * @link http://wordpress.org/extend/plugins/memcached/ 754 | * 755 | * @param string $key The key under which to store the value. 756 | * @param string $group The group value appended to the $key. 757 | * 758 | * @return array 759 | */ 760 | public function build_key( $key, $group = 'default' ) { 761 | $prefix = ''; 762 | if ( ! isset( $this->_global_groups[ $group ] ) ) { 763 | $prefix = $this->blog_prefix; 764 | } 765 | 766 | $local_key = $prefix . $key; 767 | return array( $local_key, WP_CACHE_KEY_SALT . "$prefix$group:$key" ); 768 | } 769 | 770 | /** 771 | * In multisite, switch blog prefix when switching blogs 772 | * 773 | * @param int $_blog_id 774 | * @return bool 775 | */ 776 | public function switch_to_blog( $blog_id ) { 777 | $this->blog_prefix = $this->multisite ? $blog_id . ':' : ''; 778 | } 779 | 780 | /** 781 | * Sets the list of global groups. 782 | * 783 | * @param array $groups List of groups that are global. 784 | */ 785 | public function add_global_groups( $groups ) { 786 | $groups = (array) $groups; 787 | 788 | if ( $this->can_redis() ) { 789 | $this->global_groups = array_unique( array_merge( $this->global_groups, $groups ) ); 790 | } else { 791 | $this->no_redis_groups = array_unique( array_merge( $this->no_redis_groups, $groups ) ); 792 | } 793 | 794 | $this->_global_groups = array_flip( $this->global_groups ); 795 | } 796 | 797 | /** 798 | * Sets the list of groups not to be cached by Redis. 799 | * 800 | * @param array $groups List of groups that are to be ignored. 801 | */ 802 | public function add_non_persistent_groups( $groups ) { 803 | $groups = (array) $groups; 804 | 805 | $this->no_redis_groups = array_unique( array_merge( $this->no_redis_groups, $groups ) ); 806 | } 807 | } 808 | 809 | endif; 810 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | tests 17 | 18 | 19 | 20 | 21 | . 22 | 23 | tests 24 | vendor 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | TMPDIR=${TMPDIR-/tmp} 16 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 17 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 18 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} 19 | 20 | download() { 21 | if [ `which curl` ]; then 22 | curl -s "$1" > "$2"; 23 | elif [ `which wget` ]; then 24 | wget -nv -O "$2" "$1" 25 | fi 26 | } 27 | 28 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 29 | WP_TESTS_TAG="branches/$WP_VERSION" 30 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 31 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 32 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 33 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 34 | else 35 | WP_TESTS_TAG="tags/$WP_VERSION" 36 | fi 37 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 38 | WP_TESTS_TAG="trunk" 39 | else 40 | # http serves a single offer, whereas https serves multiple. we only want one 41 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 42 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 43 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 44 | if [[ -z "$LATEST_VERSION" ]]; then 45 | echo "Latest WordPress version could not be found" 46 | exit 1 47 | fi 48 | WP_TESTS_TAG="tags/$LATEST_VERSION" 49 | fi 50 | 51 | set -ex 52 | 53 | install_wp() { 54 | 55 | if [ -d $WP_CORE_DIR ]; then 56 | return; 57 | fi 58 | 59 | mkdir -p $WP_CORE_DIR 60 | 61 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 62 | mkdir -p $TMPDIR/wordpress-nightly 63 | download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip 64 | unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ 65 | mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR 66 | else 67 | if [ $WP_VERSION == 'latest' ]; then 68 | local ARCHIVE_NAME='latest' 69 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 70 | # https serves multiple offers, whereas http serves single. 71 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 72 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 73 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 74 | LATEST_VERSION=${WP_VERSION%??} 75 | else 76 | # otherwise, scan the releases and get the most up to date minor version of the major release 77 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 78 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 79 | fi 80 | if [[ -z "$LATEST_VERSION" ]]; then 81 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 82 | else 83 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 84 | fi 85 | else 86 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 87 | fi 88 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 89 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 90 | fi 91 | 92 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 93 | } 94 | 95 | install_test_suite() { 96 | # portable in-place argument for both GNU sed and Mac OSX sed 97 | if [[ $(uname -s) == 'Darwin' ]]; then 98 | local ioption='-i.bak' 99 | else 100 | local ioption='-i' 101 | fi 102 | 103 | # set up testing suite if it doesn't yet exist 104 | if [ ! -d $WP_TESTS_DIR ]; then 105 | # set up testing suite 106 | mkdir -p $WP_TESTS_DIR 107 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 108 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 109 | fi 110 | 111 | if [ ! -f wp-tests-config.php ]; then 112 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 113 | # remove all forward slashes in the end 114 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 115 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 116 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 117 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 118 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 119 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 120 | fi 121 | 122 | } 123 | 124 | install_db() { 125 | 126 | if [ ${SKIP_DB_CREATE} = "true" ]; then 127 | return 0 128 | fi 129 | 130 | # parse DB_HOST for port or socket references 131 | local PARTS=(${DB_HOST//\:/ }) 132 | local DB_HOSTNAME=${PARTS[0]}; 133 | local DB_SOCK_OR_PORT=${PARTS[1]}; 134 | local EXTRA="" 135 | 136 | if ! [ -z $DB_HOSTNAME ] ; then 137 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 138 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 139 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 140 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 141 | elif ! [ -z $DB_HOSTNAME ] ; then 142 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 143 | fi 144 | fi 145 | 146 | # create database 147 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 148 | } 149 | 150 | install_wp 151 | install_test_suite 152 | install_db 153 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | b = $b; } 20 | public function add_global_groups() {} 21 | public function add_non_persistent_groups() {} 22 | public function add( $k, $v, $g='' ) { isset( $this->c[ "{$this->b}:$k:$g" ] ) || $this->set( $k, $v ); } 23 | public function set( $k, $v, $g='' ) { $this->c[ "{$this->b}:$k:$g" ] = $v; } 24 | public function get( $k, $g='' ) { return isset( $this->c[ "{$this->b}:$k:$g" ] ) ? $this->c[ "{$this->b}:$k:$g" ] : false; } 25 | }; 26 | 27 | global $_wp_using_ext_object_cache; 28 | $_wp_using_ext_object_cache = true; 29 | 30 | // Load test function so tests_add_filter() is available. 31 | require_once $_wp_tests_dir . '/includes/functions.php'; 32 | 33 | // Load and install the plugins. 34 | tests_add_filter( 'muplugins_loaded', function() use ( $_pj_ocr_tests_dir ) { 35 | wp_cache_init(); 36 | } ); 37 | 38 | register_shutdown_function( function() { 39 | global $wpdb; 40 | $wpdb->query( "SET foreign_key_checks = 0" ); 41 | foreach ( get_sites() as $site ) { 42 | switch_to_blog( $site->blog_id ); 43 | foreach ( $wpdb->tables() as $table => $prefixed_table ) { 44 | $wpdb->query( "DROP TABLE IF EXISTS $prefixed_table" ); 45 | } 46 | } 47 | } ); 48 | 49 | // Load the WP testing environment. 50 | require_once $_wp_tests_dir . '/includes/bootstrap.php'; 51 | -------------------------------------------------------------------------------- /tests/ms-mock.php: -------------------------------------------------------------------------------- 1 | redis = new Redis(); 9 | } 10 | 11 | public function _get( $method ) { 12 | return isset( $this->calls[ $method ] ) ? $this->calls[ $method ] : array(); 13 | } 14 | 15 | public function _reset() { 16 | $this->calls = array(); 17 | } 18 | 19 | public function __call( $method, $arguments ) { 20 | if ( ! isset( $this->calls[ $method ] ) ) { 21 | $this->calls[ $method ] = array(); 22 | } 23 | 24 | $return = call_user_func_array( array( $this->redis, $method ), $arguments ); 25 | 26 | $this->calls[ $method ][] = array( 27 | 'return' => $return, 28 | 'arguments' => $arguments, 29 | ); 30 | 31 | return $return; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/test-object-cache.php: -------------------------------------------------------------------------------- 1 | redis_spy = new Redis_Spy() ); 6 | wp_cache_flush(); 7 | } 8 | 9 | private function assertRedisCalls( $method, $count ) { 10 | $this->assertEquals( $count, $actual = count( $this->redis_spy->_get( $method ) ), "Redis::$method called $actual times" ); 11 | } 12 | 13 | public function test_simple() { 14 | $this->assertFalse( wp_cache_get( 'miss' ) ); 15 | $this->assertFalse( wp_cache_get( 'miss', 'group' ) ); 16 | 17 | $this->assertTrue( wp_cache_set( 'miss', '1', 'group' ) ); 18 | $this->assertFalse( wp_cache_get( 'miss' ) ); 19 | $this->assertEquals( '1', wp_cache_get( 'miss', 'group' ) ); 20 | } 21 | 22 | public function test_spaces_in_keys() { 23 | global $wp_object_cache; 24 | 25 | wp_cache_set( 'hello world', '1' ); 26 | $this->assertEquals( '1', wp_cache_get( 'hello world' ) ); 27 | 28 | $wp_object_cache->cache = array(); 29 | 30 | $this->assertEquals( '1', wp_cache_get( 'hello world' ) ); 31 | 32 | wp_cache_set( 'helloworld', '2' ); 33 | $this->assertEquals( '1', wp_cache_get( 'hello world') ); 34 | $this->assertEquals( '2', wp_cache_get( 'helloworld' ) ); 35 | 36 | $wp_object_cache->cache = array(); 37 | 38 | $this->assertEquals( '1', wp_cache_get( 'hello world') ); 39 | $this->assertEquals( '2', wp_cache_get( 'helloworld' ) ); 40 | } 41 | 42 | public function test_internal_cache_miss() { 43 | wp_cache_get( 'miss' ); 44 | wp_cache_get( 'miss' ); 45 | wp_cache_get( 'miss', 'default', true ); 46 | $this->assertFalse( wp_cache_get( 'miss' ) ); 47 | 48 | $this->assertRedisCalls( 'get', 2 ); 49 | } 50 | 51 | public function test_internal_cache_hit() { 52 | wp_cache_set( 'hit', '1' ); 53 | 54 | wp_cache_get( 'hit' ); 55 | wp_cache_get( 'hit' ); 56 | $this->assertEquals( '1', wp_cache_get( 'hit', 'default', true ) ); 57 | $this->assertEquals( '1', wp_cache_get( 'hit' ) ); 58 | 59 | $this->assertRedisCalls( 'get', 1 ); 60 | } 61 | 62 | public function test_incr_decr() { 63 | wp_cache_incr( 'incr' ); 64 | wp_cache_decr( 'decr' ); 65 | 66 | $this->assertEquals( 1, wp_cache_get( 'incr' ) ); 67 | $this->assertEquals( -1, wp_cache_get( 'decr' ) ); 68 | 69 | $this->assertRedisCalls( 'incrBy', 2 ); 70 | $this->assertRedisCalls( 'get', 0 ); 71 | } 72 | 73 | public function test_multi_get() { 74 | wp_cache_set( 'hit', '1' ); 75 | wp_cache_set( 'hit', '2', 'group2' ); 76 | 77 | global $wp_object_cache; 78 | $wp_object_cache->cache = array(); 79 | 80 | $result = wp_cache_get_multi( array( 81 | 'group2' => array( 'hit' ), 82 | 'default' => array( 'hit' ), 83 | ) ); 84 | 85 | $this->assertEquals( array( 86 | 'group2' => array( 87 | 'hit' => '2', 88 | ), 89 | 'default' => array( 90 | 'hit' => '1', 91 | ) 92 | ), $result ); 93 | 94 | wp_cache_get( 'hit' ); 95 | wp_cache_get( 'hit', 'group2' ); 96 | 97 | $this->assertRedisCalls( 'get', 0 ); 98 | $this->assertRedisCalls( 'mget', 1 ); 99 | } 100 | 101 | public function test_preload() { 102 | wp_cache_set( 'hit', '1' ); 103 | wp_cache_set( 'hit', '2', 'group2' ); 104 | 105 | $this->assertEquals( '1', wp_cache_get( 'hit' ) ); 106 | $this->assertEquals( '2', wp_cache_get( 'hit', 'group2' ) ); 107 | 108 | $this->redis_spy->_reset(); 109 | 110 | global $wp_object_cache; 111 | 112 | $wp_object_cache->save_preloads( 'hash' ); 113 | $wp_object_cache->cache = array(); 114 | $wp_object_cache->preload( 'hash' ); 115 | 116 | $this->assertRedisCalls( 'get', 1 ); 117 | $this->assertRedisCalls( 'mget', 1 ); 118 | 119 | $this->assertEquals( '1', wp_cache_get( 'hit' ) ); 120 | $this->assertEquals( '2', wp_cache_get( 'hit', 'group2' ) ); 121 | 122 | $result = wp_cache_get_multi( array( 123 | 'group2' => array( 'hit' ), 124 | 'default' => array( 'hit' ), 125 | ) ); 126 | 127 | $this->assertEquals( array( 128 | 'group2' => array( 129 | 'hit' => '2', 130 | ), 131 | 'default' => array( 132 | 'hit' => '1', 133 | ) 134 | ), $result ); 135 | 136 | $this->assertRedisCalls( 'mget', 1 ); 137 | $this->assertRedisCalls( 'get', 1 ); 138 | } 139 | 140 | public function test_request_preload() { 141 | global $wp_object_cache; 142 | 143 | /** 144 | * Setup. 145 | */ 146 | $_SERVER['REQUEST_URI'] = '/home/'; 147 | $request_hash = $wp_object_cache->maybe_preload(); 148 | 149 | wp_cache_set( 'home', '1' ); 150 | wp_cache_get( 'home' ); 151 | 152 | $wp_object_cache->save_preloads( $request_hash ); 153 | 154 | $wp_object_cache->cache = array(); 155 | $wp_object_cache->to_preload = array(); 156 | 157 | $_SERVER['REQUEST_URI'] = '/about/'; 158 | $request_hash = $wp_object_cache->maybe_preload(); 159 | 160 | wp_cache_set( 'about', '1' ); 161 | wp_cache_get( 'about' ); 162 | 163 | $wp_object_cache->save_preloads( $request_hash ); 164 | 165 | $wp_object_cache->cache = array(); 166 | $wp_object_cache->to_preload = array(); 167 | 168 | /** 169 | * Test. 170 | */ 171 | $_SERVER['REQUEST_URI'] = '/home/'; 172 | $wp_object_cache->maybe_preload(); 173 | 174 | $this->redis_spy->_reset(); 175 | 176 | wp_cache_get( 'about' ); 177 | $this->assertRedisCalls( 'get', 1 ); 178 | wp_cache_get( 'home' ); 179 | $this->assertRedisCalls( 'get', 1 ); 180 | 181 | $wp_object_cache->cache = array(); 182 | $wp_object_cache->to_preload = array(); 183 | 184 | $_SERVER['REQUEST_URI'] = '/about/'; 185 | $wp_object_cache->maybe_preload(); 186 | 187 | $this->redis_spy->_reset(); 188 | 189 | wp_cache_get( 'about' ); 190 | $this->assertRedisCalls( 'get', 0 ); 191 | wp_cache_get( 'home' ); 192 | $this->assertRedisCalls( 'get', 1 ); 193 | 194 | $_SERVER['REQUEST_URI'] = ''; 195 | } 196 | 197 | public function test_preload_before_flush() { 198 | wp_cache_set( 'hit', '1' ); 199 | 200 | global $wp_object_cache; 201 | 202 | $wp_object_cache->save_preloads( 'hash' ); 203 | $wp_object_cache->cache = array(); 204 | $wp_object_cache->preload( 'hash' ); 205 | 206 | $this->redis_spy->_reset(); 207 | 208 | wp_cache_flush(); 209 | 210 | $this->assertFalse( wp_cache_get( 'hit' ) ); 211 | $this->assertRedisCalls( 'get', 1 ); 212 | } 213 | 214 | public function test_preload_before_set() { 215 | wp_cache_set( 'hit', '1' ); 216 | 217 | global $wp_object_cache; 218 | 219 | $wp_object_cache->save_preloads( 'hash' ); 220 | $wp_object_cache->cache = array(); 221 | $wp_object_cache->preload( 'hash' ); 222 | 223 | $this->redis_spy->_reset(); 224 | 225 | wp_cache_set( 'hit', '2' ); 226 | 227 | $this->assertEquals( '2', wp_cache_get( 'hit' ) ); 228 | 229 | $this->assertRedisCalls( 'get', 0 ); 230 | } 231 | 232 | public function test_close() { 233 | $this->assertTrue( wp_cache_close() ); 234 | } 235 | 236 | public function test_delete() { 237 | $this->assertFalse( wp_cache_delete( 'miss' ) ); 238 | 239 | $this->assertFalse( wp_cache_get( 'hit' ) ); 240 | 241 | wp_cache_add( 'hit', '1' ); 242 | 243 | $this->assertTrue( wp_cache_delete( 'hit' ) ); 244 | $this->assertFalse( wp_cache_get( 'hit' ) ); 245 | 246 | $this->assertRedisCalls( 'get', 2 ); 247 | } 248 | 249 | public function test_flush() { 250 | wp_cache_add( 'hit', '1' ); 251 | wp_cache_flush(); 252 | $this->assertFalse( wp_cache_get( 'hit' ) ); 253 | $this->assertFalse( wp_cache_get( 'hit' ) ); 254 | 255 | $this->assertRedisCalls( 'get', 1 ); 256 | } 257 | 258 | public function test_init() { 259 | $this->assertNull( wp_cache_init() ); 260 | } 261 | 262 | public function test_replace() { 263 | wp_cache_replace( 'hit', '1' ); 264 | $this->assertFalse( wp_cache_get( 'hit' ) ); 265 | 266 | wp_cache_add( 'hit', '1' ); 267 | wp_cache_replace( 'hit', '2' ); 268 | 269 | $this->assertEquals( '2', wp_cache_get( 'hit' ) ); 270 | 271 | $this->assertRedisCalls( 'exists', 2 ); 272 | $this->assertRedisCalls( 'get', 1 ); 273 | } 274 | 275 | public function test_suspend() { 276 | wp_suspend_cache_addition( true ); 277 | 278 | wp_cache_add( 'hit', '1' ); 279 | 280 | $this->assertRedisCalls( 'set', 0 ); 281 | 282 | wp_suspend_cache_addition( false ); 283 | } 284 | 285 | public function test_non_persistent() { 286 | wp_cache_add_non_persistent_groups( 'this' ); 287 | 288 | wp_cache_add( 'hit', '1', 'this' ); 289 | wp_cache_incr( 'incr', 1, 'this' ); 290 | wp_cache_decr( 'decr', 1, 'this' ); 291 | 292 | $this->assertEquals( '1', wp_cache_get( 'hit', 'this' ) ); 293 | $this->assertEquals( 1, wp_cache_get( 'incr', 'this' ) ); 294 | $this->assertEquals( -1, wp_cache_get( 'decr', 'this' ) ); 295 | $this->assertEquals( array( 296 | 'this' => array( 297 | 'hit' => '1', 298 | ) 299 | ), wp_cache_get_multi( array( 'this' => array( 'hit' ) ) ) ); 300 | 301 | wp_cache_replace( 'hit', '2', 'this' ); 302 | wp_cache_delete( 'hit', 'this' ); 303 | 304 | $this->assertRedisCalls( 'set', 0 ); 305 | $this->assertRedisCalls( 'incrBy', 0 ); 306 | $this->assertRedisCalls( 'get', 0 ); 307 | $this->assertRedisCalls( 'mget', 0 ); 308 | $this->assertRedisCalls( 'delete', 0 ); 309 | 310 | global $wp_object_cache; 311 | $wp_object_cache->save_preloads( 'hash' ); 312 | 313 | $this->assertEmpty( wp_cache_get( 'hash', 'pj-preload' ) ); 314 | } 315 | 316 | public function test_multisite() { 317 | global $wp_object_cache; 318 | 319 | wp_cache_add_global_groups( 'global' ); 320 | 321 | wp_cache_add( 'hit', 'global', 'global' ); 322 | 323 | $site_1 = get_current_site()->blog_id; 324 | $site_2 = wpmu_create_blog( wp_generate_password( 12, false ), '/', 'Site 2', 1 ); 325 | $site_3 = wpmu_create_blog( wp_generate_password( 12, false ), '/', 'Site 3', 1 ); 326 | 327 | foreach ( array( $site_1, $site_2, $site_3 ) as $site ) { 328 | switch_to_blog( $site ); 329 | 330 | $this->redis_spy->_reset(); 331 | 332 | wp_cache_add( 'hit', "_$site" ); 333 | wp_cache_add( 'hit', $site, 'this' ); 334 | 335 | // Preheated 336 | $this->assertEquals( 'global', wp_cache_get( 'hit', 'global' ) ); 337 | $this->assertEquals( "_$site", wp_cache_get( 'hit' ) ); 338 | $this->assertEquals( $site, wp_cache_get( 'hit', 'this' ) ); 339 | 340 | $this->assertRedisCalls( 'get', 0 ); 341 | 342 | $wp_object_cache->cache = array(); 343 | 344 | // Fetched 345 | $this->assertEquals( 'global', wp_cache_get( 'hit', 'global' ) ); 346 | $this->assertEquals( "_$site", wp_cache_get( 'hit' ) ); 347 | $this->assertEquals( $site, wp_cache_get( 'hit', 'this' ) ); 348 | 349 | $this->assertRedisCalls( 'get', 3 ); 350 | 351 | $wp_object_cache->cache = array(); 352 | 353 | wp_cache_get( 'hit', 'global' ); 354 | } 355 | 356 | switch_to_blog( $site_1 ); 357 | } 358 | 359 | public function test_multisite_preloads() { 360 | global $wp_object_cache; 361 | 362 | wp_cache_add_global_groups( 'global' ); 363 | 364 | wp_cache_add( 'hit', 'global', 'global' ); 365 | 366 | $site_1 = get_current_site()->blog_id; 367 | $site_2 = wpmu_create_blog( wp_generate_password( 12, false ), '/', 'Site 2', 1 ); 368 | 369 | switch_to_blog( $site_1 ); 370 | 371 | wp_cache_add( 'hit', $site_1, 'this' ); 372 | 373 | wp_cache_get( 'hit', 'this' ); 374 | wp_cache_get( 'hit', 'global' ); 375 | 376 | $wp_object_cache->save_preloads( $site_1 ); 377 | 378 | $wp_object_cache->cache = array(); 379 | $wp_object_cache->to_preload = array(); 380 | 381 | switch_to_blog( $site_2 ); 382 | 383 | $wp_object_cache->preload( $site_2 ); 384 | $this->redis_spy->_reset(); 385 | 386 | $this->assertEquals( 'global', wp_cache_get( 'hit', 'global' ) ); 387 | $this->assertFalse( wp_cache_get( 'hit', 'this' ) ); 388 | 389 | $this->assertRedisCalls( 'get', 2 ); 390 | 391 | switch_to_blog( $site_1 ); 392 | 393 | $wp_object_cache->cache = array(); 394 | $wp_object_cache->to_preload = array(); 395 | 396 | $wp_object_cache->preload( $site_1 ); 397 | $this->redis_spy->_reset(); 398 | 399 | $this->assertEquals( 'global', wp_cache_get( 'hit', 'global' ) ); 400 | $this->assertEquals( $site_1, wp_cache_get( 'hit', 'this' ) ); 401 | 402 | $this->assertRedisCalls( 'get', 0 ); 403 | } 404 | 405 | public function test_preload_incr_decr() { 406 | global $wp_object_cache; 407 | 408 | wp_cache_incr( 'incr' ); 409 | wp_cache_get( 'incr' ); 410 | 411 | wp_cache_incr( 'decr' ); 412 | wp_cache_get( 'decr' ); 413 | 414 | $wp_object_cache->save_preloads( 'hash' ); 415 | 416 | $wp_object_cache->cache = array(); 417 | $wp_object_cache->to_preload = array(); 418 | 419 | $wp_object_cache->preload( 'hash' ); 420 | $this->redis_spy->_reset(); 421 | 422 | $this->assertEquals( 1, wp_cache_get( 'incr' ) ); 423 | $this->assertEquals( -1, wp_cache_get( 'decr' ) ); 424 | 425 | $this->assertRedisCalls( 'get', 0 ); 426 | } 427 | } 428 | --------------------------------------------------------------------------------