--- includes/bootstrap.inc.orig 2006-02-01 17:04:30.000000000 -0500 +++ includes/bootstrap.inc 2006-02-01 16:58:00.000000000 -0500 @@ -10,7 +10,8 @@ define('CACHE_TEMPORARY', -1); define('CACHE_DISABLED', 0); -define('CACHE_ENABLED', 1); +define('CACHE_DATABASE', 1); +define('CACHE_FILE', 2); define('WATCHDOG_NOTICE', 0); define('WATCHDOG_WARNING', 1); @@ -187,7 +188,7 @@ * file. */ function variable_init($conf = array()) { - // NOTE: caching the variables improves performance with 20% when serving cached pages. + // NOTE: caching variables improves performance 20% when serving cached pages. if ($cached = cache_get('variables')) { $variables = unserialize($cached->data); } @@ -196,7 +197,8 @@ while ($variable = db_fetch_object($result)) { $variables[$variable->name] = unserialize($variable->value); } - cache_set('variables', serialize($variables)); + // cache_set will serialize the $variables array for us + cache_set('variables', $variables); } foreach ($conf as $name => $value) { @@ -267,16 +269,44 @@ */ function cache_get($key) { global $user; + $cache = NULL; + + if (variable_get('cache', CACHE_DISABLED) == CACHE_FILE) { + $filecache = TRUE; + } + else { + $filecache = FALSE; + } - // Garbage collection necessary when enforcing a minimum cache lifetime $cache_flush = variable_get('cache_flush', 0); - if ($cache_flush && ($cache_flush + variable_get('cache_lifetime', 0) <= time())) { - // Time to flush old cache data - db_query("DELETE FROM {cache} WHERE expire != %d AND expire <= %d", CACHE_PERMANENT, $cache_flush); - variable_set('cache_flush', 0); + if ($filecache) { + $cachefile = cache_filename($key); + if (file_exists($cachefile)) { + // be sure cache file was created since the last cache flush + if (!$cache_flush || filemtime($cachefile) > ($cache_flush - variable_get('cache_lifetime', 0))) { + if ($fp = fopen($cachefile, 'r')) { + if (flock($fp, LOCK_SH)) { + $data = fread($fp, filesize($cachefile)); + flock($fp, LOCK_UN); + $cache = unserialize($data); + } + fclose($fp); + } + } + } + } + else { + // Database garbage collection necessary when enforcing a minimum cache + // lifetime + if ($cache_flush && ($cache_flush + variable_get('cache_lifetime', 0)) <= time()) { + // Time to flush old cache data + db_query("DELETE FROM {cache} WHERE expire != %d AND expire <= %d", CACHE_PERMANENT, $cache_flush); + variable_set('cache_flush', 0); + } + + $cache = db_fetch_object(db_query("SELECT data, created, headers, expire FROM {cache} WHERE cid = '%s'", $key)); } - $cache = db_fetch_object(db_query("SELECT data, created, headers, expire FROM {cache} WHERE cid = '%s'", $key)); if (isset($cache->data)) { // If the data is permanent or we're not enforcing a minimum cache lifetime // always return the cached data. @@ -321,12 +351,59 @@ * A string containing HTTP header information for cached pages. */ function cache_set($cid, $data, $expire = CACHE_PERMANENT, $headers = NULL) { - db_lock_table('cache'); - db_query("UPDATE {cache} SET data = %b, created = %d, expire = %d, headers = '%s' WHERE cid = '%s'", $data, time(), $expire, $headers, $cid); - if (!db_affected_rows()) { - @db_query("INSERT INTO {cache} (cid, data, created, expire, headers) VALUES ('%s', %b, %d, %d, '%s')", $cid, $data, time(), $expire, $headers); + // if caching the variable tables, a call to variable_get() won't be valid + if ($cid == 'variables') { + $filecache = ($data['cache'] == CACHE_FILE); + $data = serialize($data); } - db_unlock_tables(); + else { + $filecache = (variable_get('cache', CACHE_DISABLED) == CACHE_FILE); + } + + if ($filecache) { + // prepare the cache before grabbing the file lock + $cache->cid = $cid; + $cache->data = $data; + $cache->created = time(); + $cache->expire = $expire; + $cache->headers = $headers; + $data = serialize($cache); + + if ($fp = fopen(cache_filename($cid), 'w')) { + // only write to cache file if we can obtain an exclusive lock + if (flock($fp, LOCK_EX)) { + fwrite($fp, $data); + flock($fp, LOCK_UN); + } + fclose($fp); + } + else { + drupal_set_message('Cache error, unable to open file.', 'error'); + } + } + else { + db_lock_table('cache'); + db_query("UPDATE {cache} SET data = %b, created = %d, expire = %d, headers = '%s' WHERE cid = '%s'", $data, time(), $expire, $headers, $cid); + if (!db_affected_rows()) { + @db_query("INSERT INTO {cache} (cid, data, created, expire, headers) VALUES ('%s', %b, %d, %d, '%s')", $cid, $data, time(), $expire, $headers); + } + db_unlock_tables(); + } +} + +/** + * Sanitize the cid for use as a filename in file-based caching. + * + * @param $cid + * The cache ID of the data to store. + */ +function cache_filename($cid) { + // Use the 32 character md5 hash of the cid. A hash collision is _very_ + // unlikely, but if this ever becomes a problem we can enhance this function. + $filename = md5($cid); + + // we can't call file_directory_path() nor t() when in an early bootstrap + return variable_get('file_directory_path', 'files') .'/'. variable_get('cache_path', 'cache') .'/'. $filename; } /** @@ -343,6 +420,13 @@ function cache_clear_all($cid = NULL, $wildcard = false) { global $user; + if (variable_get('cache', CACHE_DISABLED) == CACHE_FILE) { + $filecache = TRUE; + } + else { + $filecache = FALSE; + } + if (empty($cid)) { if (variable_get('cache_lifetime', 0)) { // We store the time in the current user's $user->cache variable which @@ -352,22 +436,47 @@ $user->cache = time(); $cache_flush = variable_get('cache_flush', 0); - if ($cache_flush == 0) { - // This is the first request to clear the cache, start a timer. - variable_set('cache_flush', time()); - } - else if (time() > ($cache_flush + variable_get('cache_lifetime', 0))) { - // Clear the cache for everyone, cache_flush_delay seconds have - // passed since the first request to clear the cache. - db_query("DELETE FROM {cache} WHERE expire != %d AND expire < %d", CACHE_PERMANENT, time()); - variable_set('cache_flush', 0); + $cache_delete_files = variable_get('cache_delete_files', 0); + if ($filecache) { + if ($cache_delete_files == 0) { + variable_set('cache_flush', time()); + variable_set('cache_delete_files', time()); + } + + if (time() > ($cache_flush + variable_get('cache_lifetime', 0))) { + // Updating cache_flush invalidates all data cached before this time + variable_set('cache_flush', time()); + } + } + else { + if ($cache_flush == 0) { + // This is the first request to clear the cache, start a timer. + variable_set('cache_flush', time()); + } + elseif (time() > ($cache_flush + variable_get('cache_lifetime', 0))) { + // Clear the cache for everyone, cache_flush_delay seconds have + // passed since the first request to clear the cache. + db_query("DELETE FROM {cache} WHERE expire != %d AND expire < %d", CACHE_PERMANENT, time()); + variable_set('cache_flush', 0); + } } } - else { + elseif (!$filecache) { // No minimum cache lifetime, flush all temporary cache entries now. db_query("DELETE FROM {cache} WHERE expire != %d AND expire < %d", CACHE_PERMANENT, time()); } } + elseif ($filecache) { + // TODO: what about wildcards? won't work with md5's... + $file = cache_filename($cid); + if ($fp = fopen($file, 'w')) { + // only delete the cache file if we can obtain an exclusive lock (we + // don't want to delete a cache file someone is currently reading...) + if (flock($fp, LOCK_EX)) { + unlink($file); + } + } + } else { if ($wildcard) { db_query("DELETE FROM {cache} WHERE cid LIKE '%%%s%%'", $cid); --- modules/system.module.orig 2006-02-01 17:04:40.000000000 -0500 +++ modules/system.module 2006-02-01 16:53:37.000000000 -0500 @@ -154,6 +154,33 @@ } /** + * Implementation of hook_cron(). + */ +function system_cron() { + // garbage collection if file cache enabled + if (variable_get('cache', CACHE_DISABLED) == CACHE_FILE) { + // delete all files older than last cache_flush_all() call + if (variable_get('cache_delete_files', 0)) { + $files = file_scan_directory(file_directory_path() .'/'. variable_get('cache_path', 'cache'), '.', array('.', '..', 'CVS')); + foreach ($files as $file) { + if (file_exists($file->filename)) { + if (filemtime($file->filename) < (time() - variable_get('cache_lifetime', 0))) { + if ($fp = fopen($file->filename, 'r')) { + // We need an exclusive lock, but don't block if we can't get it + // as we can try again next time cron runs. + if (flock($fp, LOCK_EX|LOCK_UN)) { + unlink($file->filename); + } + } + } + } + } + variable_set('cache_delete_files', 0); + } + } +} + +/** * Menu callback: dummy clean URL tester. */ function system_test() { @@ -316,15 +343,16 @@ $form['cache']['cache'] = array( '#type' => 'radios', '#title' => t('Page cache'), '#default_value' => variable_get('cache', CACHE_DISABLED), - '#options' => array(CACHE_DISABLED => t('Disabled'), CACHE_ENABLED => t('Enabled')), - '#description' => t("Drupal has a caching mechanism which stores dynamically generated web pages in a database. By caching a web page, Drupal does not have to create the page each time someone wants to view it, instead it takes only one SQL query to display it, reducing response time and the server's load. Only pages requested by \"anonymous\" users are cached. In order to reduce server load and save bandwidth, Drupal stores and sends compressed cached pages.") + '#options' => array(CACHE_DISABLED => t('Disabled'), CACHE_DATABASE => t('Database'), CACHE_FILE => t('File')), + '#description' => t("Drupal provides a mechanism to cache dynamically generated web pages. By caching a web page, Drupal does not have to create the page each time someone wants to view it, instead it simply loads the page out of the cache, reducing response time and the server's load. Only pages requested by \"anonymous\" users are cached. In order to reduce server load and save bandwidth, Drupal stores and sends compressed cached pages. Caching can be done to the database or to the filesystem.") ); + $form['cache']['cachepath'] = array('#type' => 'hidden', '#value'=> file_directory_path() .'/'. 'cache', '#after_build' => 'system_check_filecache'); $period = drupal_map_assoc(array(0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 86400), 'format_interval'); $period[0] = t('none'); $form['cache']['cache_lifetime'] = array( '#type' => 'select', '#title' => t('Minimum cache lifetime'), '#default_value' => variable_get('cache_lifetime', 0), '#options' => $period, - '#description' => t('Enabling the cache will offer a sufficient performance boost for most low-traffic and medium-traffic sites. On high-traffic sites it can become necessary to enforce a minimum cache lifetime. The minimum cache lifetime is the minimum amount of time that will go by before the cache is emptied and recreated. A larger minimum cache lifetime offers better performance, but users will not see new content for a longer period of time.') + '#description' => t('Enabling the cache will offer a sufficient performance boost for most low-traffic and medium-traffic sites. On high-traffic sites it can become necessary to enforce a minimum cache lifetime. The minimum cache lifetime is the minimum amount of time that will go by before the cache is emptied and recreated. A larger minimum cache lifetime offers better performance, but users will not see new content for a longer period of time. When caching to the filesystem, you must set a minimum cache lifetime to see any benefit from the caching.') ); @@ -496,6 +524,12 @@ return $form_element; } +function system_check_filecache($form_element) { + if (variable_get('cache', CACHE_DISABLED) == CACHE_FILE) { + file_check_directory($form_element['#value'], FILE_CREATE_DIRECTORY, 'cache'); + } +} + /** * Return the cron status and errors for admin/settings. */