--- includes/bootstrap.inc.orig 2006-02-05 15:58:25.000000000 -0500 +++ includes/bootstrap.inc 2006-02-07 23:33:40.000000000 -0500 @@ -16,11 +16,15 @@ define('WATCHDOG_WARNING', 1); define('WATCHDOG_ERROR', 2); -define('DRUPAL_BOOTSTRAP_DATABASE', 0); -define('DRUPAL_BOOTSTRAP_SESSION', 1); -define('DRUPAL_BOOTSTRAP_PAGE_CACHE', 2); -define('DRUPAL_BOOTSTRAP_PATH', 3); -define('DRUPAL_BOOTSTRAP_FULL', 4); +define('NONE', 0); +define('ALL', -1); + +define('DRUPAL_BOOTSTRAP_FILE', 0); +define('DRUPAL_BOOTSTRAP_DATABASE', 1); +define('DRUPAL_BOOTSTRAP_SESSION', 2); +define('DRUPAL_BOOTSTRAP_PAGE_CACHE', 3); +define('DRUPAL_BOOTSTRAP_PATH', 4); +define('DRUPAL_BOOTSTRAP_FULL', 5); // these values should match the'role' table define('DRUPAL_ANONYMOUS_RID', 1); @@ -187,7 +191,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); } @@ -266,22 +270,29 @@ * The cache ID of the data to retrieve. */ function cache_get($key) { - global $user; + global $user, $file_cache, $cache_lifetime; - // 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 ($file_cache && is_dir($file_cache)) { + $cache = cache_get_file($key); + } + else { + // 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); + } + $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. - if ($cache->expire == CACHE_PERMANENT || !variable_get('cache_lifetime', 0)) { - $cache->data = db_decode_blob($cache->data); + if ($cache->expire == CACHE_PERMANENT || !$cache_lifetime) { + if (!$file_cache || !is_dir($file_cache)) { + $cache->data = db_decode_blob($cache->data); + } } // If enforcing a minimum cache lifetime, validate that the data is // currenly valid for this user before we return it by making sure the @@ -293,7 +304,7 @@ // This cache data is too old and thus not valid for us, ignore it. return 0; } - else { + elseif (!$file_cache || !is_dir($file_cache)) { $cache->data = db_decode_blob($cache->data); } } @@ -302,6 +313,29 @@ return 0; } +function cache_get_file($key) { + global $cache_lifetime; + + $cache = NULL; + $cache_file = cache_filename($key); + + if (file_exists($cache_file)) { + $cache_flush = variable_get('cache_flush', 0); + if (!$cache_flush || filemtime($cache_file) > ($cache_flush - $cache_lifetime)) { + if ($fp = fopen($cache_file, 'r')) { + if (flock($fp, LOCK_SH)) { + $data = fread($fp, filesize($cache_file)); + flock($fp, LOCK_UN); + $cache = unserialize($data); + } + fclose($fp); + } + } + } + + return $cache; +} + /** * Store data in the persistent cache. * @@ -321,12 +355,37 @@ * 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); + global $file_cache; + + if ($file_cache && is_dir($file_cache)) { + // 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 the 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(t('Cache error, failed to open file "%file"', array('%file' => cache_filename($cid))), '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(); } - db_unlock_tables(); } /** @@ -341,10 +400,10 @@ * complete ID. */ function cache_clear_all($cid = NULL, $wildcard = false) { - global $user; + global $user, $cache_lifetime, $file_cache; if (empty($cid)) { - if (variable_get('cache_lifetime', 0)) { + if ($cache_lifetime) { // We store the time in the current user's $user->cache variable which // will be saved into the sessions table by sess_write(). We then // simulate that the cache was flushed for this user by not returning @@ -352,15 +411,31 @@ $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_files_expired = variable_get('cache_files_expired', 0); + + if (is_dir($file_cache)) { + if ($cache_files_expired == 0) { + // updating cache_flush invalidates all data cached before this time + variable_set('cache_flush', time()); + // updating cache_files_expired so cron can delete expired files + variable_set('cache_files_expired', time()); + } + + if (time() > ($cache_flush + $cache_lifetime)) { + 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()); + } + else if (time() > ($cache_flush + $cache_lifetime)) { + // 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 { @@ -368,6 +443,17 @@ db_query("DELETE FROM {cache} WHERE expire != %d AND expire < %d", CACHE_PERMANENT, time()); } } + elseif (is_dir($file_cache)) { + // TODO: what about wildcards? Won't work with md5's... + $file = cache_filename($cid); + if ($fp = fopen($file, 'w')) { + // only delete the cache file once we obtain an exclusive lock to prevent + // deleting a cache file that is currently being read. + if (flock($fp, LOCK_EX)) { + unlink($file); + } + } + } else { if ($wildcard) { db_query("DELETE FROM {cache} WHERE cid LIKE '%%%s%%'", $cid); @@ -379,6 +465,31 @@ } /** + * Sanitize the cache ID for use as a filename when caching to the filesystem. + * + * @param $cid + * The unique ID of the cache data to store. + */ +function cache_filename($cid) { + global $file_cache; + return $file_cache .'/'. md5($cid); +} + +/** + * + */ +function drupal_file_cache() { + global $file_cache, $base_url; + if ($file_cache && is_dir($file_cache)) { + $cache = cache_get($base_url . request_uri()); + if (!empty($cache)) { + // display cached page and exit + drupal_page_header($cache); + } + } +} + +/** * Retrieve the current page from the cache. * * Note, we do not serve cached pages when status messages are waiting (from @@ -447,14 +558,59 @@ } /** + * Determine if the specified hook should be displayed to the current class of + * users. + */ +function drupal_should_invoke($hook) { + global $user, $_init, $_exit; + + if ($hook == 'init') { + $roles = $_init; + } + else { + $roles = $_exit; + } + + switch ($roles) { + case ALL: + $display = TRUE; + break; + + case NONE: + $display = FALSE; + break; + + default: + $roles = explode(',', $roles); + foreach ($roles as $rid) { + if ($user->roles[$rid]) { + $display = TRUE; + break; + } + } + $display = FALSE; + break; + + } + + return $display; +} + +/** * Set HTTP headers in preparation for a page response. * * @see page_set_cache */ -function drupal_page_header() { - if (variable_get('cache', 0)) { - if ($cache = page_get_cache()) { - bootstrap_invoke_all('init'); +function drupal_page_header($cache = NULL) { + global $file_cache; + if ($cache || $file_cache || variable_get('cache', 0)) { + // $cache will only be passed in if the file_cache is enabled and + // there was a failure to connect to the database. + $db_failed = $cache ? TRUE : FALSE; + if ($cache || $cache = page_get_cache()) { + if (!$db_failed && drupal_should_invoke('init')) { + bootstrap_invoke_all('init'); + } // Set default values: $date = gmdate('D, d M Y H:i:s', $cache->created) .' GMT'; $etag = '"'. md5($date) .'"'; @@ -497,7 +653,9 @@ } print $cache->data; - bootstrap_invoke_all('exit'); + if (!$db_failed && drupal_should_invoke('exit')) { + bootstrap_invoke_all('exit'); + } exit(); } else { @@ -663,7 +821,7 @@ * data. */ function drupal_bootstrap($phase) { - static $phases = array(DRUPAL_BOOTSTRAP_DATABASE, DRUPAL_BOOTSTRAP_SESSION, DRUPAL_BOOTSTRAP_PAGE_CACHE, DRUPAL_BOOTSTRAP_PATH, DRUPAL_BOOTSTRAP_FULL); + static $phases = array(DRUPAL_BOOTSTRAP_FILE, DRUPAL_BOOTSTRAP_DATABASE, DRUPAL_BOOTSTRAP_SESSION, DRUPAL_BOOTSTRAP_PAGE_CACHE, DRUPAL_BOOTSTRAP_PATH, DRUPAL_BOOTSTRAP_FULL); while (!is_null($current_phase = array_shift($phases))) { _drupal_bootstrap($current_phase); @@ -674,13 +832,20 @@ } function _drupal_bootstrap($phase) { - global $conf; + global $conf, $file_cache, $cache_lifetime, $_init, $_exit; switch ($phase) { - case DRUPAL_BOOTSTRAP_DATABASE: - global $db_url, $db_prefix, $base_url, $base_path; + case DRUPAL_BOOTSTRAP_FILE: + global $db_url, $db_prefix, $base_url, $base_path, $file_cache_fastpath; $conf = array(); require_once './' . conf_init() .'/settings.php'; + if (!$_COOKIE['drupal_uid'] && $file_cache_fastpath) { + // user not logged in, display page straight from file cache if enabled + drupal_file_cache(); + } + break; + + case DRUPAL_BOOTSTRAP_DATABASE: require_once './includes/database.inc'; // Initialize the default database. $parts = parse_url($base_url); --- includes/database.mysql.inc.orig 2006-02-05 15:58:45.000000000 -0500 +++ includes/database.mysql.inc 2006-02-05 15:29:52.000000000 -0500 @@ -51,6 +51,7 @@ // (matched) rows, not the number of affected rows. $connection = @mysql_connect($url['host'], $url['user'], $url['pass'], TRUE, 2); if (!$connection) { + drupal_file_cache(); drupal_maintenance_theme(); drupal_set_title('Unable to connect to database server'); print theme('maintenance_page', '

