As described at <http://wordpress.org/support/topic/201058>, WP Super Cache
normally needs a "lockdown" mode in cases where there are many comments and
a large number of page requests.

This is because it normally deletes the supercached file as soon as the
comment is posted, which causes all subsequent page requests to NOT be
served from the cache until the supercache file is rebuilt, which may take
several seconds. If the server is getting several requests per second, that
can cause dozens of non-cached requests to simultaneously start up, causing
a huge temporary spike in server load.

In other words, if rebuilding the cached page takes 5 seconds because someone
is using tons of plugins, but there are two requests per second arriving for
that page, each comment post causes ten uncached copies of WordPress to run.
In fact, it's probably worse than that, because the ten processes competing
for CPU time will probably make them take longer than 5 seconds -- I've seen
cases where a WordPress page with lots of plugins that normally takes two
seconds to load can take twenty seconds after a comment post under heavy load.

WP Super Cache's "lockdown" prevents the files from being deleted when a
comment is posted, which prevents the problem. However, it means that
supercached pages can be significantly outdated.

A different way of handling this is to still rebuild the cache file after a
comment post, but to try and make sure that only the first subsequent
request (not dozens of them) does so. In other words, if there are going to
be ten page requests in the five seconds after a comment post, make the
first one of these run the normal WordPress code and rebuild the cache file,
tricking the other nine into using the old cached version.

This might seem evil -- the other nine see an old version of the file! --
but what they're seeing is only a few seconds out of date. As soon as the
first request finishes rebuilding the supercached file, everyone sees the
current version.

This has almost all of the advantages of lockdown, but vastly reduces how
out of date a cached page will be.

We can make this happen by:

1. When the code would normally delete an old cached file, rename it
   so that it has ".needs-rebuild" on the end if it hasn't yet reached the
   $cache_max_time expiration. Also touch the modification time of that
   file (this is needed for step 2).

2. When the next request for that page arrives, run WordPress code normally,
   but also check if a ".needs-rebuild" file exists with a mtime less than
   30 seconds ago. If so, rename it back to the original name. This way,
   it will be used if any other requests for this page arrive while we're
   rebuilding it. (Checking that it's less than 30 seconds old makes sure
   that nobody ever sees anything more than 30 seconds outdated. It also
   prevents this algorithm from being used at all on a site that isn't busy
   enough to gets hits at least once every 30 seconds -- if a site isn't that
   busy, running two simultaneous uncached copies of WordPress isn't a
   problem.)

3. At the end of our code, overwrite the existing cached file, if any.
   WP Super Cache 0.8 and later already does this.

Things to check: is everything I copied from wp_cache_ob_callback() to
wp_cache_rename_expired() necessary, particularly the $cached_direct_pages
stuff?


--- wp-cache-phase2.php	2008-09-24 15:22:04.000000000 -0700
+++ wp-cache-phase2.php	2008-09-24 20:34:24.000000000 -0700
@@ -37,10 +37,54 @@
 		header('Vary: Accept-Encoding, Cookie');
 	else
 		header('Vary: Cookie');
+	wp_cache_rename_expired();
 	ob_start('wp_cache_ob_callback'); 
 	register_shutdown_function('wp_cache_shutdown_callback');
 }
 
