<?php
declare(strict_types=1);
namespace malkusch\lock\mutex;
use malkusch\lock\exception\LockAcquireException;
use malkusch\lock\exception\LockReleaseException;
use Redis;
use RedisException;
/**
* Mutex based on the Redlock algorithm using the phpredis extension.
*
* This implementation requires at least phpredis-4.0.0. If used together with
* the lzf extension, and phpredis is configured to use lzf compression, at
* least phpredis-4.3.0 is required! For reason, see github issue link.
*
* @see https://github.com/phpredis/phpredis/issues/1477
*
* @author Markus Malkusch <markus@malkusch.de>
* @license WTFPL
*
* @link http://redis.io/topics/distlock
* @link bitcoin:1P5FAZ4QhXCuwYPnLZdk3PJsqePbu1UDDA Donations
*/
class PHPRedisMutex extends RedisMutex
{
/**
* Sets the connected Redis APIs.
*
* The Redis APIs needs to be connected. I.e. Redis::connect() was
* called already.
*
* @param array<\Redis|\RedisCluster> $redisAPIs The Redis connections.
* @param string $name The lock name.
* @param int $timeout The time in seconds a lock expires after. Default is
* 3 seconds.
* @throws \LengthException The timeout must be greater than 0.
*/
public function __construct(array $redisAPIs, string $name, int $timeout = 3)
{
parent::__construct($redisAPIs, $name, $timeout);
}
/**
* @param \Redis|\RedisCluster $redisAPI The Redis or RedisCluster connection.
* @throws LockAcquireException
*/
protected function add($redisAPI, string $key, string $value, int $expire): bool
{
/** @var \Redis $redisAPI */
try {
// Will set the key, if it doesn't exist, with a ttl of $expire seconds
return $redisAPI->set($key, $value, ['nx', 'ex' => $expire]);
} catch (RedisException $e) {
$message = sprintf(
"Failed to acquire lock for key '%s'",
$key
);
throw new LockAcquireException($message, 0, $e);
}
}
/**
* @param \Redis|\RedisCluster $redis The Redis or RedisCluster connection.
* @throws LockReleaseException
*/
protected function evalScript($redis, string $script, int $numkeys, array $arguments)
{
for ($i = $numkeys; $i < count($arguments); $i++) {
/*
* If a serialization mode such as "php" or "igbinary" is enabled, the arguments must be
* serialized by us, because phpredis does not do this for the eval command.
*
* The keys must not be serialized.
*/
$arguments[$i] = $redis->_serialize($arguments[$i]);
/*
* If LZF compression is enabled for the redis connection and the runtime has the LZF
* extension installed, compress the arguments as the final step.
*/
if ($this->hasLzfCompression($redis)) {
$arguments[$i] = lzf_compress($arguments[$i]);
}
}
try {
return $redis->eval($script, $arguments, $numkeys);
} catch (RedisException $e) {
throw new LockReleaseException('Failed to release lock', 0, $e);
}
}
/**
* Determines if lzf compression is enabled for the given connection.
*
* @param \Redis|\RedisCluster $redis The Redis or RedisCluster connection.
* @return bool TRUE if lzf compression is enabled, false otherwise.
*/
private function hasLzfCompression($redis): bool
{
if (!\defined('Redis::COMPRESSION_LZF')) {
return false;
}
return Redis::COMPRESSION_LZF === $redis->getOption(Redis::OPT_COMPRESSION);
}
}