This either means that the username and password information in your settings.php file is incorrect or we can\'t contact the MySQL database server. This could mean your hosting provider\'s database server is down.

@@ -66,6 +67,7 @@ } if (!mysql_select_db(substr($url['path'], 1))) { + drupal_file_cache(); drupal_maintenance_theme(); drupal_set_title('Unable to select database'); print theme('maintenance_page', '

We were able to connect to the MySQL database server (which means your username and password are okay) but not able to select the database.

--- includes/database.pgsql.inc.orig 2006-02-05 15:58:55.000000000 -0500 +++ includes/database.pgsql.inc 2006-02-05 16:10:32.000000000 -0500 @@ -49,6 +49,7 @@ $connection = @pg_connect($conn_string); if (!$connection) { + drupal_file_cache(); drupal_maintenance_theme(); drupal_set_title('Unable to connect to database'); print theme('maintenance_page', '

This either means that the database information in your settings.php file is incorrect or we can\'t contact the PostgreSQL database server. This could mean your hosting provider\'s database server is down.

--- includes/session.inc.orig 2006-02-07 22:04:31.000000000 -0500 +++ includes/session.inc 2006-02-07 23:30:30.000000000 -0500 @@ -32,6 +32,10 @@ if ($user->uid) { $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user'; + // if the file_cache and file_cache_fastpath are both enabled, this cookie + // will be used to determine whether or not a user is logged in without + // actually loading all the session information from the database. + setcookie('drupal_uid', $user->uid); } else { $user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user'; --- modules/user.module.orig 2006-02-07 22:20:28.000000000 -0500 +++ modules/user.module 2006-02-07 22:33:54.000000000 -0500 @@ -964,6 +964,8 @@ // We have to use $GLOBALS to unset a global variable: $user = user_load(array('uid' => 0)); + setcookie('drupal_uid', ''); + drupal_goto(); } --- modules/system.module.orig 2006-02-05 16:27:53.000000000 -0500 +++ modules/system.module 2006-02-07 23:32:57.000000000 -0500 @@ -154,6 +154,33 @@ } /** + * Implementation of hook_cron(). + */ +function system_cron() { + global $file_cache, $cache_lifetime; + + // if using file-based caching, perform routine garbage collection + if ($file_cache && is_dir($file_cache)) { + // delete all files older than the last call to cache_flush_all() + if (variable_get('cache_files_expired', 0)) { + $files = file_scan_directory($file_cache, '.', array('.', '..', 'CVS')); + foreach ($files as $file) { + if (filemtime($file->filename) < (time() - $cache_lifetime)) { + if ($fp = fopen($file->filename, 'r')) { + // We need an exclusive lock, but don't block if we can't get it as + // we can simply try again next time cron is run. + if (flock($fp, LOCK_EX|LOCK_NB)) { + unlink($file->filename); + } + } + } + } + variable_set('cache_files_expired', 0); + } + } +} + +/** * Menu callback: dummy clean URL tester. */ function system_test() { @@ -322,10 +349,6 @@ $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.') - ); // File system: --- sites/default/settings.php.orig 2006-02-05 12:37:53.000000000 -0500 +++ sites/default/settings.php 2006-02-07 23:07:29.000000000 -0500 @@ -103,6 +103,81 @@ $base_url = 'http://www.example.com'; // NO trailing slash! /** + * Performance settings: + * + * The following settings can be modified to try and increase the performance + * of your Drupal-powered website. It is not advised that you try tuning these + * settings unless you fully understand what you are doing. + * + * file_cache: + * The file_cache setting allows you to specify a directory in which Drupal + * should write cache files. Anything that normally would have been cached to + * the database will now be cached to the filesystem. You can specify an + * absolute path (starting with a '/'), or a path relative to your top level + * Drupal installation directory (that is, the directory that contains files + * such as index.php and cron.php). You will need to manually create the + * specified cache directory, and to give it proper permission so that your + * webserver has read/write access. + * + * The file_cache may be helpful to you if your Database is under powered. In + * the event that Drupal is unable to connect to the database (for example + * if the database is down for maintenance, or if the database is already + * handling the maximum number of connections), it will attempt to display the + * file-cached version of the page instead of displaying an error. If it is + * unable to display the file-cached version of the page, only then will + * someone accessing your site get a database error. + * + * Example: + * $file_cache = 'files/cache'; + * $file_cache = '/tmp/cache'; + * + * file_cache_fastpath: + * When file_cache_fastpath is enabled, pages that have been cached to the + * filesystem will be displayed to anonymous users without making any database + * queries. This bypasses Drupal's session management, as well as all _init + * and _exit hooks. As always, logged in users will not be displayed the + * cached pages. + * + * cache_lifetime: + * Simply enabling the page cache will offer a sufficient performance boost + * for most low-traffic websites. However, on high-traffic websites it can + * become necessary to enforace a minimum lifetime for cached pages. The + * 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. + * + * The cache lifetime should be specified in seconds. By default there is no + * minimum cache lifetime. + * + * roll_init: + * The roll_init variable allows you to control for which user roles the + * _init hook of each module is enabled. As the _init hook is executed even + * when displaying cached pages, it can offer a significant performance boost + * to disable the hook for all or some roles. Specify roles by their rid + * (obtained from your Drupal database), or use either of the following two + * keywords: ALL, NONE + * + * By default, the _init hook is called for all user roles. + * + * roll_exit: + * The roll_exit variable allows you to control for which user roles the _exit + * hook of * each module is enabled. As the _exit hook is executed even when + * displaying cached pages, it can offer a significant performance boost to + * disable the hook for all or some roles. For example, the data stored in + * the Drupal access_log is obtained by an _exit hook in the statistics module. + * Specify roles by their rid (obtained from your Drupal database), or use + * either of the following two keywords: ALL, NONE + * + * By default, the _exit hook is called for all user roles. + */ +$file_cache = ''; +$file_cache_fastpath = FALSE; +$cache_lifetime = 0; +$roll_init = ALL; +$roll_exit = ALL; + +/** * PHP settings: * * To see what PHP settings are possible, including whether they can