From 91a24e0399c53363435829a22ce6116f548577db Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Fri, 13 Apr 2012 10:25:53 +0000 Subject: [PATCH] tests for egw_cache_provider and not using APC for cli, if apc.enable_cli is not set Also did some benchmarking on my Macbook (with a SSD and memcached running on localhost): Checking egw_cache_memcache: 0 checks failed, 100 iterations took 0.480 sec Checking egw_cache_apc: 0 checks failed, 100 iterations took 0.025 sec Checking egw_cache_files: 0 checks failed, 100 iterations took 0.826 sec --> APC is by a factor of 20 faster then memcached, which is double as fast compared to files on a SSD --- phpgwapi/inc/class.egw_cache.inc.php | 668 ++++++++++++++++++ phpgwapi/inc/class.egw_cache_apc.inc.php | 96 +++ phpgwapi/inc/class.egw_cache_files.inc.php | 24 +- phpgwapi/inc/class.egw_cache_memcache.inc.php | 119 ++++ 4 files changed, 888 insertions(+), 19 deletions(-) create mode 100644 phpgwapi/inc/class.egw_cache.inc.php create mode 100644 phpgwapi/inc/class.egw_cache_apc.inc.php create mode 100644 phpgwapi/inc/class.egw_cache_memcache.inc.php diff --git a/phpgwapi/inc/class.egw_cache.inc.php b/phpgwapi/inc/class.egw_cache.inc.php new file mode 100644 index 0000000000..d10607b1d6 --- /dev/null +++ b/phpgwapi/inc/class.egw_cache.inc.php @@ -0,0 +1,668 @@ + + * @copyright (c) 2009-12 by Ralf Becker + * @version $Id$ + */ + +/** + * Class to manage caching in eGroupware. + * + * It allows to cache on 4 levels: + * a) tree: for all instances/domains runining on a certain source path + * b) instance: for all sessions on a given instance + * c) session: for all requests of a session, same as egw_session::appsession() + * d) request: just for this request (same as using a static variable) + * + * There's a get, a set and a unset method for each level: eg. getTree() or setInstance(), + * as well as a variant allowing to specify the level as first parameter: eg. unsetCache() + * + * getXXX($app,$location,$callback=null,array $callback_params,$expiration=0) + * has three optional parameters allowing to specify: + * 3. a callback if requested data is not yes stored. In that case the callback is called + * and it's value is stored in the cache AND retured + * 4. parameters to pass to the callback as array, see call_user_func_array + * 5. an expiration time in seconds to specify how long data should be cached, + * default 0 means infinit (this time is not garantied and not supported for all levels!) + * + * Data is stored under an application name and a location, like egw_session::appsession(). + * In fact data stored at cache level egw_cache::SESSION, is stored in the same way as + * egw_session::appsession() so both methods can be used with each other. + * + * The $app parameter should be either the app or the class name, which both are unique. + * + * The tree and instance wide cache uses a certain provider class, to store the data + * eg. in memcached or if there's nothing else configured in the filesystem (eGW's temp_dir). + */ +class egw_cache +{ + /** + * tree-wide storage + */ + const TREE = 'Tree'; + /** + * instance-wide storage + */ + const INSTANCE = 'Instance'; + /** + * session-wide storage + */ + const SESSION = 'Session'; + /** + * request-wide storage + */ + const REQUEST = 'Request'; + + /** + * Default provider for tree and instance data + * + * Can be specified eg. in the header.inc.php by setting: + * $GLOBALS['egw_info']['server']['cache_provider_instance'] and optional + * $GLOBALS['egw_info']['server']['cache_provider_tree'] (defaults to instance) + * + * Default is set (if not set here) after class definition to egw_cache_apc or egw_cache_files, + * depending on function 'apc_fetch' exists or not + * + * @var array + */ + static $default_provider; // = array('egw_cache_files');// array('egw_cache_memcache','localhost'); + + /** + * Set some data in the cache + * + * @param string $level use egw_cache::(TREE|INSTANCE|SESSION|REQUEST) + * @param string $app application storing data + * @param string $location location name for data + * @param mixed $data + * @param int $expiration=0 expiration time in seconds, default 0 = never + * @return boolean true if data could be stored, false otherwise + */ + static public function setCache($level,$app,$location,$data,$expiration=0) + { + //error_log(__METHOD__."('$level','$app','$location',".array2string($data).",$expiration)"); + switch($level) + { + case self::SESSION: + case self::REQUEST: + return call_user_func(array(__CLASS__,'set'.$level),$app,$location,$data,$expiration); + + case self::INSTANCE: + case self::TREE: + if (!($provider = self::get_provider($level))) + { + return false; + } + return $provider->set(self::keys($level,$app,$location),$data,$expiration); + } + throw new egw_exception_wrong_parameter(__METHOD__."() unknown level '$level'!"); + } + + /** + * Get some data from the cache + * + * @param string $level use egw_cache::(TREE|INSTANCE|SESSION|REQUEST) + * @param string $app application storing data + * @param string $location location name for data + * @param callback $callback=null callback to get/create the value, if it's not cache + * @param array $callback_params=array() array with parameters for the callback + * @param int $expiration=0 expiration time in seconds, default 0 = never + * @return mixed NULL if data not found in cache (and no callback specified) + */ + static public function getCache($level,$app,$location,$callback=null,array $callback_params=array(),$expiration=0) + { + switch($level) + { + case self::SESSION: + case self::REQUEST: + return call_user_func(array(__CLASS__,'get'.$level),$app,$location,$callback,$callback_params,$expiration); + + case self::INSTANCE: + case self::TREE: + if (!($provider = self::get_provider($level))) + { + return null; + } + try { + $data = $provider->get($keys=self::keys($level,$app,$location)); + } + catch(Exception $e) { + $data = null; + } + if (is_null($data) && !is_null($callback)) + { + //error_log(__METHOD__."($level,$app,$location,".array2string($callback).','.array2string($callback_params).",$expiration) calling calback to create data."); + $data = call_user_func_array($callback,$callback_params); + $provider->set($keys,$data,$expiration); + } + return $data; + } + throw new egw_exception_wrong_parameter(__METHOD__."() unknown level '$level'!"); + } + + /** + * Unset some data in the cache + * + * @param string $level use egw_cache::(TREE|INSTANCE|SESSION|REQUEST) + * @param string $app application storing data + * @param string $location location name for data + * @return boolean true if data was set, false if not (like isset()) + */ + static public function unsetCache($level,$app,$location) + { + switch($level) + { + case self::SESSION: + case self::REQUEST: + return call_user_func(array(__CLASS__,'unset'.$level),$app,$location); + + case self::INSTANCE: + case self::TREE: + if (!($provider = self::get_provider($level))) + { + return false; + } + return $provider->delete(self::keys($level,$app,$location)); + } + throw new egw_exception_wrong_parameter(__METHOD__."() unknown level '$level'!"); + } + + /** + * Set some data in the cache for the whole source tree (all instances) + * + * @param string $app application storing data + * @param string $location location name for data + * @param mixed $data + * @param int $expiration=0 expiration time in seconds, default 0 = never + * @return boolean true if data could be stored, false otherwise + */ + static public function setTree($app,$location,$data,$expiration=0) + { + //error_log(__METHOD__."('$app','$location',".array2string($data).",$expiration)"); + return self::setCache(self::TREE,$app,$location,$data,$expiration); + } + + /** + * Get some data from the cache for the whole source tree (all instances) + * + * @param string $app application storing data + * @param string $location location name for data + * @param callback $callback=null callback to get/create the value, if it's not cache + * @param array $callback_params=array() array with parameters for the callback + * @param int $expiration=0 expiration time in seconds, default 0 = never + * @return mixed NULL if data not found in cache (and no callback specified) + */ + static public function getTree($app,$location,$callback=null,array $callback_params=array(),$expiration=0) + { + return self::getCache(self::TREE,$app,$location,$callback,$callback_params,$expiration); + } + + /** + * Unset some data in the cache for the whole source tree (all instances) + * + * @param string $app application storing data + * @param string $location location name for data + * @return boolean true if data was set, false if not (like isset()) + */ + static public function unsetTree($app,$location) + { + return self::unsetCache(self::TREE,$app,$location); + } + + /** + * Set some data in the cache for the whole source tree (all instances) + * + * @param string $app application storing data + * @param string $location location name for data + * @param mixed $data + * @param int $expiration=0 expiration time in seconds, default 0 = never + * @return boolean true if data could be stored, false otherwise + */ + static public function setInstance($app,$location,$data,$expiration=0) + { + return self::setCache(self::INSTANCE,$app,$location,$data,$expiration); + } + + /** + * Get some data from the cache for the whole source tree (all instances) + * + * @param string $app application storing data + * @param string $location location name for data + * @param callback $callback=null callback to get/create the value, if it's not cache + * @param array $callback_params=array() array with parameters for the callback + * @param int $expiration=0 expiration time in seconds, default 0 = never + * @return mixed NULL if data not found in cache (and no callback specified) + */ + static public function getInstance($app,$location,$callback=null,array $callback_params=array(),$expiration=0) + { + return self::getCache(self::INSTANCE,$app,$location,$callback,$callback_params,$expiration); + } + + /** + * Unset some data in the cache for the whole source tree (all instances) + * + * @param string $app application storing data + * @param string $location location name for data + * @return boolean true if data was set, false if not (like isset()) + */ + static public function unsetInstance($app,$location) + { + return self::unsetCache(self::INSTANCE,$app,$location); + } + + /** + * Set some data in the cache for the whole source tree (all instances) + * + * @param string $app application storing data + * @param string $location location name for data + * @param mixed $data + * @param int $expiration=0 expiration time in seconds, default 0 = never + * @return boolean true if data could be stored, false otherwise + */ + static public function setSession($app,$location,$data,$expiration=0) + { + if (isset($_SESSION[egw_session::EGW_SESSION_ENCRYPTED])) + { + if (egw_session::ERROR_LOG_DEBUG) error_log(__METHOD__.' called after session was encrypted --> ignored!'); + return false; // can no longer store something in the session, eg. because commit_session() was called + } + $_SESSION[egw_session::EGW_APPSESSION_VAR][$app][$location] = $data; + + return true; + } + + /** + * Get some data from the cache for the whole source tree (all instances) + * + * Returns a reference to the var in the session! + * + * @param string $app application storing data + * @param string $location location name for data + * @param callback $callback=null callback to get/create the value, if it's not cache + * @param array $callback_params=array() array with parameters for the callback + * @param int $expiration=0 expiration time in seconds, default 0 = never + * @return mixed NULL if data not found in cache (and no callback specified) + */ + static public function &getSession($app,$location,$callback=null,array $callback_params=array(),$expiration=0) + { + if (isset($_SESSION[egw_session::EGW_SESSION_ENCRYPTED])) + { + if (egw_session::ERROR_LOG_DEBUG) error_log(__METHOD__.' called after session was encrypted --> ignored!'); + return null; // can no longer store something in the session, eg. because commit_session() was called + } + if (!isset($_SESSION[egw_session::EGW_APPSESSION_VAR][$app][$location]) && !is_null($callback)) + { + $_SESSION[egw_session::EGW_APPSESSION_VAR][$app][$location] = call_user_func_array($callback,$callback_params); + } + return $_SESSION[egw_session::EGW_APPSESSION_VAR][$app][$location]; + } + + /** + * Unset some data in the cache for the whole source tree (all instances) + * + * @param string $app application storing data + * @param string $location location name for data + * @return boolean true if data was set, false if not (like isset()) + */ + static public function unsetSession($app,$location) + { + if (isset($_SESSION[egw_session::EGW_SESSION_ENCRYPTED])) + { + if (egw_session::ERROR_LOG_DEBUG) error_log(__METHOD__.' called after session was encrypted --> ignored!'); + return false; // can no longer store something in the session, eg. because commit_session() was called + } + if (!isset($_SESSION[egw_session::EGW_APPSESSION_VAR][$app][$location])) + { + return false; + } + unset($_SESSION[egw_session::EGW_APPSESSION_VAR][$app][$location]); + + return true; + } + + /** + * Static varible to cache request wide + * + * @var array + */ + private static $request_cache = array(); + + /** + * Set some data in the cache for the whole source tree (all instances) + * + * @param string $app application storing data + * @param string $location location name for data + * @param mixed $data + * @param int $expiration=0 expiration time is NOT used for REQUEST! + * @return boolean true if data could be stored, false otherwise + */ + static public function setRequest($app,$location,$data,$expiration=0) + { + self::$request_cache[$app][$location] = $data; + + return true; + } + + /** + * Get some data from the cache for the whole source tree (all instances) + * + * @param string $app application storing data + * @param string $location location name for data + * @param callback $callback=null callback to get/create the value, if it's not cache + * @param array $callback_params=array() array with parameters for the callback + * @param int $expiration=0 expiration time is NOT used for REQUEST! + * @return mixed NULL if data not found in cache (and no callback specified) + */ + static public function getRequest($app,$location,$callback=null,array $callback_params=array(),$expiration=0) + { + if (!isset(self::$request_cache[$app][$location]) && !is_null($callback)) + { + self::$request_cache[$app][$location] = call_user_func_array($callback,$callback_params); + } + return self::$request_cache[$app][$location]; + } + + /** + * Unset some data in the cache for the whole source tree (all instances) + * + * @param string $app application storing data + * @param string $location location name for data + * @return boolean true if data was set, false if not (like isset()) + */ + static public function unsetRequest($app,$location) + { + if (!isset(self::$request_cache[$app][$location])) + { + return false; + } + unset(self::$request_cache[$app][$location]); + + return $ret; + } + + /** + * Get a caching provider for tree or instance level + * + * The returned provider already has an opened connection + * + * @param string $level egw_cache::(TREE|INSTANCE) + * @return egw_cache_provider + */ + static protected function get_provider($level) + { + static $providers = array(); + + if (!isset($providers[$level])) + { + $params = $GLOBALS['egw_info']['server']['cache_provider_'.strtolower($level)]; + if (!isset($params) && $level == self::INSTANCE && isset(self::$default_provider)) + { + $params = self::$default_provider; + } + if (!isset($params)) + { + if ($level == self::TREE) // if no tree level provider use the instance level one + { + $providers[$level] = self::get_provider(self::INSTANCE); + } + else + { + $providers[$level] = false; // no provider specified + $reason = 'no provider specified'; + } + } + elseif (!$params) + { + $providers[$level] = false; // cache for $level disabled + $reason = "cache for $level disabled"; + } + else + { + if (!is_array($params)) $params = (array)$params; + + $class = array_shift($params); + if (!class_exists($class)) + { + $providers[$level] = false; // provider class not found + $reason = "provider $class not found"; + } + else + { + try + { + $providers[$level] = new $class($params); + } + catch(Exception $e) + { + $providers[$level] = false; // eg. could not open connection to backend + $reason = "error instanciating provider $class: ".$e->getMessage(); + } + } + } + if (!$providers[$level]) error_log(__METHOD__."($level) no provider found ($reason)!"); + } + //error_log(__METHOD__."($level) = ".array2string($providers[$level]).', cache_provider='.array2string($GLOBALS['egw_info']['server']['cache_provider_'.strtolower($level)])); + return $providers[$level]; + } + + /** + * Get a system configuration, even if in setup and it's not read + * + * @param string $name + * @param boolean $throw=true throw an exception, if we can't retriev the value + * @return string|boolean string with config or false if not found and !$throw + */ + static public function get_system_config($name,$throw=true) + { + if(!isset($GLOBALS['egw_info']['server'][$name])) + { + if (isset($GLOBALS['egw_setup']) && isset($GLOBALS['egw_setup']->db) || $GLOBALS['egw']->db) + { + $db = $GLOBALS['egw']->db ? $GLOBALS['egw']->db : $GLOBALS['egw_setup']->db; + + if (($rs = $db->select(config::TABLE,'config_value',array( + 'config_app' => 'phpgwapi', + 'config_name' => $name, + ),__LINE__,__FILE__))) + { + $GLOBALS['egw_info']['server'][$name] = $rs->fetchColumn(); + } + else + { + error_log(__METHOD__."('name', $throw) cound NOT query value!"); + } + } + if (!$GLOBALS['egw_info']['server'][$name] && $throw) + { + throw new Exception (__METHOD__."($name) \$GLOBALS['egw_info']['server']['$name'] is NOT set!"); + } + } + return $GLOBALS['egw_info']['server'][$name]; + } + + /** + * Get keys array from $level, $app and $location + * + * @param string $level egw_cache::(TREE|INSTANCE) + * @param string $app + * @param string $location + * @return array + */ + static public function keys($level,$app,$location) + { + static $bases = array(); + + if (!isset($bases[$level])) + { + switch($level) + { + case self::TREE: + $bases[$level] = $level.'-'.str_replace(array(':','/','\\'),'-',EGW_SERVER_ROOT); + // add charset to key, if not utf-8 (as everything we store depends on charset!) + if (($charset = self::get_system_config('system_charset',false)) && $charset != 'utf-8') + { + $bases[$level] .= '-'.$charset; + } + break; + case self::INSTANCE: + $bases[$level] = $level.'-'.self::get_system_config('install_id'); + break; + } + } + return array($bases[$level],$app,$location); + } + + /** + * Let everyone know the methods of this class should be used only statically + * + */ + function __construct() + { + throw new egw_exception_wrong_parameter("All methods of class ".__CLASS__." should be called static!"); + } +} + +// setting apc as default provide, if apc_fetch function exists AND not cli or apc enabled for cli +if (is_null(egw_cache::$default_provider)) +{ + egw_cache::$default_provider = function_exists('apc_fetch') && (PHP_SAPI != 'cli' || ini_get('apc.enable_cli')) ? + 'egw_cache_apc' : 'egw_cache_files'; +} + +/** + * Interface for a caching provider for tree and instance level + * + * The provider can eg. create subdirs under /tmp for each key + * to store data as a file or concat them with a separator to + * get a single string key to eg. store data in memcached + */ +interface egw_cache_provider +{ + /** + * Constructor, eg. opens the connection to the backend + * + * @throws Exception if connection to backend could not be established + * @param array $params eg. array(host,port) or array(directory) depending on the provider + */ + function __construct(array $params); + + /** + * 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 + */ + 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 + */ + function get(array $keys); + + /** + * Delete some data from the cache + * + * @param array $keys eg. array($level,$app,$location) + * @return boolean true on success, false on error (eg. $key not set) + */ + function delete(array $keys); +} + +abstract class egw_cache_provider_check implements egw_cache_provider +{ + /** + * Run several checks on a caching provider + * + * @param boolean $verbose=false true: echo failed checks + * @return int number of failed checks + */ + function check($verbose=false) + { + $failed = 0; + foreach(array( + egw_cache::TREE => 'tree', + egw_cache::INSTANCE => 'instance', + ) as $level => $label) + { + foreach(array('string',123,true,false,null,array(),array(1,2,3)) as $data) + { + $location = md5(microtime(true).$label.serialize($data)); + $get_before_set = $this->get(array($level,__CLASS__,$location)); + if (!is_null($get_before_set)) + { + if ($verbose) echo "$label: get_before_set=".array2string($get_before_set)." != NULL\n"; + ++$failed; + } + if (($set = $this->set(array($level,__CLASS__,$location), $data, 10)) !== true) + { + if ($verbose) echo "$label: set returned ".array2string($set)." !== TRUE\n"; + ++$failed; + } + $get_after_set = $this->get(array($level,__CLASS__,$location)); + if ($get_after_set !== $data) + { + if ($verbose) echo "$label: get_after_set=".array2string($get_after_set)." !== ".array2string($data)."\n"; + ++$failed; + } + if (($delete = $this->delete(array($level,__CLASS__,$location))) !== true) + { + if ($verbose) echo "$label: delete returned ".array2string($delete)." !== TRUE\n"; + ++$failed; + } + $get_after_delete = $this->get(array($level,__CLASS__,$location)); + if (!is_null($get_after_delete)) + { + if ($verbose) echo "$label: get_after_delete=".array2string($get_after_delete)." != NULL\n"; + ++$failed; + } + } + } + + return $failed; + } +} + +// 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 phpgwapi/inc/class.egw_cache.inc.php +/*if (isset($_SERVER['SCRIPT_FILENAME']) && realpath($_SERVER['SCRIPT_FILENAME']) == __FILE__) +{ + if (!isset($_SERVER['HTTP_HOST'])) + { + chdir(dirname(__FILE__)); // to enable our relative pathes to work + } + $GLOBALS['egw_info'] = array( + 'flags' => array( + 'noapi' => true, + ), + ); + include_once '../../header.inc.php'; + + if (isset($_SERVER['HTTP_HOST'])) echo "
\n";
+
+	foreach(array(
+		'egw_cache_memcache' => array('localhost'),
+		'egw_cache_apc' => array(),
+		'egw_cache_files' => array('/tmp'),
+	) as $class => $param)
+	{
+		echo "Checking $class:\n";
+		$start = microtime(true);
+		$provider = new $class($param);
+		$n = 100;
+		for($i=1; $i <= $n; ++$i)
+		{
+			$failed = $provider->check($i == 1);
+		}
+		printf("$failed checks failed, $n iterations took %5.3f sec\n\n", microtime(true)-$start);
+	}
+}*/
diff --git a/phpgwapi/inc/class.egw_cache_apc.inc.php b/phpgwapi/inc/class.egw_cache_apc.inc.php
new file mode 100644
index 0000000000..649735a623
--- /dev/null
+++ b/phpgwapi/inc/class.egw_cache_apc.inc.php
@@ -0,0 +1,96 @@
+
+ * @copyright (c) 2010-12 by Ralf Becker 
+ * @version $Id$
+ */
+
+/**
+ * Caching provider storing data in PHP's APC
+ *
+ * The provider concats all $keys with '::' to get a single string.
+ *
+ * To use this provider set in your header.inc.php:
+ * $GLOBALS['egw_info']['server']['cache_provider_instance'] = array('egw_cache_apc');
+ * and optional also $GLOBALS['egw_info']['server']['cache_provider_tree'] (defaults to instance)
+ */
+class egw_cache_apc extends egw_cache_provider_check implements egw_cache_provider
+{
+	/**
+	 * Constructor, eg. opens the connection to the backend
+	 *
+	 * @throws Exception if connection to backend could not be established
+	 * @param array $params eg. array('localhost'[,'localhost:11211',...])
+	 */
+	function __construct(array $params)
+	{
+		if (!function_exists('apc_fetch'))	// apc >= 3.0
+		{
+			throw new Exception (__METHOD__.'('.array2string($params).") No function apc_fetch()!");
+		}
+		if (PHP_SAPI == 'cli' && !ini_get('apc.enable_cli'))
+		{
+			throw new Exception (__METHOD__.'('.array2string($params).") APC NOT enabled for cli, check apc.enable_cli!");
+		}
+	}
+
+	/**
+	 * 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
+	 */
+	function set(array $keys,$data,$expiration=0)
+	{
+		return apc_store(self::key($keys),$data,$expiration);
+	}
+
+	/**
+	 * 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
+	 */
+	function get(array $keys)
+	{
+		$data = apc_fetch($key=self::key($keys),$success);
+
+		if (!$success)
+		{
+			//error_log(__METHOD__."(".array2string($keys).") key='$key' NOT found!");
+			return null;
+		}
+		//error_log(__METHOD__."(".array2string($keys).") key='$key' found ".bytes(serialize($data))." bytes).");
+		return $data;
+	}
+
+	/**
+	 * Delete some data from the cache
+	 *
+	 * @param array $keys eg. array($level,$app,$location)
+	 * @return boolean true on success, false on error (eg. $key not set)
+	 */
+	function delete(array $keys)
+	{
+		return apc_delete(self::key($keys));
+	}
+
+	/**
+	 * Create a single key from $keys
+	 *
+	 * @param array $keys
+	 * @return string
+	 */
+	private function key(array $keys)
+	{
+		return implode('::',$keys);
+	}
+}
diff --git a/phpgwapi/inc/class.egw_cache_files.inc.php b/phpgwapi/inc/class.egw_cache_files.inc.php
index 6625b0f6e8..78441d1663 100644
--- a/phpgwapi/inc/class.egw_cache_files.inc.php
+++ b/phpgwapi/inc/class.egw_cache_files.inc.php
@@ -1,13 +1,13 @@
 
