laipower/wp-content/plugins/w3-total-cache/Cache_Redis.php

544 lines
12 KiB
PHP

<?php
/**
* File: Cache_Redis.php
*
* @package W3TC
*
* phpcs:disable PSR2.Methods.MethodDeclaration.Underscore,PSR2.Classes.PropertyDeclaration.Underscore,WordPress.PHP.DiscouragedPHPFunctions,WordPress.PHP.NoSilencedErrors
*/
namespace W3TC;
/**
* Redis cache engine.
*/
class Cache_Redis extends Cache_Base {
/**
* Accessors.
*
* @var array
*/
private $_accessors = array();
/**
* Key value.
*
* @var array
*/
private $_key_version = array();
/**
* Persistent.
*
* @var bool
*/
private $_persistent;
/**
* Password.
*
* @var string
*/
private $_password;
/**
* Servers.
*
* @var array
*/
private $_servers;
/**
* Verify TLS certificate.
*
* @var bool
*/
private $_verify_tls_certificates;
/**
* DB id.
*
* @var string
*/
private $_dbid;
/**
* Timeout.
*
* @var int.
*/
private $_timeout;
/**
* Retry interval.
*
* @var int
*/
private $_retry_interval;
/**
* Retry timeout.
*
* @var int
*/
private $_read_timeout;
/**
* Constructor.
*
* @param array $config Config.
*/
public function __construct( $config ) {
parent::__construct( $config );
$this->_persistent = ( isset( $config['persistent'] ) && $config['persistent'] );
$this->_servers = (array) $config['servers'];
$this->_verify_tls_certificates = ( isset( $config['verify_tls_certificates'] ) && $config['verify_tls_certificates'] );
$this->_password = $config['password'];
$this->_dbid = $config['dbid'];
$this->_timeout = $config['timeout'];
$this->_retry_interval = $config['retry_interval'];
$this->_read_timeout = $config['read_timeout'];
/**
* When disabled - no extra requests are made to obtain key version,
* but flush operations not supported as a result group should be always empty.
*/
if ( isset( $config['key_version_mode'] ) && 'disabled' === $config['key_version_mode'] ) {
$this->_key_version[''] = 1;
}
}
/**
* Adds data.
*
* @param string $key Key.
* @param mixed $var Var.
* @param integer $expire Expire.
* @param string $group Used to differentiate between groups of cache values.
* @return bool
*/
public function add( $key, &$var, $expire = 0, $group = '' ) {
return $this->set( $key, $var, $expire, $group );
}
/**
* Sets data.
*
* @param string $key Key.
* @param mixed $value Value.
* @param integer $expire Expire.
* @param string $group Used to differentiate between groups of cache values.
* @return bool
*/
public function set( $key, $value, $expire = 0, $group = '' ) {
$value['key_version'] = $this->_get_key_version( $group );
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
if ( ! $expire ) {
return $accessor->set( $storage_key, serialize( $value ) );
}
return $accessor->setex( $storage_key, $expire, serialize( $value ) );
}
/**
* Returns data
*
* @param string $key Key.
* @param string $group Used to differentiate between groups of cache values.
* @return mixed
*/
public function get_with_old( $key, $group = '' ) {
$has_old_data = false;
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return array( null, false );
}
$v = $accessor->get( $storage_key );
$v = @unserialize( $v );
if ( ! is_array( $v ) || ! isset( $v['key_version'] ) ) {
return array( null, $has_old_data );
}
$key_version = $this->_get_key_version( $group );
if ( $v['key_version'] === $key_version ) {
return array( $v, $has_old_data );
}
if ( $v['key_version'] > $key_version ) {
$this->_set_key_version( $v['key_version'], $group );
return array( $v, $has_old_data );
}
// Key version is old.
if ( ! $this->_use_expired_data ) {
return array( null, $has_old_data );
}
// If we have expired data - update it for future use and let current process recalculate it.
$expires_at = isset( $v['expires_at'] ) ? $v['expires_at'] : null;
if ( is_null( $expires_at ) || time() > $expires_at ) {
$v['expires_at'] = time() + 30;
$accessor->setex( $storage_key, 60, serialize( $v ) );
$has_old_data = true;
return array( null, $has_old_data );
}
// Return old version.
return array( $v, $has_old_data );
}
/**
* Replaces data.
*
* @param string $key Key.
* @param mixed $value Value.
* @param integer $expire Expire.
* @param string $group Used to differentiate between groups of cache values.
* @return bool
*/
public function replace( $key, &$value, $expire = 0, $group = '' ) {
return $this->set( $key, $value, $expire, $group );
}
/**
* Deletes data.
*
* @param string $key Key.
* @param string $group Group.
* @return bool
*/
public function delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
if ( $this->_use_expired_data ) {
$v = $accessor->get( $storage_key );
$ttl = $accessor->ttl( $storage_key );
if ( is_array( $v ) ) {
$v['key_version'] = 0;
$accessor->setex( $storage_key, $ttl, $v );
return true;
}
}
return $accessor->setex( $storage_key, 1, '' );
}
/**
* Key to delete, deletes _old and primary if exists.
*
* @param string $key Key.
* @param string $group Group.
* @return bool
*/
public function hard_delete( $key, $group = '' ) {
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
return $accessor->setex( $storage_key, 1, '' );
}
/**
* Flushes all data.
*
* @param string $group Used to differentiate between groups of cache values.
* @return bool
*/
public function flush( $group = '' ) {
$this->_get_key_version( $group ); // Initialize $this->_key_version.
if ( isset( $this->_key_version[ $group ] ) ) {
$this->_key_version[ $group ]++;
$this->_set_key_version( $this->_key_version[ $group ], $group );
}
return true;
}
/**
* Checks if engine can function properly in this environment.
*
* @return bool
*/
public function available() {
return class_exists( 'Redis' );
}
/**
* Get statistics.
*
* @return array
*/
public function get_statistics() {
$accessor = $this->_get_accessor( '' ); // Single-server mode used for stats.
if ( is_null( $accessor ) ) {
return array();
}
$a = $accessor->info();
return $a;
}
/**
* Returns key version.
*
* @param string $group Used to differentiate between groups of cache values.
* @return int
*/
private function _get_key_version( $group = '' ) {
if ( ! isset( $this->_key_version[ $group ] ) || $this->_key_version[ $group ] <= 0 ) {
$storage_key = $this->_get_key_version_key( $group );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return 0;
}
$v_original = $accessor->get( $storage_key );
$v = intval( $v_original );
$v = ( $v > 0 ? $v : 1 );
if ( (string) $v_original !== (string) $v ) {
$accessor->set( $storage_key, $v );
}
$this->_key_version[ $group ] = $v;
}
return $this->_key_version[ $group ];
}
/**
* Sets new key version.
*
* @param string $v Version.
* @param string $group Used to differentiate between groups of cache values.
* @return bool
*/
private function _set_key_version( $v, $group = '' ) {
$storage_key = $this->_get_key_version_key( $group );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
$accessor->set( $storage_key, $v );
return true;
}
/**
* Used to replace as atomically as possible known value to new one.
*
* @param string $key Key.
* @param string $old_value Old value.
* @param string $new_value New value.
*/
public function set_if_maybe_equals( $key, $old_value, $new_value ) {
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
$accessor->watch( $storage_key );
$value = $accessor->get( $storage_key );
$value = @unserialize( $value );
if ( ! is_array( $value ) ) {
$accessor->unwatch();
return false;
}
if ( isset( $old_value['content'] ) && $value['content'] !== $old_value['content'] ) {
$accessor->unwatch();
return false;
}
return $accessor->multi()
->set( $storage_key, $new_value )
->exec();
}
/**
* Use key as a counter and add integet value to it.
*
* @param string $key Key.
* @param int $value Value.
*/
public function counter_add( $key, $value ) {
if ( empty( $value ) ) {
return true;
}
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
$r = $accessor->incrBy( $storage_key, $value );
if ( ! $r ) { // It doesn't initialize counter by itself.
$this->counter_set( $key, 0 );
}
return $r;
}
/**
* Use key as a counter and add integet value to it.
*
* @param string $key Key.
* @param int $value Value.
*/
public function counter_set( $key, $value ) {
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return false;
}
return $accessor->set( $storage_key, $value );
}
/**
* Get counter's value.
*
* @param string $key Key.
*/
public function counter_get( $key ) {
$storage_key = $this->get_item_key( $key );
$accessor = $this->_get_accessor( $storage_key );
if ( is_null( $accessor ) ) {
return 0;
}
$v = (int) $accessor->get( $storage_key );
return $v;
}
/**
* Build Redis connection arguments based on server URI
*
* @param string $server Server URI to connect to.
*/
private function build_connect_args( $server ) {
$connect_args = array();
if ( substr( $server, 0, 5 ) === 'unix:' ) {
$connect_args[] = trim( substr( $server, 5 ) );
$connect_args[] = null; // port.
} else {
list( $ip, $port ) = Util_Content::endpoint_to_host_port( $server, null );
$connect_args[] = $ip;
$connect_args[] = $port;
}
$connect_args[] = $this->_timeout;
$connect_args[] = $this->_persistent ? $this->_instance_id . '_' . $this->_dbid : null;
$connect_args[] = $this->_retry_interval;
$phpredis_version = phpversion( 'redis' );
// The read_timeout parameter was added in phpredis 3.1.3.
if ( version_compare( $phpredis_version, '3.1.3', '>=' ) ) {
$connect_args[] = $this->_read_timeout;
}
// Support for stream context was added in phpredis 5.3.2.
if ( version_compare( $phpredis_version, '5.3.2', '>=' ) ) {
$context = array();
if ( 'tls:' === substr( $server, 0, 4 ) && ! $this->_verify_tls_certificates ) {
$context['stream'] = array(
'verify_peer' => false,
'verify_peer_name' => false,
);
}
$connect_args[] = $context;
}
return $connect_args;
}
/**
* Get accessor.
*
* @param string $key Key.
* @return object
*/
private function _get_accessor( $key ) {
if ( count( $this->_servers ) <= 1 ) {
$index = 0;
} else {
$index = crc32( $key ) % count( $this->_servers );
}
if ( isset( $this->_accessors[ $index ] ) ) {
return $this->_accessors[ $index ];
}
if ( ! isset( $this->_servers[ $index ] ) ) {
$this->_accessors[ $index ] = null;
} else {
try {
$server = $this->_servers[ $index ];
$connect_args = $this->build_connect_args( $server );
$accessor = new \Redis();
if ( $this->_persistent ) {
$accessor->pconnect( ...$connect_args );
} else {
$accessor->connect( ...$connect_args );
}
if ( ! empty( $this->_password ) ) {
$accessor->auth( $this->_password );
}
$accessor->select( $this->_dbid );
} catch ( \Exception $e ) {
error_log( $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
$accessor = null;
}
$this->_accessors[ $index ] = $accessor;
}
return $this->_accessors[ $index ];
}
}