Skip to content

Commit

Permalink
MDL-69088 cache: Make file cache store purge async
Browse files Browse the repository at this point in the history
  • Loading branch information
jackson-catalyst committed Feb 3, 2022
1 parent 4f9a539 commit 18de338
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 6 deletions.
4 changes: 4 additions & 0 deletions cache/stores/file/addinstanceform.php
Expand Up @@ -58,5 +58,9 @@ protected function configuration_definition() {
$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');
}
}
53 changes: 53 additions & 0 deletions cache/stores/file/classes/task/asyncpurge.php
@@ -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;
}

}
3 changes: 3 additions & 0 deletions cache/stores/file/lang/en/cachestore_file.php
Expand Up @@ -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';
Expand All @@ -45,6 +47,7 @@
* 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:
Expand Down
68 changes: 63 additions & 5 deletions cache/stores/file/lib.php
Expand Up @@ -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
Expand Down Expand Up @@ -180,6 +187,12 @@ public function __construct($name, array $configuration = array()) {
// 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;
}
}

/**
Expand Down Expand Up @@ -271,10 +284,25 @@ public static function is_supported_mode($mode) {
* @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;
}
Expand Down Expand Up @@ -569,14 +597,38 @@ public function has_any(array $keys) {
* @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;
}
Expand Down Expand Up @@ -618,6 +670,9 @@ public static function config_get_configuration_array($data) {
if (isset($data->prescan)) {
$config['prescan'] = $data->prescan;
}
if (isset($data->asyncpurge)) {
$config['asyncpurge'] = $data->asyncpurge;
}

return $config;
}
Expand All @@ -642,6 +697,9 @@ public static function config_set_edit_form_data(moodleform $editform, array $co
if (isset($config['prescan'])) {
$data['prescan'] = (bool)$config['prescan'];
}
if (isset($config['asyncpurge'])) {
$data['asyncpurge'] = (bool)$config['asyncpurge'];
}
$editform->set_data($data);
}

Expand Down
99 changes: 99 additions & 0 deletions cache/stores/file/tests/asyncpurge_test.php
@@ -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);
}
}
2 changes: 1 addition & 1 deletion cache/stores/file/version.php
Expand Up @@ -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.

0 comments on commit 18de338

Please sign in to comment.