Completed
Push — in-memory-cache ( 3245f0 )
by André
18:20
created

InMemoryCacheAdapter::deleteItems()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
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
        $this->saveInMemoryCacheItems([$key => $item]);
88
89
        return $item;
90
    }
91
92
    public function getItems(array $keys = [])
93
    {
94
        $missingKeys = [];
95
        $items = $this->getValidInMemoryCacheItems($keys, $missingKeys);
96
97
        if (!empty($missingKeys)) {
98
            $items += $newItems = $this->inner->getItems($missingKeys);
99
            $this->saveInMemoryCacheItems($newItems);
0 ignored issues
show
Bug introduced by
It seems like $newItems defined by $this->inner->getItems($missingKeys) on line 98 can also be of type object<Traversable>; however, eZ\Publish\Core\Persiste...aveInMemoryCacheItems() does only seem to accept array<integer,object<Psr...he\CacheItemInterface>>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
100
        }
101
102
        return $items;
103
    }
104
105
    public function hasItem($key)
106
    {
107
        // We are not interested in trying to cache if we don't have the item, but if we do we can return true
108
        if (isset($this->cacheItems[$key])) {
109
            return true;
110
        }
111
112
        return $this->inner->hasItem($key);
113
    }
114
115
    public function clear()
116
    {
117
        $this->cacheItems = [];
118
        $this->cacheItemsTS = [];
119
120
        return $this->inner->clear();
121
    }
122
123
    public function deleteItem($key)
124
    {
125
        if (isset($this->cacheItems[$key])) {
126
            unset($this->cacheItems[$key], $this->cacheItemsTS[$key]);
127
        }
128
129
        return $this->inner->deleteItem($key);
130
    }
131
132
    public function deleteItems(array $keys)
133
    {
134
        foreach ($keys as $key) {
135
            if (isset($this->cacheItems[$key])) {
136
                unset($this->cacheItems[$key], $this->cacheItemsTS[$key]);
137
            }
138
        }
139
140
        return $this->inner->deleteItems($keys);
141
    }
142
143
    public function save(CacheItemInterface $item)
144
    {
145
        $this->saveInMemoryCacheItems([$item->getKey() => $item]);
146
147
        return $this->inner->save($item);
148
    }
149
150
    public function saveDeferred(CacheItemInterface $item)
151
    {
152
        // Symfony commits the deferred items as soon as getItem(s) is called on it later or on destruct.
153
        // So seems we can safely save in-memory, also we don't at the time of writing use saveDeferred().
154
        $this->saveInMemoryCacheItems([$item->getKey() => $item]);
155
156
        return $this->inner->saveDeferred($item);
157
    }
158
159
    public function commit()
160
    {
161
        return $this->inner->commit();
162
    }
163
164
    public function invalidateTags(array $tags)
165
    {
166
        // Cleanup in-Memory cache items affected
167
        foreach ($this->cacheItems as $key => $item) {
168
            if (array_intersect($item->getPreviousTags(), $tags)) {
169
                unset($this->cacheItems[$key], $this->cacheItemsTS[$key]);
170
            }
171
        }
172
173
        return $this->inner->invalidateTags($tags);
174
    }
175
176
    /**
177
     * @param \Psr\Cache\CacheItemInterface[] $items Cache items with cache key as array key.
178
     */
179
    private function saveInMemoryCacheItems(array $items): void
180
    {
181
        if (empty($items)) {
182
            return;
183
        }
184
185
        // If items accounts for more then 20% of our limit, assume it's bulk content load and skip saving in-memory
186
        if (\count($items) >= $this->limit / 5) {
187
            return;
188
        }
189
190
        // Filter out cache misses as they will immediately be re-generated
191
        foreach ($items as $key => $item) {
192
            if (!$item->isHit()) {
193
                unset($items[$key]);
194
            }
195
        }
196
197
        // Will we stay clear of the limit? If so return early
198
        if (\count($items) + \count($this->cacheItems) < $this->limit) {
199
            $this->cacheItems += $items;
200
            $this->cacheItemsTS += \array_fill_keys(\array_keys($items), \time());
201
202
            return;
203
        }
204
205
        // # Vacuuming cache in bulk so we don't end up doing this all the time
206
        // 1. Discriminate against content cache, remove up to 1/3 of max limit starting from oldest items
207
        $removeCount = 0;
208
        $removeTarget = floor($this->limit / 3);
209
        foreach ($this->cacheItems as $key => $item) {
210
            $cache = $item->get();
211
            foreach (self::CONTENT_CLASSES as $className) {
212
                if ($cache instanceof $className) {
213
                    unset($this->cacheItems[$key]);
214
                    ++$removeCount;
215
216
                    break;
217
                }
218
            }
219
220
            if ($removeCount >= $removeTarget) {
221
                break;
222
            }
223
        }
224
225
        // 2. Does cache still exceed the 80% of limit? if so remove everything above 66%
226
        // NOTE: This on purpose keeps the oldest cache around, getValidInMemoryCacheItems() handles ttl checks on that
227
        if (\count($this->cacheItems) >= $this->limit / 1.5) {
228
            $this->cacheItems = \array_slice($this->cacheItems, 0, floor($this->limit / 1.5));
229
        }
230
231
        $this->cacheItems += $items;
232
        $this->cacheItemsTS += \array_fill_keys(\array_keys($items), \time());
233
    }
234
235
    /**
236
     * @param array $keys
237
     * @param array $missingKeys
238
     *
239
     * @return array
240
     */
241
    public function getValidInMemoryCacheItems(array $keys = [], array &$missingKeys = []): array
242
    {
243
        // 1. Validate TTL and remove items that have exceeded it
244
        $expiredTime = time() - $this->ttl;
245
        foreach ($this->cacheItemsTS as $key => $ts) {
246
            if ($ts <= $expiredTime) {
247
                unset($this->cacheItemsTS[$key]);
248
249
                // Cache items might have been removed in saveInMemoryCacheItems() when enforcing limit
250
                if (isset($this->cacheItems[$key])) {
251
                    unset($this->cacheItems[$key]);
252
                }
253
            }
254
        }
255
256
        // 2. Get valid items
257
        $items = [];
258
        foreach ($keys as $key) {
259
            if (isset($this->cacheItems[$key])) {
260
                $items[$key] = $this->cacheItems[$key];
261
            } else {
262
                $missingKeys[] = $key;
263
            }
264
        }
265
266
        return $items;
267
    }
268
}
269