Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b22a584

Browse files
feature #35362 [Cache] Add LRU + max-lifetime capabilities to ArrayCache (nicolas-grekas)
This PR was merged into the 5.1-dev branch. Discussion ---------- [Cache] Add LRU + max-lifetime capabilities to ArrayCache | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix https://github.com/orgs/symfony/projects/1#card-30686676 | License | MIT | Doc PR | - In #32294 (comment), @andrerom writes: > if you plan to expose use of ArrayAdapter to a wider audience you should probably also add the following features to it: > - max item limit to avoid reaching memory limits > - own (very low, like default 100-500ms) TTL for in-memory caching, as it's in practice stale data when used in concurrent scenarios > > If you want to be advance you can also: > > - keep track of use, and evict cache items based on that using LFU when reaching limit > - in-memory cache is domain & project specific in terms of how long it's somewhat "safe" to keep items in memory, so either describe when to use and not use on a per pool term, or allow use of pool to pass in flags to opt out of in-memory cache for cases developer knows it should be ignored This PR implements these suggestions, via two new constructor arguments: `$maxLifetime` and `$maxItems`. In Yaml: ```yaml services: app.lru150_cache: parent: cache.adapter.array arguments: $maxItems: 150 $maxLifetime: 0.150 framework: cache: pools: my_chained_pool: adapters: - app.lru150_cache - cache.adapter.filesystem ``` This configuration adds a local memory cache that keeps max 150 elements for 150ms on top of a filesystem cache. /cc @lyrixx since you were also interested in it. Commits ------- 48a5d5e [Cache] Add LRU + max-lifetime capabilities to ArrayCache
2 parents 5182135 + 48a5d5e commit b22a584

File tree

3 files changed

+115
-9
lines changed

3 files changed

+115
-9
lines changed

src/Symfony/Component/Cache/Adapter/ArrayAdapter.php

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515
use Psr\Log\LoggerAwareInterface;
1616
use Psr\Log\LoggerAwareTrait;
1717
use Symfony\Component\Cache\CacheItem;
18+
use Symfony\Component\Cache\Exception\InvalidArgumentException;
1819
use Symfony\Component\Cache\ResettableInterface;
1920
use Symfony\Contracts\Cache\CacheInterface;
2021

2122
/**
23+
* An in-memory cache storage.
24+
*
25+
* Acts as a least-recently-used (LRU) storage when configured with a maximum number of items.
26+
*
2227
* @author Nicolas Grekas <[email protected]>
2328
*/
2429
class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
@@ -29,13 +34,25 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
2934
private $values = [];
3035
private $expiries = [];
3136
private $createCacheItem;
37+
private $maxLifetime;
38+
private $maxItems;
3239

