Completed
Push — in-memory-cache2 ( 0f2241...bf3815 )
by André
18:53
created

InMemoryCache::deleteMulti()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 1
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * File containing MetadataCachePool class.
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\InMemory;
12
13
/**
14
 * Simple In-Memory Cache Pool.
15
 *
16
 * Could be used as separate instances for different use cases with a shared global expiry when applicable.
17
 *
18
 * Allow to use one instance with low ttl (200-600 milliseconds) and limited items (~100) dedicated to content (and
19
 * related classes) to handle burst caching, and a separate instance with longer ttl (3-10sec) and separate limit (~200)
20
 * for meta values like content type, section, ... to handle repeated lookups for these seldom changing values.
21
 *
22
 * @internal Only for use in eZ\Publish\Core\Persistence\Cache\AbstractInMemoryHandler, may change depending on needs there.
23
 */
24
class InMemoryCache
25
{
26
    /**
27
     * @var float Cache Time to Live, in seconds. This is only for how long we keep cache object around in-memory.
28
     */
29
    private $ttl;
30
31
    /**
32
     * @var int The limit of objects in cache pool at a given time
33
     */
34
    private $limit;
35
36
    /**
37
     * @var bool Switch for enabeling/disabling in-memory cache
38
     */
39
    private $enabled;
40
41
    /**
42
     * Cache objects by primary key.
43
     *
44
     * @var object[]
45
     */
46
    private $cache = [];
47
48
    /**
49
     * @var float[] Timestamp (float microtime) for individual cache by primary key.
50
     */
51
    private $cacheTime = [];
52
53
    /**
54
     * Mapping of secondary index to primary key.
55
     *
56
     * @var string[]
57
     */
58
    private $cacheIndex = [];
59
60
    /**
61
     * @var float|null Micro timestamp with time clear($global=true) has been called to synchronize across cache pools.
62
     */
63
    private $cacheExpiryTime;
64
    protected static $globalCacheExpiry;
65
66
    /**
67
     * Language Cache constructor.
68
     *
69
     * @param float $ttl Seconds for the cache to live as a float, by default 0.3 (300 milliseconds)
70
     * @param int $limit Limit for values to keep in cache, by default 100 cache values (per pool instance).
71
     * @param bool $enabled For use by configuration to be able to disable or enable depending on needs.
72
     */
73
    public function __construct(float $ttl = 0.3, int $limit = 100, bool $enabled = true)
74
    {
75
        $this->ttl = $ttl;
76
        $this->limit = $limit;
77
        $this->enabled = $enabled;
78
    }
79
80
    /**
81
     * Returns a cache objects.
82
     *
83
     * @param string $key Primary or secondary index to look for cache on.
84
     *
85
     * @return object|null Object if found, null if not.
86
     */
87
    public function get(string $key)
88
    {
89
        if ($this->enabled === false) {
90
            return null;
91
        }
92
93
        // Check for global expiry change, if the case clear cache
94
        if ($this->cacheExpiryTime !== self::$globalCacheExpiry) {
95
            $this->clear();
96
            $this->cacheExpiryTime = self::$globalCacheExpiry;
97
98
            return null;
99
        }
100
101
        $index = $this->cacheIndex[$key] ?? $key;
102
        if (!isset($this->cache[$index]) || $this->cacheTime[$index] + $this->ttl < microtime(true)) {
103
            return null;
104
        }
105
106
        return $this->cache[$index];
107
    }
108
109
    /**
110
     * Set object in in-memory cache.
111
     *
112
     * Should only set Cache hits here!
113
     *
114
     * @param object[] $objects
115
     * @param callable $objectIndexes Return array of indexes per object (first argument), must return at least 1 primary index
116
     * @param string|null $listIndex Optional index for list of items
117
     */
118
    public function setMulti(array $objects, callable $objectIndexes, string $listIndex = null): void
119
    {
120
        // If objects accounts for more then 20% of our limit, assume it's bulk content load and skip saving in-memory
121
        if ($this->enabled === false || \count($objects) >= $this->limit / 5) {
122
            return;
123
        }
124
125
        // check if we will reach limit by adding these objects, if so remove old cache
126
        if (\count($this->cache) + \count($objects) >= $this->limit) {
127
            $this->vacuum();
128
        }
129
130
        $time = microtime(true);
131
132
        // if set add objects to cache on list index (typically a "all" key)
133
        if ($listIndex) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $listIndex of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
134
            $this->cache[$listIndex] = $objects;
135
            $this->cacheTime[$listIndex] = $time;
136
        }
137
138
        foreach ($objects as $object) {
139
            // Skip if there are no indexes
140
            if (!$indexes = $objectIndexes($object)) {
141
                continue;
142
            }
143
144
            $key = array_shift($indexes);
145
            $this->cache[$key] = $object;
146
            $this->cacheTime[$key] = $time;
147
148
            foreach ($indexes as $index) {
149
                $this->cacheIndex[$index] = $key;
150
            }
151
        }
152
    }
153
154
    /**
155
     * Removes multiple in-memory cache from the pool.
156
     *
157
     * @param string[] $keys An array of keys that should be removed from the pool.
158
     */
159
    public function deleteMulti(array $keys): void
160
    {
161
        if ($this->enabled === false) {
162
            return;
163
        }
164
165
        foreach ($keys as $key) {
166
            if ($index = $this->cacheIndex[$key] ?? null) {
167
                unset($this->cacheIndex[$key], $this->cache[$index], $this->cacheTime[$index]);
168
            } else {
169
                unset($this->cache[$key], $this->cacheTime[$key]);
170
            }
171
        }
172
    }
173
174
    /**
175
     * Deletes all cache in the in-memory pool.
176
     */
177
    public function clear(bool $global = false): void
178
    {
179
        // On purpose does not check if enabled, in case of several instances we allow clearing cache
180
        $this->cache = $this->cacheIndex = $this->cacheTime = [];
181
        if ($global) {
182
            $this->cacheExpiryTime = self::$globalCacheExpiry = microtime(true);
183
        }
184
    }
185
186
    /**
187
     * Call to reduce cache items when $limit has been reached.
188
     *
189
     * Deletes expired first, then oldest(or least used?).
190
     */
191
    private function vacuum(): void
192
    {
193
        // Vacuuming cache in bulk, clearing the 33% oldest cache values
194
        $this->cache = \array_slice($this->cache, \floor($this->limit / 3));
195
196
        // Cleanup secondary index and cache time
197
        foreach ($this->cacheIndex as $index => $key) {
198
            if (!isset($this->cache[$key])) {
199
                unset($this->cacheIndex[$index], $this->cacheTime[$key]);
200
            }
201
        }
202
    }
203
}
204