Commit f5d69718 authored by catch's avatar catch

Issue #1792536 by chx, Berdir, sun, David_Rothstein: Remove the install...

Issue #1792536 by chx, Berdir, sun, David_Rothstein: Remove the install backend and stop catching exceptions in the default database cache backend.
parent 14e0c88d
......@@ -342,17 +342,8 @@ function install_begin_request(&$install_state) {
module_list(NULL, $module_list);
drupal_load('module', 'system');
// Load the cache infrastructure using a "fake" cache implementation that
// does not attempt to write to the database. We need this during the initial
// part of the installer because the database is not available yet. We
// continue to use it even when the database does become available, in order
// to preserve consistency between interactive and command-line installations
// (the latter complete in one page request and therefore are forced to
// continue using the cache implementation they started with) and also
// 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_classes'] = array('cache' => 'Drupal\Core\Cache\InstallBackend');
$conf['cache_classes'] = array('cache' => 'Drupal\Core\Cache\MemoryBackend');
// The install process cannot use the database lock backend since the database
// is not fully up, so we use a null backend implementation during the
......@@ -1496,6 +1487,11 @@ function install_load_profile(&$install_state) {
* An array of information about the current installation state.
*/
function install_bootstrap_full(&$install_state) {
// The early stages of the installer override the cache backend since Drupal
// isn't fully set up yet. Here the override is removed so that the standard
// cache backend will be used again.
unset($GLOBALS['conf']['cache_classes']['cache']);
drupal_static_reset('cache');
// Clear the module list that was overriden earlier in the process.
// This will allow all freshly installed modules to be loaded.
module_list_reset();
......
......@@ -135,7 +135,10 @@ function update_prepare_d8_bootstrap() {
drupal_install_config_directories();
}
// Bootstrap the database.
// Bootstrap the database. During this, the DRUPAL_BOOTSTRAP_PAGE_CACHE will
// try to read the cache but the cache tables might not be Drupal 8
// compatible yet. Use the null backend by default to avoid exceptions.
$GLOBALS['conf']['cache_classes'] = array('cache' => 'Drupal\Core\Cache\NullBackend');
drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
// If the site has not updated to Drupal 8 yet, check to make sure that it is
......@@ -195,6 +198,116 @@ function update_prepare_d8_bootstrap() {
);
db_create_table('key_value', $specs);
}
if (!db_table_exists('cache_tags')) {
$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,
),
'deletions' => array(
'description' => 'Number incremented when the tag is deleted.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('tag'),
);
db_create_table('cache_tags', $table);
}
if (!db_table_exists('cache_config')) {
$spec = array(
'description' => 'Cache table for configuration data.',
'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'),
);
db_create_table('cache_config', $spec);
}
require_once DRUPAL_ROOT . '/core/modules/system/system.install';
$tables = array(
'cache',
'cache_bootstrap',
'cache_block',
'cache_field',
'cache_filter',
'cache_form',
'cache_image',
'cache_menu',
'cache_page',
'cache_path',
'cache_update',
);
foreach ($tables as $table) {
update_add_cache_columns($table);
}
// Bootstrap variables so we can update theme while preparing the update
// process.
drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES);
......@@ -299,6 +412,9 @@ function update_prepare_d8_bootstrap() {
}
}
}
// Now remove the cache override.
unset($GLOBALS['conf']['cache_classes']['cache']);
drupal_static_reset('cache');
}
/**
......@@ -1333,3 +1449,33 @@ function update_add_uuids(&$sandbox, $table, $primary_key, $values) {
$sandbox['last'] = $value;
}
}
/**
* Adds tags, checksum_invalidations, checksum_deletions to a cache table.
*
* @param string $table
* Name of the cache table.
*/
function update_add_cache_columns($table) {
if (db_table_exists($table) && !db_field_exists($table, 'tags')) {
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_invalidations', array(
'description' => 'The tag invalidation sum when this entry was saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
));
db_add_field($table, 'checksum_deletions', array(
'description' => 'The tag deletion sum when this entry was saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
));
}
}
......@@ -8,8 +8,7 @@
namespace Drupal\Core\Cache;
use Drupal\Core\Database\Database;
use Exception;
use PDO;
use Drupal\Core\Database\DatabaseException;
/**
* Defines a default cache implementation.
......@@ -52,30 +51,23 @@ public function get($cid, $allow_invalid = FALSE) {
* Implements Drupal\Core\Cache\CacheBackendInterface::getMultiple().
*/
public function getMultiple(&$cids, $allow_invalid = FALSE) {
try {
// When serving cached pages, the overhead of using ::select() was found
// to add around 30% overhead to the request. Since $this->bin is a
// variable, this means the call to ::query() here uses a concatenated
// string. This is highly discouraged under any other circumstances, and
// 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 = Database::getConnection()->query('SELECT cid, data, created, expire, serialized, tags, checksum_invalidations, checksum_deletions FROM {' . Database::getConnection()->escapeTable($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids));
$cache = array();
foreach ($result as $item) {
$item = $this->prepareItem($item, $allow_invalid);
if ($item) {
$cache[$item->cid] = $item;
}
// When serving cached pages, the overhead of using ::select() was found
// to add around 30% overhead to the request. Since $this->bin is a
// variable, this means the call to ::query() here uses a concatenated
// string. This is highly discouraged under any other circumstances, and
// 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 = Database::getConnection()->query('SELECT cid, data, created, expire, serialized, tags, checksum_invalidations, checksum_deletions FROM {' . Database::getConnection()->escapeTable($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids));
$cache = array();
foreach ($result as $item) {
$item = $this->prepareItem($item, $allow_invalid);
if ($item) {
$cache[$item->cid] = $item;
}
$cids = array_diff($cids, array_keys($cache));
return $cache;
}
catch (Exception $e) {
// If the database is never going to be available, cache requests should
// return FALSE in order to allow exception handling to occur.
return array();
}
$cids = array_diff($cids, array_keys($cache));
return $cache;
}
/**
......@@ -131,34 +123,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 {
$flat_tags = $this->flattenTags($tags);
$checksum = $this->checksumTags($flat_tags);
$fields = array(
'serialized' => 0,
'created' => REQUEST_TIME,
'expire' => $expire,
'tags' => implode(' ', $flat_tags),
'checksum_invalidations' => $checksum['invalidations'],
'checksum_deletions' => $checksum['deletions'],
);
if (!is_string($data)) {
$fields['data'] = serialize($data);
$fields['serialized'] = 1;
}
else {
$fields['data'] = $data;
$fields['serialized'] = 0;
}
Database::getConnection()->merge($this->bin)
->key(array('cid' => $cid))
->fields($fields)
->execute();
$flat_tags = $this->flattenTags($tags);
$checksum = $this->checksumTags($flat_tags);
$fields = array(
'serialized' => 0,
'created' => REQUEST_TIME,
'expire' => $expire,
'tags' => implode(' ', $flat_tags),
'checksum_invalidations' => $checksum['invalidations'],
'checksum_deletions' => $checksum['deletions'],
);
if (!is_string($data)) {
$fields['data'] = serialize($data);
$fields['serialized'] = 1;
}
catch (Exception $e) {
// The database may not be available, so we'll ignore cache_set requests.
else {
$fields['data'] = $data;
$fields['serialized'] = 0;
}
Database::getConnection()->merge($this->bin)
->key(array('cid' => $cid))
->fields($fields)
->execute();
}
/**
......@@ -316,7 +303,7 @@ protected function checksumTags($flat_tags) {
$query_tags = array_diff($flat_tags, array_keys($tag_cache));
if ($query_tags) {
$db_tags = Database::getConnection()->query('SELECT tag, invalidations, deletions FROM {cache_tags} WHERE tag IN (:tags)', array(':tags' => $query_tags))->fetchAllAssoc('tag', PDO::FETCH_ASSOC);
$db_tags = Database::getConnection()->query('SELECT tag, invalidations, deletions FROM {cache_tags} WHERE tag IN (:tags)', array(':tags' => $query_tags))->fetchAllAssoc('tag', \PDO::FETCH_ASSOC);
$tag_cache += $db_tags;
// Fill static cache with empty objects for tags not found in the database.
......
<?php
/**
* @file
* Definition of Drupal\Core\Cache\InstallBackend.
*/
namespace Drupal\Core\Cache;
use Exception;
/**
* Defines a stub cache implementation to be used during installation.
*
* The stub implementation is needed when database access is not yet available.
* Because Drupal's caching system never requires that cached data be present,
* these stub functions can short-circuit the process and sidestep the need for
* any persistent storage. Obviously, using this cache implementation during
* normal operations would have a negative impact on performance.
*
* If there is a database cache, this backend will attempt to clear it whenever
* possible. The reason for doing this is that the database cache can accumulate
* data during installation due to any full bootstraps that may occur at the
* same time (for example, Ajax requests triggered by the installer). If we
* didn't try to clear it whenever one of the delete function are called, the
* data in the cache would become stale; for example, the installer sometimes
* calls variable_set(), which updates the {variable} table and then clears the
* cache to make sure that the next page request picks up the new value.
* Not actually clearing the cache here therefore leads old variables to be
* loaded on the first page requests after installation, which can cause
* subtle bugs, some of which would not be fixed unless the site
* administrator cleared the cache manually.
*/
class InstallBackend extends DatabaseBackend {
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::get().
*/
public function get($cid, $allow_invalid = FALSE) {
return FALSE;
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::getMultiple().
*/
public function getMultiple(&$cids, $allow_invalid = FALSE) {
return array();
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::set().
*/
public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = array()) {}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::delete().
*/
public function delete($cid) {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::delete($cid);
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::deleteMultiple().
*/
public function deleteMultiple(array $cids) {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::deleteMultiple($cids);
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::deleteAll().
*/
public function deleteAll() {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::deleteAll();
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::deleteExpired().
*/
public function deleteExpired() {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::deleteExpired();
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::deleteTags().
*/
public function deleteTags(array $tags) {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::deleteTags($tags);
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::invalidate().
*/
public function invalidate($cid) {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::invalidate($cid);
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::invalidateMultiple().
*/
public function invalidateMultiple(array $cids) {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::invalidateMultiple($cids);
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::invalidateTags().
*/
public function invalidateTags(array $tags) {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::invalidateTags($tags);
}
}
catch (Exception $e) {}
}
/**
* Implements Drupal\Core\Cache\CacheBackendInterface::invalidateAll().
*/
public function invalidateAll() {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::invalidateAll($tags);
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::garbageCollection().
*/
public function garbageCollection() {
try {
if (class_exists('Drupal\Core\Database\Database')) {
parent::garbageCollection();
}
}
catch (Exception $e) {}
}
/**
* Overrides Drupal\Core\Cache\DatabaseBackend::isEmpty().
*/
public function isEmpty() {
try {
if (class_exists('Drupal\Core\Database\Database')) {
return parent::isEmpty();
}
}
catch (Exception $e) {}
return TRUE;
}
}
<?php
/**
* @file
* Definition of Drupal\system\Tests\Cache\InstallTest.
*/
namespace Drupal\system\Tests\Cache;
use Drupal\Core\Cache\DatabaseBackend;
use Drupal\Core\Cache\InstallBackend;
use Exception;
/**
* Tests the behavior of the cache backend used for installing Drupal.
*/
class InstallTest extends CacheTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('cache_test');
protected $profile = 'testing';
public static function getInfo() {
return array(
'name' => 'Cache install test',
'description' => 'Confirm that the cache backend used for installing Drupal works correctly.',
'group' => 'Cache',
);
}
/**
* Tests the behavior of the cache backend used for installing Drupal.
*
* While Drupal is being installed, the cache system must deal with the fact
* that the database is not initially available, and, after it is available,
* the fact that other requests that take place while Drupal is being
* installed (for example, Ajax requests triggered via the installer's user
* interface) may cache data in the database, which needs to be cleared when
* the installer makes changes that would result in it becoming stale.
*
* We cannot test this process directly, so instead we test it by switching
* between the normal database cache (Drupal\Core\Cache\DatabaseBackend) and
* the installer cache (Drupal\Core\Cache\InstallBackend) while setting and
* clearing various items in the cache.
*/
function testCacheInstall() {
$database_cache = new DatabaseBackend('test');
$install_cache = new InstallBackend('test');
// Store an item in the database cache, and confirm that the installer's
// cache backend recognizes that the cache is not empty.
$database_cache->set('cache_one', 'One');
$this->assertFalse($install_cache->isEmpty());
$database_cache->delete('cache_one');
$this->assertTrue($install_cache->isEmpty());
// Store an item in the database cache, then use the installer's cache
// backend to delete it. Afterwards, confirm that it is no longer in the
// database cache.
$database_cache->set('cache_one', 'One');
$this->assertEqual($database_cache->get('cache_one')->data, 'One');
$install_cache->delete('cache_one');
$this->assertFalse($database_cache->get('cache_one'));
// Store multiple items in the database cache, then use the installer's
// cache backend to delete them. Afterwards, confirm that they are no
// longer in the database cache.
$database_cache->set('cache_one', 'One');
$database_cache->set('cache_two', 'Two');
$this->assertEqual($database_cache->get('cache_one')->data, 'One');
$this->assertEqual($database_cache->get('cache_two')->data, 'Two');
$install_cache->deleteMultiple(array('cache_one', 'cache_two'));
$this->assertFalse($database_cache->get('cache_one'));
$this->assertFalse($database_cache->get('cache_two'));
// Store multiple items in the database cache, then use the installer's
// cache backend to flush the cache. Afterwards, confirm that they are no
// longer in the database cache.
$database_cache->set('cache_one', 'One');
$database_cache->set('cache_two', 'Two');
$this->assertEqual($database_cache->get('cache_one')->data, 'One');
$this->assertEqual($database_cache->get('cache_two')->data, 'Two');
$install_cache->deleteAll();
$this->assertFalse($database_cache->get('cache_one'));
$this->assertFalse($database_cache->get('cache_two'));
// Invalidate a tag using the installer cache, then check that the
// invalidation was recorded correctly in the database.
$install_cache->invalidateTags(array('tag'));
$invalidations = db_query("SELECT invalidations FROM {cache_tags} WHERE tag = 'tag'")->fetchField();
$this->assertEqual($invalidations, 1);
// For each cache clearing event that we tried above, try it again after
// dropping the {cache_test} table. This simulates the early stages of the
// installer (when the database cache tables won't be available yet) and
// thereby confirms that the installer's cache backend does not produce
// errors if the installer ever calls any code early on that tries to clear
// items from the cache.
db_drop_table('cache_test');
try {
$install_cache->isEmpty();
$install_cache->delete('cache_one');
$install_cache->deleteMultiple(array('cache_one', 'cache_two'));
$install_cache->deleteAll();
$install_cache->deleteExpired();
$install_cache->garbageCollection();
$install_cache->invalidateTags(array('tag'));
$this->pass("The installer's cache backend can be used even when the cache database tables are unavailable.");
}
catch (Exception $e) {
$this->fail("The installer's cache backend can be used even when the cache database tables are unavailable.");
}
}
}
......@@ -26,11 +26,8 @@ public static function getInfo() {
);
}
/**
* Tests DIC compilation.
*/
function testCompileDIC() {
$classloader = drupal_classloader();
function setUp() {
parent::setUp();
global $conf;
$conf['php_storage']['service_container']= array(
'bin' => 'service_container',
......@@ -38,6 +35,15 @@ function testCompileDIC() {
'directory' => DRUPAL_ROOT . '/' . variable_get('file_public_path', conf_path() . '/files') . '/php',
'secret' => $GLOBALS['drupal_hash_salt'],
);
// Use a non-persistent cache to avoid queries to non-existing tables.
$conf['cache_classes'] = array('cache' => 'Drupal\Core\Cache\MemoryBackend');
}
/**
* Tests DIC compilation.
*/
function testCompileDIC() {
$classloader = drupal_classloader();
// @todo: write a memory based storage backend for testing.
$module_enabled = array(
'system' => 'system',
......@@ -60,6 +66,7 @@ function testCompileDIC() {
// Now use the read-only storage implementation, simulating a "production"
// environment.
global $conf;
$conf['php_storage']['service_container']['class'] = 'Drupal\Component\PhpStorage\FileReadOnlyStorage';
$kernel = new DrupalKernel('testing', FALSE, $classloader);
$kernel->updateModules($module_enabled);
......
......@@ -1519,66 +1519,7 @@ function system_update_8002() {
* Creates {cache_config} cache table for the new configuration system.
*/
function system_update_8003() {
$spec = array(
'description' => 'Cache table for configuration data.',
'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',