diff --git a/core/includes/update.inc b/core/includes/update.inc index bf11562..e1ded80 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -642,6 +642,44 @@ function update_fix_d8_requirements() { // picture upgrade path. update_module_enable(array('file')); + $schema = array( + 'description' => 'Generic key/value storage table with an expiration.', + 'fields' => array( + 'collection' => array( + 'description' => 'A named collection of key and value pairs.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'name' => array( + // KEY is an SQL reserved word, so use 'name' as the key's field name. + 'description' => 'The key of the key/value pair.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'value' => array( + 'description' => 'The value of the key/value pair.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + ), + 'expire' => array( + 'description' => 'The time since Unix epoch in seconds when this item expires. Defaults to the maximum possible time.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 2147483647, + ), + ), + 'primary key' => array('collection', 'name'), + 'indexes' => array( + 'all' => array('name', 'collection', 'expire'), + ), + ); + db_create_table('key_value_expire', $schema); + update_variable_set('update_d8_requirements', TRUE); } } diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 91bba3c..19c533b 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -105,6 +105,14 @@ public function build(ContainerBuilder $container) { $container ->register('keyvalue.database', 'Drupal\Core\KeyValueStore\KeyValueDatabaseFactory') ->addArgument(new Reference('database')); + // Register the KeyValueStoreExpirable factory. + $container + ->register('keyvalue.expirable', 'Drupal\Core\KeyValueStore\KeyValueExpirableFactory') + ->addArgument(new Reference('service_container')); + $container + ->register('keyvalue.expirable.database', 'Drupal\Core\KeyValueStore\KeyValueDatabaseExpirableFactory') + ->addArgument(new Reference('database')) + ->addTag('needs_destruction'); $container->register('settings', 'Drupal\Component\Utility\Settings') ->setFactoryClass('Drupal\Component\Utility\Settings') diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php index 3ccda14..4045c48 100644 --- a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php +++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorage.php @@ -125,4 +125,12 @@ public function deleteMultiple(array $keys) { while (count($keys)); } + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteAll(). + */ + public function deleteAll() { + $this->connection->delete($this->table) + ->condition('collection', $this->collection) + ->execute(); + } } diff --git a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php index e989470..93cca8b 100644 --- a/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php +++ b/core/lib/Drupal/Core/KeyValueStore/DatabaseStorageExpirable.php @@ -7,6 +7,7 @@ namespace Drupal\Core\KeyValueStore; +use Drupal\Core\DestructableInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Database\Query\Merge; @@ -16,7 +17,7 @@ * This key/value store implementation uses the database to store key/value * data with an expire date. */ -class DatabaseStorageExpirable extends DatabaseStorage implements KeyValueStoreExpirableInterface { +class DatabaseStorageExpirable extends DatabaseStorage implements KeyValueStoreExpirableInterface, DestructableInterface { /** * The connection object for this storage. @@ -56,15 +57,6 @@ public function __construct($collection, Connection $connection, $table = 'key_v } /** - * Performs garbage collection as needed when destructing the storage object. - */ - public function __destruct() { - if ($this->needsGarbageCollection) { - $this->garbageCollection(); - } - } - - /** * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::getMultiple(). */ public function getMultiple(array $keys) { @@ -158,4 +150,13 @@ protected function garbageCollection() { ->execute(); } + /** + * Implements Drupal\Core\DestructableInterface::destruct(). + */ + public function destruct() { + if ($this->needsGarbageCollection) { + $this->garbageCollection(); + } + } + } diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseExpirableFactory.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseExpirableFactory.php index 08e673e..0d63988 100644 --- a/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseExpirableFactory.php +++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueDatabaseExpirableFactory.php @@ -7,6 +7,7 @@ namespace Drupal\Core\KeyValueStore; +use Drupal\Core\DestructableInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Database\Database; use Drupal\Core\KeyValueStore\KeyValueDatabaseFactory; @@ -14,7 +15,14 @@ /** * Defines the key/value store factory for the database backend. */ -class KeyValueDatabaseExpirableFactory extends KeyValueDatabaseFactory { +class KeyValueDatabaseExpirableFactory extends KeyValueDatabaseFactory implements DestructableInterface { + + /** + * Holds references to each instantiation so they can be terminated. + * + * @var array + */ + protected $storages; /** * Constructs a new key/value expirable database storage object for a given @@ -28,6 +36,17 @@ class KeyValueDatabaseExpirableFactory extends KeyValueDatabaseFactory { * A key/value store implementation for the given $collection. */ public function get($collection) { - return new DatabaseStorageExpirable($collection, $this->connection); + $storage = new DatabaseStorageExpirable($collection, $this->connection); + $this->storages[] = $storage; + return $storage; + } + + /** + * Implements Drupal\Core\DestructableInterface::terminate(). + */ + public function destruct() { + foreach ($this->storages as $storage) { + $storage->destruct(); + } } } diff --git a/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php index 8f006ce..372ffc5 100644 --- a/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php +++ b/core/lib/Drupal/Core/KeyValueStore/KeyValueStoreInterface.php @@ -99,4 +99,9 @@ public function delete($key); */ public function deleteMultiple(array $keys); + /** + * Deletes all items from the key/value store. + */ + public function deleteAll(); + } diff --git a/core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php b/core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php index 6101507..236b0aa 100644 --- a/core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php +++ b/core/lib/Drupal/Core/KeyValueStore/MemoryStorage.php @@ -81,4 +81,10 @@ public function deleteMultiple(array $keys) { } } + /** + * Implements Drupal\Core\KeyValueStore\KeyValueStoreInterface::deleteAll(). + */ + public function deleteAll() { + $this->data = array(); + } } diff --git a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/GarbageCollectionTest.php b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/GarbageCollectionTest.php index 6334382..82c52df 100644 --- a/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/GarbageCollectionTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/KeyValueStore/GarbageCollectionTest.php @@ -62,10 +62,10 @@ public function testGarbageCollection() { ->execute(); } - // Perform a new set operation and then manually unset the object to + // Perform a new set operation and then manually destruct the object to // trigger garbage collection. $store->setWithExpire('autumn', 'winter', rand(500, 1000000)); - unset($store); + $store->destruct(); // Query the database and confirm that the stale records were deleted. $result = db_query( diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 08a9fd9..ecc1de0 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1735,44 +1735,7 @@ function system_update_8022() { * Create the 'key_value_expire' table. */ function system_update_8023() { - $table = array( - 'description' => 'Generic key/value storage table with an expiration.', - 'fields' => array( - 'collection' => array( - 'description' => 'A named collection of key and value pairs.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - ), - 'name' => array( - // KEY is an SQL reserved word, so use 'name' as the key's field name. - 'description' => 'The key of the key/value pair.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - ), - 'value' => array( - 'description' => 'The value of the key/value pair.', - 'type' => 'blob', - 'not null' => TRUE, - 'size' => 'big', - ), - 'expire' => array( - 'description' => 'The time since Unix epoch in seconds when this item expires. Defaults to the maximum possible time.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 2147483647, - ), - ), - 'primary key' => array('collection', 'name'), - 'indexes' => array( - 'all' => array('name', 'collection', 'expire'), - ), - ); - - db_create_table('key_value_expire', $table); + // Moved to update_fix_d8_requirements(). } /** diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php index 0001cba..a697eef 100644 --- a/core/modules/system/theme.api.php +++ b/core/modules/system/theme.api.php @@ -234,5 +234,5 @@ function hook_themes_enabled($theme_list) { */ function hook_themes_disabled($theme_list) { // Clear all update module caches. - _update_cache_clear(); + update_storage_clear(); } diff --git a/core/modules/update/lib/Drupal/update/Tests/UpdateCoreTest.php b/core/modules/update/lib/Drupal/update/Tests/UpdateCoreTest.php index 6c314d2..27e45fd 100644 --- a/core/modules/update/lib/Drupal/update/Tests/UpdateCoreTest.php +++ b/core/modules/update/lib/Drupal/update/Tests/UpdateCoreTest.php @@ -211,8 +211,8 @@ function testFetchTasks() { update_create_fetch_task($projecta); $this->assertEqual($queue->numberOfItems(), 2, 'Queue still contains two items'); - // Clear cache and try again. - _update_cache_clear(); + // Clear storage and try again. + update_storage_clear(); drupal_static_reset('_update_create_fetch_task'); update_create_fetch_task($projecta); $this->assertEqual($queue->numberOfItems(), 2, 'Queue contains two items'); diff --git a/core/modules/update/lib/Drupal/update/UpdateSettingsForm.php b/core/modules/update/lib/Drupal/update/UpdateSettingsForm.php index e1f906e..92dd022 100644 --- a/core/modules/update/lib/Drupal/update/UpdateSettingsForm.php +++ b/core/modules/update/lib/Drupal/update/UpdateSettingsForm.php @@ -106,9 +106,9 @@ public function validateForm(array &$form, array &$form_state) { public function submitForm(array &$form, array &$form_state) { $config = $this->configFactory->get('update.settings'); // See if the update_check_disabled setting is being changed, and if so, - // invalidate all cached update status data. + // invalidate all update status data. if ($form_state['values']['update_check_disabled'] != $config->get('check.disabled_extensions')) { - _update_cache_clear(); + update_storage_clear(); } $config diff --git a/core/modules/update/update.authorize.inc b/core/modules/update/update.authorize.inc index 4557453..d2d6aac 100644 --- a/core/modules/update/update.authorize.inc +++ b/core/modules/update/update.authorize.inc @@ -10,6 +10,7 @@ * the Update Manager module. */ +use Drupal\Core\FileTransfer\FileTransfer; use Drupal\Core\Updater\UpdaterException; /** @@ -178,7 +179,7 @@ function update_authorize_batch_copy_project($project, $updater_name, $local_url * * This processes the results and stashes them into SESSION such that * authorize.php will render a report. Also responsible for putting the site - * back online and clearing the update status cache after a successful update. + * back online and clearing the update status storage after a successful update. * * @param $success * TRUE if the batch operation was successful; FALSE if there were errors. @@ -193,8 +194,8 @@ function update_authorize_update_batch_finished($success, $results) { } $offline = config('system.maintenance')->get('enabled'); if ($success) { - // Now that the update completed, we need to clear the cache of available - // update data and recompute our status, so prevent show bogus results. + // Now that the update completed, we need to clear the available update data + // and recompute our status, so prevent show bogus results. _update_authorize_clear_update_status(); // Take the site out of maintenance mode if it was previously that way. @@ -315,25 +316,19 @@ function _update_batch_create_message(&$project_results, $message, $success = TR } /** - * Clears cached available update status data. + * Clears available update status data. * * Since this function is run at such a low bootstrap level, the Update Manager - * module is not loaded. So, we can't just call _update_cache_clear(). However, - * the database is bootstrapped, so we can do a query ourselves to clear out - * what we want to clear. + * module is not loaded. So, we can't just call update_storage_clear(). However, + * the key-value backend is available, so we just call that. * - * Note that we do not want to just truncate the table, since that would remove - * items related to currently pending fetch attempts. + * Note that we do not want to delete items related to currently pending fetch + * attempts. * * @see update_authorize_update_batch_finished() - * @see _update_cache_clear() + * @see update_storage_clear() */ function _update_authorize_clear_update_status() { - $query = db_delete('cache_update'); - $query->condition( - db_or() - ->condition('cid', 'update_project_%', 'LIKE') - ->condition('cid', 'available_releases::%', 'LIKE') - ); - $query->execute(); + drupal_container()->get('keyvalue.expirable')->get('update')->deleteAll(); + drupal_container()->get('keyvalue.expirable')->get('update_available_release')->deleteAll(); } diff --git a/core/modules/update/update.compare.inc b/core/modules/update/update.compare.inc index 36bfe79..4ac08b1 100644 --- a/core/modules/update/update.compare.inc +++ b/core/modules/update/update.compare.inc @@ -16,13 +16,12 @@ * fetching the available release data. * * This array is fairly expensive to construct, since it involves a lot of disk - * I/O, so we cache the results into the {cache_update} table using the - * 'update_project_projects' cache ID. However, since this is not the data about + * I/O, so we store the results. However, since this is not the data about * available updates fetched from the network, it is acceptable to invalidate it * somewhat quickly. If we keep this data for very long, site administrators are * more likely to see incorrect results if they upgrade to a newer version of a * module or theme but do not visit certain pages that automatically clear this - * cache. + * data. * * @return * An associative array of currently enabled projects keyed by the @@ -51,15 +50,15 @@ * * @see update_process_project_info() * @see update_calculate_project_data() - * @see update_project_cache() + * @see update_project_storage() */ function update_get_projects() { $projects = &drupal_static(__FUNCTION__, array()); if (empty($projects)) { - // Retrieve the projects from cache, if present. - $projects = update_project_cache('update_project_projects'); + // Retrieve the projects from storage, if present. + $projects = update_project_storage('update_project_projects'); if (empty($projects)) { - // Still empty, so we have to rebuild the cache. + // Still empty, so we have to rebuild. $module_data = system_rebuild_module_data(); $theme_data = system_rebuild_theme_data(); update_process_info_list($projects, $module_data, 'module', TRUE); @@ -70,8 +69,8 @@ function update_get_projects() { } // Allow other modules to alter projects before fetching and comparing. drupal_alter('update_projects', $projects); - // Cache the site's project data for at most 1 hour. - _update_cache_set('update_project_projects', $projects, REQUEST_TIME + 3600); + // Store the site's project data for at most 1 hour. + drupal_container()->get('keyvalue.expirable')->get('update')->setWithExpire('update_project_projects', $projects, 3600); } } return $projects; @@ -90,7 +89,7 @@ function update_get_projects() { * 'Hidden' themes are ignored only if they have no enabled sub-themes. * This function also records the latest change time on the .info.yml * files for each module or theme, which is important data which is used when - * deciding if the cached available update data should be invalidated. + * deciding if the available update data should be invalidated. * * @param $projects * Reference to the array of project data of what's installed on this site. @@ -318,13 +317,12 @@ function update_process_project_info(&$projects) { * * The results of this function are expensive to compute, especially on sites * with lots of modules or themes, since it involves a lot of comparisons and - * other operations. Therefore, we cache the results into the {cache_update} - * table using the 'update_project_data' cache ID. However, since this is not + * other operations. Therefore, we store the results. However, since this is not * the data about available updates fetched from the network, it is ok to * invalidate it somewhat quickly. If we keep this data for very long, site * administrators are more likely to see incorrect results if they upgrade to a * newer version of a module or theme but do not visit certain pages that - * automatically clear this cache. + * automatically clear this. * * @param array $available * Data about available project releases. @@ -335,13 +333,13 @@ function update_process_project_info(&$projects) { * @see update_get_available() * @see update_get_projects() * @see update_process_project_info() - * @see update_project_cache() + * @see update_project_storage() */ function update_calculate_project_data($available) { - // Retrieve the projects from cache, if present. - $projects = update_project_cache('update_project_data'); - // If $projects is empty, then the cache must be rebuilt. - // Otherwise, return the cached data and skip the rest of the function. + // Retrieve the projects from storage, if present. + $projects = update_project_storage('update_project_data'); + // If $projects is empty, then the data must be rebuilt. + // Otherwise, return the data and skip the rest of the function. if (!empty($projects)) { return $projects; } @@ -361,8 +359,8 @@ function update_calculate_project_data($available) { // projects or releases). drupal_alter('update_status', $projects); - // Cache the site's update status for at most 1 hour. - _update_cache_set('update_project_data', $projects, REQUEST_TIME + 3600); + // Store the site's update status for at most 1 hour. + drupal_container()->get('keyvalue.expirable')->get('update')->setWithExpire('update_project_data', $projects, 3600); return $projects; } @@ -752,37 +750,36 @@ function update_calculate_project_update_status(&$project_data, $available) { } /** - * Retrieves data from {cache_update} or empties the cache when necessary. + * Retrieves update storage data or empties it. * * Two very expensive arrays computed by this module are the list of all - * installed modules and themes (and .info.yml data, project associations, etc), - * and the current status of the site relative to the currently available - * releases. These two arrays are cached in the {cache_update} table and used - * whenever possible. The cache is cleared whenever the administrator visits the - * status report, available updates report, or the module or theme - * administration pages, since we should always recompute the most current - * values on any of those pages. + * installed modules and themes (and .info.yml data, project associations, etc), and + * the current status of the site relative to the currently available releases. + * These two arrays are stored and used whenever possible. The data is cleared + * whenever the administrator visits the status report, available updates + * report, or the module or theme administration pages, since we should always + * recompute the most current values on any of those pages. * * Note: while both of these arrays are expensive to compute (in terms of disk * I/O and some fairly heavy CPU processing), neither of these is the actual * data about available updates that we have to fetch over the network from - * updates.drupal.org. That information is stored with the - * 'update_available_releases' cache ID -- it needs to persist longer than 1 + * updates.drupal.org. That information is stored in the + * 'update_available_releases' collection -- it needs to persist longer than 1 * hour and never get invalidated just by visiting a page on the site. * - * @param $cid - * The cache ID of data to return from the cache. Valid options are - * 'update_project_data' and 'update_project_projects'. + * @param $key + * The key of data to return. Valid options are 'update_project_data' and + * 'update_project_projects'. * * @return - * The cached value of the $projects array generated by + * The stored value of the $projects array generated by * update_calculate_project_data() or update_get_projects(), or an empty array - * when the cache is cleared. + * when the storage is cleared. */ -function update_project_cache($cid) { +function update_project_storage($key) { $projects = array(); - // On certain paths, we should clear the cache and recompute the projects for + // On certain paths, we should clear the data and recompute the projects for // update status of the site to avoid presenting stale information. $paths = array( 'admin/modules', @@ -796,13 +793,10 @@ function update_project_cache($cid) { 'admin/reports/updates/check', ); if (in_array(current_path(), $paths)) { - _update_cache_clear($cid); + drupal_container()->get('keyvalue.expirable')->get('update')->delete($key); } else { - $cache = _update_cache_get($cid); - if (!empty($cache->data) && $cache->expire > REQUEST_TIME) { - $projects = $cache->data; - } + $projects = drupal_container()->get('keyvalue.expirable')->get('update')->get($key); } return $projects; } diff --git a/core/modules/update/update.fetch.inc b/core/modules/update/update.fetch.inc index 476739a..7e11076 100644 --- a/core/modules/update/update.fetch.inc +++ b/core/modules/update/update.fetch.inc @@ -117,8 +117,8 @@ function _update_fetch_data() { /** * Processes a task to fetch available update data for a single project. * - * Once the release history XML data is downloaded, it is parsed and saved into - * the {cache_update} table in an entry just for that project. + * Once the release history XML data is downloaded, it is parsed and saved in an + * entry just for that project. * * @param $project * Associative array of information about the project to fetch data for. @@ -132,13 +132,11 @@ function _update_process_fetch_task($project) { $fail = &drupal_static(__FUNCTION__, array()); // This can be in the middle of a long-running batch, so REQUEST_TIME won't // necessarily be valid. - $now = time(); + $request_time_difference = time() - REQUEST_TIME; if (empty($fail)) { // If we have valid data about release history XML servers that we have - // failed to fetch from on previous attempts, load that from the cache. - if (($cache = _update_cache_get('fetch_failures')) && ($cache->expire > $now)) { - $fail = $cache->data; - } + // failed to fetch from on previous attempts, load that. + $fail = drupal_container()->get('keyvalue.expirable')->get('update')->get('fetch_failures'); } $max_fetch_attempts = $update_config->get('fetch.max_attempts'); @@ -179,43 +177,43 @@ function _update_process_fetch_task($project) { } $frequency = $update_config->get('check.interval_days'); - $cid = 'available_releases::' . $project_name; - _update_cache_set($cid, $available, $now + (60 * 60 * 24 * $frequency)); + $available['last_fetch'] = REQUEST_TIME + $request_time_difference; + drupal_container()->get('keyvalue.expirable')->get('update_available_releases')->setWithExpire($project_name, $available, $request_time_difference + (60 * 60 * 24 * $frequency)); // Stash the $fail data back in the DB for the next 5 minutes. - _update_cache_set('fetch_failures', $fail, $now + (60 * 5)); + drupal_container()->get('keyvalue.expirable')->get('update')->setWithExpire('fetch_failures', $fail, $request_time_difference + (60 * 5)); // Whether this worked or not, we did just (try to) check for updates. - state()->set('update.last_check', $now); + state()->set('update.last_check', REQUEST_TIME + $request_time_difference); // Now that we processed the fetch task for this project, clear out the - // record in {cache_update} for this task so we're willing to fetch again. - _update_cache_clear('fetch_task::' . $project_name); + // record for this task so we're willing to fetch again. + drupal_container()->get('keyvalue')->get('update_fetch_task')->delete($project_name); return $success; } /** - * Clears out all the cached available update data and initiates re-fetching. + * Clears out all the available update data and initiates re-fetching. */ function _update_refresh() { module_load_include('inc', 'update', 'update.compare'); // Since we're fetching new available update data, we want to clear - // our cache of both the projects we care about, and the current update - // status of the site. We do *not* want to clear the cache of available - // releases just yet, since that data (even if it's stale) can be useful - // during update_get_projects(); for example, to modules that implement + // of both the projects we care about, and the current update status of the + // site. We do *not* want to clear the cache of available releases just yet, + // since that data (even if it's stale) can be useful during + // update_get_projects(); for example, to modules that implement // hook_system_info_alter() such as cvs_deploy. - _update_cache_clear('update_project_projects'); - _update_cache_clear('update_project_data'); + drupal_container()->get('keyvalue.expirable')->get('update')->delete('update_project_projects'); + drupal_container()->get('keyvalue.expirable')->get('update')->delete('update_project_data'); $projects = update_get_projects(); - // Now that we have the list of projects, we should also clear our cache of - // available release data, since even if we fail to fetch new data, we need - // to clear out the stale data at this point. - _update_cache_clear('available_releases::', TRUE); + // Now that we have the list of projects, we should also clear the available + // release data, since even if we fail to fetch new data, we need to clear + // out the stale data at this point. + drupal_container()->get('keyvalue.expirable')->get('update_available_releases')->deleteAll(); foreach ($projects as $key => $project) { update_create_fetch_task($project); @@ -226,8 +224,7 @@ function _update_refresh() { * Adds a task to the queue for fetching release history data for a project. * * We only create a new fetch task if there's no task already in the queue for - * this particular project (based on 'fetch_task::' entries in the - * {cache_update} table). + * this particular project (based on 'update_fetch_task' key-value collection). * * @param $project * Associative array of information about a project as created by @@ -243,29 +240,13 @@ function _update_refresh() { function _update_create_fetch_task($project) { $fetch_tasks = &drupal_static(__FUNCTION__, array()); if (empty($fetch_tasks)) { - $fetch_tasks = _update_get_cache_multiple('fetch_task'); + $fetch_tasks = drupal_container()->get('keyvalue')->get('update_fetch_task')->getAll(); } - $cid = 'fetch_task::' . $project['name']; - if (empty($fetch_tasks[$cid])) { + if (empty($fetch_tasks[$project['name']])) { $queue = queue('update_fetch_tasks'); $queue->createItem($project); - // Due to race conditions, it is possible that another process already - // inserted a row into the {cache_update} table and the following query will - // throw an exception. - // @todo: Remove the need for the manual check by relying on a queue that - // enforces unique items. - try { - db_insert('cache_update') - ->fields(array( - 'cid' => $cid, - 'created' => REQUEST_TIME, - )) - ->execute(); - } - catch (Exception $e) { - // The exception can be ignored safely. - } - $fetch_tasks[$cid] = REQUEST_TIME; + drupal_container()->get('keyvalue')->get('update_fetch_task')->set($project['name'], $project); + $fetch_tasks[$project['name']] = REQUEST_TIME; } } diff --git a/core/modules/update/update.install b/core/modules/update/update.install index d6c32d6..2d4ec4b 100644 --- a/core/modules/update/update.install +++ b/core/modules/update/update.install @@ -57,15 +57,6 @@ function update_requirements($phase) { } /** - * Implements hook_schema(). - */ -function update_schema() { - $schema['cache_update'] = drupal_get_schema_unprocessed('system', 'cache'); - $schema['cache_update']['description'] = 'Cache table for the Update module to store information about available releases, fetched from central server.'; - return $schema; -} - -/** * Implements hook_install(). */ function update_install() { @@ -175,3 +166,10 @@ function update_update_8001() { ); update_variables_to_state($variable_map); } + +/** + * Deletes the {cache_update} table. + */ +function update_update_8002() { + db_drop_table('cache_update'); +} diff --git a/core/modules/update/update.module b/core/modules/update/update.module index 596e990..d750949 100644 --- a/core/modules/update/update.module +++ b/core/modules/update/update.module @@ -286,7 +286,7 @@ function update_cron() { $last_check = state()->get('update.last_check') ?: 0; if ((REQUEST_TIME - $last_check) > $interval) { // If the configured update interval has elapsed, we want to invalidate - // the cached data for all projects, attempt to re-fetch, and trigger any + // the data for all projects, attempt to re-fetch, and trigger any // configured notifications about the new status. update_refresh(); update_fetch_data(); @@ -311,33 +311,33 @@ function update_cron() { /** * Implements hook_themes_enabled(). * - * If themes are enabled, we invalidate the cache of available updates. + * If themes are enabled, we invalidate the information of available updates. */ function update_themes_enabled($themes) { - // Clear all update module caches. - _update_cache_clear(); + // Clear all update module data. + update_storage_clear(); } /** * Implements hook_themes_disabled(). * - * If themes are disabled, we invalidate the cache of available updates. + * If themes are disabled, we invalidate the information of available updates. */ function update_themes_disabled($themes) { - // Clear all update module caches. - _update_cache_clear(); + // Clear all update module data. + update_storage_clear(); } /** * Implements hook_form_FORM_ID_alter() for system_modules(). * * Adds a form submission handler to the system modules form, so that if a site - * admin saves the form, we invalidate the cache of available updates. + * admin saves the form, we invalidate the information of available updates. * * @see _update_cache_clear() */ function update_form_system_modules_alter(&$form, $form_state) { - $form['#submit'][] = 'update_cache_clear_submit'; + $form['#submit'][] = 'update_storage_clear_submit'; } /** @@ -345,9 +345,9 @@ function update_form_system_modules_alter(&$form, $form_state) { * * @see update_form_system_modules_alter() */ -function update_cache_clear_submit($form, &$form_state) { - // Clear all update module caches. - _update_cache_clear(); +function update_storage_clear_submit($form, &$form_state) { + // Clear all update module data. + update_storage_clear(); } /** @@ -362,9 +362,9 @@ function _update_no_data() { } /** - * Tries to get update information from cache and refreshes it when necessary. + * Tries to get update information and refreshes it when necessary. * - * In addition to checking the cache lifetime, this function also ensures that + * In addition to checking the lifetime, this function also ensures that * there are no .info.yml files for enabled modules or themes that have a newer * modification timestamp than the last time we checked for available update * data. If any .info.yml file was modified, it almost certainly means a new @@ -373,8 +373,8 @@ function _update_no_data() { * bogus results. * * @param $refresh - * (optional) Boolean to indicate if this method should refresh the cache - * automatically if there's no data. Defaults to FALSE. + * (optional) Boolean to indicate if this method should refresh automatically + * if there's no data. Defaults to FALSE. * * @return * Array of data about available releases, keyed by project shortname. @@ -386,9 +386,8 @@ function update_get_available($refresh = FALSE) { module_load_include('inc', 'update', 'update.compare'); $needs_refresh = FALSE; - // Grab whatever data we currently have cached in the DB. - $available = _update_get_cached_available_releases(); - $num_avail = count($available); + // Grab whatever data we currently have. + $available = drupal_container()->get('keyvalue.expirable')->get('update_available_releases')->getAll(); $projects = update_get_projects(); foreach ($projects as $key => $project) { @@ -426,8 +425,8 @@ function update_get_available($refresh = FALSE) { // Attempt to drain the queue of fetch tasks. update_fetch_data(); // After processing the queue, we've (hopefully) got better data, so pull - // the latest from the cache again and use that directly. - $available = _update_get_cached_available_releases(); + // the latest data again and use that directly. + $available = drupal_container()->get('keyvalue.expirable')->get('update_available_releases')->getAll(); } return $available; @@ -468,29 +467,6 @@ function update_fetch_data() { } /** - * Returns all currently cached data about available releases for all projects. - * - * @return - * Array of data about available releases, keyed by project shortname. - */ -function _update_get_cached_available_releases() { - $data = array(); - $cache_items = _update_get_cache_multiple('available_releases'); - foreach ($cache_items as $cid => $cache) { - $cache->data['last_fetch'] = $cache->created; - if ($cache->expire < REQUEST_TIME) { - $cache->data['fetch_status'] = UPDATE_FETCH_PENDING; - } - // The project shortname is embedded in the cache ID, even if there's no - // data for this project in the DB at all, so use that for the indexes in - // the array. - $parts = explode('::', $cid, 2); - $data[$parts[1]] = $cache->data; - } - return $data; -} - -/** * Implements hook_mail(). * * Constructs the e-mail notification message when the site is out of date. @@ -725,165 +701,14 @@ function update_verify_update_archive($project, $archive_file, $directory) { } /** - * @defgroup update_status_cache Private update status cache system - * @{ - * Functions to manage the update status cache. - * - * We specifically do NOT use the core cache API for saving the fetched data - * about available updates. It is vitally important that this cache is only - * cleared when we're populating it after successfully fetching new available - * update data. Usage of the core cache API results in all sorts of potential - * problems that would result in attempting to fetch available update data all - * the time, including if a site has a "minimum cache lifetime" (which is both a - * minimum and a maximum) defined, or if a site uses memcache or another - * pluggable cache system that assumes volatile caches. - * - * The Update Manager module still uses the {cache_update} table, but instead of - * using the cache API, there are private helper functions that implement these - * same basic tasks but ensure that the cache is not prematurely cleared, and - * that the data is always stored in the database, even if memcache or another - * cache backend is in use. - */ - -/** - * Stores data in the private update status cache table. - * - * @param $cid - * The cache ID to save the data with. - * @param $data - * The data to store. - * @param $expire - * One of the following values: - * - CacheBackendInterface::CACHE_PERMANENT: Indicates that the item should - * never be removed except by explicitly using _update_cache_clear(). - * - A Unix timestamp: Indicates that the item should be kept at least until - * the given time, after which it will be invalidated. - * - * @see _update_cache_get() - */ -function _update_cache_set($cid, $data, $expire) { - $fields = array( - 'created' => REQUEST_TIME, - 'expire' => $expire, - ); - if (!is_string($data)) { - $fields['data'] = serialize($data); - $fields['serialized'] = 1; - } - else { - $fields['data'] = $data; - $fields['serialized'] = 0; - } - db_merge('cache_update') - ->key(array('cid' => $cid)) - ->fields($fields) - ->execute(); -} - -/** - * Retrieves data from the private update status cache table. - * - * @param $cid - * The cache ID to retrieve. - * - * @return - * An array of data for the given cache ID, or NULL if the ID was not found. - * - * @see _update_cache_set() - */ -function _update_cache_get($cid) { - $cache = db_query("SELECT data, created, expire, serialized FROM {cache_update} WHERE cid = :cid", array(':cid' => $cid))->fetchObject(); - if (isset($cache->data)) { - if ($cache->serialized) { - $cache->data = unserialize($cache->data); - } - } - return $cache; -} - -/** - * Returns an array of cache items with a given cache ID prefix. - * - * @param string $cid_prefix - * The cache ID prefix. - * - * @return - * Associative array of cache items, keyed by cache ID. - */ -function _update_get_cache_multiple($cid_prefix) { - $data = array(); - $result = db_select('cache_update') - ->fields('cache_update', array('cid', 'data', 'created', 'expire', 'serialized')) - ->condition('cache_update.cid', $cid_prefix . '::%', 'LIKE') - ->execute(); - foreach ($result as $cache) { - if ($cache) { - if ($cache->serialized) { - $cache->data = unserialize($cache->data); - } - $data[$cache->cid] = $cache; - } - } - return $data; -} - -/** - * Invalidates cached data relating to update status. - * - * @param $cid - * (optional) Cache ID of the record to clear from the private update module - * cache. If empty, all records will be cleared from the table except fetch - * tasks. Defaults to NULL. - * @param $wildcard - * (optional) If TRUE, cache IDs starting with $cid are deleted in addition to - * the exact cache ID specified by $cid. Defaults to FALSE. - */ -function _update_cache_clear($cid = NULL, $wildcard = FALSE) { - if (empty($cid)) { - db_delete('cache_update') - // Clear everything except fetch task information because these are used - // to ensure that the fetch task queue items are not added multiple times. - ->condition('cid', 'fetch_task::%', 'NOT LIKE') - ->execute(); - } - else { - $query = db_delete('cache_update'); - if ($wildcard) { - $query->condition('cid', $cid . '%', 'LIKE'); - } - else { - $query->condition('cid', $cid); - } - $query->execute(); - } -} - -/** - * Implements hook_cache_flush(). - * - * Called from update.php (among others) to flush the caches. Since we're - * running update.php, we are likely to install a new version of something, in - * which case, we want to check for available update data again. However, - * because we have our own caching system, we need to directly clear the - * the database table ourselves at this point and return nothing. - * - * However, we only want to do this from update.php, since otherwise, we'd lose - * all the available update data on every cron run. So, we specifically check if - * the site is in MAINTENANCE_MODE == 'update' (which indicates update.php is - * running, not update module... alas for overloaded names). + * Invalidates stored data relating to update status. */ -function update_cache_flush() { - if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update') { - _update_cache_clear(); - } - return array(); +function update_storage_clear() { + drupal_container()->get('keyvalue.expirable')->get('update')->deleteAll(); + drupal_container()->get('keyvalue.expirable')->get('update_available_release')->deleteAll(); } /** - * @} End of "defgroup update_status_cache". - */ - -/** * Returns a short unique identifier for this Drupal installation. * * @return diff --git a/core/update.php b/core/update.php index 99975d0..7d41e9f 100644 --- a/core/update.php +++ b/core/update.php @@ -271,9 +271,8 @@ function update_info_page() { // Change query-strings on css/js files to enforce reload for all users. _drupal_flush_css_js(); // Flush the cache of all data for the update status module. - if (db_table_exists('cache_update')) { - cache('update')->deleteAll(); - } + drupal_container()->get('keyvalue.expirable')->get('update')->deleteAll(); + drupal_container()->get('keyvalue.expirable')->get('update_available_release')->deleteAll(); update_task_list('info'); drupal_set_title('Drupal database update');