MDL-69088 cache: Make file cache store purge async

This commit is contained in:
Jackson D'souza 2021-12-08 14:39:43 +00:00
parent 4f9a539600
commit 18de3388f8
6 changed files with 223 additions and 6 deletions

View File

@ -58,5 +58,9 @@ class cachestore_file_addinstance_form extends cachestore_addinstance_form {
$form->addElement('checkbox', 'prescan', get_string('prescan', 'cachestore_file'));
$form->setType('prescan', PARAM_BOOL);
$form->addHelpButton('prescan', 'prescan', 'cachestore_file');
$form->addElement('checkbox', 'asyncpurge', get_string('asyncpurge', 'cachestore_file'));
$form->setType('asyncpurge', PARAM_BOOL);
$form->addHelpButton('asyncpurge', 'asyncpurge', 'cachestore_file');
}
}

View File

@ -0,0 +1,53 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_file\task;
/**
* Task deletes old cache revision directory.
*
* @package cachestore_file
* @copyright Catalyst IT Europe Ltd 2021
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Jackson D'Souza <jackson.dsouza@catalyst-eu.net>
*/
class asyncpurge extends \core\task\adhoc_task {
/**
* Executes the scheduled task.
*
* @return boolean True if old cache revision directory exists and is deleted. False otherwise.
*/
public function execute(): bool {
$returnvar = true;
$output = 'Cleaning up file store old cache revision directory:' . PHP_EOL;
$data = $this->get_custom_data();
if (is_dir($data->path)) {
remove_dir($data->path);
$output .= 'Directory deleted: ' . $data->path;
} else {
$output .= 'Directory not found: ' . $data->path;
$returnvar = false;
}
if (!PHPUNIT_TEST) {
mtrace($output);
}
return $returnvar;
}
}

View File

@ -28,6 +28,8 @@
defined('MOODLE_INTERNAL') || die();
$string['asyncpurge'] = 'Asynchronously purge directory';
$string['asyncpurge_help'] = 'If enabled, new directory is created with cache revision and old directory will be deleted Asynchronously via schedule task';
$string['autocreate'] = 'Auto create directory';
$string['autocreate_help'] = 'If enabled the directory specified in path will be automatically created if it does not already exist.';
$string['path'] = 'Cache path';
@ -45,6 +47,7 @@ It is advisable to only turn this on if the following is true:
* If you know the number of items in the cache is going to be small enough that it won\'t cause issues on the file system you are running with.
* The data being cached is not expensive to generate. If it is then sticking with the default may still be the better option as it reduces the chance of issues.';
$string['task_asyncpurge'] = 'Asynchronously purge file store old cache revision directories';
/**
* This is is like the file store, but designed for siutations where:

View File

@ -77,6 +77,13 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
*/
protected $autocreate = false;
/**
* Set to true if new cache revision directory needs to be created. Old directory will be purged asynchronously
* via Schedule task.
* @var bool
*/
protected $asyncpurge = false;
/**
* Set to true if a custom path is being used.
* @var bool
@ -180,6 +187,12 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
// Default: No, we will use multiple directories.
$this->singledirectory = false;
}
// Check if directory needs to be purged asynchronously.
if (array_key_exists('asyncpurge', $configuration)) {
$this->asyncpurge = (bool)$configuration['asyncpurge'];
} else {
$this->asyncpurge = false;
}
}
/**
@ -271,10 +284,25 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
* @param cache_definition $definition
*/
public function initialise(cache_definition $definition) {
global $CFG;
$this->definition = $definition;
$hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
$this->path = $this->filestorepath.'/'.$hash;
make_writable_directory($this->path, false);
if ($this->asyncpurge) {
$timestampfile = $this->path . '/.lastpurged';
if (!file_exists($timestampfile)) {
touch($timestampfile);
@chmod($timestampfile, $CFG->filepermissions);
}
$cacherev = gmdate("YmdHis", filemtime($timestampfile));
// Update file path with new cache revision.
$this->path .= '/' . $cacherev;
make_writable_directory($this->path, false);
}
if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) {
$this->prescan = false;
}
@ -569,14 +597,38 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
* @return boolean True on success. False otherwise.
*/
public function purge() {
global $CFG;
if ($this->isready) {
$files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT);
if (is_array($files)) {
foreach ($files as $filename) {
@unlink($filename);
// If asyncpurge = true, create a new cache revision directory and adhoc task to delete old directory.
if ($this->asyncpurge && isset($this->definition)) {
$hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
$filepath = $this->filestorepath . '/' . $hash;
$timestampfile = $filepath . '/.lastpurged';
if (file_exists($timestampfile)) {
$oldcacherev = gmdate("YmdHis", filemtime($timestampfile));
$oldcacherevpath = $filepath . '/' . $oldcacherev;
// Delete old cache revision file.
@unlink($timestampfile);
// Create adhoc task to delete old cache revision folder.
$purgeoldcacherev = new \cachestore_file\task\asyncpurge();
$purgeoldcacherev->set_custom_data(['path' => $oldcacherevpath]);
\core\task\manager::queue_adhoc_task($purgeoldcacherev);
}
touch($timestampfile, time());
@chmod($timestampfile, $CFG->filepermissions);
$newcacherev = gmdate("YmdHis", filemtime($timestampfile));
$filepath .= '/' . $newcacherev;
make_writable_directory($filepath, false);
} else {
$files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT);
if (is_array($files)) {
foreach ($files as $filename) {
@unlink($filename);
}
}
$this->keys = [];
}
$this->keys = array();
}
return true;
}
@ -618,6 +670,9 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
if (isset($data->prescan)) {
$config['prescan'] = $data->prescan;
}
if (isset($data->asyncpurge)) {
$config['asyncpurge'] = $data->asyncpurge;
}
return $config;
}
@ -642,6 +697,9 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
if (isset($config['prescan'])) {
$data['prescan'] = (bool)$config['prescan'];
}
if (isset($config['asyncpurge'])) {
$data['asyncpurge'] = (bool)$config['asyncpurge'];
}
$editform->set_data($data);
}

