RedisMutex   A
last analyzed

Complexity

Total Complexity 11

Size/Duplication

Total Lines 95
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 35
c 0
b 0
f 0
dl 0
loc 95
ccs 35
cts 35
cp 1
rs 10
wmc 11

5 Methods

Rating   Name   Duplication   Size   Complexity  
A isExpired() 0 3 2
A __destruct() 0 3 1
A acquire() 0 12 3
A release() 0 24 3
A __construct() 0 13 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Mutex\Redis;
6
7
use InvalidArgumentException;
8
use Predis\ClientInterface;
9
use Yiisoft\Mutex\Exception\MutexReleaseException;
10
use Yiisoft\Mutex\MutexInterface;
11
use Yiisoft\Mutex\RetryAcquireTrait;
12
use Yiisoft\Security\Random;
13
14
use function md5;
15
use function time;
16
17
/**
18
 * RedisMutex implements mutex "lock" mechanism via Redis locks.
19
 */
20
final class RedisMutex implements MutexInterface
21
{
22
    use RetryAcquireTrait;
23
24
    private ClientInterface $client;
25
    private string $lockKey;
26
    private string $lockValue;
27
    private string $mutexName;
28
    private ?int $expired = null;
29
    private int $ttl;
30
31
    /**
32
     * @param string $name Mutex name.
33
     * @param ClientInterface $client Predis client instance to use.
34
     * @param int $ttl Number of seconds in which the lock will be auto released.
35
     */
36 10
    public function __construct(string $name, ClientInterface $client, int $ttl = 30)
37
    {
38 10
        if ($ttl < 1) {
39 1
            throw new InvalidArgumentException(
40 1
                "TTL must be a positive number greater than zero, \"$ttl\" is received.",
41 1
            );
42
        }
43
44 9
        $this->client = $client;
45 9
        $this->lockKey = md5(self::class . $name);
46 9
        $this->lockValue = Random::string(20);
47 9
        $this->mutexName = $name;
48 9
        $this->ttl = $ttl;
49
    }
50
51 9
    public function __destruct()
52
    {
53 9
        $this->release();
54
    }
55
56
    /**
57
     * {@inheritdoc}
58
     *
59
     * @see https://redis.io/topics/distlock
60
     */
61 8
    public function acquire(int $timeout = 0): bool
62
    {
63 8
        return $this->retryAcquire($timeout, function (): bool {
64
            if (
65 8
                !$this->isExpired()
66 8
                || $this->client->set($this->lockKey, $this->lockValue, 'EX', $this->ttl, 'NX') === null
67
            ) {
68 4
                return false;
69
            }
70
71 8
            $this->expired = $this->ttl + time();
72 8
            return true;
73 8
        });
74
    }
75
76
    /**
77
     * {@inheritdoc}
78
     *
79
     * @see https://redis.io/topics/distlock
80
     */
81 9
    public function release(): void
82
    {
83 9
        if ($this->isExpired()) {
84 7
            return;
85
        }
86
87 8
        $released = (bool) $this->client->eval(
88 8
            <<<LUA
89
                if redis.call('GET',KEYS[1])==ARGV[1] then
90
                    return redis.call('DEL',KEYS[1])
91
                else
92
                    return 0
93
                end
94 8
            LUA,
95 8
            1,
96 8
            $this->lockKey,
97 8
            $this->lockValue,
98 8
        );
99
100 8
        if (!$released) {
101 1
            throw new MutexReleaseException("Unable to release the \"$this->mutexName\" mutex.");
102
        }
103
104 7
        $this->expired = null;
105
    }
106
107
    /**
108
     * Checks whether a lock has been set and whether it has expired.
109
     *
110
     * @return bool Whether a lock has been set and whether it has expired.
111
     */
112 9
    private function isExpired(): bool
113
    {
114 9
        return $this->expired === null || $this->expired <= time();
115
    }
116
}
117