diff --git a/core/lib/Drupal/Core/Cache/LRU.php b/core/lib/Drupal/Core/Cache/LRU.php new file mode 100644 index 0000000..c3175b0 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/LRU.php @@ -0,0 +1,63 @@ +slots = $slots; + } + + function offsetGet($offset) { + if (parent::offsetExists($offset)) { + // If the item is not at the end of the positions array, move it + // there. + if (end($this->positions) != $offset) { + $current_position = array_search($offset, $this->positions); + unset($this->positions[$current_position]); + $this->positions[] = $offset; + } + } + return parent::offsetGet($offset); + } + + function offsetSet($offset, $value) { + parent::offsetSet($offset, $value); + if ($this->slots > 0) { + $this->slots--; + } + else { + // Replace the item in first position in the array. + $key = array_shift($this->positions); + $this->offsetUnset($key); + } + $this->positions[] = $offset; + } +} diff --git a/core/lib/Drupal/Core/Cache/MemoryLRUBackend.php b/core/lib/Drupal/Core/Cache/MemoryLRUBackend.php new file mode 100644 index 0000000..0d720ba --- /dev/null +++ b/core/lib/Drupal/Core/Cache/MemoryLRUBackend.php @@ -0,0 +1,234 @@ +cache = new LRU($slots); + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::get(). + */ + function get($cid) { + $cids = array($cid); + $cache = $this->getMultiple($cids); + return reset($cache); + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::getMultiple(). + */ + function getMultiple(&$cids) { + $return = array(); + $items = array_intersect_key($this->cache->getArrayCopy(), array_flip($cids)); + foreach ($items as $cid => $item) { + // We have $this->cache[$cid] so that LRU positions are updateed. + $item = $this->prepareItem($this->cache[$cid]); + if ($item) { + $return[$item->cid] = $item; + } + } + return $return; + } + + /** + * Prepares a cached item. + * + * Checks that items are either permanent or did not expire, and unserializes + * data as appropriate. + * + * @param stdClass $cache + * An item loaded from cache_get() or cache_get_multiple(). + * + * @return mixed + * The item with data unserialized as appropriate or FALSE if there is no + * valid item to load. + */ + protected function prepareItem($cache) { + if (!isset($cache->data)) { + return FALSE; + } + + // The cache data is invalid if any of its tags have been cleared since. + if ($cache->tags) { + $cache->tags = explode(' ', $cache->tags); + if (!$this->validTags($cache->checksum, $cache->tags)) { + return FALSE; + } + } + + return $cache; + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::set(). + */ + function set($cid, $data, $expire = CACHE_PERMANENT, array $tags = array()) { + $obj = new \stdClass(); + $obj->cid = $cid; + $obj->data = $data; + $obj->exprire = $expire; + $obj->tags = $tags; + $this->cache[$cid] = $obj; + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::delete(). + */ + function delete($cid) { + if (isset($this->cache[$cid])) { + unset($this->cache[$cid]); + } + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::deleteMultiple(). + */ + function deleteMultiple(array $cids) { + $this->cache = array_diff($this->cache, array_flip($cids)); + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::deletePrefix(). + */ + function deletePrefix($prefix) { + foreach ($this->cache as $cid => $item) { + if (strpos($cid, $prefix) === 0) { + unset($this->cache[$cid]); + } + } + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::flush(). + */ + function flush() { + $this->data = new LRU($this->slots); + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::expire(). + */ + function expire() { + // @todo: minimum cache lifetime is on its way out. + } + + /** + * Compares two checksums of tags. Used to determine whether to serve a cached + * item or treat it as invalidated. + * + * @param integer @checksum + * The initial checksum to compare against. + * @param array @tags + * An array of tags to calculate a checksum for. + * + * @return boolean + * TRUE if the checksums match, FALSE otherwise. + */ + protected function validTags($checksum, array $tags) { + return $checksum == $this->checksumTags($tags); + } + + /** + * Flattens a tags array into a numeric array suitable for string storage. + * + * @param array $tags + * Associative array of tags to flatten. + * + * @return + * Numeric array of flattened tag identifiers. + */ + protected function flattenTags(array $tags) { + if (isset($tags[0])) { + return $tags; + } + + $flat_tags = array(); + foreach ($tags as $namespace => $values) { + if (is_array($values)) { + foreach ($values as $value) { + $flat_tags[] = "$namespace:$value"; + } + } + else { + $flat_tags[] = "$namespace:$values"; + } + } + return $flat_tags; + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::garbageCollection(). + */ + function garbageCollection() {} + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::invalidateTags(). + */ + public function invalidateTags(array $tags) { + foreach ($this->flattenTags($tags) as $tag) { + foreach ($this->tags as $key => $value) { + if ($key == $tag) { + $this->tags[$key]++; + } + } + } + } + + /** + * Returns the sum total of validations for a given set of tags. + * + * @param array $tags + * Associative array of tags. + * + * @return integer + * Sum of all invalidations. + */ + protected function checksumTags($tags) { + $checksum = 0; + + foreach ($this->flattenTags($tags) as $tag) { + if (isset($this->tags[$tag])) { + $checksum += $this->tags[$tag]; + } + } + return $checksum; + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::isEmpty(). + */ + function isEmpty() { + return empty($this->cache); + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Cache/MemoryLRUBackendTest.php b/core/modules/system/lib/Drupal/system/Tests/Cache/MemoryLRUBackendTest.php new file mode 100644 index 0000000..4b3863d --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Cache/MemoryLRUBackendTest.php @@ -0,0 +1,54 @@ + 'Cache MemoryLRUBackend test', + 'description' => 'Tests the cache MemoryLRUBackend.', + 'group' => 'Cache', + ); + } + + /** + * Tests that the NullBackend does not actually store variables. + */ + function testMemoryLRU() { + $LRU_cache = new MemoryLRUBackend('test', 5); + + $LRU_cache->set('banana', 'banana'); + // Confirm the item is in the cache. + $this->assertEqual($LRU_cache->get('banana')->data, 'banana'); + // Now add five more items to the cache. + foreach (array('apple', 'mango', 'grapefruit', 'kiwi', 'orange') as $fruit) { + $LRU_cache->set($fruit, $fruit); + $this->assertEqual($LRU_cache->get($fruit)->data, $fruit); + } + // When 'orange' was added, it should have remove 'banana' from the cache. + $this->assertEqual($LRU_cache->get('banana'), FALSE); + // Apple should be in the first position of the array, request it from the + // cache. This should move it to the front of the array, and put the mango + // into the LRU position. + $this->assertEqual($LRU_cache->get('apple')->data, 'apple'); + // Add another fruit + $LRU_cache->set('peach', 'peach'); + // Confirm that mango was removed. + $this->assertEqual($LRU_cache->get('mango'), FALSE); + // Confirm that the other fruits are cached. + foreach (array('apple', 'grapefruit', 'kiwi', 'orange') as $fruit) { + $this->assertEqual($LRU_cache->get($fruit)->data, $fruit); + } + } +}