224 lines
6.5 KiB
PHP
224 lines
6.5 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* This program is free software; you can redistribute it and/or
|
||
|
* modify it under the terms of the GNU General Public License
|
||
|
* as published by the Free Software Foundation; under version 2
|
||
|
* of the License (non-upgradable).
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful,
|
||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
* GNU General Public License for more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License
|
||
|
* along with this program; if not, write to the Free Software
|
||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
*
|
||
|
* Copyright (c) 2013-2017 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
|
||
|
*
|
||
|
* @author Lionel Lecaque <lionel@taotesting.com>
|
||
|
* @license GPLv2
|
||
|
* @package
|
||
|
*
|
||
|
*/
|
||
|
class common_persistence_PhpRedisDriver implements common_persistence_AdvKvDriver, common_persistence_KeyValue_Nx
|
||
|
{
|
||
|
|
||
|
const DEFAULT_PORT = 6379;
|
||
|
const DEFAULT_ATTEMPT = 3;
|
||
|
const DEFAULT_TIMEOUT = 5; // in seconds
|
||
|
const RETRY_DELAY = 500000; // Eq to 0.5s
|
||
|
|
||
|
/**
|
||
|
* @var Redis
|
||
|
*/
|
||
|
private $connection;
|
||
|
|
||
|
/**
|
||
|
* @var $params
|
||
|
*/
|
||
|
private $params;
|
||
|
|
||
|
/**
|
||
|
* store connection params and try to connect
|
||
|
* @see common_persistence_Driver::connect()
|
||
|
*/
|
||
|
function connect($key, array $params)
|
||
|
{
|
||
|
$this->params = $params;
|
||
|
$this->connectionSet($params);
|
||
|
|
||
|
return new common_persistence_AdvKeyValuePersistence($params, $this);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* create a new connection using stored parameters
|
||
|
* @param array $params
|
||
|
* @throws common_exception_Error
|
||
|
*/
|
||
|
function connectionSet(array $params)
|
||
|
{
|
||
|
if (!isset($params['host'])) {
|
||
|
throw new common_exception_Error('Missing host information for Redis driver');
|
||
|
}
|
||
|
|
||
|
$port = isset($params['port']) ? $params['port'] : self::DEFAULT_PORT;
|
||
|
$timeout = isset($params['timeout']) ? $params['timeout'] : self::DEFAULT_TIMEOUT;
|
||
|
$persist = isset($params['pconnect']) ? $params['pconnect'] : true;
|
||
|
$this->params['attempt'] = isset($params['attempt']) ? $params['attempt'] : self::DEFAULT_ATTEMPT;
|
||
|
|
||
|
if (is_array($params['host'])) {
|
||
|
$this->connectToCluster($params['host'], $timeout, $persist);
|
||
|
} else {
|
||
|
$this->connectToSingleNode($params['host'], $port, $timeout, $persist);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private function connectToSingleNode(string $host, int $port, int $timeout, bool $persist)
|
||
|
{
|
||
|
$this->connection = new Redis();
|
||
|
if ($this->connection == false) {
|
||
|
throw new common_exception_Error("Redis php module not found");
|
||
|
}
|
||
|
if ($persist) {
|
||
|
$this->connection->pconnect($host, $port, $timeout);
|
||
|
} else {
|
||
|
$this->connection->connect($host, $port, $timeout);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private function connectToCluster(array $host, int $timeout, bool $persist)
|
||
|
{
|
||
|
$this->connection = new RedisCluster(null, $host, $timeout, null, $persist);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param $method
|
||
|
* @param array $params
|
||
|
* @param $retry
|
||
|
* @param int $attempt
|
||
|
* @return mixed
|
||
|
* @throws Exception
|
||
|
*/
|
||
|
protected function callWithRetry($method, array $params, $attempt = 1)
|
||
|
{
|
||
|
|
||
|
$success = false;
|
||
|
$lastException = null;
|
||
|
$result = false;
|
||
|
|
||
|
$retry = $this->params['attempt'];
|
||
|
|
||
|
while (!$success && $attempt <= $retry) {
|
||
|
try {
|
||
|
$result = call_user_func_array([$this->connection , $method], $params);
|
||
|
$success = true;
|
||
|
} catch (\Exception $e) {
|
||
|
$lastException = $e;
|
||
|
\common_Logger::d('Redis ' . $method . ' failed ' . $attempt . '/' . $retry . ' : ' . $e->getMessage());
|
||
|
if ($e->getMessage() == 'Failed to AUTH connection' && isset($this->params['password'])) {
|
||
|
\common_Logger::d('Authenticating Redis connection');
|
||
|
$this->connection->auth($this->params['password']);
|
||
|
}
|
||
|
$delay = rand(self::RETRY_DELAY, self::RETRY_DELAY * 2);
|
||
|
usleep($delay);
|
||
|
$this->connectionSet($this->params);
|
||
|
}
|
||
|
$attempt++;
|
||
|
}
|
||
|
|
||
|
if (!$success) {
|
||
|
throw $lastException;
|
||
|
}
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* (non-PHPdoc)
|
||
|
* @see common_persistence_KvDriver::set()
|
||
|
*/
|
||
|
public function set($key, $value, $ttl = null, $nx = false)
|
||
|
{
|
||
|
$options = [];
|
||
|
if (!is_null($ttl)) {
|
||
|
$options['ex'] = $ttl;
|
||
|
};
|
||
|
if ($nx) {
|
||
|
$options[] = 'nx';
|
||
|
};
|
||
|
return $this->callWithRetry('set', [$key, $value, $options]);
|
||
|
}
|
||
|
|
||
|
public function get($key)
|
||
|
{
|
||
|
|
||
|
return $this->callWithRetry('get', [$key]);
|
||
|
}
|
||
|
|
||
|
public function exists($key)
|
||
|
{
|
||
|
return (bool)$this->callWithRetry('exists', [$key]);
|
||
|
}
|
||
|
|
||
|
public function del($key)
|
||
|
{
|
||
|
return $this->callWithRetry('del', [$key]);
|
||
|
}
|
||
|
|
||
|
//O(N) where N is the number of fields being set.
|
||
|
public function hmSet($key, $fields)
|
||
|
{
|
||
|
return $this->callWithRetry('hmSet', [$key, $fields]);
|
||
|
}
|
||
|
//Time complexity: O(1)
|
||
|
public function hExists($key, $field)
|
||
|
{
|
||
|
return (bool)$this->callWithRetry('hExists', [$key, $field]);
|
||
|
}
|
||
|
|
||
|
//Time complexity: O(1)
|
||
|
public function hSet($key, $field, $value)
|
||
|
{
|
||
|
return $this->callWithRetry('hSet', [$key, $field, $value]);
|
||
|
}
|
||
|
//Time complexity: O(1)
|
||
|
public function hGet($key, $field)
|
||
|
{
|
||
|
return $this->callWithRetry('hGet', [$key, $field]);
|
||
|
}
|
||
|
|
||
|
public function hDel($key, $field): bool
|
||
|
{
|
||
|
return (bool) $this->callWithRetry('hDel', [$key, $field]);
|
||
|
}
|
||
|
|
||
|
//Time complexity: O(N) where N is the size of the hash.
|
||
|
public function hGetAll($key)
|
||
|
{
|
||
|
return $this->callWithRetry('hGetAll', [$key]);
|
||
|
}
|
||
|
//Time complexity: O(N)
|
||
|
public function keys($pattern)
|
||
|
{
|
||
|
return $this->callWithRetry('keys', [$pattern]);
|
||
|
}
|
||
|
//Time complexity: O(1)
|
||
|
public function incr($key)
|
||
|
{
|
||
|
return $this->callWithRetry('incr', [$key]);
|
||
|
}
|
||
|
//Time complexity: O(1)
|
||
|
public function decr($key)
|
||
|
{
|
||
|
return $this->callWithRetry('decr', [$key]);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return Redis
|
||
|
*/
|
||
|
public function getConnection()
|
||
|
{
|
||
|
return $this->connection;
|
||
|
}
|
||
|
}
|