Index: CHANGELOG.txt =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/update_status/CHANGELOG.txt,v retrieving revision 1.11.2.20 diff -u -p -r1.11.2.20 CHANGELOG.txt --- CHANGELOG.txt 27 Sep 2008 05:59:47 -0000 1.11.2.20 +++ CHANGELOG.txt 1 Jun 2009 17:42:53 -0000 @@ -1,6 +1,8 @@ $Id: CHANGELOG.txt,v 1.11.2.20 2008/09/27 05:59:47 dww Exp $ Update Status 5.x-2.x-dev + o #155450 by JoshuaRogers, dww: Moved cached data into a separate + table in the database using a private (non-volatile) cache API. o #314009 by igor.stamatovski: Added a Macedonian (mk) translation. @@ -207,4 +209,4 @@ Update Status 5.x-1.1 o #124440 and #126298: Settings page, set to check or not, and notes to remind yourself why. Update Status 5.x-1.0 - Initial release \ No newline at end of file + Initial release Index: update_status.install =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/update_status/update_status.install,v retrieving revision 1.1.2.6 diff -u -p -r1.1.2.6 update_status.install --- update_status.install 15 Aug 2007 15:54:00 -0000 1.1.2.6 +++ update_status.install 1 Jun 2009 17:42:53 -0000 @@ -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_status}"); cache_clear_all(); } @@ -62,3 +90,33 @@ function update_status_update_5202() { menu_rebuild(); return $ret; } + +/** + * Backport seperate cache table and functionality from 6.x (#155450). + */ +function update_status_update_5203() { + $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; +} + Index: update_status.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/update_status/update_status.module,v retrieving revision 1.83.2.40 diff -u -p -r1.83.2.40 update_status.module --- update_status.module 14 Aug 2008 19:01:19 -0000 1.83.2.40 +++ update_status.module 1 Jun 2009 17:42:54 -0000 @@ -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. @@ -632,7 +642,7 @@ function update_status_get_projects() { return $projects; } // Retrieve the projects from cache, if present. - $projects = update_status_project_cache('update_status_projects'); + $projects = update_status_project_cache('update_status_project_projects'); if (!empty($projects)) { return $projects; } @@ -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_project_projects', serialize($projects), time() + (60 * 60)); return $projects; } @@ -838,17 +848,28 @@ 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) { // Retrieve the projects from cache, if present. - $projects = update_status_project_cache('update_status_data'); + $projects = update_status_project_cache('update_status_project_data'); // If $projects is empty, then the cache must be rebuilt. // Otherwise, return the cached data and skip the rest of the function. if (!empty($projects)) { @@ -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_project_data', serialize($projects), time() + (60 * 60)); return $projects; } @@ -1384,11 +1405,13 @@ 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 +1420,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 +1439,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_available_releases', 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 +1510,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_available_releases')) && $cache->expire > time()) { $available = unserialize($cache->data); } @@ -1494,13 +1521,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 +1618,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 +1632,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 + * current status of the site relative to the currently available 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 @@ -1625,7 +1645,7 @@ function _update_status_project_status_s * * @param $cid * The cache id of data to return from the cache. Valid options are - * 'update_status_data' and 'update_status_projects'. + * 'update_status_project_data' and 'update_status_project_projects'. * * @return * The cached value of the $projects array generated by @@ -1635,18 +1655,104 @@ 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 status 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 status + * 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); + } +} + +/** + * Invalidates all cached data relating to update status. + */ +function update_status_invalidate_cache() { + _update_status_cache_clear(); +} + +/** + * @} End of "defgroup update_status_cache". + */