--- includes/bootstrap.inc.orig 2006-06-05 16:55:05.000000000 -0400 +++ includes/bootstrap.inc 2006-06-05 17:45:01.000000000 -0400 @@ -16,13 +16,14 @@ 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('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 +// These values should match the 'role' table. define('DRUPAL_ANONYMOUS_RID', 1); define('DRUPAL_AUTHENTICATED_RID', 2); @@ -149,7 +150,7 @@ * Loads the configuration and sets the base URL correctly. */ function conf_init() { - global $db_url, $db_prefix, $base_url, $base_path, $base_root, $conf; + global $db_url, $db_prefix, $base_url, $base_path, $base_root, $conf, $file_cache, $cache_lifetime, $file_cache_fastpath; $conf = array(); require_once './'. conf_path() .'/settings.php'; @@ -234,7 +235,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); } @@ -313,22 +314,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 // currently valid for this user before we return it by making sure the @@ -340,7 +348,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); } } @@ -349,6 +357,24 @@ return 0; } +function cache_get_file($key) { + $cache = NULL; + $cache_file = cache_filename($key); + + if (file_exists($cache_file)) { + 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. * @@ -368,12 +394,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(); } /** @@ -388,10 +439,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 || ($file_cache && is_dir($file_cache))) { // 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 @@ -399,15 +450,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 ($file_cache && 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 { @@ -415,6 +482,17 @@ db_query("DELETE FROM {cache} WHERE expire != %d AND expire < %d", CACHE_PERMANENT, time()); } } + elseif ($file_cache && 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); @@ -426,6 +504,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 @@ -498,10 +601,16 @@ * * @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) { + bootstrap_invoke_all('init'); + } // Set default values: $date = gmdate('D, d M Y H:i:s', $cache->created) .' GMT'; $etag = '"'. md5($date) .'"'; @@ -544,7 +653,9 @@ } print $cache->data; - bootstrap_invoke_all('exit'); + if (!$db_failed) { + bootstrap_invoke_all('exit'); + } exit(); } else { @@ -713,6 +824,7 @@ * * @param $phase * A constant. Allowed values are: + * DRUPAL_BOOTSTRAP_FILE: initialize file-cache. * DRUPAL_BOOTSTRAP_DATABASE: initialize database layer. * DRUPAL_BOOTSTRAP_SESSION: initialize session handling. * DRUPAL_BOOTSTRAP_PAGE_CACHE: load bootstrap.inc and module.inc, start @@ -721,7 +833,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); @@ -735,10 +847,19 @@ global $conf; switch ($phase) { - case DRUPAL_BOOTSTRAP_DATABASE: + case DRUPAL_BOOTSTRAP_FILE: drupal_unset_globals(); // Initialize the configuration conf_init(); + // Display the page straight from the file cache if fastpath is enabled, + // unless the user is logged in or they have posted a form. + if ($file_cache_fastpath && !$_COOKIE['drupal_uid'] && + $_POST == array()) { + drupal_file_cache(); + } + break; + + case DRUPAL_BOOTSTRAP_DATABASE: // Initialize the default database. require_once './includes/database.inc'; db_set_active(); --- includes/database.mysql.inc.orig 2006-06-05 17:12:14.000000000 -0400 +++ includes/database.mysql.inc 2006-06-05 16:56:32.000000000 -0400 @@ -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.mysqli.inc.orig 2006-06-05 17:12:27.000000000 -0400 +++ includes/database.mysqli.inc 2006-06-05 16:56:32.000000000 -0400 @@ -43,6 +43,7 @@ // Find all database connection errors and error 1045 for access denied for user account if (mysqli_connect_errno() >= 2000 || mysqli_connect_errno() == 1045) { + 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 through the mysqli libraries. This could also mean your hosting provider\'s database server is down.

@@ -58,6 +59,7 @@ exit; } else if (mysqli_connect_errno() > 0) { + 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-06-05 17:12:39.000000000 -0400 +++ includes/database.pgsql.inc 2006-06-05 16:56:32.000000000 -0400 @@ -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.

--- modules/user.module.orig 2006-06-05 16:55:05.000000000 -0400 +++ modules/user.module 2006-06-05 17:42:02.000000000 -0400 @@ -941,6 +941,12 @@ // Update the user table timestamp noting user has logged in. db_query("UPDATE {users} SET login = %d WHERE uid = %d", time(), $user->uid); + // 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. It + // is valid for one month, and will be unset when the user logs out. + setcookie('drupal_uid', $user->uid, time() + (60 * 60 * 24 * 30), '/'); + user_module_invoke('login', $form_values, $user); $old_session_id = session_id(); @@ -1006,6 +1012,10 @@ watchdog('user', t('Session closed for %name.', array('%name' => theme('placeholder', $user->name)))); + // This cookie is used by the file_cache to know when a user is logged in. + // We delete it by setting it to ''. + setcookie('drupal_uid', ''); + // Destroy the current session: session_destroy(); module_invoke_all('user', 'logout', NULL, $user); --- modules/system.module.orig 2006-06-05 16:55:05.000000000 -0400 +++ modules/system.module 2006-06-05 17:47:03.000000000 -0400 @@ -147,6 +147,59 @@ } /** + * Implementation of hook_exit(). + */ +function system_exit() { + global $base_url, $file_cache, $cache_lifetime; + + // if using file-based caching, perform routine garbage collection + if ($file_cache && is_dir($file_cache)) { + $cache_file = cache_filename($base_url . request_uri()); + if (file_exists($cache_file)) { + if ($cache_flush = variable_get('cache_flush', 0)) { + $mtime = filemtime($cache_file); + if (($mtime < $cache_flush) && $cache_lifetime && + $mtime < (time() - $cache_lifetime)) { + if ($fp = fopen($cache_file, 'r')) { + if (flock($fp, LOCK_EX)) { + unlink($cache_file); + } + } + } + } + } + } +} + +/** + * 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); + } + } +} + + +/** * Implementation of hook_user(). * * Allows users to individually set their theme and time zone. @@ -246,6 +299,8 @@ } function system_view_general() { + global $file_cache, $file_cache_fastpath; + // General settings: $form['general'] = array( '#type' => 'fieldset', '#title' => t('General settings'), @@ -334,17 +389,20 @@ $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.") + '#description' => t("Drupal has a caching mechanism which stores dynamically generated web pages by default to the database, or if configured in settings.php to the filesystem. By caching a web page, Drupal does not have to create the page each time someone wants to view it. Only pages requested by \"anonymous\" users are cached. In order to reduce server load and save bandwidth, Drupal stores and sends compressed cached pages.") ); - - $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.') + if ($file_cache && is_dir($file_cache)) { + $location = $file_cache_fastpath ? t('filesystem, fastpath enabled') : t('filesystem, fastpath disabled'); + } + else { + $location .= t('database'); + } + $form['cache']['location'] = array( + '#type' => 'item', '#title' => t('Cache location'), + '#value' => $location, + '#description' => t('By default, the Drupal cache is stored in the database. Alternatively, you may modify %settings, your Drupal configuration file, to have the cache stored as files in the filesystem. Also in settings.php you may enable Drupal\'s filecache fastpath, telling Drupal to display cached pages without accessing the database. Additional information about these advanced cache configuration options can be found in settings.php.', array('%settings' => './'. conf_path() .'/settings.php')) ); - // File system: $form['files'] = array('#type' => 'fieldset', '#title' => t('File system settings'), '#collapsible' => TRUE, '#collapsed' => TRUE); --- sites/default/settings.php.orig 2006-06-05 17:37:55.000000000 -0400 +++ sites/default/settings.php 2006-06-05 17:47:30.000000000 -0400 @@ -109,6 +109,58 @@ # $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. + */ +$file_cache = ''; +$file_cache_fastpath = FALSE; +$cache_lifetime = 0; + +/** * PHP settings: * * To see what PHP settings are possible, including whether they can