View File

@ -0,0 +1,99 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace cachestore_file;
use advanced_testcase;
use cache_definition;
use cache_store;
use cachestore_file;
/**
* Async purge support test for File cache.
*
* @package cachestore_file
* @copyright Catalyst IT Europe Ltd 2021
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Jackson D'Souza <jackson.dsouza@catalyst-eu.net>
* @coversDefaultClass \cachestore_file
*/
class asyncpurge_test extends advanced_testcase {
/**
* Testing Asynchronous file store cache purge
*
* @covers ::initialise
* @covers ::set
* @covers ::get
* @covers ::purge
*/
public function test_cache_async_purge() {
$this->resetAfterTest(true);
// Cache definition.
$definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_file', 'phpunit_test');
// Extra config, set async purge = true.
$extraconfig = ['asyncpurge' => true, 'filecacherev' => time()];
$configuration = array_merge(cachestore_file::unit_test_configuration(), $extraconfig);
$name = 'File async test';
// Create file cache store.
$cache = new cachestore_file($name, $configuration);
// Initialise file cache store.
$cache->initialise($definition);
$cache->set('foo', 'bar');
$this->assertSame('bar', $cache->get('foo'));
// Purge this file cache store.
$cache->purge();
// Purging file cache store shouldn't purge the data but create a new cache revision directory.
$this->assertSame('bar', $cache->get('foo'));
$cache->set('foo', 'bar 2');
$this->assertSame('bar 2', $cache->get('foo'));
}
/**
* Testing Adhoc Cron - deletes old cache revision directory
*
* @covers \cachestore_file\task
*/
public function test_cache_async_purge_cron() {
global $CFG, $USER;
$this->resetAfterTest(true);
$tmpdir = realpath($CFG->tempdir);
$directorypath = '/cachefile_store';
$cacherevdir = $tmpdir . $directorypath;
// Create cache revision directory.
mkdir($cacherevdir, $CFG->directorypermissions, true);
// Create / execute adhoc task to delete cache revision directory.
$asynctask = new cachestore_file\task\asyncpurge();
$asynctask->set_blocking(false);
$asynctask->set_custom_data(['path' => $cacherevdir]);
$asynctask->set_userid($USER->id);
\core\task\manager::queue_adhoc_task($asynctask);
$asynctask->execute();
// Check if cache revision directory has been deleted.
$this->assertDirectoryDoesNotExist($cacherevdir);
}
}

View File

@ -27,6 +27,6 @@
defined('MOODLE_INTERNAL') || die;
$plugin->version = 2021052500; // The current module version (Date: YYYYMMDDXX).
$plugin->version = 2021052501; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2021052500; // Requires this Moodle version.
$plugin->component = 'cachestore_file'; // Full name of the plugin.