Completed
Push — in-memory-cache ( a24fa8 )
by André
26:08
created

InMemoryCacheAdapter   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
dl 0
loc 221
rs 9.6
c 0
b 0
f 0
wmc 35
lcom 1
cbo 3

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getItem() 0 13 3
A getItems() 0 21 5
A hasItem() 0 9 2
A clear() 0 7 1
A deleteItem() 0 8 2
A deleteItems() 0 10 3
A save() 0 6 1
A saveDeferred() 0 8 1
A commit() 0 4 1
A invalidateTags() 0 11 3
B saveCacheHitsInMemory() 0 27 6
B getValidInMemoryCacheItems() 0 27 6
1
<?php
2
3
/**
4
 * File containing the ContentHandler implementation.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
declare(strict_types=1);
10
11
namespace eZ\Publish\Core\Persistence\Cache\Adapter;
12
13
14
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
15
use Psr\Cache\CacheItemInterface;
16
17
final class InMemoryCacheAdapter implements TagAwareAdapterInterface
18
{
19
    /**
20
     * Default limits to in-memory cache usage, max objects cached and max ttl to live in-memory.
21
     */
22
    private const LIMIT = 100;
23
    private const TTL = 500;
24
25
    /**
26
     * @TODO Change to rather be whitelist in jected as argument, this is purely simplest way to see how much stuff gets
27
     *       cached if we allow everything byt content to be cached.
28
     * This matches:
29
     * - ez-content-${contentId}${versionKey}-${translationsKey}
30
     * - ez-content-${contentId}-version-list
31
     */
32
    private const BLACK_LIST = '/^ez-content-\d+-';
33
34
    /**
35
     * @var \Symfony\Component\Cache\CacheItem[] Cache of cache items by their keys.
36
     */
37
    private $cacheItems = [];
38
39
    /**
40
     * @var array Timestamp per cache key for TTL checks.
41
     */
42
    private $cacheItemsTS = [];
43
44
    /**
45
     * @var TagAwareAdapterInterface
46
     */
47
    private $pool;
48
49
    /**
50
     * @var int
51
     */
52
    private $limit;
53
54
    /**
55
     * @var float
56
     */
57
    private $ttl;
58
59
    public function __construct(TagAwareAdapterInterface $pool, int $limit = self::LIMIT, int $ttl = self::TTL)
60
    {
61
        $this->pool = $pool;
62
        $this->limit = $limit;
63
        $this->ttl = $ttl / 1000;
0 ignored issues
show
Documentation Bug introduced by
The property $ttl was declared of type double, but $ttl / 1000 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
64
    }
65
66
    public function getItem($key)
67
    {
68
        if ($items = $this->getValidInMemoryCacheItems([$key])) {
69
            return $items[$key];
70
        }
71
72
        $item = $this->pool->getItem($key);
73
        if ($item->isHit()) {
74
            $this->saveCacheHitsInMemory([$key => $item]);
75
        }
76
77
        return $item;
78
    }
79
80
    public function getItems(array $keys = [])
81
    {
82
        $missingKeys = [];
83
        foreach ($this->getValidInMemoryCacheItems($keys, $missingKeys) as $key => $item) {
84
            yield $key => $item;
85
        }
86
87
        if (!empty($missingKeys)) {
88
            $hits = [];
89
            $items = $this->pool->getItems($missingKeys);
90
            foreach ($items as $key => $item) {
91
                yield $key => $item;
92
93
                if ($item->isHit()) {
94
                    $hits[$key] = $item;
95
                }
96
            }
97
98
            $this->saveCacheHitsInMemory($hits);
99
        }
100
    }
101
102
    public function hasItem($key)
103
    {
104
        // We are not interested in trying to cache if we don't have the item, but if we do we can return true
105
        if (isset($this->cacheItems[$key])) {
106
            return true;
107
        }
108
109
        return $this->pool->hasItem($key);
110
    }
111
112
    public function clear()
113
    {
114
        $this->cacheItems = [];
115
        $this->cacheItemsTS = [];
116
117
        return $this->pool->clear();
118
    }
