Passed
Push — master ( d9c8cf...7342b9 )
by Dmitry
02:37
created

TarantoolStore::getUniqueToken()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 2
rs 10
1
<?php
2
3
namespace Tarantool\SymfonyLock;
4
5
use InvalidArgumentException;
6
use Symfony\Component\Lock\Exception\InvalidTtlException;
7
use Symfony\Component\Lock\Exception\LockConflictedException;
8
use Symfony\Component\Lock\Key;
9
use Symfony\Component\Lock\PersistingStoreInterface;
10
use Symfony\Component\Lock\Store\ExpiringStoreTrait;
11
use Tarantool\Client\Client;
12
use Tarantool\Client\Exception\RequestFailed;
13
use Tarantool\Client\Schema\Criteria;
14
use Tarantool\Client\Schema\Operations;
15
use Tarantool\Client\Schema\Space;
16
17
class TarantoolStore implements PersistingStoreInterface
18
{
19
    use OptionalConstructor;
20
    use ExpiringStoreTrait;
21
22
    /**
23
     * Expiration delay of locks in seconds
24
     */
25
    protected int $initialTtl = 300;
26
27
    /**
28
     * Space name
29
     */
30
    protected string $space = 'lock';
31
32 13
    protected function validateOptions()
33
    {
34 13
        if ($this->initialTtl <= 0) {
35 1
            $message = sprintf(
36 1
                'InitialTtl expects a strictly positive TTL. Got %d.',
37 1
                $this->initialTtl
38
            );
39 1
            throw new InvalidTtlException($message);
40
        }
41
42 13
        if ($this->space == '') {
43 1
            throw new InvalidArgumentException("Space should be defined");
44
        }
45 13
    }
46
47
    /**
48
     * {@inheritdoc}
49
     */
50 4
    public function delete(Key $key, ?string $token = null)
51
    {
52
        $arguments = [
53 4
            (string) $key,
54 4
            $token ?: $this->getUniqueToken($key),
55
        ];
56
57
        $script = <<<LUA
58 4
        local key, token = ...
59 4
        local tuple = box.space.$this->space:get(key)
60
        if tuple and tuple.token == token then
61 4
            box.space.$this->space:delete(tuple.key)
62
        end
63
        LUA;
64
65 4
        $this->client->evaluate($script, ...$arguments);
66 4
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71 4
    public function exists(Key $key)
72
    {
73 4
        $data = $this->client
74 4
            ->getSpace($this->space)
75 4
            ->select(Criteria::key([ (string) $key ]));
76
77 4
        if (count($data)) {
78 4
            [$tuple] = $data;
79 4
            return $tuple[1] == $this->getUniqueToken($key)
80 4
                && $tuple[2] >= microtime(true);
81
        }
82 3
        return false;
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88 1
    public function putOffExpiration(Key $key, float $ttl)
89
    {
90 1
        if ($this->exists($key)) {
91 1
            $key->resetLifetime();
92 1
            $key->reduceLifetime($ttl);
93
94 1
            $this->getSpace()->update(
95 1
                [ (string) $key ],
96 1
                Operations::set(2, $this->getExpirationTimestamp($key)),
97
            );
98
        }
99 1
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104 5
    public function save(Key $key)
105
    {
106 5
        $key->reduceLifetime($this->initialTtl);
107
108
        try {
109
            $tuple = [
110 5
                (string) $key,
111 5
                $this->getUniqueToken($key),
112 5
                $this->getExpirationTimestamp($key),
113
            ];
114 5
            $this->getSpace()->insert($tuple);
115 5
            $this->checkNotExpired($key);
116 2
        } catch (RequestFailed $e) {
117 2
            $data = $this->client
118 2
                ->getSpace($this->space)
119 2
                ->select(Criteria::key([ (string) $key ]));
120
121 2
            if (count($data)) {
122 2
                [$tuple] = $data;
123
124 2
                if ($tuple[1] == $this->getUniqueToken($key)) {
125 1
                    $this->checkNotExpired($key);
126 1
                    return true;
127
                }
128
129 1
                if ($tuple[2] < microtime(true)) {
130
                    $this->delete($key, $tuple[1]);
131
                    return $this->save($key);
132
                }
133
            }
134
135 1
            throw new LockConflictedException(null, null, $e);
136
        }
137 5
    }
138
139 5
    protected function getUniqueToken(Key $key): string
140
    {
141 5
        if (!$key->hasState(__CLASS__)) {
142 5
            $token = base64_encode(random_bytes(32));
143 5
            $key->setState(__CLASS__, $token);
144
        }
145
146 5
        return $key->getState(__CLASS__);
147
    }
148
149 5
    protected function getExpirationTimestamp(Key $key): float
150
    {
151 5
        return microtime(true) + $key->getRemainingLifetime();
152
    }
153
154 5
    protected function getSpace(): Space
155
    {
156 5
        return $this->client->getSpace($this->space);
157
    }
158
}
159