Commit 67df46eb by Scott

Caching mass-delete option

parent 15f0e033
......@@ -25,17 +25,23 @@
*/
class Q2A_Storage_CacheFactory
{
private static $cacheDriver = null;
/**
* Get the appropriate cache handler.
* @return Q2A_Storage_CacheInterface The cache handler.
*/
public static function getCacheDriver()
{
if (self::$cacheDriver === null) {
$config = array(
'enabled' => (int) qa_opt('caching_enabled') === 1,
'dir' => defined('QA_CACHE_DIRECTORY') ? QA_CACHE_DIRECTORY : null,
);
return new Q2A_Storage_FileCacheDriver($config);
self::$cacheDriver = new Q2A_Storage_FileCacheDriver($config);
}
return self::$cacheDriver;
}
}
......@@ -110,7 +110,7 @@ class Q2A_Storage_FileCacheDriver
$file = $this->getFilename($key);
$dir = dirname($file);
if (is_dir($dir) || mkdir($dir, 0777, true)) {
$success = file_put_contents($file, $cache) !== false;
$success = @file_put_contents($file, $cache) !== false;
}
}
......@@ -118,6 +118,74 @@ class Q2A_Storage_FileCacheDriver
}
/**
* Delete an item from the cache.
* @param string $key The unique cache identifier.
* @return bool Whether the operation succeeded.
*/
public function delete($key)
{
if ($this->enabled) {
$file = $this->getFilename($key);
$dir = dirname($key);
return $this->deleteFile($file);
}
return false;
}
/**
* Delete multiple items from the cache.
* @param int $limit Maximum number of items to process. 0 = unlimited
* @param int $start Offset from which to start (used for 'batching' deletes).
* @param bool $expiredOnly Delete cache only if it has expired.
* @return int Number of files deleted.
*/
public function clear($limit=0, $start=0, $expiredOnly=false)
{
$seek = $processed = $deleted = 0;
// fetch directories first to lower memory usage
$cacheDirs = glob($this->cacheDir . '/*/*', GLOB_ONLYDIR);
foreach ($cacheDirs as $dir) {
$cacheFiles = glob($dir . '/*');
foreach ($cacheFiles as $file) {
if ($seek < $start) {
$seek++;
continue;
}
$wasDeleted = false;
if ($expiredOnly) {
if (is_readable($file)) {
$fp = fopen($file, 'r');
$key = fgets($fp);
$expiry = fgets($fp);
// fclose($fp);
if (is_numeric($expiry) && time() > $expiry) {
$wasDeleted = $this->deleteFile($file);
}
}
} else {
$wasDeleted = $this->deleteFile($file);
}
if ($wasDeleted) {
$deleted++;
}
$processed++;
if ($processed >= $limit) {
break 2;
}
}
}
// return how many files were deleted - caller can figure out how many to skip next time
return $deleted;
}
/**
* Whether caching is available.
* @return bool
*/
......@@ -136,6 +204,49 @@ class Q2A_Storage_FileCacheDriver
}
/**
* Get current statistics for the cache.
* @return array Array of stats: 'files' => number of files, 'size' => total file size in bytes.
*/
public function getStats()
{
if (!$this->enabled) {
return array('files' => 0, 'size' => 0);
}
$totalFiles = 0;
$totalBytes = 0;
$dirIter = new RecursiveDirectoryIterator($this->cacheDir);
foreach (new RecursiveIteratorIterator($dirIter) as $file) {
if (strpos($file->getFilename(), '.') === 0) {
// TODO: use FilesystemIterator::SKIP_DOTS once we're on minimum PHP 5.3
continue;
}
$totalFiles++;
$totalBytes += $file->getSize();
}
return array(
'files' => $totalFiles,
'size' => $totalBytes,
);
}
/**
* Delete a specific file
* @param string $file Filename to delete
* @return bool Whether the file was deleted successfully.
*/
private function deleteFile($file)
{
if (is_writable($file)) {
return @unlink($file) === true;
}
return false;
}
/**
* Generates filename for cache key, of the form `1/23/123abc`
* @param string $key The unique cache key.
* @return string
......
......@@ -523,6 +523,29 @@ function qa_recalc_perform_step(&$state)
qa_recalc_transition($state, 'doblobstodb_complete');
break;
case 'docacheclear':
qa_recalc_transition($state, 'docacheclear_process');
break;
case 'docacheclear_process':
$limit = 5;
$cacheDriver = Q2A_Storage_CacheFactory::getCacheDriver();
$cacheStats = $cacheDriver->getStats();
if ($next > $length) {
qa_recalc_transition($state, 'docacheclear_error');
break;
}
if ($cacheStats['files'] > 0) {
$deleted = $cacheDriver->clear($limit, $next);
$done += $deleted;
$next += $limit - $deleted; // if some files were not deleted, skip them next time
$continue = true;
} else {
qa_recalc_transition($state, 'docacheclear_complete');
}
break;
default:
$state = '';
break;
......@@ -609,6 +632,13 @@ function qa_recalc_stage_length($operation)
$length = qa_db_count_blobs_on_disk();
break;
case 'docachetrim_process':
case 'docacheclear_process':
$cacheDriver = Q2A_Storage_CacheFactory::getCacheDriver();
$cacheStats = $cacheDriver->getStats();
$length = $cacheStats['files'];
break;
default:
$length = 0;
break;
......@@ -749,6 +779,20 @@ function qa_recalc_get_message($state)
$message = qa_lang('admin/blobs_move_complete');
break;
case 'docachetrim_process':
case 'docacheclear_process':
$message = qa_recalc_progress_lang('admin/caching_delete_progress', $done, $length);
break;
case 'docacheclear_error':
$message = qa_lang('admin/caching_delete_error');
break;
case 'docachetrim_complete':
case 'docacheclear_complete':
$message = qa_lang('admin/caching_delete_complete');
break;
default:
$message = '';
break;
......
......@@ -53,9 +53,17 @@ return array(
'block_ips_note' => 'Use a hyphen for ranges or * to match any number. Examples: 192.168.0.4 , 192.168.0.0-192.168.0.31 , 192.168.0.*',
'block_user_popup' => 'Block user',
'block_words_note' => 'Use a * to match any letters. Examples: doh (will only match exact word doh) , doh* (will match doh or dohno) , do*h (will match doh, dooh, dough).',
'caching_dir_missing' => 'Cache directory has not been defined.',
'caching_cleanup' => 'Caching clean-up operations',
'caching_delete_all' => 'Delete entire cache',
'caching_delete_complete' => 'Cache successfully deleted',
'caching_delete_error' => 'There was an error deleting some cache files',
'caching_delete_expired' => 'Delete expired cache',
'caching_delete_progress' => 'Deleted ^1 of ^2 cache files...',
'caching_dir_error' => 'The directory ^ defined as QA_CACHE_DIRECTORY is not writable by the web server.',
'caching_dir_missing' => 'Cache directory has not been defined.',
'caching_dir_public' => 'The directory ^ defined as QA_CACHE_DIRECTORY must be outside the public root.',
'caching_disk_size' => 'Total size of cache',
'caching_num_files' => 'Number of files',
'caching_title' => 'Caching',
'cancel_mailing_button' => 'Cancel Mailing',
'categories' => 'Categories',
......
......@@ -1792,6 +1792,27 @@ switch ($adminsection) {
case 'caching':
$cacheDriver = Q2A_Storage_CacheFactory::getCacheDriver();
$qa_content['error'] = $cacheDriver->getError();
$cacheStats = $cacheDriver->getStats();
$qa_content['form_2'] = array(
'tags' => 'method="post" action="' . qa_path_html('admin/recalc') . '"',
'title' => qa_lang_html('admin/caching_cleanup'),
'style' => 'wide',
'buttons' => array(
'delete_all' => array(
'label' => qa_lang_html('admin/caching_delete_all'),
'tags' => 'name="docacheclear" onclick="return qa_recalc_click(this.name, this, ' . qa_js(qa_lang_html('admin/delete_stop')) . ', \'cacheclear_note\');"',
'note' => '<span id="cacheclear_note"></span>',
),
),
'hidden' => array(
'code' => qa_get_form_security_code('admin/recalc'),
),
);
break;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment