Completed
Push — master ( 7342b9...e2ee63 )
by Dmitry
02:39
created

TarantoolStore   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 151
Duplicated Lines 0 %

Test Coverage

Coverage 97.06%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 68
c 1
b 0
f 0
dl 0
loc 151
ccs 66
cts 68
cp 0.9706
rs 10
wmc 21

8 Methods

Rating   Name   Duplication   Size   Complexity  
A validateOptions() 0 12 3
A putOffExpiration() 0 9 2
A getExpirationTimestamp() 0 3 1
A delete() 0 16 2
A getUniqueToken() 0 8 2
A getSpace() 0 11 3
A exists() 0 11 3
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 15
    protected function validateOptions()
38
    {
39 15
        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 15
        if ($this->space == '') {
48 1
            throw new InvalidArgumentException("Space should be defined");
49
        }
50 15
    }
51
52
    /**
53
     * {@inheritdoc}
54
     */
55 4
    public function delete(Key $key, ?string $token = null)
56
    {
57
        $arguments = [
58 4
            (string) $key,
59 4
            $token ?: $this->getUniqueToken($key),
60
        ];
61
62
        $script = <<<LUA
63 4
        local key, token = ...
64 4
        local tuple = box.space.$this->space:get(key)
65
        if tuple and tuple.token == token then
66 4
            box.space.$this->space:delete(tuple.key)
67
        end
68
        LUA;
69
70 4
        $this->client->evaluate($script, ...$arguments);
71 4
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76 4
    public function exists(Key $key)
77
    {
78 4
        $data = $this->getSpace()
79 4
            ->select(Criteria::key([ (string) $key ]));
80
81 4
        if (count($data)) {
82 4
            [$tuple] = $data;
83 4
            return $tuple[1] == $this->getUniqueToken($key)
84 4
                && $tuple[2] >= microtime(true);
85
        }
86 3
        return false;
87
    }
88
89
    /**
90
     * {@inheritdoc}
91
     */
92 1
    public function putOffExpiration(Key $key, float $ttl)
93
    {
94 1
        if ($this->exists($key)) {
95 1
            $key->resetLifetime();
96 1
            $key->reduceLifetime($ttl);
97
98 1
            $this->getSpace()->update(
99 1
                [ (string) $key ],
100 1
                Operations::set(2, $this->getExpirationTimestamp($key)),
101
            );
102
        }
103 1
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108 7
    public function save(Key $key)
109
    {
110 7
        $key->reduceLifetime($this->initialTtl);
111
112
        try {
113
            $tuple = [
114 7
                (string) $key,
115 7
                $this->getUniqueToken($key),
116 7
                $this->getExpirationTimestamp($key),
117
            ];
118 7
            $this->getSpace()->insert($tuple);
119 6
            $this->checkNotExpired($key);
120 3
        } catch (RequestFailed $e) {
121 3
            $data = $this->getSpace()
122 2
                ->select(Criteria::key([ (string) $key ]));
123
124 2
            if (count($data)) {
125 2
                [$tuple] = $data;
126
127 2
                if ($tuple[1] == $this->getUniqueToken($key)) {
128 1
                    $this->checkNotExpired($key);
129 1
                    return true;
130
                }
131
132 1
                if ($tuple[2] < microtime(true)) {
133
                    $this->delete($key, $tuple[1]);
134
                    return $this->save($key);
135
                }
136
            }
137
138 1
            throw new LockConflictedException(null, null, $e);
139
        }
140 6
    }
141
142 7
    protected function getUniqueToken(Key $key): string
143
    {
144 7
        if (!$key->hasState(__CLASS__)) {
145 7
            $token = base64_encode(random_bytes(32));
146 7
            $key->setState(__CLASS__, $token);
147
        }
148
149 7
        return $key->getState(__CLASS__);
150
    }
151
152 7
    protected function getExpirationTimestamp(Key $key): float
153
    {
154 7
        return microtime(true) + $key->getRemainingLifetime();
155
    }
156
157 7
    protected function getSpace(): Space
158
    {
159
        try {
160 7
            return $this->client->getSpace($this->space);
161 2
        } catch (RequestFailed $e) {
162 2
            if ($this->createSchema) {
163 1
                $schema = new SchemaManager($this->client, [ 'space' => $this->space ]);
164 1
                $schema->setup();
165 1
                return $this->client->getSpace($this->space);
166
            }
167 1
            throw $e;
168
        }
169
    }
170
}
171