3340
/**
3441
* @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise
3542
*/
36-
public function __construct(int $defaultLifetime = 0, bool $storeSerialized = true)
43+
public function __construct(int $defaultLifetime = 0, bool $storeSerialized = true, int $maxLifetime = 0, int $maxItems = 0)
3744
{
45+
if (0 > $maxLifetime) {
46+
throw new InvalidArgumentException(sprintf('Argument $maxLifetime must be a positive integer, %d passed.', $maxLifetime));
47+
}
48+
49+
if (0 > $maxItems) {
50+
throw new InvalidArgumentException(sprintf('Argument $maxItems must be a positive integer, %d passed.', $maxItems));
51+
}
52+
3853
$this->storeSerialized = $storeSerialized;
54+
$this->maxLifetime = $maxLifetime;
55+
$this->maxItems = $maxItems;
3956
$this->createCacheItem = \Closure::bind(
4057
static function ($key, $value, $isHit) use ($defaultLifetime) {
4158
$item = new CacheItem();
@@ -84,6 +101,13 @@ public function delete(string $key): bool
84101
public function hasItem($key)
85102
{
86103
if (\is_string($key) && isset($this->expiries[$key]) && $this->expiries[$key] > microtime(true)) {
104+
if ($this->maxItems) {
105+
// Move the item last in the storage
106+
$value = $this->values[$key];
107+
unset($this->values[$key]);
108+
$this->values[$key] = $value;
109+
}
110+
87111
return true;
88112
}
89113
CacheItem::validateKey($key);
@@ -97,7 +121,12 @@ public function hasItem($key)
97121
public function getItem($key)
98122
{
99123
if (!$isHit = $this->hasItem($key)) {
100-
$this->values[$key] = $value = null;
124+
$value = null;
125+
126+
if (!$this->maxItems) {
127+
// Track misses in non-LRU mode only
128+
$this->values[$key] = null;
129+
}
101130
} else {
102131
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
103132
}
@@ -164,7 +193,9 @@ public function save(CacheItemInterface $item)
164193
$value = $item["\0*\0value"];
165194
$expiry = $item["\0*\0expiry"];
166195

167-
if (null !== $expiry && $expiry <= microtime(true)) {
196+
$now = microtime(true);
197+
198+
if (null !== $expiry && $expiry <= $now) {
168199
$this->deleteItem($key);
169200

170201
return true;
@@ -173,7 +204,23 @@ public function save(CacheItemInterface $item)
173204
return false;
174205
}
175206
if (null === $expiry && 0 < $item["\0*\0defaultLifetime"]) {
176-
$expiry = microtime(true) + $item["\0*\0defaultLifetime"];
207+
$expiry = $item["\0*\0defaultLifetime"];
208+
$expiry = $now + ($expiry > ($this->maxLifetime ?: $expiry) ? $this->maxLifetime : $expiry);
209+
} elseif ($this->maxLifetime && (null === $expiry || $expiry > $now + $this->maxLifetime)) {
210+
$expiry = $now + $this->maxLifetime;
211+
}
212+
213+
if ($this->maxItems) {
214+
unset($this->values[$key]);
215+
216+
// Iterate items and vacuum expired ones while we are at it
217+
foreach ($this->values as $k => $v) {
218+
if ($this->expiries[$k] > $now && \count($this->values) < $this->maxItems) {
219+
break;
220+
}
221+
222+
unset($this->values[$k], $this->expiries[$k]);
223+
}
177224
}
178225

179226
$this->values[$key] = $value;
@@ -210,15 +257,21 @@ public function commit()
210257
public function clear(string $prefix = '')
211258
{
212259
if ('' !== $prefix) {
260+
$now = microtime(true);
261+
213262
foreach ($this->values as $key => $value) {
214-
if (0 === strpos($key, $prefix)) {
263+
if (!isset($this->expiries[$key]) || $this->expiries[$key] <= $now || 0 === strpos($key, $prefix)) {
215264
unset($this->values[$key], $this->expiries[$key]);
216265
}
217266
}
218-
} else {
219-
$this->values = $this->expiries = [];
267+
268+
if ($this->values) {
269+
return true;
270+
}
220271
}
221272

273+
$this->values = $this->expiries = [];
274+
222275
return true;
223276
}
224277

@@ -258,8 +311,20 @@ private function generateItems(array $keys, $now, $f)
258311
{
259312
foreach ($keys as $i => $key) {
260313
if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] > $now || !$this->deleteItem($key))) {
261-
$this->values[$key] = $value = null;
314+
$value = null;
315+
316+
if (!$this->maxItems) {
317+
// Track misses in non-LRU mode only
318+
$this->values[$key] = null;
319+
}
262320
} else {
321+
if ($this->maxItems) {
322+
// Move the item last in the storage
323+
$value = $this->values[$key];
324+
unset($this->values[$key]);
325+
$this->values[$key] = $value;
326+
}
327+
263328
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
264329
}
265330
unset($keys[$i]);
@@ -314,8 +379,12 @@ private function unfreeze(string $key, bool &$isHit)
314379
$value = false;
315380
}
316381
if (false === $value) {
317-
$this->values[$key] = $value = null;
382+
$value = null;
318383
$isHit = false;
384+
385+
if (!$this->maxItems) {
386+
$this->values[$key] = null;
387+
}
319388
}
320389
}
321390

src/Symfony/Component/Cache/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.1.0
5+
-----
6+
7+
* added max-items + LRU + max-lifetime capabilities to `ArrayCache`
8+
49
5.0.0
510
-----
611

src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,36 @@ public function testGetValuesHitAndMiss()
5555
$this->assertArrayHasKey('bar', $values);
5656
$this->assertNull($values['bar']);
5757
}
58+
59+
public function testMaxLifetime()
60+
{
61+
$cache = new ArrayAdapter(0, false, 1);
62+
63+
$item = $cache->getItem('foo');
64+
$item->expiresAfter(2);
65+
$cache->save($item->set(123));
66+
67+
$this->assertTrue($cache->hasItem('foo'));
68+
sleep(1);
69+
$this->assertFalse($cache->hasItem('foo'));
70+
}
71+
72+
public function testMaxItems()
73+
{
74+
$cache = new ArrayAdapter(0, false, 0, 2);
75+
76+
$cache->save($cache->getItem('foo'));
77+
$cache->save($cache->getItem('bar'));
78+
$cache->save($cache->getItem('buz'));
79+
80+
$this->assertFalse($cache->hasItem('foo'));
81+
$this->assertTrue($cache->hasItem('bar'));
82+
$this->assertTrue($cache->hasItem('buz'));
83+
84+
$cache->save($cache->getItem('foo'));
85+
86+
$this->assertFalse($cache->hasItem('bar'));
87+
$this->assertTrue($cache->hasItem('buz'));
88+
$this->assertTrue($cache->hasItem('foo'));
89+
}
5890
}

0 commit comments

Comments
 (0)