Completed
Pull Request — master (#31)
by Anthony
04:19
created

PredisRedisLock::clearLock()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 9
ccs 0
cts 5
cp 0
rs 9.6666
cc 2
eloc 5
nc 2
nop 1
crap 6
1
<?php
2
/**
3
 * This file is part of ninja-mutex.
4
 *
5
 * (C) Kamil Dziedzic <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace NinjaMutex\Lock;
11
12
use Predis;
13
14
/**
15
 * Lock implementor using Predis (client library for Redis)
16
 *
17
 * @author Kamil Dziedzic <[email protected]>
18
 */
19
class PredisRedisLock extends LockAbstract implements LockExpirationInterface
20
{
21
    /**
22
     * Predis connection
23
     *
24
     * @var Predis\Client
25
     */
26
    protected $client;
27
28
    /**
29
     * @var int Expiration time of the lock in seconds
30
     */
31
    protected $expiration = 0;
32
33
    /**
34
     * @param $client Predis\Client
35
     */
36 4
    public function __construct(Predis\Client $client)
37
    {
38 4
        parent::__construct();
39
40 4
        $this->client = $client;
41 4
    }
42
43
    /**
44
     * @param int $expiration Expiration time of the lock in seconds
45
     */
46 1
    public function setExpiration($expiration)
47
    {
48 1
        $this->expiration = $expiration;
49
50
        // Regenerate the lock information
51 1
        $this->lockInformation = $this->generateLockInformation();
52 1
    }
53
54
    /**
55
     * @inheritDoc
56
     */
57 4
    protected function generateLockInformation()
58
    {
59 4
        $params = parent::generateLockInformation();
60
61 4
        if ($this->expiration) {
62 1
            $params[] = time() + $this->expiration;
63 1
        }
64
65 4
        return $params;
66
    }
67
68
    /**
69
     * @param  string $name
70
     * @param  bool   $blocking
71
     * @return bool
72
     */
73 21
    protected function getLock($name, $blocking)
74
    {
75
        /**
76
         * Perform the process recommended by Redis for acquiring a lock, from here: https://redis.io/commands/setnx
77
         * We are "C4" in this example...
78
         *
79
         * 1. C4 sends SETNX lock.foo in order to acquire the lock (sets the value if it does not already exist).
80
         * 2. The crashed client C3 still holds it, so Redis will reply with 0 to C4.
81
         * 3. C4 sends GET lock.foo to check if the lock expired.
82
         *    If it is not, it will sleep for some time and retry from the start.
83
         * 4. Instead, if the lock is expired because the Unix time at lock.foo is older than the current Unix time,
84
         *    C4 tries to perform:
85
         *    GETSET lock.foo <current Unix timestamp + lock timeout + 1>
86
         *    Because of the GETSET semantic, C4 can check if the old value stored at key is still an expired timestamp
87
         *    If it is, the lock was acquired.
88
         * 5. If another client, for instance C5, was faster than C4 and acquired the lock with the GETSET operation,
89
         *    the C4 GETSET operation will return a non expired timestamp.
90
         *    C4 will simply restart from the first step. Note that even if C4 wrote they key and set the expiry time
91
         *    a few seconds in the future this is not a problem. C5's timeout will just be a few seconds later.
92
         */
93
94 21
        $lockValue = serialize($this->getLockInformation());
95
96 21
        if ($this->client->setnx($name, $lockValue)) {
97 21
            return true;
98
        }
99
100
        // Check if the existing lock has an expiry time. If it does and it has expired, delete the lock.
101 6
        if ($existingValue = $this->client->get($name)) {
102 6
            $existingValue = unserialize($existingValue);
103 6
            if (!empty($existingValue[3]) && $existingValue[3] <= time()) {
104
                // The existing lock has expired. We can delete it and take over.
105 1
                $newExistingValue = unserialize($this->client->getset($name, $lockValue));
106
107
                // GETSET atomically sets key to value and returns the old value that was stored at key.
108
                // If the old value from getset does not still contain an expired timestamp
109
                // another probably acquired the lock in the meantime.
110 1
                if ($newExistingValue[3] > time()) {
111
                    return false;
112
                }
113
114
                // Got him.
115 1
                return true;
116
            }
117 6
        }
118
119 6
        return false;
120
    }
121
122
    /**
123
     * Release lock
124
     *
125
     * @param  string $name name of lock
126
     * @return bool
127
     */
128 20
    public function releaseLock($name)
129
    {
130 20
        if (isset($this->locks[$name]) && $this->client->del([$name])) {
131 20
            unset($this->locks[$name]);
132
133 20
            return true;
134
        }
135
136 4
        return false;
137
    }
138
139
    /**
140
     * Check if lock is locked
141
     *
142
     * @param  string $name name of lock
143
     * @return bool
144
     */
145 5
    public function isLocked($name)
146
    {
147 5
        return null !== $this->client->get($name);
148
    }
149
150
    /**
151
     * Clear lock without releasing it
152
     * Do not use this method unless you know what you do
153
     *
154
     * @param  string $name name of lock
155
     * @return bool
156
     */
157
    public function clearLock($name)
158
    {
159
        if (!isset($this->locks[$name])) {
160
            return false;
161
        }
162
163
        unset($this->locks[$name]);
164
        return true;
165
    }
166
}
167