119
120
    public function deleteItem($key)
121
    {
122
        if (isset($this->cacheItems[$key])) {
123
            unset($this->cacheItems[$key], $this->cacheItemsTS[$key]);
124
        }
125
126
        return $this->pool->deleteItem($key);
127
    }
128
129
    public function deleteItems(array $keys)
130
    {
131
        foreach ($keys as $key) {
132
            if (isset($this->cacheItems[$key])) {
133
                unset($this->cacheItems[$key], $this->cacheItemsTS[$key]);
134
            }
135
        }
136
137
        return $this->pool->deleteItems($keys);
138
    }
139
140
    public function save(CacheItemInterface $item)
141
    {
142
        $this->saveCacheHitsInMemory([$item->getKey() => $item]);
143
144
        return $this->pool->save($item);
145
    }
146
147
    public function saveDeferred(CacheItemInterface $item)
148
    {
149
        // Symfony commits the deferred items as soon as getItem(s) is called on it later or on destruct.
150
        // So seems we can safely save in-memory, also we don't at the time of writing use saveDeferred().
151
        $this->saveCacheHitsInMemory([$item->getKey() => $item]);
152
153
        return $this->pool->saveDeferred($item);
154
    }
155
156
    public function commit()
157
    {
158
        return $this->pool->commit();
159
    }
160
161
    public function invalidateTags(array $tags)
162
    {
163
        // Cleanup in-Memory cache items affected
164
        foreach ($this->cacheItems as $key => $item) {
165
            if (array_intersect($item->getPreviousTags(), $tags)) {
166
                unset($this->cacheItems[$key], $this->cacheItemsTS[$key]);
167
            }
168
        }
169
170
        return $this->pool->invalidateTags($tags);
171
    }
172
173
    /**
174
     * @param \Psr\Cache\CacheItemInterface[] $items Save Cache hits in-memory with cache key as array key.
175
     */
176
    private function saveCacheHitsInMemory(array $items): void
177
    {
178
        // If items accounts for more then 20% of our limit, assume it's bulk content load and skip saving in-memory
179
        if (\count($items) >= $this->limit / 5) {
180
            return;
181
        }
182
183
        // Skips items if they match BLACK_LIST pattern
184
        foreach ($items as $key => $item) {
185
            if (preg_match(self::BLACK_LIST, $key)) {
186
                unset($items[$key]);
187
            }
188
        }
189
190
        // Skip if empty
191
        if (empty($items)) {
192
            return;
193
        }
194
195
        // Will we stay clear of the limit? If so remove clearing the 33% oldest cache values
196
        if (\count($items) + \count($this->cacheItems) >= $this->limit) {
197
            $this->cacheItems = \array_slice($this->cacheItems, (int) ($this->limit / 3));
198
        }
199
200
        $this->cacheItems += $items;
201
        $this->cacheItemsTS += \array_fill_keys(\array_keys($items), \microtime(true));
202
    }
203
204
    /**
205
     * @param array $keys
206
     * @param array $missingKeys
207
     *
208
     * @return array
209
     */
210
    public function getValidInMemoryCacheItems(array $keys = [], array &$missingKeys = []): array
211
    {
212
        // 1. Validate TTL and remove items that have exceeded it (on purpose not prefixed for global scope, see tests)
213
        $expiredTime = \microtime(true) - $this->ttl;
214
        foreach ($this->cacheItemsTS as $key => $ts) {
215
            if ($ts <= $expiredTime) {
216
                unset($this->cacheItemsTS[$key]);
217
218
                // Cache items might have been removed in saveInMemoryCacheItems() when enforcing limit
219
                if (isset($this->cacheItems[$key])) {
220
                    unset($this->cacheItems[$key]);
221
                }
222
            }
223
        }
224
225
        // 2. Get valid items
226
        $items = [];
227
        foreach ($keys as $key) {
228
            if (isset($this->cacheItems[$key])) {
229
                $items[$key] = $this->cacheItems[$key];
230
            } else {
231
                $missingKeys[] = $key;
232
            }
233
        }
234
235
        return $items;
236
    }
237
}
238