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 <p@tchwork.com>
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy