mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-26 16:48:49 +01:00
implement (increment|decrement)Cache to avoid race-conditions if multiple processes update a value
implemented in memcached and APCu backends, default implementation using get&set in base-class
This commit is contained in:
parent
3248e82d65
commit
67a6a9f1f3
@ -179,6 +179,82 @@ class Cache
|
|||||||
throw new Exception\WrongParameter(__METHOD__."() unknown level '$level'!");
|
throw new Exception\WrongParameter(__METHOD__."() unknown level '$level'!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment some value in the cache
|
||||||
|
*
|
||||||
|
* @param string $level use Cache::(TREE|INSTANCE), Cache::(SESSION|REQUEST) are NOT supported!
|
||||||
|
* @param string $app application storing data
|
||||||
|
* @param string $location location name for data
|
||||||
|
* @param int $offset =1 how much to increment by
|
||||||
|
* @param int $intial_value =0 value to set and return, if not in cache
|
||||||
|
* @param int $expiration =0
|
||||||
|
* @return false|int new value on success, false on error
|
||||||
|
*/
|
||||||
|
public static function incrementCache(string $level, string $app, string $location, int $offset=1, int $intial_value=0, int $expiration=0)
|
||||||
|
{
|
||||||
|
//error_log(__METHOD__."('$level', '$app', '$location', $offset, $inital_value, $expiration)");
|
||||||
|
switch($level)
|
||||||
|
{
|
||||||
|
case self::SESSION:
|
||||||
|
case self::REQUEST:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::INSTANCE:
|
||||||
|
case self::TREE:
|
||||||
|
default:
|
||||||
|
if (!($provider = self::get_provider($level)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// limit expiration to configured maximum time
|
||||||
|
if (isset(self::$max_expiration) && (!$expiration || $expiration > self::$max_expiration))
|
||||||
|
{
|
||||||
|
$expiration = self::$max_expiration;
|
||||||
|
}
|
||||||
|
return $provider->increment(self::keys($level, $app, $location), $offset, $intial_value, $expiration);
|
||||||
|
}
|
||||||
|
throw new Exception\WrongParameter(__METHOD__."() unsupported level '$level'!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrements value in cache, but never below 0
|
||||||
|
*
|
||||||
|
* If new value would be below 0, 0 will be set as new value!
|
||||||
|
*
|
||||||
|
* @param string $level use Cache::(TREE|INSTANCE), Cache::(SESSION|REQUEST) are NOT supported!
|
||||||
|
* @param string $app application storing data
|
||||||
|
* @param string $location location name for data
|
||||||
|
* @param int $offset =1 how much to increment by
|
||||||
|
* @param int $intial_value =0 value to set and return, if not in cache
|
||||||
|
* @param int $expiration =0
|
||||||
|
* @return false|int new value on success, false on error
|
||||||
|
*/
|
||||||
|
public static function decrementCache(string $level, string $app, string $location, int $offset=1, int $intial_value=0, int $expiration=0)
|
||||||
|
{
|
||||||
|
//error_log(__METHOD__."('$level', '$app', '$location', $offset, $inital_value, $expiration)");
|
||||||
|
switch($level)
|
||||||
|
{
|
||||||
|
case self::SESSION:
|
||||||
|
case self::REQUEST:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case self::INSTANCE:
|
||||||
|
case self::TREE:
|
||||||
|
default:
|
||||||
|
if (!($provider = self::get_provider($level)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// limit expiration to configured maximum time
|
||||||
|
if (isset(self::$max_expiration) && (!$expiration || $expiration > self::$max_expiration))
|
||||||
|
{
|
||||||
|
$expiration = self::$max_expiration;
|
||||||
|
}
|
||||||
|
return $provider->decrement(self::keys($level, $app, $location), $offset, $intial_value, $expiration);
|
||||||
|
}
|
||||||
|
throw new Exception\WrongParameter(__METHOD__."() unsupported level '$level'!");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content stored to signal one callback is already running and calculating the data, to not run it multiple times
|
* Content stored to signal one callback is already running and calculating the data, to not run it multiple times
|
||||||
*/
|
*/
|
||||||
|
@ -129,6 +129,53 @@ class Apcu extends Base implements Provider
|
|||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments value in cache
|
||||||
|
*
|
||||||
|
* Default implementation emulating increment using get & set.
|
||||||
|
*
|
||||||
|
* @param array $keys
|
||||||
|
* @param int $offset =1 how much to increment by
|
||||||
|
* @param int $intial_value =0 value to set and return, if not in cache
|
||||||
|
* @param int $expiration =0
|
||||||
|
* @return false|int new value on success, false on error
|
||||||
|
*/
|
||||||
|
function increment(array $keys, int $offset=1, int $intial_value=0, int $expiration=0)
|
||||||
|
{
|
||||||
|
$key = self::key($keys);
|
||||||
|
if ($intial_value !== 0 && !apcu_exists($key))
|
||||||
|
{
|
||||||
|
return apcu_store($key, $intial_value, $expiration) ? $intial_value : false;
|
||||||
|
}
|
||||||
|
return apcu_inc($key, $offset, $success, $expiration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrements value in cache, but never below 0
|
||||||
|
*
|
||||||
|
* If new value would be below 0, 0 will be set as new value!
|
||||||
|
* Default implementation emulating decrement using get & set.
|
||||||
|
*
|
||||||
|
* @param array $keys
|
||||||
|
* @param int $offset =1 how much to increment by
|
||||||
|
* @param int $intial_value =0 value to set and return, if not in cache
|
||||||
|
* @param int $expiration =0
|
||||||
|
* @return false|int new value on success, false on error
|
||||||
|
*/
|
||||||
|
function decrement(array $keys, int $offset=1, int $intial_value=0, int $expiration=0)
|
||||||
|
{
|
||||||
|
$key = self::key($keys);
|
||||||
|
if ($intial_value !== 0 && !apcu_exists($key))
|
||||||
|
{
|
||||||
|
return apcu_store($key, $intial_value, $expiration) ? $intial_value : false;
|
||||||
|
}
|
||||||
|
if (($value = apcu_dec($key, $offset, $success, $expiration)) < 0)
|
||||||
|
{
|
||||||
|
$value = apcu_store($key, 0, $expiration) ? 0 : false;
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete some data from the cache
|
* Delete some data from the cache
|
||||||
*
|
*
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
namespace EGroupware\Api\Cache;
|
namespace EGroupware\Api\Cache;
|
||||||
|
|
||||||
/*if (isset($_SERVER['SCRIPT_FILENAME']) && realpath($_SERVER['SCRIPT_FILENAME']) == __FILE__)
|
/*if (isset($_SERVER['SCRIPT_FILENAME']) && realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__)
|
||||||
{
|
{
|
||||||
require_once dirname(__DIR__).'/loader/common.php';
|
require_once dirname(__DIR__).'/loader/common.php';
|
||||||
}*/
|
}*/
|
||||||
@ -104,7 +104,7 @@ abstract class Base implements Provider
|
|||||||
++$failed;
|
++$failed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif (!is_null($data)) // emulation can NOT distinquish between null and not set
|
elseif (!is_null($data)) // emulation can NOT distinguish between null and not set
|
||||||
{
|
{
|
||||||
$locations[$location] = $data;
|
$locations[$location] = $data;
|
||||||
}
|
}
|
||||||
@ -149,6 +149,61 @@ abstract class Base implements Provider
|
|||||||
++$failed;
|
++$failed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// test increment
|
||||||
|
$keys = [$level, __CLASS__, 'increment'];
|
||||||
|
$this->delete($keys);
|
||||||
|
|
||||||
|
if (($val=$this->increment($keys, 3, 8)) !== 8)
|
||||||
|
{
|
||||||
|
if ($verbose) echo "$label: increment(\$keys, 3, 8)=".array2string($val)." !== 8 for initial/unset \$keys\n";
|
||||||
|
++$failed;
|
||||||
|
}
|
||||||
|
if (($val=$this->get($keys)) != 8) // get always returns string!
|
||||||
|
{
|
||||||
|
if ($verbose) echo "$label: get(\$keys)=".array2string($val)." != 8 for reading back incremented value\n";
|
||||||
|
++$failed;
|
||||||
|
}
|
||||||
|
if (($val=$this->increment($keys, 2, 5)) !== 10)
|
||||||
|
{
|
||||||
|
if ($verbose) echo "$label: increment(\$keys, 2, 5)=".array2string($val)." !== 10 for current \$keys === 8\n";
|
||||||
|
++$failed;
|
||||||
|
}
|
||||||
|
if (($val=$this->get($keys)) != 10)
|
||||||
|
{
|
||||||
|
if ($verbose) echo "$label: get(\$keys)=".array2string($val)." != 10 for reading back incremented value\n";
|
||||||
|
++$failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// test decrement
|
||||||
|
$keys = [$level, __CLASS__, 'decrement'];
|
||||||
|
$this->delete($keys);
|
||||||
|
|
||||||
|
if (($val=$this->decrement($keys, 3, 2)) !== 2)
|
||||||
|
{
|
||||||
|
if ($verbose) echo "$label: decrement(\$keys, 3, 2)=".array2string($val)." !== 2 for initial/unset \$keys\n";
|
||||||
|
++$failed;
|
||||||
|
}
|
||||||
|
if (($val=$this->get($keys)) != 2)
|
||||||
|
{
|
||||||
|
if ($verbose) echo "$label: get(\$keys)=".array2string($val)." != 2 for reading back decremented value\n";
|
||||||
|
++$failed;
|
||||||
|
}
|
||||||
|
if (($val=$this->decrement($keys, 2, 5)) !== 0)
|
||||||
|
{
|
||||||
|
if ($verbose) echo "$label: decrement(\$keys, 2, 5)=".array2string($val)." !== 0 for current \$keys === 2\n";
|
||||||
|
++$failed;
|
||||||
|
}
|
||||||
|
if (($val=$this->get($keys)) != 0)
|
||||||
|
{
|
||||||
|
if ($verbose) echo "$label: get(\$keys)=".array2string($val)." != 0 for reading back decremented value\n";
|
||||||
|
++$failed;
|
||||||
|
}
|
||||||
|
if (($val=$this->decrement($keys, 2, 5)) !== 0)
|
||||||
|
{
|
||||||
|
if ($verbose) echo "$label: decrement(\$keys, 2, 5)=".array2string($val)." !== 0 for current \$keys === 0 (value never less then 0!)\n";
|
||||||
|
++$failed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $failed;
|
return $failed;
|
||||||
@ -167,15 +222,90 @@ abstract class Base implements Provider
|
|||||||
unset($keys); // required by function signature
|
unset($keys); // required by function signature
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores some data in the cache
|
||||||
|
*
|
||||||
|
* @param array $keys eg. array($level,$app,$location)
|
||||||
|
* @param mixed $data
|
||||||
|
* @param int $expiration =0
|
||||||
|
* @return boolean true on success, false on error
|
||||||
|
*/
|
||||||
|
abstract function set(array $keys, $data, $expiration=0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get some data from the cache
|
||||||
|
*
|
||||||
|
* @param array $keys eg. array($level,$app,$location)
|
||||||
|
* @return mixed data stored or NULL if not found in cache
|
||||||
|
*/
|
||||||
|
abstract function get(array $keys);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments value in cache
|
||||||
|
*
|
||||||
|
* Default implementation emulating increment using get & set.
|
||||||
|
*
|
||||||
|
* @param array $keys
|
||||||
|
* @param int $offset =1 how much to increment by
|
||||||
|
* @param int $intial_value =0 value to set and return, if not in cache
|
||||||
|
* @param int $expiration =0
|
||||||
|
* @return false|int new value on success, false on error
|
||||||
|
*/
|
||||||
|
function increment(array $keys, int $offset=1, int $intial_value=0, int $expiration=0)
|
||||||
|
{
|
||||||
|
if (($value = $this->get($keys)) === false)
|
||||||
|
{
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
if (!isset($value))
|
||||||
|
{
|
||||||
|
$value = $intial_value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$value += $offset;
|
||||||
|
}
|
||||||
|
return $this->set($keys, $value, $expiration) ? $value : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrements value in cache, but never below 0
|
||||||
|
*
|
||||||
|
* If new value would be below 0, 0 will be set as new value!
|
||||||
|
* Default implementation emulating decrement using get & set.
|
||||||
|
*
|
||||||
|
* @param array $keys
|
||||||
|
* @param int $offset =1 how much to increment by
|
||||||
|
* @param int $intial_value =0 value to set and return, if not in cache
|
||||||
|
* @param int $expiration =0
|
||||||
|
* @return false|int new value on success, false on error
|
||||||
|
*/
|
||||||
|
function decrement(array $keys, int $offset=1, int $intial_value=0, int $expiration=0)
|
||||||
|
{
|
||||||
|
if (($value = $this->get($keys)) === false)
|
||||||
|
{
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
if (!isset($value))
|
||||||
|
{
|
||||||
|
$value = $intial_value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (($value -= $offset) < 0) $value = 0;
|
||||||
|
}
|
||||||
|
return $this->set($keys, $value, $expiration) ? $value : false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// some testcode, if this file is called via it's URL
|
// some testcode, if this file is called via it's URL
|
||||||
// can be run on command-line: sudo php -d apc.enable_cli=1 -f api/src/Cache/Base.php
|
// can be run on command-line: sudo php -d apc.enable_cli=1 -f api/src/Cache/Base.php
|
||||||
/*if (isset($_SERVER['SCRIPT_FILENAME']) && realpath($_SERVER['SCRIPT_FILENAME']) == __FILE__)
|
/*if (isset($_SERVER['SCRIPT_FILENAME']) && realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__)
|
||||||
{
|
{
|
||||||
if (!isset($_SERVER['HTTP_HOST']))
|
if (!isset($_SERVER['HTTP_HOST']))
|
||||||
{
|
{
|
||||||
chdir(dirname(__FILE__)); // to enable our relative pathes to work
|
chdir(__DIR__); // to enable our relative pathes to work
|
||||||
}
|
}
|
||||||
$GLOBALS['egw_info'] = array(
|
$GLOBALS['egw_info'] = array(
|
||||||
'flags' => array(
|
'flags' => array(
|
||||||
|
@ -45,7 +45,7 @@ class Memcached extends Base implements ProviderMultiple
|
|||||||
/**
|
/**
|
||||||
* Use Libketama consistent hashing
|
* Use Libketama consistent hashing
|
||||||
*
|
*
|
||||||
* Off by default as for just 2 Memcached servers it creates an extrem
|
* Off by default as for just 2 Memcached servers it creates an extreme
|
||||||
* unbalanced distribution favoring the 2. server and has no benefits
|
* unbalanced distribution favoring the 2. server and has no benefits
|
||||||
* as requests to the failed node can only go to the other one anyway.
|
* as requests to the failed node can only go to the other one anyway.
|
||||||
*
|
*
|
||||||
@ -54,7 +54,7 @@ class Memcached extends Base implements ProviderMultiple
|
|||||||
private $consistent = false;
|
private $consistent = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retry on node failure: 0: no retry, 1: retry on set/add/delete, 2: allways retry
|
* Retry on node failure: 0: no retry, 1: retry on set/add/delete, 2: always retry
|
||||||
*
|
*
|
||||||
* @var retry
|
* @var retry
|
||||||
*/
|
*/
|
||||||
@ -245,6 +245,48 @@ class Memcached extends Base implements ProviderMultiple
|
|||||||
$this->memcache->delete(self::key($keys));
|
$this->memcache->delete(self::key($keys));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments value in cache
|
||||||
|
*
|
||||||
|
* @param array $keys
|
||||||
|
* @param int $offset =1 how much to increment by
|
||||||
|
* @param int $intial_value =0 value to set and return, if not in cache
|
||||||
|
* @param int $expiration =0
|
||||||
|
* @return false|int new value on success, false on error
|
||||||
|
*/
|
||||||
|
function increment(array $keys, int $offset=1, int $intial_value=0, int $expiration=0)
|
||||||
|
{
|
||||||
|
$ret = $this->memcache->increment($key=self::key($keys), $offset, $intial_value, $expiration);
|
||||||
|
if ($ret === false && $this->retry > 0)
|
||||||
|
{
|
||||||
|
$ret = $this->memcache->increment($key, $offset, $intial_value, $expiration);
|
||||||
|
}
|
||||||
|
// fix memcached returning strings
|
||||||
|
return $ret !== false ? (int)$ret : $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrements value in cache, but never below 0
|
||||||
|
*
|
||||||
|
* If new value would be below 0, 0 will be set as new value!
|
||||||
|
*
|
||||||
|
* @param array $keys
|
||||||
|
* @param int $offset =1 how much to increment by
|
||||||
|
* @param int $intial_value =0 value to set and return, if not in cache
|
||||||
|
* @param int $expiration =0
|
||||||
|
* @return false|int new value on success, false on error
|
||||||
|
*/
|
||||||
|
function decrement(array $keys, int $offset=1, int $intial_value=0, int $expiration=0)
|
||||||
|
{
|
||||||
|
$ret = $this->memcache->decrement($key=self::key($keys), $offset, $intial_value, $expiration);
|
||||||
|
if ($ret === false && $this->retry > 0)
|
||||||
|
{
|
||||||
|
$ret = $this->memcache->decrement($key, $offset, $intial_value, $expiration);
|
||||||
|
}
|
||||||
|
// fix memcached returning strings
|
||||||
|
return $ret !== false ? (int)$ret : $ret;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a single key from $keys
|
* Create a single key from $keys
|
||||||
*
|
*
|
||||||
|
@ -58,6 +58,30 @@ interface Provider
|
|||||||
*/
|
*/
|
||||||
function get(array $keys);
|
function get(array $keys);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments value in cache
|
||||||
|
*
|
||||||
|
* @param array $keys
|
||||||
|
* @param int $offset =1 how much to increment by
|
||||||
|
* @param int $intial_value =0 value to use if not in cache
|
||||||
|
* @param int $expiration =0
|
||||||
|
* @return false|int new value on success, false on error
|
||||||
|
*/
|
||||||
|
function increment(array $keys, int $offset=1, int $intial_value=0, int $expiration=0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrements value in cache, but never below 0
|
||||||
|
*
|
||||||
|
* If new value would be below 0, 0 will be set as new value!
|
||||||
|
*
|
||||||
|
* @param array $keys
|
||||||
|
* @param int $offset =1 how much to increment by
|
||||||
|
* @param int $intial_value =0 value to use if not in cache
|
||||||
|
* @param int $expiration =0
|
||||||
|
* @return false|int new value on success, false on error
|
||||||
|
*/
|
||||||
|
function decrement(array $keys, int $offset=1, int $intial_value=0, int $expiration=0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete some data from the cache
|
* Delete some data from the cache
|
||||||
*
|
*
|
||||||
|
Loading…
Reference in New Issue
Block a user