+function wp_cache_rename_expired()
+{
+	global $super_cache_enabled, $cache_path, $cached_direct_pages, $cache_compression;
+	
+	// Generate the correct paths and determine if we're really going to
+	// save a supercached version of the file later on in
+	// wp_cache_ob_callback (which is where this code is copied from). If
+	// so, and we have an old (but not very old) ".needs-rebuild" version
+	// of the cached file hanging around, put the ".needs-rebuild" version
+	// back so that it's temporarily used for new requests while we're
+	// building a *new* cached file.
+	
+	$uri = preg_replace('/[ <>\'\"\r\n\t\(\)]/', '', str_replace( '/index.php', '/', str_replace( '..', '', $_SERVER['REQUEST_URI']) ) );
+	$dir = strtolower(preg_replace('/:.*$/', '',  $_SERVER["HTTP_HOST"])) . $uri; // To avoid XSS attacs
+	$dir = trailingslashit( $cache_path . 'supercache/' . $dir );
+	if( is_array( $cached_direct_pages ) && in_array( $_SERVER[ 'REQUEST_URI' ], $cached_direct_pages ) ) {
+		$dir = trailingslashit( ABSPATH . $uri );
+	}
+	$supercachedir = $cache_path . 'supercache/' . preg_replace('/:.*$/', '',  $_SERVER["HTTP_HOST"]);
+	if( !empty( $_GET ) || is_feed() || ( $super_cache_enabled == true && is_dir( substr( $supercachedir, 0, -1 ) . '.disabled' ) ) )
+		$super_cache_enabled = false;
+	
+	$files_to_check = array( $dir . '/index.html', $dir . '/index.html.gz' );
+	
+	if ($super_cache_enabled) {
+		foreach( $files_to_check as $cache_file ) {
+			$mtime = @filemtime($cache_file . '.needs-rebuild');
+			if ($mtime && (time() - $mtime) < 30) {
+				@rename ($cache_file . '.needs-rebuild', $cache_file);
+			}
+		}
+	}
+	
+	// Clean up any mess: if the ".needs-rebuild" file is still there for
+	// any reason (the file was there but more than 30 seconds old,
+	// or $super_cache_enabled is false, or the rename failed, etc.),
+	// just delete it. We don't even check for existence first, since that
+	// wastes time.
+	foreach( $files_to_check as $cache_file ) {
+		@unlink($cache_file . '.needs-rebuild');
+	}
+}
+
 function wp_cache_get_response_headers() {
 	if(function_exists('apache_response_headers')) {
 		flush();
@@ -270,7 +314,7 @@
 	wp_cache_writers_exit();
 }
 
-function prune_super_cache($directory, $force = false) {
+function prune_super_cache($directory, $force = false, $possibly_rename = false) {
 	global $cache_max_time, $cache_path, $super_cache_enabled;
 
 	if( !is_admin() && $super_cache_enabled == 0 )
@@ -289,7 +333,7 @@
 		$entries = glob($directory. '*');
 		if( is_array( $entries ) && !empty( $entries ) ) foreach ($entries as $entry) {
 			if ($entry != '.' && $entry != '..') {
-				prune_super_cache($entry, $force);
+				prune_super_cache($entry, $force, $possibly_rename);
 				if( is_dir( $entry ) && ( $force || @filemtime( $entry ) + $cache_max_time <= $now ) ) {
 					$oktodelete = true;
 					if( in_array( $entry, $protected_directories ) )
@@ -304,8 +348,12 @@
 			$oktodelete = true;
 			if( in_array( $directory, $protected_directories ) )
 				$oktodelete = false;
-			if( $oktodelete )
-				@unlink( addslashes( $directory ) );
+			if( $oktodelete ) {
+				if( $possibly_rename )
+					wp_cache_unlink_or_save_for_rebuild( addslashes( $directory ) );
+				else
+					@unlink( addslashes( $directory ) );
+			}
 		}
 	}
 }
@@ -465,11 +513,11 @@
 	if( $super_cache_enabled ) {
 		$siteurl = trailingslashit( strtolower( preg_replace( '/:.*$/', '', str_replace( 'http://', '', get_option( 'home' ) ) ) ) );
 		if( $post_id == 0 ) {
-			prune_super_cache( $cache_path . 'supercache/' . $siteurl );
+			prune_super_cache( $cache_path . 'supercache/' . $siteurl, false, true );
 		} else {
 			$permalink = trailingslashit( str_replace( get_option( 'home' ), '', post_permalink( $post_id ) ) );
 			$dir = $cache_path . 'supercache/' . $siteurl;
-			prune_super_cache( $dir . $permalink, true );
+			prune_super_cache( $dir . $permalink, true, true );
 			@rmdir( $dir . $permalink );
 			prune_super_cache( $dir . 'page/', true );
 		}
@@ -490,8 +538,8 @@
 						@unlink($meta_pathname);
 						@unlink($content_pathname);
 						if( $super_cache_enabled ) {
-							@unlink( $cache_path . 'supercache/' . trailingslashit( $meta->uri ) . 'index.html' );
-							@unlink( $cache_path . 'supercache/' . trailingslashit( $meta->uri ) . 'index.html.gz' );
+							wp_cache_unlink_or_save_for_rebuild( $cache_path . 'supercache/' . trailingslashit( $meta->uri ) . 'index.html' );
+							wp_cache_unlink_or_save_for_rebuild( $cache_path . 'supercache/' . trailingslashit( $meta->uri ) . 'index.html.gz' );
 						}
 					}
 				} elseif ($meta->blog_id == $blog_id) {
@@ -529,4 +577,18 @@
 	wp_cache_phase2_clean_expired($file_prefix);
 }
 
+function wp_cache_unlink_or_save_for_rebuild($path) {
+	global $cache_max_time;
+
+	$filemtime = @filemtime($path);
+	if ($filemtime && ($filemtime + $cache_max_time) > $now) {
+		if (@rename($path, $path . '.needs-rebuild'))
+			@touch( $path . '.needs-rebuild' );
+		else
+			@unlink( $path );
+	} else {
+		@unlink( $path );
+	}
+}
+
 ?>