- * @copyright (c) 2009 by Ralf Becker 
+ * @copyright (c) 2009-12 by Ralf Becker 
  * @version $Id$
  */
 
@@ -17,7 +17,7 @@
  * The provider creates subdirs under a given path
  * for each values in $key
  */
-class egw_cache_files implements egw_cache_provider
+class egw_cache_files extends egw_cache_provider_check implements egw_cache_provider
 {
 	/**
 	 * Extension of file used to store expiration > 0
@@ -45,21 +45,7 @@ class egw_cache_files implements egw_cache_provider
 		}
 		else
 		{
-			if(!isset($GLOBALS['egw_info']['server']['temp_dir']))
-			{
-				if (isset($GLOBALS['egw_setup']) && isset($GLOBALS['egw_setup']->db))
-				{
-					$GLOBALS['egw_info']['server']['temp_dir'] = $GLOBALS['egw_setup']->db->select(config::TABLE,'config_value',array(
-						'config_app'	=> 'phpgwapi',
-						'config_name'	=> 'temp_dir',
-					),__LINE__,__FILE__)->fetchColumn();
-				}
-				if (!$GLOBALS['egw_info']['server']['temp_dir'])
-				{
-					throw new Exception (__METHOD__."() server/temp_dir is NOT set!");
-				}
-			}
-			$this->base_path = $GLOBALS['egw_info']['server']['temp_dir'].'/egw_cache';
+			$this->base_path = egw_cache::get_system_config('temp_dir').'/egw_cache';
 		}
 		if (!isset($this->base_path) || !file_exists($this->base_path) && !mkdir($this->base_path,0700,true))
 		{
@@ -135,7 +121,7 @@ class egw_cache_files implements egw_cache_provider
 	 * @param boolean $mkdir=false should we create the directory
 	 * @return string
 	 */
-	private function filename(array $keys,$mkdir=false)
+	function filename(array $keys,$mkdir=false)
 	{
 		$fname = $this->base_path.'/'.str_replace(array(':','*'),'-',implode('/',$keys));
 
diff --git a/phpgwapi/inc/class.egw_cache_memcache.inc.php b/phpgwapi/inc/class.egw_cache_memcache.inc.php
new file mode 100644
index 0000000000..5673765fd5
--- /dev/null
+++ b/phpgwapi/inc/class.egw_cache_memcache.inc.php
@@ -0,0 +1,119 @@
+
+ * @copyright (c) 2009-12 by Ralf Becker 
+ * @version $Id$
+ */
+
+/**
+ * Caching provider storing data in memcached via PHP's memcache extension
+ *
+ * The provider concats all $keys with '::' to get a single string.
+ *
+ * To use this provider set in your header.inc.php:
+ * $GLOBALS['egw_info']['server']['cache_provider_instance'] = array('egw_cache_memcache','localhost'[,'otherhost:port']);
+ * and optional also $GLOBALS['egw_info']['server']['cache_provider_tree'] (defaults to instance)
+ *
+ * You can set more then one server and specify a port, if it's not the default one 11211.
+ */
+class egw_cache_memcache extends egw_cache_provider_check implements egw_cache_provider
+{
+	/**
+	 * Instance of Memcache
+	 *
+	 * @var Memcache
+	 */
+	private $memcache;
+
+	/**
+	 * Flags used to store content
+	 *
+	 */
+	const STORE_FLAGS = MEMCACHE_COMPRESSED;
+
+	/**
+	 * Constructor, eg. opens the connection to the backend
+	 *
+	 * @throws Exception if connection to backend could not be established
+	 * @param array $params eg. array('localhost'[,'localhost:11211',...])
+	 */
+	function __construct(array $params)
+	{
+		check_load_extension('memcache',true);
+		$this->memcache = new Memcache();
+
+		if (!$params) $params = array('localhost');	// some reasonable default
+
+		$ok = false;
+		foreach($params as $host_port)
+		{
+			list($host,$port) = explode(':',$host_port);
+			if (!$port) $port = 11211;	// default port
+
+			$ok = $this->memcache->addServer($host,$port) || $ok;
+			//error_log(__METHOD__."(".array2string($params).") memcache->addServer('$host',$port) = ".(int)$ok);
+		}
+		if (!$ok)
+		{
+			throw new Exception (__METHOD__.'('.array2string($params).") Can't open connection to any memcached server!");
+		}
+	}
+
+	/**
+	 * 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
+	 */
+	function set(array $keys,$data,$expiration=0)
+	{
+		return $this->memcache->set(self::key($keys),serialize($data),self::STORE_FLAGS,$expiration);
+	}
+
+	/**
+	 * 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
+	 */
+	function get(array $keys)
+	{
+		if (($data = $this->memcache->get($key=self::key($keys))) === false)
+		{
+			//error_log(__METHOD__."(".array2string($keys).") key='$key' NOT found!");
+			return null;
+		}
+		//error_log(__METHOD__."(".array2string($keys).") key='$key' found ".bytes($data)." bytes).");
+		return unserialize($data);
+	}
+
+	/**
+	 * Delete some data from the cache
+	 *
+	 * @param array $keys eg. array($level,$app,$location)
+	 * @return boolean true on success, false on error (eg. $key not set)
+	 */
+	function delete(array $keys)
+	{
+		return $this->memcache->delete(self::key($keys));
+	}
+
+	/**
+	 * Create a single key from $keys
+	 *
+	 * @param array $keys
+	 * @return string
+	 */
+	private function key(array $keys)
+	{
+		return implode('::',$keys);
+	}
+}