TarantoolStore   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 157
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 22
eloc 71
c 3
b 1
f 0
dl 0
loc 157
ccs 70
cts 70
cp 1
rs 10

8 Methods

Rating   Name   Duplication   Size   Complexity  
A validateOptions() 0 12 3
A delete() 0 16 2
A putOffExpiration() 0 9 2
A getExpirationTimestamp() 0 3 1
A getUniqueToken() 0 8 2
A getSpace() 0 11 3
A exists() 0 17 4
A save() 0 31 5
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
     * Initialize database schema if not exists
24
     */
25
    protected bool $createSchema = false;
26
27
    /**
28
     * Expiration delay of locks in seconds
29
     */
30
    protected int $initialTtl = 300;
31
32
    /**
33
     * Space name
34
     */
35
    protected string $space = 'lock';
36
37 17
    protected function validateOptions()
38
    {
39 17
        if ($this->initialTtl <= 0) {
40 1
            $message = sprintf(
41 1
                'InitialTtl expects a strictly positive TTL. Got %d.',
42 1
                $this->initialTtl
43
            );
44 1
            throw new InvalidTtlException($message);
45
        }
46
47 17
        if ($this->space == '') {
48 1
            throw new InvalidArgumentException("Space should be defined");
49
        }
50 17
    }
51
52
    /**
53
     * {@inheritdoc}
54
     */
55 5
    public function delete(Key $key, ?string $token = null): void
56
    {
57
        $arguments = [
58 5
            (string) $key,
59 5
            $token ?: $this->getUniqueToken($key),
60
        ];
61
62
        $script = <<<LUA
63 5
        local key, token = ...
64 5
        local tuple = box.space.$this->space:get(key)
65
        if tuple and tuple.token == token then
66 5
            box.space.$this->space:delete(tuple.key)
67
        end
68
        LUA;
69
70 5
        $this->client->evaluate($script, ...$arguments);
71 5
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76 6
    public function exists(Key $key): bool
77
    {
78
        try {
79 6
            $data = $this->client
80 6
                ->getSpace($this->space)
81 6
                ->select(Criteria::key([ (string) $key ]));
82
83 5
            if (count($data)) {
84 5
                [$tuple] = $data;
85 5
                return $tuple[1] == $this->getUniqueToken($key)
86 5
                    && $tuple[2] >= microtime(true);
87
            }
88 1
        } catch (RequestFailed $e) {
89
            // query failure means there is no valid space
90
            // it means that key is not exists in store
91
        }
92 4
        return false;
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98 1
    public function putOffExpiration(Key $key, float $ttl): void
99
    {
100 1
        if ($this->exists($key)) {
101 1
            $key->resetLifetime();
102 1
            $key->reduceLifetime($ttl);
103
104 1
            $this->getSpace()->update(
105 1
                [ (string) $key ],
106 1
                Operations::set(2, $this->getExpirationTimestamp($key)),
107
            );
108
        }
109 1
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114 9
    public function save(Key $key): void
115
    {
116 9
        $key->reduceLifetime($this->initialTtl);
117
118
        try {
119
            $tuple = [
120 9
                (string) $key,
121 9
                $this->getUniqueToken($key),
122 9
                $this->getExpirationTimestamp($key),
123
            ];
124 9
            $this->getSpace()->insert($tuple);
125 8
            $this->checkNotExpired($key);
126 4
        } catch (RequestFailed $e) {
127 4
            $data = $this->getSpace()
128 3
                ->select(Criteria::key([ (string) $key ]));
129
130 3
            if (count($data)) {
131 3
                [$tuple] = $data;
132
133 3
                if ($tuple[1] == $this->getUniqueToken($key)) {
134 1
                    $this->checkNotExpired($key);
135 1
                    return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the type-hinted return void.
Loading history...
136
                }
137
138 2
                if ($tuple[2] < microtime(true)) {
139 1
                    $this->delete($key, $tuple[1]);
140 1
                    return $this->save($key);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->save($key) targeting Tarantool\SymfonyLock\TarantoolStore::save() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
141
                }
142
            }
143
144 1
            throw new LockConflictedException($e->getMessage(), $e->getCode(), $e);
145
        }
146 8
    }
147
148 9
    protected function getUniqueToken(Key $key): string
149
    {
150 9
        if (!$key->hasState(__CLASS__)) {
151 9
            $token = base64_encode(random_bytes(32));
152 9
            $key->setState(__CLASS__, $token);
153
        }
154
155 9
        return $key->getState(__CLASS__);
156
    }
157
158 9
    protected function getExpirationTimestamp(Key $key): float
159
    {
160 9
        return microtime(true) + $key->getRemainingLifetime();
161
    }
162
163 9
    protected function getSpace(): Space
164
    {
165
        try {
166 9
            return $this->client->getSpace($this->space);
167 2
        } catch (RequestFailed $e) {
168 2
            if ($this->createSchema) {
169 1
                $schema = new SchemaManager($this->client, [ 'space' => $this->space ]);
170 1
                $schema->setup();
171 1
                return $this->client->getSpace($this->space);
172
            }
173 1
            throw $e;
174
        }
175
    }
176
}
177