Commit 87462db6 authored by catch's avatar catch
Browse files

Issue #1167144 by chx, catch, claudiu.cristea: Make cache backends responsible...

Issue #1167144 by chx, catch, claudiu.cristea: Make cache backends responsible for their own storage.
parent 2508db12
......@@ -5,9 +5,11 @@
* API for loading and interacting with Drupal modules.
*/
use Drupal\Component\Graph\Graph;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Yaml\Parser;
/**
* Builds a list of bootstrap modules and enabled modules and themes.
......@@ -566,7 +568,6 @@ function module_uninstall($module_list = array(), $uninstall_dependents = TRUE)
$module_list = array_keys($module_list);
}
$storage = drupal_container()->get('config.storage');
$schema_store = Drupal::keyValue('system.schema');
$disabled_config = config('system.module.disabled');
foreach ($module_list as $module) {
......@@ -578,6 +579,38 @@ function module_uninstall($module_list = array(), $uninstall_dependents = TRUE)
// Remove all configuration belonging to the module.
config_uninstall_default_config('module', $module);
// Remove any cache bins defined by the module.
$service_yaml_file = drupal_get_path('module', $module) . "/$module.services.yml";
if (file_exists($service_yaml_file)) {
$parser = new Parser;
$definitions = $parser->parse(file_get_contents($service_yaml_file));
if (isset($definitions['services'])) {
foreach ($definitions['services'] as $id => $definition) {
if (isset($definition['tags'])) {
foreach ($definition['tags'] as $tag) {
// This works for the default cache registration and even in some
// cases when a non-default "super" factory is used. That should
// be extremely rare.
if ($tag['name'] == 'cache.bin' && isset($definition['factory_service']) && isset($definition['factory_method']) && !empty($definition['arguments'])) {
try {
$factory = Drupal::service($definition['factory_service']);
if (method_exists($factory, $definition['factory_method'])) {
$backend = call_user_func_array(array($factory, $definition['factory_method']), $definition['arguments']);
if ($backend instanceof CacheBackendInterface) {
$backend->removeBin();
}
}
}
catch (Exception $e) {
watchdog_exception('system', $e, 'Failed to remove cache bin defined by the service %id.', array('%id' => $id));
}
}
}
}
}
}
}
watchdog('system', '%module module uninstalled.', array('%module' => $module), WATCHDOG_INFO);
$schema_store->delete($module);
$disabled_config->clear($module);
......
......@@ -222,4 +222,14 @@ public function isEmpty() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function removeBin() {
foreach ($this->backends as $backend) {
$this->removeBin();
}
}
}
......@@ -286,6 +286,11 @@ public function invalidateAll();
*/
public function garbageCollection();
/**
* Remove a cache bin.
*/
public function removeBin();
/**
* Checks if a cache bin is empty.
*
......
......@@ -9,6 +9,7 @@
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\SchemaObjectExistsException;
/**
* Defines a default cache implementation.
......@@ -69,7 +70,13 @@ public function getMultiple(&$cids, $allow_invalid = FALSE) {
// is used here only due to the performance overhead we would incur
// otherwise. When serving an uncached page, the overhead of using
// ::select() is a much smaller proportion of the request.
$result = $this->connection->query('SELECT cid, data, created, expire, serialized, tags, checksum_invalidations, checksum_deletions FROM {' . $this->connection->escapeTable($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids));
$result = array();
try {
$result = $this->connection->query('SELECT cid, data, created, expire, serialized, tags, checksum_invalidations, checksum_deletions FROM {' . $this->connection->escapeTable($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids));
}
catch (\Exception $e) {
// Nothing to do.
}
$cache = array();
foreach ($result as $item) {
$item = $this->prepareItem($item, $allow_invalid);
......@@ -134,6 +141,29 @@ protected function prepareItem($cache, $allow_invalid) {
* Implements Drupal\Core\Cache\CacheBackendInterface::set().
*/
public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = array()) {
$try_again = FALSE;
try {
// The bin might not yet exist.
$this->doSet($cid, $data, $expire, $tags);
}
catch (\Exception $e) {
// If there was an exception, try to create the bins.
if (!$try_again = $this->ensureBinExists()) {
// If the exception happened for other reason than the missing bin
// table, propagate the exception.
throw $e;
}
}
// Now that the bin has been created, try again if necessary.
if ($try_again) {
$this->doSet($cid, $data, $expire, $tags);
}
}
/**
* Actually set the cache.
*/
protected function doSet($cid, $data, $expire, $tags) {
$flat_tags = $this->flattenTags($tags);
$checksum = $this->checksumTags($flat_tags);
$fields = array(
......@@ -163,22 +193,25 @@ public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANEN
* Implements Drupal\Core\Cache\CacheBackendInterface::delete().
*/
public function delete($cid) {
$this->connection->delete($this->bin)
->condition('cid', $cid)
->execute();
$this->deleteMultiple(array($cid));
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteMultiple().
*/
public function deleteMultiple(array $cids) {
// Delete in chunks when a large array is passed.
do {
$this->connection->delete($this->bin)
->condition('cid', array_splice($cids, 0, 1000), 'IN')
->execute();
try {
// Delete in chunks when a large array is passed.
do {
$this->connection->delete($this->bin)
->condition('cid', array_splice($cids, 0, 1000), 'IN')
->execute();
}
while (count($cids));
}
catch (\Exception $e) {
$this->catchException($e);
}
while (count($cids));
}
/**
......@@ -188,11 +221,16 @@ public function deleteTags(array $tags) {
$tag_cache = &drupal_static('Drupal\Core\Cache\CacheBackendInterface::tagCache');
foreach ($this->flattenTags($tags) as $tag) {
unset($tag_cache[$tag]);
$this->connection->merge('cache_tags')
->insertFields(array('deletions' => 1))
->expression('deletions', 'deletions + 1')
->key(array('tag' => $tag))
->execute();
try {
$this->connection->merge('cache_tags')
->insertFields(array('deletions' => 1))
->expression('deletions', 'deletions + 1')
->key(array('tag' => $tag))
->execute();
}
catch (\Exception $e) {
$this->catchException($e, 'cache_tags');
}
}
}
......@@ -200,7 +238,12 @@ public function deleteTags(array $tags) {
* Implements Drupal\Core\Cache\CacheBackendInterface::deleteAll().
*/
public function deleteAll() {
$this->connection->truncate($this->bin)->execute();
try {
$this->connection->truncate($this->bin)->execute();
}
catch (\Exception $e) {
$this->catchException($e);
}
}
/**
......@@ -214,28 +257,38 @@ public function invalidate($cid) {
* Implements Drupal\Core\Cache\CacheBackendInterface::invalideMultiple().
*/
public function invalidateMultiple(array $cids) {
// Update in chunks when a large array is passed.
do {
$this->connection->update($this->bin)
->fields(array('expire' => REQUEST_TIME - 1))
->condition('cid', array_splice($cids, 0, 1000), 'IN')
->execute();
try {
// Update in chunks when a large array is passed.
do {
$this->connection->update($this->bin)
->fields(array('expire' => REQUEST_TIME - 1))
->condition('cid', array_splice($cids, 0, 1000), 'IN')
->execute();
}
while (count($cids));
}
catch (\Exception $e) {
$this->catchException($e);
}
while (count($cids));
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateTags().
*/
public function invalidateTags(array $tags) {
$tag_cache = &drupal_static('Drupal\Core\Cache\CacheBackendInterface::tagCache');
foreach ($this->flattenTags($tags) as $tag) {
unset($tag_cache[$tag]);
$this->connection->merge('cache_tags')
->insertFields(array('invalidations' => 1))
->expression('invalidations', 'invalidations + 1')
->key(array('tag' => $tag))
->execute();
try {
$tag_cache = &drupal_static('Drupal\Core\Cache\CacheBackendInterface::tagCache');
foreach ($this->flattenTags($tags) as $tag) {
unset($tag_cache[$tag]);
$this->connection->merge('cache_tags')
->insertFields(array('invalidations' => 1))
->expression('invalidations', 'invalidations + 1')
->key(array('tag' => $tag))
->execute();
}
}
catch (\Exception $e) {
$this->catchException($e, 'cache_tags');
}
}
......@@ -243,19 +296,31 @@ public function invalidateTags(array $tags) {
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateAll().
*/
public function invalidateAll() {
$this->connection->update($this->bin)
->fields(array('expire' => REQUEST_TIME - 1))
->execute();
try {
$this->connection->update($this->bin)
->fields(array('expire' => REQUEST_TIME - 1))
->execute();
}
catch (\Exception $e) {
$this->catchException($e);
}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::garbageCollection().
*/
public function garbageCollection() {
Database::getConnection()->delete($this->bin)
->condition('expire', CacheBackendInterface::CACHE_PERMANENT, '<>')
->condition('expire', REQUEST_TIME, '<')
->execute();
try {
Database::getConnection()->delete($this->bin)
->condition('expire', CacheBackendInterface::CACHE_PERMANENT, '<>')
->condition('expire', REQUEST_TIME, '<')
->execute();
}
catch (\Exception $e) {
// If the table does not exist, it surely does not have garbage in it.
// If the table exists, the next garbage collection will clean up.
// There is nothing to do.
}
}
/**
......@@ -329,9 +394,160 @@ public function isEmpty() {
$this->garbageCollection();
$query = $this->connection->select($this->bin);
$query->addExpression('1');
$result = $query->range(0, 1)
->execute()
->fetchField();
try {
$result = $query->range(0, 1)
->execute()
->fetchField();
}
catch (\Exception $e) {
$this->catchException($e);
}
return empty($result);
}
/**
* {@inheritdoc}
*/
public function removeBin() {
try {
$this->connection->schema()->dropTable($this->bin);
}
catch (\Exception $e) {
$this->catchException($e);
}
}
/**
* Check if the cache bin exists and create it if not.
*/
protected function ensureBinExists() {
try {
$database_schema = $this->connection->schema();
if (!$database_schema->tableExists($this->bin)) {
$schema_definition = $this->schemaDefinition();
$database_schema->createTable($this->bin, $schema_definition['bin']);
// If the bin doesn't exist, the cache tags table may also not exist.
if (!$database_schema->tableExists('cache_tags')) {
$database_schema->createTable('cache_tags', $schema_definition['cache_tags']);
}
return TRUE;
}
}
// If another process has already created the cache table, attempting to
// recreate it will throw an exception. In this case just catch the
// exception and do nothing.
catch (SchemaObjectExistsException $e) {
return TRUE;
}
return FALSE;
}
/**
* Act on an exception when cache might be stale.
*
* If the cache_tags table does not yet exist, that's fine but if the table
* exists and yet the query failed, then the cache is stale and the
* exception needs to propagate.
*
* @param $e
* The exception.
* @param string|null $table_name
* The table name, defaults to $this->bin. Can be cache_tags.
*/
protected function catchException(\Exception $e, $table_name = NULL) {
if ($this->connection->schema()->tableExists($table_name ?: $this->bin)) {
throw $e;
}
}
/**
* Defines the schema for the cache bin and cache_tags table.
*/
public function schemaDefinition() {
$schema['bin'] = array(
'description' => 'Storage for the cache API.',
'fields' => array(
'cid' => array(
'description' => 'Primary Key: Unique cache ID.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'data' => array(
'description' => 'A collection of data to cache.',
'type' => 'blob',
'not null' => FALSE,
'size' => 'big',
),
'expire' => array(
'description' => 'A Unix timestamp indicating when the cache entry should expire, or 0 for never.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'created' => array(
'description' => 'A Unix timestamp indicating when the cache entry was created.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'serialized' => array(
'description' => 'A flag to indicate whether content is serialized (1) or not (0).',
'type' => 'int',
'size' => 'small',
'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_invalidations' => array(
'description' => 'The tag invalidation sum when this entry was saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'checksum_deletions' => array(
'description' => 'The tag deletion sum when this entry was saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'indexes' => array(
'expire' => array('expire'),
),
'primary key' => array('cid'),
);
$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,
),
'deletions' => array(
'description' => 'Number incremented when the tag is deleted.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('tag'),
);
return $schema;
}
}
......@@ -215,4 +215,10 @@ public function isEmpty() {
*/
public function garbageCollection() {
}
/**
* {@inheritdoc}
*/
public function removeBin() {}
}
......@@ -98,4 +98,9 @@ public function garbageCollection() {}
public function isEmpty() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function removeBin() {}
}
......@@ -480,7 +480,7 @@ public function prepareComment($comment, $length = NULL) {
// Truncate comment to maximum comment length.
if (isset($length)) {
// Add table prefixes before truncating.
$comment = truncate_utf8($this->connection->prefixTables($comment), $length, TRUE, TRUE);
$comment = substr($this->connection->prefixTables($comment), 0, $length);
}
return $this->connection->quote($comment);
......
......@@ -6,17 +6,6 @@
*/
use Drupal\Component\Uuid\Uuid;
/**
* Implements hook_schema().
*/
function block_schema() {
$schema['cache_block'] = drupal_get_schema_unprocessed('system', 'cache');
$schema['cache_block']['description'] = 'Cache table for the Block module to store already built blocks, identified by module, delta, and various contexts which may change the block, such as theme, locale, and caching mode defined for the block.';
return $schema;
}
/**
* Implements hook_install().
*/
......
......@@ -8,16 +8,6 @@
use Drupal\Component\Uuid\Uuid;
use Drupal\field\Plugin\Core\Entity\Field;
/**
* Implements hook_schema().
*/
function field_schema() {
$schema['cache_field'] = drupal_get_schema_unprocessed('system', 'cache');
$schema['cache_field']['description'] = 'Cache table for the Field module to store already built field informations.';
return $schema;
}
/**
* Creates a field by writing directly to the database.
*
......
......@@ -7,16 +7,6 @@
use Drupal\Component\Uuid\Uuid;
/**
* Implements hook_schema().
*/
function filter_schema() {
$schema['cache_filter'] = drupal_get_schema_unprocessed('system', 'cache');
$schema['cache_filter']['description'] = 'Cache table for the Filter module to store already filtered pieces of text, identified by text format and hash of the text.';
return $schema;
}
/**
* @addtogroup updates-7.x-to-8.x
* @{
......
......@@ -143,8 +143,7 @@ function testFilterAdmin() {
$this->drupalGet('admin/config/content/formats/' . $basic);
$this->assertFieldByName('filters[filter_html][settings][allowed_html]', $edit['filters[filter_html][settings][allowed_html]'], 'Allowed HTML tag added.');
$result = db_query('SELECT * FROM {cache_filter}')->fetchObject();
$this->assertFalse($result, 'Cache cleared.');
$this->assertTrue(cache('filter')->isEmpty(), 'Cache cleared.');
$elements = $this->xpath('//select[@name=:first]/following::select[@name=:second]', array(
':first' => 'filters[' . $first_filter . '][weight]',
......
......@@ -40,17 +40,4 @@ protected function createCacheBackend($bin) {
return new DatabaseBackend($this->container->get('database'), $bin);
}
/**
* Installs system schema.
*/
public function setUpCacheBackend() {
$this->installSchema('system', array('cache', 'cache_page', 'cache_tags', 'cache_path', 'cache_bootstrap'));
}
/**
* Uninstalls system schema.
*/
public function tearDownCacheBackend() {
drupal_uninstall_schema('system');
}
}
......@@ -1934,12 +1934,12 @@ function hook_mail($key, &$message, $params) {
/**
* Flush all persistent and static caches.
*
* This hook asks your module to clear all of its persistent (database) and
* static caches, in order to ensure a clean environment for subsequently
* This hook asks your module to clear all of its static caches,
* in order to ensure a clean environment for subsequently
* invoked data rebuilds.
*
* Do NOT use this hook for rebuilding information. Only use it to flush custom
* caches and return the names of additional cache bins to flush.
* caches.
*
* Static caches using drupal_static() do not need to be reset manually.
* However, all other static variables that do not use drupal_static() must be
......
......@@ -597,102 +597,6 @@ function system_schema() {
),
);
$schema['cache_tags'] = array(
'description' => 'Cache table for tracking cache tags related to the cache bin.',
'fields' => array(