Skip to content

Commit

Permalink
Merge pull request #18 from pantheon-systems/4-core-object-cache-fall…
Browse files Browse the repository at this point in the history
…back

Better handling when Redis is enabled, but unavailable or gone away
  • Loading branch information
Josh Koenig committed Oct 31, 2015
2 parents a10c293 + ee98d09 commit f3502b6
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 62 deletions.
163 changes: 106 additions & 57 deletions object-cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,14 @@ class WP_Object_Cache {
*/
var $blog_prefix;

/**
* Whether or not Redis is connected
*
* @var bool
* @access private
*/
var $is_redis_connected = false;

/**
* Adds data to the cache if it doesn't already exist.
*
Expand Down Expand Up @@ -393,14 +401,14 @@ function decr( $key, $offset = 1, $group = 'default' ) {
}

if ( $offset > 1 ) {
$result = $this->redis->decrBy( $id, $offset );
$result = $this->_call_redis( 'decrBy', $id, $offset );
} else {
$result = $this->redis->decr( $id );
$result = $this->_call_redis( 'decr', $id );
}

if ( $result < 0 ) {
$result = 0;
$this->redis->set( $id, $result );
$this->_call_redis( 'set', $id, $result );
}

if ( is_int( $result ) ) {
Expand Down Expand Up @@ -430,7 +438,7 @@ function delete( $key, $group = 'default', $force = false ) {
return false;

if ( $this->_should_persist( $group ) ) {
$result = $this->redis->delete( $id );
$result = $this->_call_redis( 'delete', $id );
if ( 1 != $result ) {
return false;
}
Expand All @@ -455,7 +463,7 @@ function delete( $key, $group = 'default', $force = false ) {
function flush( $redis = true ) {
$this->cache = array();
if ( $redis ) {
$this->redis->flushAll();
$this->_call_redis( 'flushAll' );
}

return true;
Expand Down Expand Up @@ -484,7 +492,7 @@ function get( $key, $group = 'default', $force = false, &$found = null ) {
$this->cache_hits += 1;

if ( $this->_should_persist( $group ) && ( $force || ( ! isset( $this->cache[ $id ] ) && ! array_key_exists( $id, $this->cache ) ) ) ) {
$this->cache[ $id ] = $this->redis->get( $id );
$this->cache[ $id ] = $this->_call_redis( 'get', $id );
if ( ! is_numeric( $this->cache[ $id ] ) ) {
$this->cache[ $id ] = unserialize( $this->cache[ $id ] );
}
Expand Down Expand Up @@ -534,9 +542,9 @@ function incr( $key, $offset = 1, $group = 'default' ) {
}

if ( $offset > 1 ) {
$result = $this->redis->incrBy( $id, $offset );
$result = $this->_call_redis( 'incrBy', $id, $offset );
} else {
$result = $this->redis->incr( $id );
$result = $this->_call_redis( 'incr', $id );
}

if ( is_int( $result ) ) {
Expand Down Expand Up @@ -605,9 +613,9 @@ function set( $key, $data, $group = 'default', $expire = 0 ) {
}

if ( empty( $expire ) ) {
$this->redis->set( $id, $data );
$this->_call_redis( 'set', $id, $data );
} else {
$this->redis->setex( $id, $expire, $data );
$this->_call_redis( 'setex', $id, $expire, $data );
}
}

Expand Down Expand Up @@ -653,7 +661,7 @@ protected function _exists( $id ) {
if ( isset( $this->cache[ $id ] ) || array_key_exists( $id, $this->cache ) ) {
return true;
} else {
return $this->redis->exists( $id );
return $this->_call_redis( 'exists', $id );
}
}

Expand Down Expand Up @@ -689,15 +697,16 @@ protected function _should_persist( $group ) {
}

/**
* Sets up object properties; PHP 5 style constructor
*
* @return null|WP_Object_Cache If cache is disabled, returns null.
* Wrapper method for connecting to Redis, which lets us retry the connection
*/
function __construct() {
global $blog_id, $redis_server, $table_prefix;
protected function _connect_redis() {
global $redis_server;

$this->multisite = is_multisite();
$this->blog_prefix = $this->multisite ? $blog_id . ':' : '';
if ( ! class_exists( 'Redis' ) ) {
$this->is_redis_connected = false;
$this->missing_redis_message = 'Alert! PHPRedis module is unavailable, which is required by WP Redis object cache.';
return $this->is_redis_connected;
}

if ( empty( $redis_server ) ) {
# Attempt to automatically load Pantheon's Redis config from the env.
Expand All @@ -711,56 +720,60 @@ function __construct() {
}
}

$this->redis = new WP_Redis();
$this->redis->wp_object_cache = &$this;
$this->redis = new Redis;
$this->redis->connect( $redis_server['host'], $redis_server['port'], 1, NULL, 100 ); # 1s timeout, 100ms delay between reconnections
if ( ! empty( $redis_server['auth'] ) ) {
$this->redis->auth( $redis_server['auth'] );
}

$this->global_prefix = '';
if ( function_exists( 'is_multisite' ) ) {
$this->global_prefix = ( is_multisite() || defined( 'CUSTOM_USER_TABLE' ) && defined( 'CUSTOM_USER_META_TABLE' ) ) ? '' : $table_prefix;
$this->is_redis_connected = $this->redis->isConnected();
if ( ! $this->is_redis_connected ) {
$this->missing_redis_message = 'Alert! WP Redis object cache cannot connect to Redis server.';
}

/**
* @todo This should be moved to the PHP4 style constructor, PHP5
* already calls __destruct()
*/
register_shutdown_function( array( $this, '__destruct' ) );
return $this->is_redis_connected;
}

/**
* Will save the object cache before object is completely destroyed.
* Wrapper method for calls to Redis, which fails gracefully when Redis is unavailable
*
* Called upon object destruction, which should be when PHP ends.
*
* @return bool True value. Won't be used by PHP
* @param string $method
* @param mixed $args
* @return mixed
*/
function __destruct() {
return true;
}
}

if ( class_exists( 'Redis' ) ) {
class WP_Redis extends Redis {

}
} else {
class WP_Redis {
protected function _call_redis( $method ) {

$arguments = func_get_args();
array_shift( $arguments ); // ignore $method

if ( $this->is_redis_connected ) {
try {
$retval = call_user_func_array( array( $this->redis, $method ), $arguments );
return $retval;
} catch( RedisException $e ) {
if ( in_array( $e->getMessage(), array( 'Connection closed', 'Redis server went away' ) ) ) {
// Attempt to refresh the connection if it was successfully established once
// $this->is_redis_connected will be set inside _connect_redis()
if ( $this->_connect_redis() ) {
return call_user_func_array( array( $this, '_call_redis' ), array_merge( array( $method ), $arguments ) );
}
// Fall through to fallback below
} else {
throw $e;
}
}
}

public function __call( $name, $arguments ) {
switch ( $name ) {
// Mock expected behavior from Redis for these methods
switch ( $method ) {
case 'incr':
case 'incrBy':
$val = $this->wp_object_cache->cache[ $arguments[0] ];
$offset = isset( $arguments[1] ) && 'incrBy' === $name ? $arguments[1] : 1;
$val = $this->cache[ $arguments[0] ];
$offset = isset( $arguments[1] ) && 'incrBy' === $method ? $arguments[1] : 1;
$val = $val + $offset;
return $val;
case 'decrBy':
case 'decr':
$val = $this->wp_object_cache->cache[ $arguments[0] ];
$offset = isset( $arguments[1] ) && 'decrBy' === $name ? $arguments[1] : 1;
$val = $this->cache[ $arguments[0] ];
$offset = isset( $arguments[1] ) && 'decrBy' === $method ? $arguments[1] : 1;
$val = $val - $offset;
return $val;
case 'delete':
Expand All @@ -769,18 +782,54 @@ public function __call( $name, $arguments ) {
case 'exists':
return false;
}

}

/**
* Admin UI to let the end user know something about the Redis connection isn't working.
*/
public function wp_action_admin_notices_warn_missing_redis() {
if ( ! current_user_can( 'manage_options' ) || empty( $this->missing_redis_message ) ) {
return;
}
echo '<div class="message error"><p>' . esc_html( $this->missing_redis_message ) . '</p></div>';
}

/**
* Sets up object properties; PHP 5 style constructor
*
* @return null|WP_Object_Cache If cache is disabled, returns null.
*/
function __construct() {
global $blog_id, $table_prefix;

public function __construct() {
$this->multisite = is_multisite();
$this->blog_prefix = $this->multisite ? $blog_id . ':' : '';

if ( ! $this->_connect_redis() ) {
add_action( 'admin_notices', array( $this, 'wp_action_admin_notices_warn_missing_redis' ) );
}

public function wp_action_admin_notices_warn_missing_redis() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
echo '<div class="message error"><p>Alert! PHPRedis module is unavailable, which is required by WP Redis object cache.</p></div>';
$this->global_prefix = '';
if ( function_exists( 'is_multisite' ) ) {
$this->global_prefix = ( is_multisite() || defined( 'CUSTOM_USER_TABLE' ) && defined( 'CUSTOM_USER_META_TABLE' ) ) ? '' : $table_prefix;
}

/**
* @todo This should be moved to the PHP4 style constructor, PHP5
* already calls __destruct()
*/
register_shutdown_function( array( $this, '__destruct' ) );
}

/**
* Will save the object cache before object is completely destroyed.
*
* Called upon object destruction, which should be when PHP ends.
*
* @return bool True value. Won't be used by PHP
*/
function __destruct() {
return true;
}
}
28 changes: 23 additions & 5 deletions tests/test-cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,34 @@ function &init_cache() {

public function test_loaded() {
$this->assertTrue( WP_REDIS_OBJECT_CACHE );
$this->assertTrue( class_exists( 'WP_Redis' ) );
}

public function test_redis_connected() {
if ( ! class_exists( 'Redis' ) ) {
$this->markTestSkipped( 'PHPRedis extension not available.' );
}
$this->assertTrue( isset( $this->cache->redis ) );
if ( class_exists( 'Redis' ) ) {
$this->assertTrue( $this->cache->redis->IsConnected() );
} else {
$this->assertFalse( $this->cache->redis->IsConnected() );
$this->assertTrue( $this->cache->redis->IsConnected() );
}

public function test_redis_reload_connection_gone_away() {
if ( ! class_exists( 'Redis' ) ) {
$this->markTestSkipped( 'PHPRedis extension not available.' );
}
// Connection is live
$this->cache->set( 'foo', 'bar' );
$this->assertTrue( $this->cache->redis->IsConnected() );
$this->assertTrue( $this->cache->is_redis_connected );
$this->assertEquals( 'bar', $this->cache->get( 'foo', 'default', true ) );
// Connection is closed, and refreshed the next time it's requested
$this->cache->redis->close();
$this->assertTrue( $this->cache->is_redis_connected );
$this->assertFalse( $this->cache->redis->IsConnected() );
// Reload occurs with set()
$this->cache->set( 'foo', 'banana' );
$this->assertEquals( 'banana', $this->cache->get( 'foo' ) );
$this->assertTrue( $this->cache->is_redis_connected );
$this->assertTrue( $this->cache->redis->IsConnected() );
}

function test_miss() {
Expand Down

0 comments on commit f3502b6

Please sign in to comment.