MDL-72837 core_cache: Add versioned cache support
Adds new set_versioned and get_versioned APIs to cache, which means you can request a specific version from cache and it will not return an outdated version. This is important when using multi-layer-caches where a local cache might have an outdated version of the cache, but the shared cache has a current version. With this feature, the content of the cache does not have to be rebuilt, as it will automatically retrieve it from the shared cache if necessary.
This commit is contained in:
Vendored
+227
-18
@@ -42,6 +42,11 @@ defined('MOODLE_INTERNAL') || die();
|
||||
*/
|
||||
class cache implements cache_loader {
|
||||
|
||||
/**
|
||||
* @var int Constant for cache entries that do not have a version number
|
||||
*/
|
||||
const VERSION_NONE = -1;
|
||||
|
||||
/**
|
||||
* We need a timestamp to use within the cache API.
|
||||
* This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with
|
||||
@@ -397,13 +402,101 @@ class cache implements cache_loader {
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function get($key, $strictness = IGNORE_MISSING) {
|
||||
return $this->get_implementation($key, self::VERSION_NONE, $strictness);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value and actual version for the given key, with at least the required version.
|
||||
*
|
||||
* If there is no value for the key, or there is a value but it doesn't have the required
|
||||
* version, then this function will return null (or throw an exception if you set strictness
|
||||
* to MUST_EXIST).
|
||||
*
|
||||
* This function can be used to make it easier to support localisable caches (where the cache
|
||||
* could be stored on a local server as well as a shared cache). Specifying the version means
|
||||
* that it will automatically retrieve the correct version if available, either from the local
|
||||
* server or [if that has an older version] from the shared server.
|
||||
*
|
||||
* If the cached version is newer than specified version, it will be returned regardless. For
|
||||
* example, if you request version 4, but the locally cached version is 5, it will be returned.
|
||||
* If you request version 6, and the locally cached version is 5, then the system will look in
|
||||
* higher-level caches (if any); if there still isn't a version 6 or greater, it will return
|
||||
* null.
|
||||
*
|
||||
* You must use this function if you use set_versioned.
|
||||
*
|
||||
* @param string|int $key The key for the data being requested.
|
||||
* @param int $requiredversion Minimum required version of the data
|
||||
* @param int $strictness One of IGNORE_MISSING or MUST_EXIST.
|
||||
* @param mixed $actualversion If specified, will be set to the actual version number retrieved
|
||||
* @return mixed Data from the cache, or false if the key did not exist or was too old
|
||||
* @throws \coding_exception If you call get_versioned on a non-versioned cache key
|
||||
*/
|
||||
public function get_versioned($key, int $requiredversion, int $strictness = IGNORE_MISSING, &$actualversion = null) {
|
||||
return $this->get_implementation($key, $requiredversion, $strictness, $actualversion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks returned data to see if it matches the specified version number.
|
||||
*
|
||||
* For versioned data, this returns the version_wrapper object (or false). For other
|
||||
* data, it returns the actual data (or false).
|
||||
*
|
||||
* @param mixed $result Result data
|
||||
* @param int $requiredversion Required version number or VERSION_NONE if there must be no version
|
||||
* @return bool True if version is current, false if not (or if result is false)
|
||||
* @throws \coding_exception If unexpected type of data (versioned vs non-versioned) is found
|
||||
*/
|
||||
protected static function check_version($result, int $requiredversion): bool {
|
||||
if ($requiredversion === self::VERSION_NONE) {
|
||||
if ($result instanceof \core_cache\version_wrapper) {
|
||||
throw new \coding_exception('Unexpectedly found versioned cache entry');
|
||||
} else {
|
||||
// No version checks, so version is always correct.
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// If there's no result, obviously it doesn't meet the required version.
|
||||
if (!$result) {
|
||||
return false;
|
||||
}
|
||||
if (!($result instanceof \core_cache\version_wrapper)) {
|
||||
throw new \coding_exception('Unexpectedly found non-versioned cache entry');
|
||||
}
|
||||
// If the result doesn't match the required version tag, return false.
|
||||
if ($result->version < $requiredversion) {
|
||||
return false;
|
||||
}
|
||||
// The version meets the requirement.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value for the given key from the cache.
|
||||
*
|
||||
* @param string|int $key The key for the data being requested.
|
||||
* It can be any structure although using a scalar string or int is recommended in the interests of performance.
|
||||
* In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
|
||||
* @param int $requiredversion Minimum required version of the data or cache::VERSION_NONE
|
||||
* @param int $strictness One of IGNORE_MISSING | MUST_EXIST
|
||||
* @param mixed $actualversion If specified, will be set to the actual version number retrieved
|
||||
* @return mixed|false The data from the cache or false if the key did not exist within the cache.
|
||||
* @throws coding_exception
|
||||
*/
|
||||
protected function get_implementation($key, int $requiredversion, int $strictness, &$actualversion = null) {
|
||||
// 1. Get it from the static acceleration array if we can (only when it is enabled and it has already been requested/set).
|
||||
$usesstaticacceleration = $this->use_static_acceleration();
|
||||
|
||||
if ($usesstaticacceleration) {
|
||||
$result = $this->static_acceleration_get($key);
|
||||
if ($result !== false) {
|
||||
return $result;
|
||||
if ($result && self::check_version($result, $requiredversion)) {
|
||||
if ($requiredversion === self::VERSION_NONE) {
|
||||
return $result;
|
||||
} else {
|
||||
$actualversion = $result->version;
|
||||
return $result->data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,18 +505,58 @@ class cache implements cache_loader {
|
||||
|
||||
// 3. Get it from the store. Obviously wasn't in the static acceleration array.
|
||||
$result = $this->store->get($parsedkey);
|
||||
if ($result) {
|
||||
// Check the result has at least the required version.
|
||||
try {
|
||||
$validversion = self::check_version($result, $requiredversion);
|
||||
} catch (\coding_exception $e) {
|
||||
// If we get an exception because there is incorrect data in the cache (not
|
||||
// versioned when it ought to be), delete it so this exception goes away next time.
|
||||
// The exception should only happen if there is a code bug (which is why we still
|
||||
// throw it) but there are unusual scenarios where it might happen and that would
|
||||
// be annoying if it doesn't fix itself.
|
||||
$this->store->delete($parsedkey);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if (!$validversion) {
|
||||
// If the result was too old, don't use it.
|
||||
$result = false;
|
||||
|
||||
// Also delete it immediately. This improves performance in the
|
||||
// case when the cache item is large and there may be multiple clients simultaneously
|
||||
// requesting it - they won't all have to do a megabyte of IO just in order to find
|
||||
// that it's out of date.
|
||||
$this->store->delete($parsedkey);
|
||||
}
|
||||
}
|
||||
if ($result !== false) {
|
||||
if ($result instanceof cache_ttl_wrapper) {
|
||||
if ($result->has_expired()) {
|
||||
// Look to see if there's a TTL wrapper. It might be inside a version wrapper.
|
||||
if ($requiredversion !== self::VERSION_NONE) {
|
||||
$ttlconsider = $result->data;
|
||||
} else {
|
||||
$ttlconsider = $result;
|
||||
}
|
||||
if ($ttlconsider instanceof cache_ttl_wrapper) {
|
||||
if ($ttlconsider->has_expired()) {
|
||||
$this->store->delete($parsedkey);
|
||||
$result = false;
|
||||
} else if ($requiredversion === self::VERSION_NONE) {
|
||||
// Use the data inside the TTL wrapper as the result.
|
||||
$result = $ttlconsider->data;
|
||||
} else {
|
||||
$result = $result->data;
|
||||
// Put the data from the TTL wrapper directly inside the version wrapper.
|
||||
$result->data = $ttlconsider->data;
|
||||
}
|
||||
}
|
||||
if ($usesstaticacceleration) {
|
||||
$this->static_acceleration_set($key, $result);
|
||||
}
|
||||
// Remove version wrapper if necessary.
|
||||
if ($requiredversion !== self::VERSION_NONE) {
|
||||
$actualversion = $result->version;
|
||||
$result = $result->data;
|
||||
}
|
||||
if ($result instanceof cache_cached_object) {
|
||||
$result = $result->restore_object();
|
||||
}
|
||||
@@ -439,9 +572,23 @@ class cache implements cache_loader {
|
||||
// We must pass the original (unparsed) key to the next loader in the chain.
|
||||
// The next loader will parse the key as it sees fit. It may be parsed differently
|
||||
// depending upon the capabilities of the store associated with the loader.
|
||||
$result = $this->loader->get($key);
|
||||
if ($requiredversion === self::VERSION_NONE) {
|
||||
$result = $this->loader->get($key);
|
||||
} else {
|
||||
$result = $this->loader->get_versioned($key, $requiredversion, IGNORE_MISSING, $actualversion);
|
||||
}
|
||||
} else if ($this->datasource !== false) {
|
||||
$result = $this->datasource->load_for_cache($key);
|
||||
if ($requiredversion === self::VERSION_NONE) {
|
||||
$result = $this->datasource->load_for_cache($key);
|
||||
} else {
|
||||
if (!$this->datasource instanceof cache_data_source_versionable) {
|
||||
throw new \coding_exception('Data source is not versionable');
|
||||
}
|
||||
$result = $this->datasource->load_for_cache_versioned($key, $requiredversion, $actualversion);
|
||||
if ($result && $actualversion < $requiredversion) {
|
||||
throw new \coding_exception('Data source returned outdated version');
|
||||
}
|
||||
}
|
||||
}
|
||||
$setaftervalidation = ($result !== false);
|
||||
} else if ($this->perfdebug) {
|
||||
@@ -452,9 +599,14 @@ class cache implements cache_loader {
|
||||
if ($strictness === MUST_EXIST && $result === false) {
|
||||
throw new coding_exception('Requested key did not exist in any cache stores and could not be loaded.');
|
||||
}
|
||||
// 6. Set it to the store if we got it from the loader/datasource.
|
||||
// 6. Set it to the store if we got it from the loader/datasource. Only set to this direct
|
||||
// store; parent method will have set it to all stores if needed.
|
||||
if ($setaftervalidation) {
|
||||
$this->set($key, $result);
|
||||
if ($requiredversion === self::VERSION_NONE) {
|
||||
$this->set_implementation($key, self::VERSION_NONE, $result, false);
|
||||
} else {
|
||||
$this->set_implementation($key, $actualversion, $result, false);
|
||||
}
|
||||
}
|
||||
// 7. Make sure we don't pass back anything that could be a reference.
|
||||
// We don't want people modifying the data in the cache.
|
||||
@@ -630,10 +782,52 @@ class cache implements cache_loader {
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
public function set($key, $data) {
|
||||
if ($this->loader !== false) {
|
||||
return $this->set_implementation($key, self::VERSION_NONE, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value for the given key with the given version.
|
||||
*
|
||||
* The cache does not store multiple versions - any existing version will be overwritten with
|
||||
* this one. This function should only be used if there is a known 'current version' (e.g.
|
||||
* stored in a database table). It only ensures that the cache does not return outdated data.
|
||||
*
|
||||
* This function can be used to help implement localisable caches (where the cache could be
|
||||
* stored on a local server as well as a shared cache). The version will be recorded alongside
|
||||
* the item and get_versioned will always return the correct version.
|
||||
*
|
||||
* The version number must be an integer that always increases. This could be based on the
|
||||
* current time, or a stored value that increases by 1 each time it changes, etc.
|
||||
*
|
||||
* If you use this function you must use get_versioned to retrieve the data.
|
||||
*
|
||||
* @param string|int $key The key for the data being set.
|
||||
* @param int $version Integer for the version of the data
|
||||
* @param mixed $data The data to set against the key.
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
public function set_versioned($key, int $version, $data): bool {
|
||||
return $this->set_implementation($key, $version, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value for the given key, optionally with a version tag.
|
||||
*
|
||||
* @param string|int $key The key for the data being set.
|
||||
* @param int $version Version number for the data or cache::VERSION_NONE if none
|
||||
* @param mixed $data The data to set against the key.
|
||||
* @param bool $setparents If true, sets all parent loaders, otherwise only this one
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
protected function set_implementation($key, int $version, $data, bool $setparents = true): bool {
|
||||
if ($this->loader !== false && $setparents) {
|
||||
// We have a loader available set it there as well.
|
||||
// We have to let the loader do its own parsing of data as it may be unique.
|
||||
$this->loader->set($key, $data);
|
||||
if ($version === self::VERSION_NONE) {
|
||||
$this->loader->set($key, $data);
|
||||
} else {
|
||||
$this->loader->set_versioned($key, $version, $data);
|
||||
}
|
||||
}
|
||||
$usestaticacceleration = $this->use_static_acceleration();
|
||||
|
||||
@@ -648,7 +842,12 @@ class cache implements cache_loader {
|
||||
}
|
||||
|
||||
if ($usestaticacceleration) {
|
||||
$this->static_acceleration_set($key, $data);
|
||||
// Static acceleration cache should include the cache version wrapper, but not TTL.
|
||||
if ($version === self::VERSION_NONE) {
|
||||
$this->static_acceleration_set($key, $data);
|
||||
} else {
|
||||
$this->static_acceleration_set($key, new \core_cache\version_wrapper($data, $version));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) {
|
||||
@@ -656,6 +855,10 @@ class cache implements cache_loader {
|
||||
}
|
||||
$parsedkey = $this->parse_key($key);
|
||||
|
||||
if ($version !== self::VERSION_NONE) {
|
||||
$data = new \core_cache\version_wrapper($data, $version);
|
||||
}
|
||||
|
||||
$success = $this->store->set($parsedkey, $data);
|
||||
if ($this->perfdebug) {
|
||||
cache_helper::record_cache_set($this->store, $this->definition, 1,
|
||||
@@ -1505,14 +1708,16 @@ class cache_application extends cache implements cache_loader_with_locking {
|
||||
* </code>
|
||||
*
|
||||
* @param string|int $key The key for the data being requested.
|
||||
* @param int $version Version number
|
||||
* @param mixed $data The data to set against the key.
|
||||
* @param bool $setparents If true, sets all parent loaders, otherwise only this one
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
public function set($key, $data) {
|
||||
protected function set_implementation($key, int $version, $data, bool $setparents = true): bool {
|
||||
if ($this->requirelockingwrite && !$this->acquire_lock($key)) {
|
||||
return false;
|
||||
}
|
||||
$result = parent::set($key, $data);
|
||||
$result = parent::set_implementation($key, $version, $data, $setparents);
|
||||
if ($this->requirelockingwrite && !$this->release_lock($key)) {
|
||||
debugging('Failed to release cache lock on set operation... this should not happen.', DEBUG_DEVELOPER);
|
||||
}
|
||||
@@ -1569,15 +1774,17 @@ class cache_application extends cache implements cache_loader_with_locking {
|
||||
* Retrieves the value for the given key from the cache.
|
||||
*
|
||||
* @param string|int $key The key for the data being requested.
|
||||
* @param int $requiredversion Minimum required version of the data or cache::VERSION_NONE
|
||||
* @param int $strictness One of IGNORE_MISSING | MUST_EXIST
|
||||
* @param mixed &$actualversion If specified, will be set to the actual version number retrieved
|
||||
* @return mixed|false The data from the cache or false if the key did not exist within the cache.
|
||||
*/
|
||||
public function get($key, $strictness = IGNORE_MISSING) {
|
||||
protected function get_implementation($key, int $requiredversion, int $strictness, &$actualversion = null) {
|
||||
if ($this->requirelockingread && $this->check_lock_state($key) === false) {
|
||||
// Read locking required and someone else has the read lock.
|
||||
return false;
|
||||
}
|
||||
return parent::get($key, $strictness);
|
||||
return parent::get_implementation($key, $requiredversion, $strictness, $actualversion);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1830,16 +2037,18 @@ class cache_session extends cache {
|
||||
* @param string|int $key The key for the data being requested.
|
||||
* It can be any structure although using a scalar string or int is recommended in the interests of performance.
|
||||
* In advanced cases an array may be useful such as in situations requiring the multi-key functionality.
|
||||
* @param int $requiredversion Minimum required version of the data or cache::VERSION_NONE
|
||||
* @param int $strictness One of IGNORE_MISSING | MUST_EXIST
|
||||
* @param mixed &$actualversion If specified, will be set to the actual version number retrieved
|
||||
* @return mixed|false The data from the cache or false if the key did not exist within the cache.
|
||||
* @throws coding_exception
|
||||
*/
|
||||
public function get($key, $strictness = IGNORE_MISSING) {
|
||||
protected function get_implementation($key, int $requiredversion, int $strictness, &$actualversion = null) {
|
||||
// Check the tracked user.
|
||||
$this->check_tracked_user();
|
||||
|
||||
// Use parent code.
|
||||
return parent::get($key, $strictness);
|
||||
return parent::get_implementation($key, $requiredversion, $strictness, $actualversion);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user