544 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			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 ];
 | |
| 	}
 | |
| }
 |