Completed
Push — in-memory-cache ( 3245f0...8b8156 )
by André
20:40
created

InMemoryCacheAdapter::saveInMemoryCacheItems()   C

Complexity

Conditions 11
Paths 47

Size

Total Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
nc 47
nop 1
dl 0
loc 55
rs 6.8351
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
use eZ\Publish\SPI\Persistence\Content;
14
use eZ\Publish\SPI\Variation\Values\Variation;
15
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
16
use Psr\Cache\CacheItemInterface;
17
18
final class InMemoryCacheAdapter implements TagAwareAdapterInterface
19
{
20
    /**
21
     * Default limits to in-memory cache usage, max objects cached and max ttl to live in-memory.
22
     */
23
    private const LIMIT = 100;
24
    private const TTL = 10;
25
26
    /**
27
     * Indication of content classes for cache discrimination.
28
     *
29
     * By design we prefer to keep meta info (type, sections, ..) in cache for as long as possible and rather cleanup
30
     * cache representing content when we need to vacuum the cache (when reaching limits) first.
31
     *
32
     * Reason for that is:
33
     * - meta data more likely to be attempted to be loaded again
34
     * - content more likely to get updates more frequently
35
     *
36
     * So for those reasons content in-memory cache only aims to cache content for short bursts where code typically
37
     * deals with a given content before it moves on to the next, while keeping meta cache around for a bit longer.
38
     */
39
    private const CONTENT_CLASSES = [
40
        Content::class,
41
        Content\ContentInfo::class,
42
        Content\Location::class, // Will also match Content\Location\Trashed
43
        Content\VersionInfo::class,
44
        Content\Relation::class,
45
        Variation::class,
46
    ];
47
48
    /**
49
     * @var \Symfony\Component\Cache\CacheItem[] Cache of cache items by their keys.
50
     */
51
    private $cacheItems = [];
52
53
    /**
54
     * @var array Timestamp per cache key for TTL checks.
55
     */
56
    private $cacheItemsTS = [];
57
58
    /**
59
     * @var TagAwareAdapterInterface
60
     */
61
    private $inner;
62
63
    /**
64
     * @var int
65
     */
66
    private $limit;
67
68
    /**
69
     * @var int
70
     */
71
    private $ttl;
72
73
    public function __construct(TagAwareAdapterInterface $inner, int $limit = self::LIMIT, int $ttl = self::TTL)
74
    {
75
        $this->inner = $inner;
76
        $this->limit = $limit;
77
        $this->ttl = $ttl;
78
    }
79
80
    public function getItem($key)
81
    {
82
        if ($items = $this->getValidInMemoryCacheItems([$key])) {
83
            return $items[$key];
84
        }
85
86
        $item = $this->inner->getItem($key);
87
        if ($item->isHit()) {
88
            $this->saveCacheHitsInMemory([$key => $item]);
89
        }
90
91
        return $item;
92
    }
93
94
    public function getItems(array $keys = [])
95
    {
96
        $missingKeys = [];
97
        foreach($this->getValidInMemoryCacheItems($keys, $missingKeys) as $key => $item) {
98
            yield $key => $item;
99
        }
100
101
        if (!empty($missingKeys)) {
102
            $hits = [];
103
            $items = $this->inner->getItems($missingKeys);
104
            foreach ($items as $key => $item) {
105
                yield $key => $item;
106
107
                if ($item->isHit()) {
108
                    $hits[$key] = $item;
109
                }
110
            }
111
112
            $this->saveCacheHitsInMemory($hits);
113
        }
114
    }
115
116
    public function hasItem($key)
117
    {
118
        // We are not interested in trying to cache if we don't have the item, but if we do we can return true
119
        if (isset($this->cacheItems[$key])) {
120
            return true;
121
        }
122
123
        return $this->inner->hasItem($key);
124
    }
125
126
    public function clear()
127
    {
128
        $this->cacheItems = [];
129
        $this->cacheItemsTS = [];
130
131
        return $this->inner->clear();
132
    }
133
134
    public function deleteItem($key)
135
    {
136
        if (isset($this->cacheItems[$key])) {
137
            unset($this->cacheItems[$key], $this->cacheItemsTS[$key]);
138
        }
139
140
        return $this->inner->deleteItem($key);
141
    }
142
143
    public function deleteItems(array $keys)
144
    {
145
        foreach ($keys as $key) {
146
            if (isset($this->cacheItems[$key])) {
147
                unset($this->cacheItems[$key], $this->cacheItemsTS[$key]);
148
            }
149
        }
150
151
        return $this->inner->deleteItems($keys);
152
    }
153
154
    public function save(CacheItemInterface $item)
155
    {
156
        $this->saveCacheHitsInMemory([$item->getKey() => $item]);
157
158
        return $this->inner->save($item);
159
    }
160
161
    public function saveDeferred(CacheItemInterface $item)
162
    {
163
        // Symfony commits the deferred items as soon as getItem(s) is called on it later or on destruct.
164
        // So seems we can safely save in-memory, also we don't at the time of writing use saveDeferred().
165
        $this->saveCacheHitsInMemory([$item->getKey() => $item]);
166
167
        return $this->inner->saveDeferred($item);
168
    }
169
170
    public function commit()
171
    {
172
        return $this->inner->commit();
173
    }
174
175
    public function invalidateTags(array $tags)
176
    {
177
        // Cleanup in-Memory cache items affected
178
        foreach ($this->cacheItems as $key => $item) {
179
            if (array_intersect($item->getPreviousTags(), $tags)) {
180
                unset($this->cacheItems[$key], $this->cacheItemsTS[$key]);
181
            }
182
        }
183
184
        return $this->inner->invalidateTags($tags);
185
    }
186
187
    /**
188
     * @param \Psr\Cache\CacheItemInterface[] $items Save Cache hits in-memory with cache key as array key.
189
     */
190
    private function saveCacheHitsInMemory(array $items): void
191
    {
192
        // Skip if empty
193
        if (empty($items)) {
194
            return;
195
        }
196
197
        // If items accounts for more then 20% of our limit, assume it's bulk content load and skip saving in-memory
198
        if (\count($items) >= $this->limit / 5) {
199
            return;
200
        }
201
202
        // Will we stay clear of the limit? If so return early
203
        if (\count($items) + \count($this->cacheItems) < $this->limit) {
204
            $this->cacheItems += $items;
205
            $this->cacheItemsTS += \array_fill_keys(\array_keys($items), \time());
206
207
            return;
208
        }
209
210
        // # Vacuuming cache in bulk so we don't end up doing this all the time
211
        // 1. Discriminate against content cache, remove up to 1/3 of max limit starting from oldest items
212
        $removeCount = 0;
213
        $removeTarget = floor($this->limit / 3);
214
        foreach ($this->cacheItems as $key => $item) {
215
            $cache = $item->get();
216
            foreach (self::CONTENT_CLASSES as $className) {
217
                if ($cache instanceof $className) {
218
                    unset($this->cacheItems[$key]);
219
                    ++$removeCount;
220
221
                    break;
222
                }
223
            }
224
225
            if ($removeCount >= $removeTarget) {
226
                break;
227
            }
228
        }
229
230
        // 2. Does cache still exceed the 80% of limit? if so remove everything above 66%
231
        // NOTE: This on purpose keeps the oldest cache around, getValidInMemoryCacheItems() handles ttl checks on that
232
        if (\count($this->cacheItems) >= $this->limit / 1.5) {
233
            $this->cacheItems = \array_slice($this->cacheItems, 0, floor($this->limit / 1.5));
234
        }
235
236
        $this->cacheItems += $items;
237
        $this->cacheItemsTS += \array_fill_keys(\array_keys($items), \time());
238
    }
239
240
    /**
241
     * @param array $keys
242
     * @param array $missingKeys
243
     *
244
     * @return array
245
     */
246
    public function getValidInMemoryCacheItems(array $keys = [], array &$missingKeys = []): array
247
    {
248
        // 1. Validate TTL and remove items that have exceeded it
249
        $expiredTime = time() - $this->ttl;
250
        foreach ($this->cacheItemsTS as $key => $ts) {
251
            if ($ts <= $expiredTime) {
252
                unset($this->cacheItemsTS[$key]);
253
254
                // Cache items might have been removed in saveInMemoryCacheItems() when enforcing limit
255
                if (isset($this->cacheItems[$key])) {
256
                    unset($this->cacheItems[$key]);
257
                }
258
            }
259
        }
260
261
        // 2. Get valid items
262
        $items = [];
263
        foreach ($keys as $key) {
264
            if (isset($this->cacheItems[$key])) {
265
                $items[$key] = $this->cacheItems[$key];
266
            } else {
267
                $missingKeys[] = $key;
268
            }
269
        }
270
271
        return $items;
272
    }
273
}
274