diff -uprN modules/update_status/update_status.install modules/update_status_new/update_status.install --- modules/update_status/update_status.install 2007-08-15 10:54:00.000000000 -0500 +++ modules/update_status_new/update_status.install 2009-05-10 15:47:55.351003037 -0500 @@ -2,6 +2,33 @@ // $Id: update_status.install,v 1.1.2.6 2007/08/15 15:54:00 dww Exp $ /** + * Implementation of hook_install(). + */ +function update_status_install() { + switch ($GLOBALS['db_type']) { + case 'mysql': + case 'mysqli': + db_query("CREATE TABLE {cache_update_status} ( + cid varchar(255) NOT NULL default '', + data longblob, + expire int(11) NOT NULL default '0', + created int(11) NOT NULL default '0', + PRIMARY KEY (cid) + )"); + break; + case 'pgsql': + db_query("CREATE TABLE {cache_update_status} ( + cid varchar(255) NOT NULL default '', + data bytea, + expire int NOT NULL default '0', + created int NOT NULL default '0', + PRIMARY KEY (cid) + )"); + break; + } +} + +/** * Implementation of hook_uninstall(). */ function update_status_uninstall() { @@ -16,6 +43,7 @@ function update_status_uninstall() { foreach ($variables as $variable) { variable_del($variable); } + db_query("DROP TABLE {cache_update}"); cache_clear_all(); } @@ -62,3 +90,32 @@ function update_status_update_5202() { menu_rebuild(); return $ret; } + +/** + * Backport cache functionality from 6.x (#155450.) + */ +function update_status_update_5205() { + $ret = array(); + switch ($GLOBALS['db_type']) { + case 'mysql': + case 'mysqli': + $ret[] = update_sql("CREATE TABLE {cache_update_status} ( + cid varchar(255) NOT NULL default '', + data longblob, + expire int(11) NOT NULL default '0', + created int(11) NOT NULL default '0', + PRIMARY KEY (cid) + )"); + break; + case 'pgsql': + $ret[] = update_sql("CREATE TABLE {cache_update_status} ( + cid varchar(255) NOT NULL default '', + data bytea, + expire int NOT NULL default '0', + created int NOT NULL default '0', + PRIMARY KEY (cid) + )"); + break; + } + return $ret; +} diff -uprN modules/update_status/update_status.module modules/update_status_new/update_status.module --- modules/update_status/update_status.module 2008-08-14 14:01:19.000000000 -0500 +++ modules/update_status_new/update_status.module 2009-05-10 16:25:33.097442821 -0500 @@ -390,7 +390,7 @@ function update_status_requirements($pha * * @param $project * Array of information about the project we're testing as returned by - * update_calculate_project_data(). + * update_status_calculate_project_data(). * @param $type * What kind of project is this ('core' or 'contrib'). * @@ -460,7 +460,7 @@ function update_status_cron() { * Perform any notifications that should be done once cron fetches new data. * * This method checks the status of the site using the new data and depending - * on the configuration of the site, notifys administrators via email if there + * on the configuration of the site, notifies administrators via email if there * are new releases or missing security updates. * * @see update_status_requirements() @@ -514,9 +514,9 @@ function update_status_system_submit($fo * Helper function to return the appropriate message text when the site is out * of date or missing a security update. * - * These error messages are shared by both update_requirements() for the + * These error messages are shared by both update_status_requirements() for the * site-wide status report at admin/logs/status and in the body of the - * notification emails generated by update_cron(). + * notification emails generated by update_status_cron(). * * @param $msg_type * String to indicate what kind of message to generate. Can be either @@ -620,8 +620,18 @@ function _update_status_no_data() { * logic is only required when preparing the status report, not for 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_status} table using the + * 'update_status_project_projects' cache ID. 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. + * * @see update_status_process_project_info() * @see update_status_calculate_project_data() + * @see update_status_project_cache() * * @todo * Extend this to include themes and theme engines when they get .info files. @@ -706,7 +716,7 @@ function update_status_get_projects() { } } asort($projects); - cache_set('update_status_projects', 'cache', serialize($projects), time() + (60 * 60)); + _update_status_cache_set('update_status_projects', serialize($projects), time() + (60 * 60)); return $projects; } @@ -838,12 +848,23 @@ function update_status_process_project_i * version (e.g. 5.x-1.5-beta1, 5.x-1.5-beta2, and 5.x-1.5). Development * snapshots for a given major version are always listed last. * + * The results of this function are expensive to compute, especially on sites + * with lots of modules, since it involves a lot of comparisons and + * other operations. Therefore, we cache the results into the {cache_update_status} + * table using the 'update_status_project_data' cache ID. 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. + * * @param $available * Array of data about available project releases. * * @see update_status_get_available() * @see update_status_get_projects() * @see update_status_process_project_info() + * @see update_status_project_cache() */ function update_status_calculate_project_data($available) { @@ -1061,7 +1082,7 @@ function update_status_calculate_project // If we're running a dev snapshot and have a timestamp, stop // searching for security updates once we hit an official release - // older than what we've got. Allow 100 seconds of leeway to handle + // older than what we've got. Allow 100 seconds of leeway to handle // differences between the datestamp in the .info file and the // timestamp of the tarball itself (which are usually off by 1 or 2 // seconds) so that we don't flag that as a new release. @@ -1186,7 +1207,7 @@ function update_status_calculate_project $projects[$project]['reason'] = t('No available releases found'); } } - cache_set('update_status_data', 'cache', serialize($projects), time() + (60 * 60)); + _update_status_cache_set('update_status_data', serialize($projects), time() + (60 * 60)); return $projects; } @@ -1384,11 +1405,14 @@ function update_status_refresh() { global $base_url; // Since we're fetching new available update data, we want to clear - // everything in our cache, to ensure we recompute the status. Note that - // this does not cause update_status_get_projects() to be recomputed twice - // in the same page load (e.g. when manually checking) since that function - // stashes its answer in a static array. - update_status_invalidate_cache(); + // 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_status_get_projects(); for example, to modules that implement + // hook_system_info_alter() such as cvs_deploy. + _update_status_cache_clear('update_status_project_projects'); + _update_status_cache_clear('update_status_project_data'); + // If not in 'safe mode', increase the maximum execution time: if (!ini_get('safe_mode')) { @@ -1397,10 +1421,14 @@ function update_status_refresh() { $available = array(); $data = array(); - $site_key = md5($base_url . drupal_get_private_key()); - $projects = update_status_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_status_cache_clear('update_status_available_releases'); + foreach ($projects as $key => $project) { $url = _update_status_build_fetch_url($project, $site_key); $xml = drupal_http_request($url); @@ -1412,7 +1440,7 @@ function update_status_refresh() { if ($data) { $parser = new update_status_xml_parser; $available = $parser->parse($data); - cache_set('update_status_info', 'cache', serialize($available), time() + (60 * 60 * 24)); + _update_status_cache_set('update_status_info', serialize($available), time() + (60 * 60 * 24)); variable_set('update_status_last', time()); watchdog('update_status', t('Fetched information about all available new releases and updates.'), WATCHDOG_NOTICE, l(t('view'), 'admin/logs/updates')); } @@ -1483,7 +1511,7 @@ function update_status_get_available($re } } - if (!$needs_refresh && ($cache = cache_get('update_status_info', 'cache')) + if (!$needs_refresh && ($cache = _update_status_cache_get('update_status_info')) && $cache->expire > time()) { $available = unserialize($cache->data); } @@ -1494,13 +1522,6 @@ function update_status_get_available($re } /** - * Invalidates any cached data relating to update status. - */ -function update_status_invalidate_cache() { - cache_clear_all('update_status_', 'cache', TRUE); -} - -/** * XML Parser object to read Drupal's project info files * This uses PHP4's lame XML parsing, but it works. Mostly. */ @@ -1598,7 +1619,7 @@ class update_status_xml_parser { /** * Private sort function to order projects based on their status. * - * @see update_requirements() + * @see update_status_requirements() * @see uasort() */ function _update_status_project_status_sort($a, $b) { @@ -1612,12 +1633,12 @@ function _update_status_project_status_s } /** - * Retrieve data from {cache} or empty the cache when necessary. + * Retrieve data from {cache_update_status} or empty the cache when necessary. * * Two very expensive arrays computed by this module are the list of all * installed modules (and .info 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} table and used + * releases. These two arrays are cached in the {cache_update_status} table and used * whenever possible. The cache is cleared whenever the administrator visits * the status report, available updates report, or the module administration * pages, since we should always recompute the most current values on any of @@ -1635,18 +1656,126 @@ function _update_status_project_status_s function update_status_project_cache($cid) { $projects = array(); - // In some cases, we must clear the cache. Rather than do so on a time - // basis, we check for specific paths. + // On certain paths, we should clear the cache and recompute the projects or + // update status of the site to avoid presenting stale information. $q = $_GET['q']; $paths = array('admin/build/modules', 'admin/logs', 'admin/logs/updates', 'admin/logs/status', 'admin/logs/updates/check'); if (in_array($q, $paths)) { - cache_clear_all($cid, 'cache'); + _update_status_cache_clear($cid); } else { - $cache = cache_get($cid, 'cache'); + $cache = _update_status_cache_get($cid); if (!empty($cache->data) && $cache->expire > time()) { $projects = unserialize($cache->data); } } return $projects; } + +/** + * @defgroup update_status_cache Private update status cache system + * @{ + * + * 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 + * plug-able cache system that assumes volatile caches. + * + * Update module uses the {cache_update_status} table, but instead of using + * cache_set(), cache_get(), and cache_clear_all(), 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. + */ + +/** + * Store 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: + * - CACHE_PERMANENT: Indicates that the item should never be removed except + * by explicitly using _update_status_cache_clear() or update_status_invalidate_cache(). + * - A Unix timestamp: Indicates that the item should be kept at least until + * the given time, after which it will be invalidated. + */ +function _update_status_cache_set($cid, $data, $expire) { + $created = time(); + db_query("UPDATE {cache_update_status} SET data = %b, created = %d, expire = %d WHERE cid = '%s'", $data, $created, $expire, $cid); + if (!db_affected_rows()) { + @db_query("INSERT INTO {cache_update_status} (cid, data, created, expire) VALUES ('%s', %b, %d, %d)", $cid, $data, $created, $expire); + } +} + +/** + * Retrieve data from the private update status cache table. + * + * @param $cid + * The cache ID to retrieve. + * @return + * The data for the given cache ID, or NULL if the ID was not found. + */ +function _update_status_cache_get($cid) { + $cache = db_fetch_object(db_query("SELECT data, created, expire FROM {cache_update_status} WHERE cid = '%s'", $cid)); + if (isset($cache->data)) { + $cache->data = db_decode_blob($cache->data); + } + return $cache; +} + +/** + * Invalidates specific 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. + */ +function _update_status_cache_clear($cid = NULL) { + if (empty($cid)) { + db_query("DELETE FROM {cache_update_status}"); + } + else { + db_query("DELETE FROM {cache_update_status} WHERE cid = '%s'", $cid); + } +} + +/** + * Implementation of hook_flush_caches(). + * + * 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 database table ourselves at this point and return nothing, for example, + * on sites that use memcache where cache_clear_all() won't know how to purge + * this data. + * + * 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). + */ +function update_status_flush_caches() { + if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update') { + _update_status_cache_clear(); + } + return array(); +} + +/** + * Invalidates all cached data relating to update status. + */ +function update_status_invalidate_cache() { + _update_status_cache_clear(); +} + +/** + * @} End of "defgroup update_status_cache". + */