MDL-63101 cache: Improve accuracy of cache event invalidation

This commit is contained in:
Andrew Nicols
2018-08-27 14:43:42 +08:00
parent 8df868e9e0
commit 0a197875df
5 changed files with 201 additions and 27 deletions
+104 -14
View File
@@ -50,6 +50,14 @@ class cache implements cache_loader {
*/
protected static $now;
/**
* A purge token used to distinguish between multiple cache purges in the same second.
* This is in the format <microtime>-<random string>.
*
* @var string
*/
protected static $purgetoken;
/**
* The definition used when loading this cache if there was one.
* @var cache_definition
@@ -286,33 +294,58 @@ class cache implements cache_loader {
return;
}
// Each cache stores the current 'lastinvalidation' value within the cache itself.
$lastinvalidation = $this->get('lastinvalidation');
if ($lastinvalidation === false) {
// This is a new cache or purged globally, there won't be anything to invalidate.
// Set the time of the last invalidation and move on.
$this->set('lastinvalidation', self::now());
// There is currently no value for the lastinvalidation token, therefore the token is not set, and there
// can be nothing to invalidate.
// Set the lastinvalidation value to the current purge token and return early.
$this->set('lastinvalidation', self::get_purge_token());
return;
} else if ($lastinvalidation == self::now()) {
// We've already invalidated during this request.
} else if ($lastinvalidation == self::get_purge_token()) {
// The current purge request has already been fully handled by this cache.
return;
}
// Get the event invalidation cache.
/*
* Now that the whole cache check is complete, we check the meaning of any specific cache invalidation events.
* These are stored in the core/eventinvalidation cache as an multi-dimensinoal array in the form:
* [
* eventname => [
* keyname => purgetoken,
* ]
* ]
*
* The 'keyname' value is used to delete a specific key in the cache.
* If the keyname is set to the special value 'purged', then the whole cache is purged instead.
*
* The 'purgetoken' is the token that this key was last purged.
* a) If the purgetoken matches the last invalidation, then the key/cache is not purged.
* b) If the purgetoken is newer than the last invalidation, then the key/cache is not purged.
* c) If the purge token is older than the last invalidation, or it has a different token component, then the
* cache is purged.
*
* Option b should not happen under normal operation, but may happen in race condition whereby a long-running
* request's cache is cleared in another process during that request, and prior to that long-running request
* creating the cache. In such a condition, it would be incorrect to clear that cache.
*/
$cache = self::make('core', 'eventinvalidation');
$events = $cache->get_many($this->definition->get_invalidation_events());
$todelete = array();
$purgeall = false;
// Iterate the returned data for the events.
foreach ($events as $event => $keys) {
if ($keys === false) {
// No data to be invalidated yet.
continue;
}
// Look at each key and check the timestamp.
foreach ($keys as $key => $timestamp) {
foreach ($keys as $key => $purgetoken) {
// If the timestamp of the event is more than or equal to the last invalidation (happened between the last
// invalidation and now)then we need to invaliate the key.
if ($timestamp >= $lastinvalidation) {
// invalidation and now), then we need to invaliate the key.
if (self::compare_purge_tokens($purgetoken, $lastinvalidation) > 0) {
if ($key === 'purged') {
$purgeall = true;
break;
@@ -330,7 +363,7 @@ class cache implements cache_loader {
}
// Set the time of the last invalidation.
if ($purgeall || !empty($todelete)) {
$this->set('lastinvalidation', self::now());
$this->set('lastinvalidation', self::get_purge_token(true));
}
}
@@ -1186,13 +1219,70 @@ class cache implements cache_loader {
* This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with
* timing issues.
*
* @return int
* @param bool $float Whether to use floating precision accuracy.
* @return int|float
*/
public static function now() {
public static function now($float = false) {
if (self::$now === null) {
self::$now = time();
self::$now = microtime(true);
}
if ($float) {
return self::$now;
} else {
return (int) self::$now;
}
}
/**
* Get a 'purge' token used for cache invalidation handling.
*
* Note: This function is intended for use from within the Cache API only and not by plugins, or cache stores.
*
* @param bool $reset Whether to reset the token and generate a new one.
* @return string
*/
public static function get_purge_token($reset = false) {
if (self::$purgetoken === null || $reset) {
self::$now = null;
self::$purgetoken = self::now(true) . '-' . uniqid('', true);
}
return self::$purgetoken;
}
/**
* Compare a pair of purge tokens.
*
* If the two tokens are identical, then the return value is 0.
* If the time component of token A is newer than token B, then a positive value is returned.
* If the time component of token B is newer than token A, then a negative value is returned.
*
* Note: This function is intended for use from within the Cache API only and not by plugins, or cache stores.
*
* @param string $tokena
* @param string $tokenb
* @return int
*/
public static function compare_purge_tokens($tokena, $tokenb) {
if ($tokena === $tokenb) {
// There is an exact match.
return 0;
}
// The token for when the cache was last invalidated.
list($atime) = explode('-', "{$tokena}-", 2);
// The token for this cache.
list($btime) = explode('-', "{$tokenb}-", 2);
if ($atime >= $btime) {
// Token A is newer.
return 1;
} else {
// Token A is older.
return -1;
}
return self::$now;
}
}