From 3628b62aebe187f49b5e3019ed74fa6e0dd815db Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Wed, 28 Feb 2018 23:48:09 -0500 Subject: [PATCH] Add support for Memcached/Improve database cache - Create Cache Driver interface - Update cache table fields - Add CacheSessionHandler --- boot.php | 2 +- src/Core/Cache.php | 210 +++++------------- src/Core/Cache/DatabaseCacheDriver.php | 56 +++++ src/Core/Cache/ICacheDriver.php | 50 +++++ src/Core/Cache/MemcacheCacheDriver.php | 77 +++++++ src/Core/Cache/MemcachedCacheDriver.php | 66 ++++++ src/Core/Session.php | 10 +- ...ionHandler.php => CacheSessionHandler.php} | 35 +-- src/Database/DBStructure.php | 12 +- 9 files changed, 326 insertions(+), 192 deletions(-) create mode 100644 src/Core/Cache/DatabaseCacheDriver.php create mode 100644 src/Core/Cache/ICacheDriver.php create mode 100644 src/Core/Cache/MemcacheCacheDriver.php create mode 100644 src/Core/Cache/MemcachedCacheDriver.php rename src/Core/Session/{MemcacheSessionHandler.php => CacheSessionHandler.php} (65%) diff --git a/boot.php b/boot.php index 4ba8c1ad8d..5dc7ce1e9d 100644 --- a/boot.php +++ b/boot.php @@ -39,7 +39,7 @@ define('FRIENDICA_PLATFORM', 'Friendica'); define('FRIENDICA_CODENAME', 'Asparagus'); define('FRIENDICA_VERSION', '3.6-dev'); define('DFRN_PROTOCOL_VERSION', '2.23'); -define('DB_UPDATE_VERSION', 1255); +define('DB_UPDATE_VERSION', 1256); define('NEW_UPDATE_ROUTINE_VERSION', 1170); /** diff --git a/src/Core/Cache.php b/src/Core/Cache.php index 5ceb97676f..58969f0819 100644 --- a/src/Core/Cache.php +++ b/src/Core/Cache.php @@ -4,44 +4,46 @@ */ namespace Friendica\Core; +use Friendica\Core\Cache; use Friendica\Core\Config; -use Friendica\Database\DBM; -use Friendica\Util\DateTimeFormat; -use dba; -use Memcache; - -require_once 'include/dba.php'; /** * @brief Class for storing data for a short time */ class Cache { + const MONTH = 0; + const WEEK = 1; + const DAY = 2; + const HOUR = 3; + const HALF_HOUR = 4; + const QUARTER_HOUR = 5; + const FIVE_MINUTES = 6; + const MINUTE = 7; + /** - * @brief Check for Memcache and open a connection if configured - * - * @return Memcache|boolean The Memcache object - or "false" if not successful + * @var Cache\ICacheDriver */ - public static function memcache() + static $driver = null; + + public static function init() { - if (!class_exists('Memcache', false)) { - return false; + switch(Config::get('system', 'cache_driver', 'database')) { + case 'memcache': + $memcache_host = Config::get('system', 'memcache_host', '127.0.0.1'); + $memcache_port = Config::get('system', 'memcache_port', 11211); + + self::$driver = new Cache\MemcacheCacheDriver($memcache_host, $memcache_port); + break; + case 'memcached': + $memcached_host = Config::get('system', 'memcached_host', '127.0.0.1'); + $memcached_port = Config::get('system', 'memcached_port', 11211); + + self::$driver = new Cache\MemcachedCacheDriver($memcached_host, $memcached_port); + break; + default: + self::$driver = new Cache\DatabaseCacheDriver(); } - - if (!Config::get('system', 'memcache')) { - return false; - } - - $memcache_host = Config::get('system', 'memcache_host', '127.0.0.1'); - $memcache_port = Config::get('system', 'memcache_port', 11211); - - $memcache = new Memcache(); - - if (!$memcache->connect($memcache_host, $memcache_port)) { - return false; - } - - return $memcache; } /** @@ -51,31 +53,31 @@ class Cache * * @return integer The cache duration in seconds */ - private static function duration($level) + public static function duration($level) { switch ($level) { - case CACHE_MONTH: + case self::MONTH: $seconds = 2592000; break; - case CACHE_WEEK: + case self::WEEK: $seconds = 604800; break; - case CACHE_DAY: + case self::DAY: $seconds = 86400; break; - case CACHE_HOUR: + case self::HOUR: $seconds = 3600; break; - case CACHE_HALF_HOUR: + case self::HALF_HOUR: $seconds = 1800; break; - case CACHE_QUARTER_HOUR: + case self::QUARTER_HOUR: $seconds = 900; break; - case CACHE_FIVE_MINUTES: + case self::FIVE_MINUTES: $seconds = 300; break; - case CACHE_MINUTE: + case self::MINUTE: default: $seconds = 60; break; @@ -83,6 +85,20 @@ class Cache return $seconds; } + /** + * Returns the current cache driver + * + * @return Cache\ICacheDriver + */ + private static function getDriver() + { + if (self::$driver === null) { + self::init(); + } + + return self::$driver; + } + /** * @brief Fetch cached data according to the key * @@ -92,40 +108,7 @@ class Cache */ public static function get($key) { - $memcache = self::memcache(); - if (is_object($memcache)) { - // We fetch with the hostname as key to avoid problems with other applications - $cached = $memcache->get(get_app()->get_hostname().":".$key); - $value = @unserialize($cached); - - // Only return a value if the serialized value is valid. - // We also check if the db entry is a serialized - // boolean 'false' value (which we want to return). - if ($cached === serialize(false) || $value !== false) { - return $value; - } - - return null; - } - - // Frequently clear cache - self::clear(); - - $cache = dba::selectFirst('cache', ['v'], ['k' => $key]); - - if (DBM::is_result($cache)) { - $cached = $cache['v']; - $value = @unserialize($cached); - - // Only return a value if the serialized value is valid. - // We also check if the db entry is a serialized - // boolean 'false' value (which we want to return). - if ($cached === serialize(false) || $value !== false) { - return $value; - } - } - - return null; + return self::getDriver()->get($key); } /** @@ -137,20 +120,11 @@ class Cache * @param mixed $value The value that is about to be stored * @param integer $duration The cache lifespan * - * @return void + * @return bool */ - public static function set($key, $value, $duration = CACHE_MONTH) + public static function set($key, $value, $duration = self::MONTH) { - // Do we have an installed memcache? Use it instead. - $memcache = self::memcache(); - if (is_object($memcache)) { - // We store with the hostname as key to avoid problems with other applications - $memcache->set(get_app()->get_hostname().":".$key, serialize($value), MEMCACHE_COMPRESSED, self::duration($duration)); - return; - } - $fields = ['v' => serialize($value), 'expire_mode' => $duration, 'updated' => DateTimeFormat::utcNow()]; - $condition = ['k' => $key]; - dba::update('cache', $fields, $condition, true); + return self::getDriver()->set($key, $value, $duration); } /** @@ -160,76 +134,8 @@ class Cache * * @return void */ - public static function clear($max_level = CACHE_MONTH) + public static function clear() { - // Clear long lasting cache entries only once a day - if (Config::get("system", "cache_cleared_day") < time() - self::duration(CACHE_DAY)) { - if ($max_level == CACHE_MONTH) { - $condition = ["`updated` < ? AND `expire_mode` = ?", - DateTimeFormat::utc("now - 30 days"), - CACHE_MONTH]; - dba::delete('cache', $condition); - } - - if ($max_level <= CACHE_WEEK) { - $condition = ["`updated` < ? AND `expire_mode` = ?", - DateTimeFormat::utc("now - 7 days"), - CACHE_WEEK]; - dba::delete('cache', $condition); - } - - if ($max_level <= CACHE_DAY) { - $condition = ["`updated` < ? AND `expire_mode` = ?", - DateTimeFormat::utc("now - 1 days"), - CACHE_DAY]; - dba::delete('cache', $condition); - } - Config::set("system", "cache_cleared_day", time()); - } - - if (($max_level <= CACHE_HOUR) && (Config::get("system", "cache_cleared_hour")) < time() - self::duration(CACHE_HOUR)) { - $condition = ["`updated` < ? AND `expire_mode` = ?", - DateTimeFormat::utc("now - 1 hours"), - CACHE_HOUR]; - dba::delete('cache', $condition); - - Config::set("system", "cache_cleared_hour", time()); - } - - if (($max_level <= CACHE_HALF_HOUR) && (Config::get("system", "cache_cleared_half_hour")) < time() - self::duration(CACHE_HALF_HOUR)) { - $condition = ["`updated` < ? AND `expire_mode` = ?", - DateTimeFormat::utc("now - 30 minutes"), - CACHE_HALF_HOUR]; - dba::delete('cache', $condition); - - Config::set("system", "cache_cleared_half_hour", time()); - } - - if (($max_level <= CACHE_QUARTER_HOUR) && (Config::get("system", "cache_cleared_quarter_hour")) < time() - self::duration(CACHE_QUARTER_HOUR)) { - $condition = ["`updated` < ? AND `expire_mode` = ?", - DateTimeFormat::utc("now - 15 minutes"), - CACHE_QUARTER_HOUR]; - dba::delete('cache', $condition); - - Config::set("system", "cache_cleared_quarter_hour", time()); - } - - if (($max_level <= CACHE_FIVE_MINUTES) && (Config::get("system", "cache_cleared_five_minute")) < time() - self::duration(CACHE_FIVE_MINUTES)) { - $condition = ["`updated` < ? AND `expire_mode` = ?", - DateTimeFormat::utc("now - 5 minutes"), - CACHE_FIVE_MINUTES]; - dba::delete('cache', $condition); - - Config::set("system", "cache_cleared_five_minute", time()); - } - - if (($max_level <= CACHE_MINUTE) && (Config::get("system", "cache_cleared_minute")) < time() - self::duration(CACHE_MINUTE)) { - $condition = ["`updated` < ? AND `expire_mode` = ?", - DateTimeFormat::utc("now - 1 minutes"), - CACHE_MINUTE]; - dba::delete('cache', $condition); - - Config::set("system", "cache_cleared_minute", time()); - } + return self::getDriver()->clear(); } } diff --git a/src/Core/Cache/DatabaseCacheDriver.php b/src/Core/Cache/DatabaseCacheDriver.php new file mode 100644 index 0000000000..0aefc812b9 --- /dev/null +++ b/src/Core/Cache/DatabaseCacheDriver.php @@ -0,0 +1,56 @@ + + */ +class DatabaseCacheDriver implements ICacheDriver +{ + public function get($key) + { + $cache = dba::selectFirst('cache', ['v'], ['`k` = ? AND `expires` >= NOW()`', $key]); + + if (DBM::is_result($cache)) { + $cached = $cache['v']; + $value = @unserialize($cached); + + // Only return a value if the serialized value is valid. + // We also check if the db entry is a serialized + // boolean 'false' value (which we want to return). + if ($cached === serialize(false) || $value !== false) { + return $value; + } + } + + return null; + } + + public function set($key, $value, $duration = Cache::MONTH) + { + $fields = [ + 'v' => serialize($value), + 'expires' => DateTimeFormat::utc('now + ' . Cache::duration($duration) . ' seconds'), + 'updated' => DateTimeFormat::utcNow() + ]; + + return dba::update('cache', $fields, ['k' => $key], true); + } + + public function delete($key) + { + return dba::delete('cache', ['k' => $key]); + } + + public function clear() + { + return dba::delete('cache', ['`expires` < NOW()']); + } +} diff --git a/src/Core/Cache/ICacheDriver.php b/src/Core/Cache/ICacheDriver.php new file mode 100644 index 0000000000..9ed622693c --- /dev/null +++ b/src/Core/Cache/ICacheDriver.php @@ -0,0 +1,50 @@ + + */ +interface ICacheDriver +{ + /** + * Fetches cached data according to the key + * + * @param string $key The key to the cached data + * + * @return mixed Cached $value or "null" if not found + */ + public function get($key); + + /** + * Stores data in the cache identified by the key. The input $value can have multiple formats. + * + * @param string $key The cache key + * @param mixed $value The value to store + * @param integer $duration The cache lifespan, must be one of the Cache constants + * + * @return bool + */ + public function set($key, $value, $duration = Cache::MONTH); + + + /** + * Delete a key from the cache + * + * @param string $key + * + * @return bool + */ + public function delete($key); + + /** + * Remove outdated data from the cache + * + * @return bool + */ + public function clear(); +} diff --git a/src/Core/Cache/MemcacheCacheDriver.php b/src/Core/Cache/MemcacheCacheDriver.php new file mode 100644 index 0000000000..03fc075f4b --- /dev/null +++ b/src/Core/Cache/MemcacheCacheDriver.php @@ -0,0 +1,77 @@ + + */ +class MemcacheCacheDriver extends BaseObject implements ICacheDriver +{ + /** + * @var Memcache + */ + private $memcache; + + public function __construct($memcache_host, $memcache_port) + { + if (!class_exists('Memcache', false)) { + throw new \Exception('Memcache class isn\'t available'); + } + + $this->memcache = new \Memcache(); + + if (!$this->memcache->connect($memcache_host, $memcache_port)) { + throw new \Exception('Expected Memcache server at ' . $memcache_host . ':' . $memcache_port . ' isn\'t available'); + } + } + + public function get($key) + { + $return = null; + + // We fetch with the hostname as key to avoid problems with other applications + $cached = $this->memcache->get(self::getApp()->get_hostname() . ':' . $key); + + // @see http://php.net/manual/en/memcache.get.php#84275 + if (is_bool($cached) || is_double($cached) || is_long($cached)) { + return $return; + } + + $value = @unserialize($cached); + + // Only return a value if the serialized value is valid. + // We also check if the db entry is a serialized + // boolean 'false' value (which we want to return). + if ($cached === serialize(false) || $value !== false) { + $return = $value; + } + + return $return; + } + + public function set($key, $value, $duration = Cache::MONTH) + { + // We store with the hostname as key to avoid problems with other applications + return $this->memcache->set( + self::getApp()->get_hostname() . ":" . $key, + serialize($value), + MEMCACHE_COMPRESSED, + Cache::duration($duration) + ); + } + + public function delete($key) + { + return $this->memcache->delete($key); + } + + public function clear() + { + return true; + } +} diff --git a/src/Core/Cache/MemcachedCacheDriver.php b/src/Core/Cache/MemcachedCacheDriver.php new file mode 100644 index 0000000000..9101c79195 --- /dev/null +++ b/src/Core/Cache/MemcachedCacheDriver.php @@ -0,0 +1,66 @@ + + */ +class MemcachedCacheDriver extends BaseObject implements ICacheDriver +{ + /** + * @var Memcached + */ + private $memcached; + + public function __construct($memcached_host, $memcached_port) + { + if (!class_exists('Memcached', false)) { + throw new \Exception('Memcached class isn\'t available'); + } + + $this->memcached = new \Memcached(); + + if (!$this->memcached->addServer($memcached_host, $memcached_port)) { + throw new \Exception('Expected Memcached server at ' . $memcached_host . ':' . $memcached_port . ' isn\'t available'); + } + } + + public function get($key) + { + $return = null; + + // We fetch with the hostname as key to avoid problems with other applications + $value = $this->memcached->get(self::getApp()->get_hostname() . ':' . $key); + + if ($this->memcached->getResultCode() === \Memcached::RES_SUCCESS) { + $return = $value; + } + + return $return; + } + + public function set($key, $value, $duration = Cache::MONTH) + { + // We store with the hostname as key to avoid problems with other applications + return $this->memcached->set( + self::getApp()->get_hostname() . ":" . $key, + $value, + Cache::duration($duration) + ); + } + + public function delete($key) + { + return $this->memcached->delete($key); + } + + public function clear() + { + return true; + } +} diff --git a/src/Core/Session.php b/src/Core/Session.php index 20d1e9ef7b..b245c675b0 100644 --- a/src/Core/Session.php +++ b/src/Core/Session.php @@ -5,8 +5,8 @@ */ namespace Friendica\Core; +use Friendica\Core\Session\CacheSessionHandler; use Friendica\Core\Session\DatabaseSessionHandler; -use Friendica\Core\Session\MemcacheSessionHandler; /** * High-level Session service class @@ -28,10 +28,10 @@ class Session ini_set('session.cookie_secure', 1); } - if (!Config::get('system', 'disable_database_session')) { - $memcache = Cache::memcache(); - if (is_object($memcache)) { - $SessionHandler = new MemcacheSessionHandler($memcache); + $session_handler = Config::get('system', 'session_handler', 'database'); + if ($session_handler != 'native') { + if ($session_handler == 'cache' && Config::get('system', 'cache_driver', 'database') != 'database') { + $SessionHandler = new CacheSessionHandler(); } else { $SessionHandler = new DatabaseSessionHandler(); } diff --git a/src/Core/Session/MemcacheSessionHandler.php b/src/Core/Session/CacheSessionHandler.php similarity index 65% rename from src/Core/Session/MemcacheSessionHandler.php rename to src/Core/Session/CacheSessionHandler.php index 0bc5e8bfe9..463fd33d3e 100644 --- a/src/Core/Session/MemcacheSessionHandler.php +++ b/src/Core/Session/CacheSessionHandler.php @@ -3,34 +3,20 @@ namespace Friendica\Core\Session; use Friendica\BaseObject; +use Friendica\Core\Cache; use Friendica\Core\Session; use SessionHandlerInterface; -use Memcache; require_once 'boot.php'; require_once 'include/text.php'; /** - * SessionHandler using Memcache + * SessionHandler using Friendica Cache * * @author Hypolite Petovan */ -class MemcacheSessionHandler extends BaseObject implements SessionHandlerInterface +class CacheSessionHandler extends BaseObject implements SessionHandlerInterface { - /** - * @var Memcache - */ - private $memcache = null; - - /** - * - * @param Memcache $memcache - */ - public function __construct(Memcache $memcache) - { - $this->memcache = $memcache; - } - public function open($save_path, $session_name) { return true; @@ -42,8 +28,8 @@ class MemcacheSessionHandler extends BaseObject implements SessionHandlerInterfa return ''; } - $data = $this->memcache->get(self::getApp()->get_hostname() . ":session:" . $session_id); - if (!is_bool($data)) { + $data = Cache::get('session:' . $session_id); + if (!empty($data)) { Session::$exists = true; return $data; } @@ -72,14 +58,7 @@ class MemcacheSessionHandler extends BaseObject implements SessionHandlerInterfa return true; } - $expire = time() + Session::$expire; - - $this->memcache->set( - self::getApp()->get_hostname() . ":session:" . $session_id, - $session_data, - MEMCACHE_COMPRESSED, - $expire - ); + Cache::set('session:' . $session_id, $session_data, Session::$expire); return true; } @@ -91,7 +70,7 @@ class MemcacheSessionHandler extends BaseObject implements SessionHandlerInterfa public function destroy($id) { - $this->memcache->delete(self::getApp()->get_hostname() . ":session:" . $id); + Cache::delete('session:' . $id); return true; } diff --git a/src/Database/DBStructure.php b/src/Database/DBStructure.php index f4a88871d7..d2d0bd703e 100644 --- a/src/Database/DBStructure.php +++ b/src/Database/DBStructure.php @@ -712,16 +712,16 @@ class DBStructure ] ]; $database["cache"] = [ - "comment" => "Used to store different data that doesn't to be stored for a long time", + "comment" => "Stores temporary data", "fields" => [ - "k" => ["type" => "varbinary(255)", "not null" => "1", "primary" => "1", "comment" => ""], - "v" => ["type" => "mediumtext", "comment" => ""], - "expire_mode" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => ""], - "updated" => ["type" => "datetime", "not null" => "1", "default" => NULL_DATE, "comment" => ""], + "k" => ["type" => "varbinary(255)", "not null" => "1", "primary" => "1", "comment" => "cache key"], + "v" => ["type" => "mediumtext", "comment" => "cached serialized value"], + "expires" => ["type" => "datetime", "not null" => "1", "default" => NULL_DATE, "comment" => "datetime of cache expiration"], + "updated" => ["type" => "datetime", "not null" => "1", "default" => NULL_DATE, "comment" => "datetime of cache insertion"], ], "indexes" => [ "PRIMARY" => ["k"], - "expire_mode_updated" => ["expire_mode", "updated"], + "k_expires" => ["k", "expires"], ] ]; $database["challenge"] = [