Completed
Push — in-memory-cache2 ( de4787...b8bac7 )
by André
24:03
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
    /**
111
     * Set object in in-memory cache.
112
     *
113
     * Should only set Cache hits here!
114
     *
115
     * @param object[] $objects
116
     * @param callable $objectIndexes Return array of indexes per object (first argument), must return at least 1 primary index
117
     * @param string|null $listIndex Optional index for list of items
118
     */
119
    public function setMulti(array $objects, callable $objectIndexes, string $listIndex = null): void
120
    {
121
        // If objects accounts for more then 20% of our limit, assume it's bulk content load and skip saving in-memory
122
        if ($this->enabled === false || \count($objects) >= $this->limit / 5) {
123
            return;
124
        }
125
126
        // check if we will reach limit by adding these objects, if so remove old cache
127
        if (\count($this->cache) + \count($objects) >= $this->limit) {
128
            $this->vacuum();
129
        }
130
131
        $time = microtime(true);
132
133
        // if set add objects to cache on list index (typically a "all" key)
134
        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...
135
            $this->cache[$listIndex] = $objects;
136
            $this->cacheTime[$listIndex] = $time;
137
        }
138
139
140
        foreach ($objects as $object) {
141
            // Skip if there are no indexes
142
            if (!$indexes = $objectIndexes($object)) {
143
                continue;
144
            }
145
146
            $key = array_shift($indexes);
147
            $this->cache[$key] = $objects;
148
            $this->cacheTime[$key] = $time;
149
150
            foreach ($indexes as $index) {
151
                $this->cacheIndex[$index] = $key;
152
            }
153
        }
154
    }
155
156
    /**
157
     * Removes multiple in-memory cache from the pool.
158
     *
159
     * @param string[] $keys An array of keys that should be removed from the pool.
160
     */
161
    public function deleteMulti(array $keys): void
162
    {
163
        if ($this->enabled === false) {
164
            return;
165
        }
166
167
        foreach ($keys as $key) {
168
            if ($index = $this->cacheIndex[$key] ?? null) {
169
                unset($this->cacheIndex[$key], $this->cache[$index], $this->cacheTime[$index]);
170
            } else {
171
                unset($this->cache[$key], $this->cacheTime[$key]);
172
            }
173
        }
174
    }
175
176
    /**
177
     * Deletes all cache in the in-memory pool.
178
     */
179
    public function clear(bool $global = false): void
180
    {
181
        // On purpose does not check if enabled, in case of several instances we allow clearing cache
182
        $this->cache = $this->cacheIndex = $this->cacheTime = [];
183
        if ($global) {
184
            $this->cacheExpiryTime = self::$globalCacheExpiry = microtime(true);
185
        }
186
    }
187
188
    /**
189
     * Call to reduce cache items when $limit has been reached.
190
     *
191
     * Deletes expired first, then oldest(or least used?).
192
     */
193
    private function vacuum(): void
194
    {
195
        // Vacuuming cache in bulk, clearing the 33% oldest cache values
196
        $this->cache = \array_slice($this->cache, \floor($this->limit/3));
197
198
        // Cleanup secondary index and cache time
199
        foreach ($this->cacheIndex as $index => $key) {
200
            if (!isset($this->cache[$key])) {
201
                unset($this->cacheIndex[$index], $this->cacheTime[$key]);
202
            }
203
        }
204
    }
205
}
206