Skip to content

Commit

Permalink
Provide support for RedisCluster connections
Browse files Browse the repository at this point in the history
  • Loading branch information
kchason committed Oct 27, 2024
1 parent 11d9721 commit 8b41515
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 52 deletions.
2 changes: 2 additions & 0 deletions app/Config/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ class Cache extends BaseConfig
'port' => 6379,
'timeout' => 0,
'database' => 0,
'clustered' => false,
'ca_file' => null,
];

/**
Expand Down
138 changes: 86 additions & 52 deletions system/Cache/Handlers/RedisHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use CodeIgniter\I18n\Time;
use Config\Cache;
use Redis;
use RedisCluster;
use RedisClusterException;
use RedisException;

/**
Expand All @@ -32,11 +34,13 @@ class RedisHandler extends BaseHandler
* @var array
*/
protected $config = [
'host' => '127.0.0.1',
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
'port' => 6379,
'timeout' => 0,
'database' => 0,
'clustered' => false,
'ca_file' => null,
];

/**
Expand Down Expand Up @@ -73,53 +77,63 @@ public function initialize()
{
$config = $this->config;

$this->redis = new Redis();

try {
// Note:: If Redis is your primary cache choice, and it is "offline", every page load will end up been delayed by the timeout duration.
// I feel like some sort of temporary flag should be set, to indicate that we think Redis is "offline", allowing us to bypass the timeout for a set period of time.
if (isset($config['clustered']) && $config['clustered']) {
// If the config["clustered"] is set to true, use the RedisCluster class and check if TLS is also enabled
// with the provided certificate authority (CA) file.
$options = [];
if ($config['ca_file']) {
$options['tls'] = [
'verify_peer' => true,
'verify_peer_name' => true,
'allow_self_signed' => false,
'cafile' => $config['ca_file'],
];
}

if (! $this->redis->connect($config['host'], ($config['host'][0] === '/' ? 0 : $config['port']), $config['timeout'])) {
// Note:: I'm unsure if log_message() is necessary, however I'm not 100% comfortable removing it.
log_message('error', 'Cache: Redis connection failed. Check your configuration.');
// Build the authentification array based on provided configuration
$auth = [];
if ($config['username']) {
$auth['username'] = $config['username'];
}
if ($config['password']) {
$auth['password'] = $config['password'];
}

throw new CriticalError('Cache: Redis connection failed. Check your configuration.');
try {
$this->redis = new RedisCluster(null, ['tls://' . $config['host'] . ':' . $config['port']], $config['timeout'], $config['timeout'], false, $auth, $options);
} catch (RedisClusterException $e) {
throw new CriticalError('Cache: RedisException occurred with message (' . $e->getMessage() . ').');
}
} else {

if (isset($config['password']) && ! $this->redis->auth($config['password'])) {
log_message('error', 'Cache: Redis authentication failed.');
$this->redis = new Redis();

throw new CriticalError('Cache: Redis authentication failed.');
}
try {
// Note:: If Redis is your primary cache choice, and it is "offline", every page load will end up been delayed by the timeout duration.
// I feel like some sort of temporary flag should be set, to indicate that we think Redis is "offline", allowing us to bypass the timeout for a set period of time.

if (isset($config['database']) && ! $this->redis->select($config['database'])) {
log_message('error', 'Cache: Redis select database failed.');
if (!$this->redis->connect($config['host'], ($config['host'][0] === '/' ? 0 : $config['port']), $config['timeout'])) {
// Note:: I'm unsure if log_message() is necessary, however I'm not 100% comfortable removing it.
log_message('error', 'Cache: Redis connection failed. Check your configuration.');

throw new CriticalError('Cache: Redis select database failed.');
}
} catch (RedisException $e) {
throw new CriticalError('Cache: RedisException occurred with message (' . $e->getMessage() . ').');
}
}
throw new CriticalError('Cache: Redis connection failed. Check your configuration.');
}

/**
* {@inheritDoc}
*/
public function get(string $key)
{
$key = static::validateKey($key, $this->prefix);
$data = $this->redis->hMget($key, ['__ci_type', '__ci_value']);
if (isset($config['password']) && !$this->redis->auth($config['password'])) {
log_message('error', 'Cache: Redis authentication failed.');

if (! isset($data['__ci_type'], $data['__ci_value']) || $data['__ci_value'] === false) {
return null;
}
throw new CriticalError('Cache: Redis authentication failed.');
}

return match ($data['__ci_type']) {
'array', 'object' => unserialize($data['__ci_value']),
// Yes, 'double' is returned and NOT 'float'
'boolean', 'integer', 'double', 'string', 'NULL' => settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null,
default => null,
};
if (isset($config['database']) && !$this->redis->select($config['database'])) {
log_message('error', 'Cache: Redis select database failed.');

throw new CriticalError('Cache: Redis select database failed.');
}
} catch (RedisException $e) {
throw new CriticalError('Cache: RedisException occurred with message (' . $e->getMessage() . ').');
}
}
}

/**
Expand Down Expand Up @@ -147,7 +161,7 @@ public function save(string $key, $value, int $ttl = 60)
return false;
}

if (! $this->redis->hMset($key, ['__ci_type' => $dataType, '__ci_value' => $value])) {
if (!$this->redis->hMset($key, ['__ci_type' => $dataType, '__ci_value' => $value])) {
return false;
}

Expand Down Expand Up @@ -177,8 +191,8 @@ public function deleteMatching(string $pattern)
{
/** @var list<string> $matchedKeys */
$matchedKeys = [];
$pattern = static::validateKey($pattern, $this->prefix);
$iterator = null;
$pattern = static::validateKey($pattern, $this->prefix);
$iterator = null;

do {
/** @var false|list<string>|Redis $keys */
Expand All @@ -195,19 +209,19 @@ public function deleteMatching(string $pattern)
/**
* {@inheritDoc}
*/
public function increment(string $key, int $offset = 1)
public function decrement(string $key, int $offset = 1)
{
$key = static::validateKey($key, $this->prefix);

return $this->redis->hIncrBy($key, '__ci_value', $offset);
return $this->increment($key, -$offset);
}

/**
* {@inheritDoc}
*/
public function decrement(string $key, int $offset = 1)
public function increment(string $key, int $offset = 1)
{
return $this->increment($key, -$offset);
$key = static::validateKey($key, $this->prefix);

return $this->redis->hIncrBy($key, '__ci_value', $offset);
}

/**
Expand Down Expand Up @@ -235,19 +249,39 @@ public function getMetaData(string $key)

if ($value !== null) {
$time = Time::now()->getTimestamp();
$ttl = $this->redis->ttl(static::validateKey($key, $this->prefix));
$ttl = $this->redis->ttl(static::validateKey($key, $this->prefix));
assert(is_int($ttl));

return [
'expire' => $ttl > 0 ? $time + $ttl : null,
'mtime' => $time,
'data' => $value,
'mtime' => $time,
'data' => $value,
];
}

return null;
}

/**
* {@inheritDoc}
*/
public function get(string $key)
{
$key = static::validateKey($key, $this->prefix);
$data = $this->redis->hMget($key, ['__ci_type', '__ci_value']);

if (!isset($data['__ci_type'], $data['__ci_value']) || $data['__ci_value'] === false) {
return null;
}

return match ($data['__ci_type']) {
'array', 'object' => unserialize($data['__ci_value']),
// Yes, 'double' is returned and NOT 'float'
'boolean', 'integer', 'double', 'string', 'NULL' => settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null,
default => null,
};
}

/**
* {@inheritDoc}
*/
Expand Down

0 comments on commit 8b41515

Please sign in to comment.