Skip to content
Snippets Groups Projects
Commit aec131e3 authored by Geoff Appleby's avatar Geoff Appleby
Browse files

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
Pipeline: drupal

#388901

    <?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;
    }
    }
    }
    <?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,
    );
    }
    }
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Please register or to comment