Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
D
drupal
Manage
Activity
Members
Labels
Plan
Wiki
Custom issue tracker
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Model registry
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Terms and privacy
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
project
drupal
Commits
aec131e3
Commit
aec131e3
authored
5 months ago
by
Geoff Appleby
Browse files
Options
Downloads
Plain Diff
Merge branch '1199866-memory-lru-cache' into '11.x'
#1199866
Add an in-memory LRU cache See merge request
!10336
parents
554b7c60
1e5444e1
No related branches found
No related tags found
No related merge requests found
Pipeline
#388897
failed
5 months ago
Stage: 🪄 Lint
Stage: 🗜️ Test
Pipeline: drupal
#388901
Changes
2
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
core/lib/Drupal/Core/Cache/MemoryCache/LruMemoryCache.php
+138
-0
138 additions, 0 deletions
core/lib/Drupal/Core/Cache/MemoryCache/LruMemoryCache.php
core/tests/Drupal/Tests/Core/Cache/LruMemoryCacheTest.php
+311
-0
311 additions, 0 deletions
core/tests/Drupal/Tests/Core/Cache/LruMemoryCacheTest.php
with
449 additions
and
0 deletions
core/lib/Drupal/Core/Cache/MemoryCache/LruMemoryCache.php
0 → 100644
+
138
−
0
View file @
aec131e3
<?php
namespace
Drupal\Core\Cache\MemoryCache
;
use
Drupal\Component\Datetime\TimeInterface
;
use
Drupal\Core\Cache\Cache
;
/**
* Defines a least recently used (LRU) static cache implementation.
*
* Stores cache items in memory using a PHP array. The number of cache items is
* limited to a fixed number of slots. When the all slots are full, older items
* are purged based on least recent usage.
*
* @ingroup cache
*/
class
LruMemoryCache
extends
MemoryCache
{
/**
* Constructs an LruMemoryCache object.
*
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param int $allowedSlots
* (optional) The number of slots to allocate for items in the cache.
* Defaults to 300.
*/
public
function
__construct
(
TimeInterface
$time
,
protected
readonly
int
$allowedSlots
=
300
,
)
{
parent
::
__construct
(
$time
);
}
/**
* {@inheritdoc}
*/
public
function
get
(
$cid
,
$allow_invalid
=
FALSE
)
{
if
(
$cached
=
parent
::
get
(
$cid
,
$allow_invalid
))
{
if
(
$cached
->
valid
&&
$cid
!==
array_key_last
(
$this
->
cache
))
{
// Move valid items to the end of the array, so they will be removed
// last.
unset
(
$this
->
cache
[
$cid
]);
$this
->
cache
[
$cid
]
=
$cached
;
}
}
return
$cached
;
}
/**
* {@inheritdoc}
*/
public
function
getMultiple
(
&
$cids
,
$allow_invalid
=
FALSE
)
{
$ret
=
parent
::
getMultiple
(
$cids
,
$allow_invalid
);
$last_key
=
array_key_last
(
$this
->
cache
);
foreach
(
$ret
as
$cid
=>
$cached
)
{
if
(
$cached
->
valid
&&
$cid
!==
$last_key
)
{
// Move valid items to the end of the array, so they will be removed
// last.
unset
(
$this
->
cache
[
$cid
]);
$this
->
cache
[
$cid
]
=
$cached
;
$last_key
=
$cid
;
}
}
return
$ret
;
}
/**
* {@inheritdoc}
*/
public
function
set
(
$cid
,
$data
,
$expire
=
Cache
::
PERMANENT
,
array
$tags
=
[]):
void
{
if
(
isset
(
$this
->
cache
[
$cid
]))
{
// If the item is already in the cache, move it to end of the array.
unset
(
$this
->
cache
[
$cid
]);
}
elseif
(
count
(
$this
->
cache
)
>
$this
->
allowedSlots
-
1
)
{
// Remove one item from the cache to ensure we remain within the allowed
// number of slots. Avoid using array_slice() because it makes a copy of the
// array, and avoid using array_splice() or array_shift() because they
// re-index numeric keys.
unset
(
$this
->
cache
[
array_key_first
(
$this
->
cache
)]);
}
parent
::
set
(
$cid
,
$data
,
$expire
,
$tags
);
}
/**
* {@inheritdoc}
*/
public
function
invalidate
(
$cid
):
void
{
if
(
isset
(
$this
->
cache
[
$cid
]))
{
parent
::
invalidate
(
$cid
);
// Move the item to the least recently used position if it's not already
// there. This cannot use array_unshift() because it would reindex an array
// with numeric cache IDs.
if
(
$cid
!==
array_key_first
(
$this
->
cache
))
{
$this
->
cache
=
[
$cid
=>
$this
->
cache
[
$cid
]]
+
$this
->
cache
;
}
}
}
/**
* {@inheritdoc}
*/
public
function
invalidateMultiple
(
array
$cids
):
void
{
$items
=
[];
foreach
(
$cids
as
$cid
)
{
if
(
isset
(
$this
->
cache
[
$cid
]))
{
$items
[
$cid
]
=
$this
->
cache
[
$cid
];
parent
::
invalidate
(
$cid
);
}
}
// Move the items to the least recently used positions. This cannot use
// array_unshift() because it would reindex an array with numeric cache IDs.
if
(
!
empty
(
$items
))
{
$this
->
cache
=
$items
+
$this
->
cache
;
}
}
/**
* {@inheritdoc}
*/
public
function
invalidateTags
(
array
$tags
):
void
{
$items
=
[];
foreach
(
$this
->
cache
as
$cid
=>
$item
)
{
if
(
array_intersect
(
$tags
,
$item
->
tags
))
{
parent
::
invalidate
(
$cid
);
$items
[
$cid
]
=
$this
->
cache
[
$cid
];
}
}
// Move the items to the least recently used positions. This cannot use
// array_unshift() because it would reindex an array with numeric cache IDs.
if
(
!
empty
(
$items
))
{
$this
->
cache
=
$items
+
$this
->
cache
;
}
}
}
This diff is collapsed.
Click to expand it.
core/tests/Drupal/Tests/Core/Cache/LruMemoryCacheTest.php
0 → 100644
+
311
−
0
View file @
aec131e3
<?php
declare
(
strict_types
=
1
);
namespace
Drupal\Tests\Core\Cache
;
use
Drupal\Component\Datetime\TimeInterface
;
use
Drupal\Core\Cache\MemoryCache\LruMemoryCache
;
use
Drupal\Tests\UnitTestCase
;
/**
* @coversDefaultClass \Drupal\Core\Cache\MemoryCache\LruMemoryCache
* @group Cache
*/
class
LruMemoryCacheTest
extends
UnitTestCase
{
/**
* Tests getting, setting and deleting items from the LRU memory cache.
*
* @covers ::get
* @covers ::set
* @covers ::delete
* @covers ::getMultiple
*/
public
function
testGetSetDelete
():
void
{
$lru_cache
=
$this
->
getLruMemoryCache
(
3
);
$cids
=
[
[
'sparrow'
,
'sparrow'
],
[
'pidgin'
,
'pidgin'
],
[
'crow'
,
'crow'
],
];
foreach
(
$cids
as
$items
)
{
$lru_cache
->
set
(
$items
[
0
],
$items
[
1
]);
}
$this
->
assertCids
(
$lru_cache
,
[
[
'sparrow'
,
'sparrow'
],
[
'pidgin'
,
'pidgin'
],
[
'crow'
,
'crow'
],
]);
$lru_cache
->
set
(
'cuckoo'
,
'cuckoo'
);
$this
->
assertCids
(
$lru_cache
,
[
[
'pidgin'
,
'pidgin'
],
[
'crow'
,
'crow'
],
[
'cuckoo'
,
'cuckoo'
],
]);
// Now bring pidgin to the most recently used spot.
$lru_cache
->
get
(
'pidgin'
);
$lru_cache
->
set
(
'bigger_cuckoo'
,
'bigger_cuckoo'
);
$this
->
assertCids
(
$lru_cache
,
[
[
'cuckoo'
,
'cuckoo'
],
[
'pidgin'
,
'pidgin'
],
[
'bigger_cuckoo'
,
'bigger_cuckoo'
],
]);
// Confirm that setting the same item multiple times only uses one slot.
$lru_cache
->
set
(
'bigger_cuckoo'
,
'bigger_cuckoo'
);
$lru_cache
->
set
(
'bigger_cuckoo'
,
'bigger_cuckoo'
);
$lru_cache
->
set
(
'bigger_cuckoo'
,
'bigger_cuckoo'
);
$lru_cache
->
set
(
'bigger_cuckoo'
,
'bigger_cuckoo'
);
$lru_cache
->
set
(
'bigger_cuckoo'
,
'bigger_cuckoo'
);
$this
->
assertCids
(
$lru_cache
,
[
[
'cuckoo'
,
'cuckoo'
],
[
'pidgin'
,
'pidgin'
],
[
'bigger_cuckoo'
,
'bigger_cuckoo'
],
]);
// Confirm that deleting the same item multiple times only frees up one
// slot.
$lru_cache
->
delete
(
'bigger_cuckoo'
);
$lru_cache
->
delete
(
'bigger_cuckoo'
);
$lru_cache
->
delete
(
'bigger_cuckoo'
);
$lru_cache
->
delete
(
'bigger_cuckoo'
);
$lru_cache
->
delete
(
'bigger_cuckoo'
);
$lru_cache
->
delete
(
'bigger_cuckoo'
);
$lru_cache
->
set
(
'bigger_cuckoo'
,
'bigger_cuckoo'
);
$this
->
assertCids
(
$lru_cache
,
[
[
'cuckoo'
,
'cuckoo'
],
[
'pidgin'
,
'pidgin'
],
[
'bigger_cuckoo'
,
'bigger_cuckoo'
],
]);
$lru_cache
->
set
(
'crow'
,
'crow'
);
$this
->
assertCids
(
$lru_cache
,
[
[
'pidgin'
,
'pidgin'
],
[
'bigger_cuckoo'
,
'bigger_cuckoo'
],
[
'crow'
,
'crow'
],
]);
$cids
=
[
'crow'
,
'pidgin'
];
$lru_cache
->
getMultiple
(
$cids
);
// @todo This result suggests the order of the arguments in the
// \Drupal\Core\Cache\MemoryBackend::getMultiple() array_intersect_key()
// should be swapped as this order of the cache items returned should
// probably be in the same order as the passed in $cids. I.e. pidgin
// should be at the ends of the array and not crow.
$this
->
assertCids
(
$lru_cache
,
[
[
'bigger_cuckoo'
,
'bigger_cuckoo'
],
[
'pidgin'
,
'pidgin'
],
[
'crow'
,
'crow'
],
]);
}
/**
* Tests setting items with numeric keys in the LRU memory cache.
*
* @covers ::set
*/
public
function
testSetNumericKeys
():
void
{
$lru_cache
=
$this
->
getLruMemoryCache
(
3
);
$cids
=
[
[
4
,
'sparrow'
],
[
10
,
'pidgin'
],
[
7
,
'crow'
],
];
foreach
(
$cids
as
$item
)
{
$lru_cache
->
set
(
$item
[
0
],
$item
[
1
]);
}
$this
->
assertCids
(
$lru_cache
,
$cids
);
$lru_cache
->
set
(
1
,
'cuckoo'
);
$this
->
assertCids
(
$lru_cache
,
[
[
10
,
'pidgin'
],
[
7
,
'crow'
],
[
1
,
'cuckoo'
],
]);
$lru_cache
->
set
(
7
,
'crow'
);
$this
->
assertCids
(
$lru_cache
,
[
[
10
,
'pidgin'
],
[
1
,
'cuckoo'
],
[
7
,
'crow'
],
]);
}
/**
* Tests setting multiple items in the LRU memory cache.
*
* @covers ::setMultiple
*/
public
function
testSetMultiple
():
void
{
$lru_cache
=
$this
->
getLruMemoryCache
(
3
);
$lru_cache
->
setMultiple
([
'sparrow'
=>
[
'data'
=>
'sparrow'
],
'pidgin'
=>
[
'data'
=>
'pidgin'
],
'crow'
=>
[
'data'
=>
'crow'
],
]);
$this
->
assertCids
(
$lru_cache
,
[
[
'sparrow'
,
'sparrow'
],
[
'pidgin'
,
'pidgin'
],
[
'crow'
,
'crow'
],
]);
$lru_cache
->
setMultiple
([
'sparrow'
=>
[
'data'
=>
'sparrow2'
],
'bluejay'
=>
[
'data'
=>
'bluejay'
],
]);
$this
->
assertCids
(
$lru_cache
,
[
[
'crow'
,
'crow'
],
[
'sparrow'
,
'sparrow2'
],
[
'bluejay'
,
'bluejay'
],
]);
$lru_cache
->
setMultiple
([
3
=>
[
'data'
=>
'pidgin'
],
2
=>
[
'data'
=>
'eagle'
],
1
=>
[
'data'
=>
'wren'
],
]);
$this
->
assertCids
(
$lru_cache
,
[
[
3
,
'pidgin'
],
[
2
,
'eagle'
],
[
1
,
'wren'
],
]);
$lru_cache
->
setMultiple
([
2
=>
[
'data'
=>
'eagle2'
],
4
=>
[
'data'
=>
'cuckoo'
],
]);
$this
->
assertCids
(
$lru_cache
,
[
[
1
,
'wren'
],
[
2
,
'eagle2'
],
[
4
,
'cuckoo'
],
]);
}
/**
* Tests invalidation from the LRU memory cache.
*
* @covers ::invalidate
* @covers ::invalidateMultiple
*/
public
function
testInvalidate
():
void
{
$lru_cache
=
$this
->
getLruMemoryCache
(
3
);
$cids
=
[
[
'sparrow'
,
'sparrow'
],
[
'pidgin'
,
'pidgin'
],
[
'crow'
,
'crow'
],
];
foreach
(
$cids
as
$items
)
{
$lru_cache
->
set
(
$items
[
0
],
$items
[
1
]);
}
$this
->
assertCids
(
$lru_cache
,
[
[
'sparrow'
,
'sparrow'
],
[
'pidgin'
,
'pidgin'
],
[
'crow'
,
'crow'
],
]);
$lru_cache
->
invalidate
(
'crow'
);
$this
->
assertCids
(
$lru_cache
,
[
[
'crow'
,
'crow'
],
[
'sparrow'
,
'sparrow'
],
[
'pidgin'
,
'pidgin'
],
]);
$this
->
assertFalse
(
$lru_cache
->
get
(
'crow'
));
// Ensure that getting an invalid cache does not move it to the end of the
// array.
$this
->
assertSame
(
'crow'
,
$lru_cache
->
get
(
'crow'
,
TRUE
)
->
data
);
$this
->
assertCids
(
$lru_cache
,
[
[
'crow'
,
'crow'
],
[
'sparrow'
,
'sparrow'
],
[
'pidgin'
,
'pidgin'
],
]);
$lru_cache
->
set
(
'cuckoo'
,
'cuckoo'
,
LruMemoryCache
::
CACHE_PERMANENT
,
[
'cuckoo'
]);
$this
->
assertCids
(
$lru_cache
,
[
[
'sparrow'
,
'sparrow'
],
[
'pidgin'
,
'pidgin'
],
[
'cuckoo'
,
'cuckoo'
],
]);
$lru_cache
->
invalidateTags
([
'cuckoo'
]);
$this
->
assertFalse
(
$lru_cache
->
get
(
'cuckoo'
));
$this
->
assertSame
(
'cuckoo'
,
$lru_cache
->
get
(
'cuckoo'
,
TRUE
)
->
data
);
$lru_cache
->
set
(
'crow'
,
'crow'
);
$this
->
assertCids
(
$lru_cache
,
[
[
'sparrow'
,
'sparrow'
],
[
'pidgin'
,
'pidgin'
],
[
'crow'
,
'crow'
],
]);
$lru_cache
->
invalidateMultiple
([
'pidgin'
,
'crow'
]);
$cids
=
[
'pidgin'
,
'crow'
];
$this
->
assertEmpty
(
$lru_cache
->
getMultiple
(
$cids
));
$this
->
assertSame
([
'pidgin'
,
'crow'
],
$cids
);
$this
->
assertCount
(
2
,
$lru_cache
->
getMultiple
(
$cids
,
TRUE
));
$this
->
assertSame
([],
$cids
);
$this
->
assertCids
(
$lru_cache
,
[
[
'pidgin'
,
'pidgin'
],
[
'crow'
,
'crow'
],
[
'sparrow'
,
'sparrow'
],
]);
$lru_cache
->
set
(
'duck'
,
'duck'
);
$lru_cache
->
set
(
'chicken'
,
'chicken'
);
$this
->
assertCids
(
$lru_cache
,
[
[
'sparrow'
,
'sparrow'
],
[
'duck'
,
'duck'
],
[
'chicken'
,
'chicken'
],
]);
}
/**
* Assert that the given cache ID's match the given value in the memory cache.
*
* @param \Drupal\Core\Cache\MemoryCache\LruMemoryCache $lru_cache
* The LRU cache under test.
* @param array $cids
* Array whose first element is the cache ID and whose second element is
* the value to check. This should contain all the keys in the cache and in
* the expected order.
*/
protected
function
assertCids
(
LruMemoryCache
$lru_cache
,
array
$cids
):
void
{
// Use reflection to access data because using ::get() affects the LRU
// cache.
$reflectedClass
=
new
\ReflectionClass
(
$lru_cache
);
$reflection
=
$reflectedClass
->
getProperty
(
'cache'
);
$cache
=
$reflection
->
getValue
(
$lru_cache
);
$keys
=
[];
foreach
(
$cids
as
$item
)
{
$keys
[]
=
$item
[
0
];
$this
->
assertSame
(
$item
[
1
],
$cache
[
$item
[
0
]]
->
data
,
"
$item[0]
found in cache."
);
}
// Ensure the cache only contains the supply keys and the order is as
// expected.
$this
->
assertSame
(
$keys
,
array_keys
(
$cache
));
}
/**
* Creates a LRU cache for testing.
*
* @param int $slots
* The number of slots in the LRU cache.
*
* @return \Drupal\Core\Cache\MemoryCache\LruMemoryCache
* The LRU cache.
*/
private
function
getLruMemoryCache
(
int
$slots
):
LruMemoryCache
{
$time_mock
=
$this
->
createMock
(
TimeInterface
::
class
);
$time_mock
->
expects
(
$this
->
any
())
->
method
(
'getRequestTime'
)
->
willReturnCallback
(
'time'
);
return
new
LruMemoryCache
(
$time_mock
,
$slots
,
);
}
}
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment