Commit 969256bd authored by catch's avatar catch
Browse files

Issue #636454 by catch, carlos8f, msonnabaum, beejeebus, Berdir, moshe...

Issue #636454 by catch, carlos8f, msonnabaum, beejeebus, Berdir, moshe weitzman, pillarsdotnet: Added Cache tag support.
parent 17a7b1a6
......@@ -32,15 +32,42 @@ function cache($bin = 'cache') {
// storage of a cache bin mid-request.
static $cache_objects;
if (!isset($cache_objects[$bin])) {
$class = variable_get('cache_class_' . $bin);
if (!isset($class)) {
$class = variable_get('cache_default_class', 'Drupal\Core\Cache\DatabaseBackend');
}
$cache_backends = cache_get_backends();
$class = isset($cache_backends[$bin]) ? $cache_backends[$bin] : $cache_backends['cache'];
$cache_objects[$bin] = new $class($bin);
}
return $cache_objects[$bin];
}
/**
* Invalidates the items associated with given list of tags.
*
* Many sites have more than one active cache backend, and each backend my use
* a different strategy for storing tags against cache items, and invalidating
* cache items associated with a given tag.
*
* When invalidating a given list of tags, we iterate over each cache backend,
* and call invalidate on each.
*
* @param array $tags
* The list of tags to invalidate cache items for.
*/
function cache_invalidate(array $tags) {
foreach (cache_get_backends() as $bin => $class) {
cache($bin)->invalidateTags($tags);
}
}
/**
* Returns a list of cache backends for this site.
*
* @return
* An associative array with cache bins as keys, and backend classes as value.
*/
function cache_get_backends() {
return variable_get('cache_classes', array('cache' => 'Drupal\Core\Cache\DatabaseBackend'));
}
/**
* Expires data from the block and page caches.
*/
......
......@@ -297,7 +297,7 @@ function install_begin_request(&$install_state) {
// because any data put in the cache during the installer is inherently
// suspect, due to the fact that Drupal is not fully set up yet.
require_once DRUPAL_ROOT . '/core/includes/cache.inc';
$conf['cache_default_class'] = 'Drupal\Core\Cache\InstallBackend';
$conf['cache_classes'] = array('cache' => 'Drupal\Core\Cache\InstallBackend');
// The install process cannot use the database lock backend since the database
// is not fully up, so we use a null backend implementation durting the
......
......@@ -99,8 +99,14 @@ function getMultiple(&$cids);
* general cache wipe.
* - A Unix timestamp: Indicates that the item should be kept at least until
* the given time, after which it behaves like CACHE_TEMPORARY.
* @param array $tags
* An array of tags to be stored with the cache item. These should normally
* identify objects used to build the cache item, which should trigger
* cache invalidation when updated. For example if a cached item represents
* a node, both the node ID and the author's user ID might be passed in as
* tags. For example array('node' => array(123), 'user' => array(92)).*
*/
function set($cid, $data, $expire = CACHE_PERMANENT);
function set($cid, $data, $expire = CACHE_PERMANENT, array $tags = array());
/**
* Deletes an item from the cache.
......@@ -136,6 +142,17 @@ function flush();
*/
function expire();
/**
* Invalidates each tag in the $tags array.
*
* @param array $tags
* Associative array of tags, in the same format that is passed to
* CacheBackendInterface::set().
*
* @see CacheBackendInterface::set()
*/
function invalidateTags(array $tags);
/**
* Performs garbage collection on a cache bin.
*/
......
......@@ -22,6 +22,11 @@ class DatabaseBackend implements CacheBackendInterface {
*/
protected $bin;
/**
* A static cache of all tags checked during the request.
*/
protected static $tagCache = array();
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::__construct().
*/
......@@ -58,7 +63,7 @@ function getMultiple(&$cids) {
// is used here only due to the performance overhead we would incur
// otherwise. When serving an uncached page, the overhead of using
// db_select() is a much smaller proportion of the request.
$result = db_query('SELECT cid, data, created, expire, serialized FROM {' . db_escape_table($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids));
$result = db_query('SELECT cid, data, created, expire, serialized, tags, checksum FROM {' . db_escape_table($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids));
$cache = array();
foreach ($result as $item) {
$item = $this->prepareItem($item);
......@@ -104,6 +109,14 @@ protected function prepareItem($cache) {
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;
}
}
// If the data is permanent or not subject to a minimum cache lifetime,
// unserialize and return the cached data.
if ($cache->serialized) {
......@@ -116,11 +129,13 @@ protected function prepareItem($cache) {
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::set().
*/
function set($cid, $data, $expire = CACHE_PERMANENT) {
function set($cid, $data, $expire = CACHE_PERMANENT, array $tags = array()) {
$fields = array(
'serialized' => 0,
'created' => REQUEST_TIME,
'expire' => $expire,
'tags' => implode(' ', $this->flattenTags($tags)),
'checksum' => $this->checksumTags($tags),
);
if (!is_string($data)) {
$fields['data'] = serialize($data);
......@@ -154,7 +169,7 @@ function delete($cid) {
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteMultiple().
*/
function deleteMultiple(Array $cids) {
function deleteMultiple(array $cids) {
// Delete in chunks when a large array is passed.
do {
db_delete($this->bin)
......@@ -253,6 +268,94 @@ function garbageCollection() {
}
}
/**
* 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::invalidateTags().
*/
public function invalidateTags(array $tags) {
foreach ($this->flattenTags($tags) as $tag) {
unset(self::$tagCache[$tag]);
db_merge('cache_tags')
->key(array('tag' => $tag))
->fields(array('invalidations' => 1))
->expression('invalidations', 'invalidations + 1')
->execute();
}
}
/**
* 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;
$query_tags = array();
foreach ($this->flattenTags($tags) as $tag) {
if (isset(self::$tagCache[$tag])) {
$checksum += self::$tagCache[$tag];
}
else {
$query_tags[] = $tag;
}
}
if ($query_tags) {
if ($db_tags = db_query('SELECT tag, invalidations FROM {cache_tags} WHERE tag IN (:tags)', array(':tags' => $query_tags))->fetchAllKeyed()) {
self::$tagCache = array_merge(self::$tagCache, $db_tags);
$checksum += array_sum($db_tags);
}
}
return $checksum;
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::isEmpty().
*/
......
......@@ -50,7 +50,7 @@ function getMultiple(&$cids) {
/**
* Overrides Drupal\Core\Cache\CacheBackendInterface::set().
*/
function set($cid, $data, $expire = CACHE_PERMANENT) {}
function set($cid, $data, $expire = CACHE_PERMANENT, array $tags = array()) {}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::delete().
......@@ -88,6 +88,15 @@ function deletePrefix($prefix) {
catch (Exception $e) {}
}
function invalidateTags(array $tags) {
try {
if (class_exists('Database')) {
parent::invalidateTags($tags);
}
}
catch (Exception $e) {}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::flush().
*/
......
......@@ -370,6 +370,51 @@ class CacheClearCase extends CacheTestCase {
$cached = cache('page')->get($data);
$this->assertFalse($cached, 'Cached item was invalidated');
}
/**
* Test clearing using cache tags.
*/
function testClearTags() {
$cache = cache($this->default_bin);
$cache->set('test_cid_clear1', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(1)));
$cache->set('test_cid_clear2', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(1)));
$this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
&& $this->checkCacheExists('test_cid_clear2', $this->default_value),
t('Two cache items were created.'));
cache_invalidate(array('test_tag' => array(1)));
$this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value)
|| $this->checkCacheExists('test_cid_clear2', $this->default_value),
t('Two caches removed after clearing a cache tag.'));
$cache->set('test_cid_clear1', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(1)));
$cache->set('test_cid_clear2', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(2)));
$cache->set('test_cid_clear3', $this->default_value, CACHE_PERMANENT, array('test_tag_foo' => array(3)));
$this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
&& $this->checkCacheExists('test_cid_clear2', $this->default_value)
&& $this->checkCacheExists('test_cid_clear3', $this->default_value),
t('Two cached items were created.'));
cache_invalidate(array('test_tag_foo' => array(3)));
$this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value)
&& $this->checkCacheExists('test_cid_clear2', $this->default_value),
t('Cached items not matching the tag were not cleared.'));
$this->assertFalse($this->checkCacheExists('test_cid_clear3', $this->default_value),
t('Cached item matching the tag was removed.'));
// For our next trick, we will attempt to clear data in multiple bins.
$tags = array('test_tag' => array(1, 2, 3));
$bins = array('cache', 'cache_page', 'cache_bootstrap');
foreach ($bins as $bin) {
cache($bin)->set('test', $this->default_value, CACHE_PERMANENT, $tags);
$this->assertTrue($this->checkCacheExists('test', $this->default_value, $bin), 'Cache item was set in bin.');
}
cache_invalidate(array('test_tag' => array(2)));
foreach ($bins as $bin) {
$this->assertFalse($this->checkCacheExists('test', $this->default_value, $bin), 'Tag expire affected item in bin.');
}
$this->assertFalse($this->checkCacheExists('test_cid_clear2', $this->default_value), 'Cached items matching tag were cleared.');
$this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value), 'Cached items not matching tag were not cleared.');
}
}
/**
......
......@@ -645,6 +645,26 @@ function system_schema() {
'primary key' => array('iid'),
);
$schema['cache_tags'] = array(
'description' => 'Cache table for tracking cache tags related to the cache bin.',
'fields' => array(
'tag' => array(
'description' => 'Namespace-prefixed tag string.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'invalidations' => array(
'description' => 'Number incremented when the tag is invalidated.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('tag'),
);
$schema['cache'] = array(
'description' => 'Generic cache table for caching things not separated out into their own tables. Contributed modules may also use this to store cached items.',
'fields' => array(
......@@ -680,6 +700,18 @@ function system_schema() {
'not null' => TRUE,
'default' => 0,
),
'tags' => array(
'description' => 'Space-separated list of cache tags for this entry.',
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
),
'checksum' => array(
'description' => 'The tag invalidation sum when this entry was saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'indexes' => array(
'expire' => array('expire'),
......@@ -1761,6 +1793,59 @@ function system_update_8005() {
db_drop_field('session', 'cache');
}
/**
* Adds the {cache_tags} table.
*/
function system_update_8006() {
$table = array(
'description' => 'Cache table for tracking cache tags related to the cache bin.',
'fields' => array(
'tag' => array(
'description' => 'Namespace-prefixed tag string.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'invalidations' => array(
'description' => 'Number incremented when the tag is invalidated.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('tag'),
);
db_create_table('cache_tags', $table);
}
/**
* Modifies existing cache tables, adding support for cache tags.
*/
function system_update_8007() {
// Find all potential cache tables.
$tables = db_find_tables(Database::getConnection()->prefixTables('{cache}') . '%');
foreach ($tables as $table) {
// Assume we have a valid cache table if there is both 'cid' and 'data'
// columns.
if (db_field_exists($table, 'cid') && db_field_exists($table, 'data')) {
db_add_field($table, 'tags', array(
'description' => 'Space-separated list of cache tags for this entry.',
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
));
db_add_field($table, 'checksum', array(
'description' => 'The tag invalidation sum when this entry was saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
));
}
}
}
/**
* @} End of "defgroup updates-7.x-to-8.x"
* The next series of updates should start at 9000.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment