From: Philipp Date: Sat, 23 Oct 2021 09:36:37 +0000 (+0200) Subject: Restructure Lock to follow new paradigm X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=ff1a812e1a669ca2828ccd488cd1f3e8bd884a3f;p=friendica.git Restructure Lock to follow new paradigm --- diff --git a/src/Core/BaseLock.php b/src/Core/BaseLock.php deleted file mode 100644 index 474fe98ea6..0000000000 --- a/src/Core/BaseLock.php +++ /dev/null @@ -1,83 +0,0 @@ -. - * - */ - -namespace Friendica\Core; - -use Friendica\Core\Lock\ILock; - -/** - * Basic class for Locking with common functions (local acquired locks, releaseAll, ..) - */ -abstract class BaseLock implements ILock -{ - /** - * @var array The local acquired locks - */ - protected $acquiredLocks = []; - - /** - * Check if we've locally acquired a lock - * - * @param string key The Name of the lock - * - * @return bool Returns true if the lock is set - */ - protected function hasAcquiredLock($key) - { - return isset($this->acquireLock[$key]) && $this->acquiredLocks[$key] === true; - } - - /** - * Mark a locally acquired lock - * - * @param string $key The Name of the lock - */ - protected function markAcquire($key) - { - $this->acquiredLocks[$key] = true; - } - - /** - * Mark a release of a locally acquired lock - * - * @param string $key The Name of the lock - */ - protected function markRelease($key) - { - unset($this->acquiredLocks[$key]); - } - - /** - * {@inheritDoc} - */ - public function releaseAll($override = false) - { - $return = true; - - foreach ($this->acquiredLocks as $acquiredLock => $hasLock) { - if (!$this->release($acquiredLock, $override)) { - $return = false; - } - } - - return $return; - } -} diff --git a/src/Core/Lock/CacheLock.php b/src/Core/Lock/CacheLock.php deleted file mode 100644 index 20705e06e9..0000000000 --- a/src/Core/Lock/CacheLock.php +++ /dev/null @@ -1,162 +0,0 @@ -. - * - */ - -namespace Friendica\Core\Lock; - -use Friendica\Core\BaseLock; -use Friendica\Core\Cache\Enum\Duration; -use Friendica\Core\Cache\IMemoryCache; - -class CacheLock extends BaseLock -{ - /** - * @var string The static prefix of all locks inside the cache - */ - const CACHE_PREFIX = 'lock:'; - - /** - * @var \Friendica\Core\Cache\ICache; - */ - private $cache; - - /** - * CacheLock constructor. - * - * @param IMemoryCache $cache The CacheDriver for this type of lock - */ - public function __construct(IMemoryCache $cache) - { - $this->cache = $cache; - } - - /** - * (@inheritdoc) - */ - public function acquire($key, $timeout = 120, $ttl = Duration::FIVE_MINUTES) - { - $got_lock = false; - $start = time(); - - $cachekey = self::getLockKey($key); - - do { - $lock = $this->cache->get($cachekey); - // When we do want to lock something that was already locked by us. - if ((int)$lock == getmypid()) { - $got_lock = true; - } - - // When we do want to lock something new - if (is_null($lock)) { - // At first initialize it with "0" - $this->cache->add($cachekey, 0); - // Now the value has to be "0" because otherwise the key was used by another process meanwhile - if ($this->cache->compareSet($cachekey, 0, getmypid(), $ttl)) { - $got_lock = true; - $this->markAcquire($key); - } - } - - if (!$got_lock && ($timeout > 0)) { - usleep(rand(10000, 200000)); - } - } while (!$got_lock && ((time() - $start) < $timeout)); - - return $got_lock; - } - - /** - * (@inheritdoc) - */ - public function release($key, $override = false) - { - $cachekey = self::getLockKey($key); - - if ($override) { - $return = $this->cache->delete($cachekey); - } else { - $return = $this->cache->compareDelete($cachekey, getmypid()); - } - $this->markRelease($key); - - return $return; - } - - /** - * (@inheritdoc) - */ - public function isLocked($key) - { - $cachekey = self::getLockKey($key); - $lock = $this->cache->get($cachekey); - return isset($lock) && ($lock !== false); - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return $this->cache->getName(); - } - - /** - * {@inheritDoc} - */ - public function getLocks(string $prefix = '') - { - $locks = $this->cache->getAllKeys(self::CACHE_PREFIX . $prefix); - - array_walk($locks, function (&$lock, $key) { - $lock = substr($lock, strlen(self::CACHE_PREFIX)); - }); - - return $locks; - } - - /** - * {@inheritDoc} - */ - public function releaseAll($override = false) - { - $success = parent::releaseAll($override); - - $locks = $this->getLocks(); - - foreach ($locks as $lock) { - if (!$this->release($lock, $override)) { - $success = false; - } - } - - return $success; - } - - /** - * @param string $key The original key - * - * @return string The cache key used for the cache - */ - private static function getLockKey($key) - { - return self::CACHE_PREFIX . $key; - } -} diff --git a/src/Core/Lock/DatabaseLock.php b/src/Core/Lock/DatabaseLock.php deleted file mode 100644 index 14e78f625a..0000000000 --- a/src/Core/Lock/DatabaseLock.php +++ /dev/null @@ -1,178 +0,0 @@ -. - * - */ - -namespace Friendica\Core\Lock; - -use Friendica\Core\BaseLock; -use Friendica\Core\Cache\Enum\Duration; -use Friendica\Database\Database; -use Friendica\Util\DateTimeFormat; - -/** - * Locking driver that stores the locks in the database - */ -class DatabaseLock extends BaseLock -{ - /** - * The current ID of the process - * - * @var int - */ - private $pid; - - /** - * @var Database The database connection of Friendica - */ - private $dba; - - /** - * @param null|int $pid The Id of the current process (null means determine automatically) - */ - public function __construct(Database $dba, $pid = null) - { - $this->dba = $dba; - $this->pid = isset($pid) ? $pid : getmypid(); - } - - /** - * (@inheritdoc) - */ - public function acquire($key, $timeout = 120, $ttl = Duration::FIVE_MINUTES) - { - $got_lock = false; - $start = time(); - - do { - $this->dba->lock('locks'); - $lock = $this->dba->selectFirst('locks', ['locked', 'pid'], ['`name` = ? AND `expires` >= ?', $key, DateTimeFormat::utcNow()]); - - if ($this->dba->isResult($lock)) { - if ($lock['locked']) { - // We want to lock something that was already locked by us? So we got the lock. - if ($lock['pid'] == $this->pid) { - $got_lock = true; - } - } - if (!$lock['locked']) { - $this->dba->update('locks', ['locked' => true, 'pid' => $this->pid, 'expires' => DateTimeFormat::utc('now + ' . $ttl . 'seconds')], ['name' => $key]); - $got_lock = true; - } - } else { - $this->dba->insert('locks', ['name' => $key, 'locked' => true, 'pid' => $this->pid, 'expires' => DateTimeFormat::utc('now + ' . $ttl . 'seconds')]); - $got_lock = true; - $this->markAcquire($key); - } - - $this->dba->unlock(); - - if (!$got_lock && ($timeout > 0)) { - usleep(rand(100000, 2000000)); - } - } while (!$got_lock && ((time() - $start) < $timeout)); - - return $got_lock; - } - - /** - * (@inheritdoc) - */ - public function release($key, $override = false) - { - if ($override) { - $where = ['name' => $key]; - } else { - $where = ['name' => $key, 'pid' => $this->pid]; - } - - if ($this->dba->exists('locks', $where)) { - $return = $this->dba->delete('locks', $where); - } else { - $return = false; - } - - $this->markRelease($key); - - return $return; - } - - /** - * (@inheritdoc) - */ - public function releaseAll($override = false) - { - $success = parent::releaseAll($override); - - if ($override) { - $where = ['1 = 1']; - } else { - $where = ['pid' => $this->pid]; - } - $return = $this->dba->delete('locks', $where); - - $this->acquiredLocks = []; - - return $return && $success; - } - - /** - * (@inheritdoc) - */ - public function isLocked($key) - { - $lock = $this->dba->selectFirst('locks', ['locked'], ['`name` = ? AND `expires` >= ?', $key, DateTimeFormat::utcNow()]); - - if ($this->dba->isResult($lock)) { - return $lock['locked'] !== false; - } else { - return false; - } - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return Type::DATABASE; - } - - /** - * {@inheritDoc} - */ - public function getLocks(string $prefix = '') - { - if (empty($prefix)) { - $where = ['`expires` >= ?', DateTimeFormat::utcNow()]; - } else { - $where = ['`expires` >= ? AND `name` LIKE CONCAT(?, \'%\')', DateTimeFormat::utcNow(), $prefix]; - } - - $stmt = $this->dba->select('locks', ['name'], $where); - - $keys = []; - while ($key = $this->dba->fetch($stmt)) { - array_push($keys, $key['name']); - } - $this->dba->close($stmt); - - return $keys; - } -} diff --git a/src/Core/Lock/Enum/Type.php b/src/Core/Lock/Enum/Type.php new file mode 100644 index 0000000000..1ad1ef78ec --- /dev/null +++ b/src/Core/Lock/Enum/Type.php @@ -0,0 +1,35 @@ +. + * + */ + +namespace Friendica\Core\Lock\Enum; + +use Friendica\Core\Cache\Enum\Type as CacheType; + +/** + * Enumeration for lock types + * + * There's no "Cache" lock type, because the type depends on the concrete, used cache + */ +abstract class Type +{ + const DATABASE = CacheType::DATABASE; + const SEMAPHORE = 'semaphore'; +} diff --git a/src/Core/Lock/Factory/LockFactory.php b/src/Core/Lock/Factory/LockFactory.php new file mode 100644 index 0000000000..124ed2f08d --- /dev/null +++ b/src/Core/Lock/Factory/LockFactory.php @@ -0,0 +1,146 @@ +. + * + */ + +namespace Friendica\Core\Lock\Factory; + +use Friendica\Core\Cache\Factory\CacheFactory; +use Friendica\Core\Cache\IMemoryCache; +use Friendica\Core\Cache\Enum\Type; +use Friendica\Core\Config\IConfig; +use Friendica\Core\Lock; +use Friendica\Database\Database; +use Psr\Log\LoggerInterface; + +/** + * Class LockFactory + * + * @package Friendica\Core\Cache + * + * A basic class to generate a LockDriver + */ +class LockFactory +{ + /** + * @var string The default driver for caching + */ + const DEFAULT_DRIVER = 'default'; + + /** + * @var IConfig The configuration to read parameters out of the config + */ + private $config; + + /** + * @var Database The database connection in case that the cache is used the dba connection + */ + private $dba; + + /** + * @var CacheFactory The memory cache driver in case we use it + */ + private $cacheFactory; + + /** + * @var LoggerInterface The Friendica Logger + */ + private $logger; + + public function __construct(CacheFactory $cacheFactory, IConfig $config, Database $dba, LoggerInterface $logger) + { + $this->cacheFactory = $cacheFactory; + $this->config = $config; + $this->dba = $dba; + $this->logger = $logger; + } + + public function create() + { + $lock_type = $this->config->get('system', 'lock_driver', self::DEFAULT_DRIVER); + + try { + switch ($lock_type) { + case Type::MEMCACHE: + case Type::MEMCACHED: + case Type::REDIS: + case Type::APCU: + $cache = $this->cacheFactory->create($lock_type); + if ($cache instanceof IMemoryCache) { + return new Lock\Type\CacheLock($cache); + } else { + throw new \Exception(sprintf('Incompatible cache driver \'%s\' for lock used', $lock_type)); + } + break; + + case 'database': + return new Lock\Type\DatabaseLock($this->dba); + break; + + case 'semaphore': + return new Lock\Type\SemaphoreLock(); + break; + + default: + return self::useAutoDriver(); + } + } catch (\Exception $exception) { + $this->logger->alert('Driver \'' . $lock_type . '\' failed - Fallback to \'useAutoDriver()\'', ['exception' => $exception]); + return self::useAutoDriver(); + } + } + + /** + * This method tries to find the best - local - locking method for Friendica + * + * The following sequence will be tried: + * 1. Semaphore Locking + * 2. Cache Locking + * 3. Database Locking + * + * @return Lock\ILock + */ + private function useAutoDriver() + { + // 1. Try to use Semaphores for - local - locking + if (function_exists('sem_get')) { + try { + return new Lock\Type\SemaphoreLock(); + } catch (\Exception $exception) { + $this->logger->warning('Using Semaphore driver for locking failed.', ['exception' => $exception]); + } + } + + // 2. Try to use Cache Locking (don't use the DB-Cache Locking because it works different!) + $cache_type = $this->config->get('system', 'cache_driver', 'database'); + if ($cache_type != Type::DATABASE) { + try { + $cache = $this->cacheFactory->create($cache_type); + if ($cache instanceof IMemoryCache) { + return new Lock\Type\CacheLock($cache); + } + } catch (\Exception $exception) { + $this->logger->warning('Using Cache driver for locking failed.', ['exception' => $exception]); + } + } + + // 3. Use Database Locking as a Fallback + return new Lock\Type\DatabaseLock($this->dba); + } +} diff --git a/src/Core/Lock/SemaphoreLock.php b/src/Core/Lock/SemaphoreLock.php deleted file mode 100644 index fa5cf5e876..0000000000 --- a/src/Core/Lock/SemaphoreLock.php +++ /dev/null @@ -1,144 +0,0 @@ -. - * - */ - -namespace Friendica\Core\Lock; - -use Friendica\Core\BaseLock; -use Friendica\Core\Cache\Enum\Duration; - -class SemaphoreLock extends BaseLock -{ - private static $semaphore = []; - - public function __construct() - { - if (!function_exists('sem_get')) { - throw new \Exception('Semaphore lock not supported'); - } - } - - /** - * (@inheritdoc) - */ - private static function semaphoreKey($key) - { - $success = true; - - $temp = get_temppath(); - - $file = $temp . '/' . $key . '.sem'; - - if (!file_exists($file)) { - $success = !empty(file_put_contents($file, $key)); - } - - return $success ? ftok($file, 'f') : false; - } - - /** - * (@inheritdoc) - */ - public function acquire($key, $timeout = 120, $ttl = Duration::FIVE_MINUTES) - { - self::$semaphore[$key] = sem_get(self::semaphoreKey($key)); - if (!empty(self::$semaphore[$key])) { - if ((bool)sem_acquire(self::$semaphore[$key], ($timeout === 0))) { - $this->markAcquire($key); - return true; - } - } - - return false; - } - - /** - * (@inheritdoc) - * - * @param bool $override not necessary parameter for semaphore locks since the lock lives as long as the execution - * of the using function - */ - public function release($key, $override = false) - { - $success = false; - - if (!empty(self::$semaphore[$key])) { - try { - $success = @sem_release(self::$semaphore[$key]); - unset(self::$semaphore[$key]); - $this->markRelease($key); - } catch (\Exception $exception) { - $success = false; - } - } - - return $success; - } - - /** - * (@inheritdoc) - */ - public function isLocked($key) - { - return isset(self::$semaphore[$key]); - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return Type::SEMAPHORE; - } - - /** - * {@inheritDoc} - */ - public function getLocks(string $prefix = '') - { - // We can just return our own semaphore keys, since we don't know - // the state of other semaphores, even if the .sem files exists - $keys = array_keys(self::$semaphore); - - if (empty($prefix)) { - return $keys; - } else { - $result = []; - - foreach ($keys as $key) { - if (strpos($key, $prefix) === 0) { - array_push($result, $key); - } - } - - return $result; - } - } - - /** - * {@inheritDoc} - */ - public function releaseAll($override = false) - { - // Semaphores are just alive during a run, so there is no need to release - // You can just release your own locks - return parent::releaseAll($override); - } -} diff --git a/src/Core/Lock/Type.php b/src/Core/Lock/Type.php deleted file mode 100644 index 769bad12eb..0000000000 --- a/src/Core/Lock/Type.php +++ /dev/null @@ -1,35 +0,0 @@ -. - * - */ - -namespace Friendica\Core\Lock; - -use Friendica\Core\Cache\Enum\Type as CacheType; - -/** - * Enumeration for lock types - * - * There's no "Cache" lock type, because the type depends on the concrete, used cache - */ -abstract class Type -{ - const DATABASE = CacheType::DATABASE; - const SEMAPHORE = 'semaphore'; -} diff --git a/src/Core/Lock/Type/BaseLock.php b/src/Core/Lock/Type/BaseLock.php new file mode 100644 index 0000000000..518475b44c --- /dev/null +++ b/src/Core/Lock/Type/BaseLock.php @@ -0,0 +1,83 @@ +. + * + */ + +namespace Friendica\Core\Lock\Type; + +use Friendica\Core\Lock\ILock; + +/** + * Basic class for Locking with common functions (local acquired locks, releaseAll, ..) + */ +abstract class BaseLock implements ILock +{ + /** + * @var array The local acquired locks + */ + protected $acquiredLocks = []; + + /** + * Check if we've locally acquired a lock + * + * @param string key The Name of the lock + * + * @return bool Returns true if the lock is set + */ + protected function hasAcquiredLock($key) + { + return isset($this->acquireLock[$key]) && $this->acquiredLocks[$key] === true; + } + + /** + * Mark a locally acquired lock + * + * @param string $key The Name of the lock + */ + protected function markAcquire($key) + { + $this->acquiredLocks[$key] = true; + } + + /** + * Mark a release of a locally acquired lock + * + * @param string $key The Name of the lock + */ + protected function markRelease($key) + { + unset($this->acquiredLocks[$key]); + } + + /** + * {@inheritDoc} + */ + public function releaseAll($override = false) + { + $return = true; + + foreach ($this->acquiredLocks as $acquiredLock => $hasLock) { + if (!$this->release($acquiredLock, $override)) { + $return = false; + } + } + + return $return; + } +} diff --git a/src/Core/Lock/Type/CacheLock.php b/src/Core/Lock/Type/CacheLock.php new file mode 100644 index 0000000000..8a85f07653 --- /dev/null +++ b/src/Core/Lock/Type/CacheLock.php @@ -0,0 +1,161 @@ +. + * + */ + +namespace Friendica\Core\Lock\Type; + +use Friendica\Core\Cache\Enum\Duration; +use Friendica\Core\Cache\IMemoryCache; + +class CacheLock extends BaseLock +{ + /** + * @var string The static prefix of all locks inside the cache + */ + const CACHE_PREFIX = 'lock:'; + + /** + * @var \Friendica\Core\Cache\ICache; + */ + private $cache; + + /** + * CacheLock constructor. + * + * @param IMemoryCache $cache The CacheDriver for this type of lock + */ + public function __construct(IMemoryCache $cache) + { + $this->cache = $cache; + } + + /** + * (@inheritdoc) + */ + public function acquire($key, $timeout = 120, $ttl = Duration::FIVE_MINUTES) + { + $got_lock = false; + $start = time(); + + $cachekey = self::getLockKey($key); + + do { + $lock = $this->cache->get($cachekey); + // When we do want to lock something that was already locked by us. + if ((int)$lock == getmypid()) { + $got_lock = true; + } + + // When we do want to lock something new + if (is_null($lock)) { + // At first initialize it with "0" + $this->cache->add($cachekey, 0); + // Now the value has to be "0" because otherwise the key was used by another process meanwhile + if ($this->cache->compareSet($cachekey, 0, getmypid(), $ttl)) { + $got_lock = true; + $this->markAcquire($key); + } + } + + if (!$got_lock && ($timeout > 0)) { + usleep(rand(10000, 200000)); + } + } while (!$got_lock && ((time() - $start) < $timeout)); + + return $got_lock; + } + + /** + * (@inheritdoc) + */ + public function release($key, $override = false) + { + $cachekey = self::getLockKey($key); + + if ($override) { + $return = $this->cache->delete($cachekey); + } else { + $return = $this->cache->compareDelete($cachekey, getmypid()); + } + $this->markRelease($key); + + return $return; + } + + /** + * (@inheritdoc) + */ + public function isLocked($key) + { + $cachekey = self::getLockKey($key); + $lock = $this->cache->get($cachekey); + return isset($lock) && ($lock !== false); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->cache->getName(); + } + + /** + * {@inheritDoc} + */ + public function getLocks(string $prefix = '') + { + $locks = $this->cache->getAllKeys(self::CACHE_PREFIX . $prefix); + + array_walk($locks, function (&$lock, $key) { + $lock = substr($lock, strlen(self::CACHE_PREFIX)); + }); + + return $locks; + } + + /** + * {@inheritDoc} + */ + public function releaseAll($override = false) + { + $success = parent::releaseAll($override); + + $locks = $this->getLocks(); + + foreach ($locks as $lock) { + if (!$this->release($lock, $override)) { + $success = false; + } + } + + return $success; + } + + /** + * @param string $key The original key + * + * @return string The cache key used for the cache + */ + private static function getLockKey($key) + { + return self::CACHE_PREFIX . $key; + } +} diff --git a/src/Core/Lock/Type/DatabaseLock.php b/src/Core/Lock/Type/DatabaseLock.php new file mode 100644 index 0000000000..de44e2e2b9 --- /dev/null +++ b/src/Core/Lock/Type/DatabaseLock.php @@ -0,0 +1,178 @@ +. + * + */ + +namespace Friendica\Core\Lock\Type; + +use Friendica\Core\Cache\Enum\Duration; +use Friendica\Core\Lock\Enum\Type; +use Friendica\Database\Database; +use Friendica\Util\DateTimeFormat; + +/** + * Locking driver that stores the locks in the database + */ +class DatabaseLock extends BaseLock +{ + /** + * The current ID of the process + * + * @var int + */ + private $pid; + + /** + * @var Database The database connection of Friendica + */ + private $dba; + + /** + * @param null|int $pid The Id of the current process (null means determine automatically) + */ + public function __construct(Database $dba, $pid = null) + { + $this->dba = $dba; + $this->pid = isset($pid) ? $pid : getmypid(); + } + + /** + * (@inheritdoc) + */ + public function acquire($key, $timeout = 120, $ttl = Duration::FIVE_MINUTES) + { + $got_lock = false; + $start = time(); + + do { + $this->dba->lock('locks'); + $lock = $this->dba->selectFirst('locks', ['locked', 'pid'], ['`name` = ? AND `expires` >= ?', $key, DateTimeFormat::utcNow()]); + + if ($this->dba->isResult($lock)) { + if ($lock['locked']) { + // We want to lock something that was already locked by us? So we got the lock. + if ($lock['pid'] == $this->pid) { + $got_lock = true; + } + } + if (!$lock['locked']) { + $this->dba->update('locks', ['locked' => true, 'pid' => $this->pid, 'expires' => DateTimeFormat::utc('now + ' . $ttl . 'seconds')], ['name' => $key]); + $got_lock = true; + } + } else { + $this->dba->insert('locks', ['name' => $key, 'locked' => true, 'pid' => $this->pid, 'expires' => DateTimeFormat::utc('now + ' . $ttl . 'seconds')]); + $got_lock = true; + $this->markAcquire($key); + } + + $this->dba->unlock(); + + if (!$got_lock && ($timeout > 0)) { + usleep(rand(100000, 2000000)); + } + } while (!$got_lock && ((time() - $start) < $timeout)); + + return $got_lock; + } + + /** + * (@inheritdoc) + */ + public function release($key, $override = false) + { + if ($override) { + $where = ['name' => $key]; + } else { + $where = ['name' => $key, 'pid' => $this->pid]; + } + + if ($this->dba->exists('locks', $where)) { + $return = $this->dba->delete('locks', $where); + } else { + $return = false; + } + + $this->markRelease($key); + + return $return; + } + + /** + * (@inheritdoc) + */ + public function releaseAll($override = false) + { + $success = parent::releaseAll($override); + + if ($override) { + $where = ['1 = 1']; + } else { + $where = ['pid' => $this->pid]; + } + $return = $this->dba->delete('locks', $where); + + $this->acquiredLocks = []; + + return $return && $success; + } + + /** + * (@inheritdoc) + */ + public function isLocked($key) + { + $lock = $this->dba->selectFirst('locks', ['locked'], ['`name` = ? AND `expires` >= ?', $key, DateTimeFormat::utcNow()]); + + if ($this->dba->isResult($lock)) { + return $lock['locked'] !== false; + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return Type::DATABASE; + } + + /** + * {@inheritDoc} + */ + public function getLocks(string $prefix = '') + { + if (empty($prefix)) { + $where = ['`expires` >= ?', DateTimeFormat::utcNow()]; + } else { + $where = ['`expires` >= ? AND `name` LIKE CONCAT(?, \'%\')', DateTimeFormat::utcNow(), $prefix]; + } + + $stmt = $this->dba->select('locks', ['name'], $where); + + $keys = []; + while ($key = $this->dba->fetch($stmt)) { + array_push($keys, $key['name']); + } + $this->dba->close($stmt); + + return $keys; + } +} diff --git a/src/Core/Lock/Type/SemaphoreLock.php b/src/Core/Lock/Type/SemaphoreLock.php new file mode 100644 index 0000000000..4393e45153 --- /dev/null +++ b/src/Core/Lock/Type/SemaphoreLock.php @@ -0,0 +1,145 @@ +. + * + */ + +namespace Friendica\Core\Lock\Type; + +use Friendica\Core\Cache\Enum\Duration; +use Friendica\Core\Lock\Enum\Type; +use function get_temppath; + +class SemaphoreLock extends BaseLock +{ + private static $semaphore = []; + + public function __construct() + { + if (!function_exists('sem_get')) { + throw new \Exception('Semaphore lock not supported'); + } + } + + /** + * (@inheritdoc) + */ + private static function semaphoreKey($key) + { + $success = true; + + $temp = get_temppath(); + + $file = $temp . '/' . $key . '.sem'; + + if (!file_exists($file)) { + $success = !empty(file_put_contents($file, $key)); + } + + return $success ? ftok($file, 'f') : false; + } + + /** + * (@inheritdoc) + */ + public function acquire($key, $timeout = 120, $ttl = Duration::FIVE_MINUTES) + { + self::$semaphore[$key] = sem_get(self::semaphoreKey($key)); + if (!empty(self::$semaphore[$key])) { + if ((bool)sem_acquire(self::$semaphore[$key], ($timeout === 0))) { + $this->markAcquire($key); + return true; + } + } + + return false; + } + + /** + * (@inheritdoc) + * + * @param bool $override not necessary parameter for semaphore locks since the lock lives as long as the execution + * of the using function + */ + public function release($key, $override = false) + { + $success = false; + + if (!empty(self::$semaphore[$key])) { + try { + $success = @sem_release(self::$semaphore[$key]); + unset(self::$semaphore[$key]); + $this->markRelease($key); + } catch (\Exception $exception) { + $success = false; + } + } + + return $success; + } + + /** + * (@inheritdoc) + */ + public function isLocked($key) + { + return isset(self::$semaphore[$key]); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return Type::SEMAPHORE; + } + + /** + * {@inheritDoc} + */ + public function getLocks(string $prefix = '') + { + // We can just return our own semaphore keys, since we don't know + // the state of other semaphores, even if the .sem files exists + $keys = array_keys(self::$semaphore); + + if (empty($prefix)) { + return $keys; + } else { + $result = []; + + foreach ($keys as $key) { + if (strpos($key, $prefix) === 0) { + array_push($result, $key); + } + } + + return $result; + } + } + + /** + * {@inheritDoc} + */ + public function releaseAll($override = false) + { + // Semaphores are just alive during a run, so there is no need to release + // You can just release your own locks + return parent::releaseAll($override); + } +} diff --git a/src/Factory/LockFactory.php b/src/Factory/LockFactory.php deleted file mode 100644 index 2a54910d45..0000000000 --- a/src/Factory/LockFactory.php +++ /dev/null @@ -1,146 +0,0 @@ -. - * - */ - -namespace Friendica\Factory; - -use Friendica\Core\Cache\Factory\CacheFactory; -use Friendica\Core\Cache\IMemoryCache; -use Friendica\Core\Cache\Enum\Type; -use Friendica\Core\Config\IConfig; -use Friendica\Core\Lock; -use Friendica\Database\Database; -use Psr\Log\LoggerInterface; - -/** - * Class LockFactory - * - * @package Friendica\Core\Cache - * - * A basic class to generate a LockDriver - */ -class LockFactory -{ - /** - * @var string The default driver for caching - */ - const DEFAULT_DRIVER = 'default'; - - /** - * @var IConfig The configuration to read parameters out of the config - */ - private $config; - - /** - * @var Database The database connection in case that the cache is used the dba connection - */ - private $dba; - - /** - * @var CacheFactory The memory cache driver in case we use it - */ - private $cacheFactory; - - /** - * @var LoggerInterface The Friendica Logger - */ - private $logger; - - public function __construct(CacheFactory $cacheFactory, IConfig $config, Database $dba, LoggerInterface $logger) - { - $this->cacheFactory = $cacheFactory; - $this->config = $config; - $this->dba = $dba; - $this->logger = $logger; - } - - public function create() - { - $lock_type = $this->config->get('system', 'lock_driver', self::DEFAULT_DRIVER); - - try { - switch ($lock_type) { - case Type::MEMCACHE: - case Type::MEMCACHED: - case Type::REDIS: - case Type::APCU: - $cache = $this->cacheFactory->create($lock_type); - if ($cache instanceof IMemoryCache) { - return new Lock\CacheLock($cache); - } else { - throw new \Exception(sprintf('Incompatible cache driver \'%s\' for lock used', $lock_type)); - } - break; - - case 'database': - return new Lock\DatabaseLock($this->dba); - break; - - case 'semaphore': - return new Lock\SemaphoreLock(); - break; - - default: - return self::useAutoDriver(); - } - } catch (\Exception $exception) { - $this->logger->alert('Driver \'' . $lock_type . '\' failed - Fallback to \'useAutoDriver()\'', ['exception' => $exception]); - return self::useAutoDriver(); - } - } - - /** - * This method tries to find the best - local - locking method for Friendica - * - * The following sequence will be tried: - * 1. Semaphore Locking - * 2. Cache Locking - * 3. Database Locking - * - * @return Lock\ILock - */ - private function useAutoDriver() - { - // 1. Try to use Semaphores for - local - locking - if (function_exists('sem_get')) { - try { - return new Lock\SemaphoreLock(); - } catch (\Exception $exception) { - $this->logger->warning('Using Semaphore driver for locking failed.', ['exception' => $exception]); - } - } - - // 2. Try to use Cache Locking (don't use the DB-Cache Locking because it works different!) - $cache_type = $this->config->get('system', 'cache_driver', 'database'); - if ($cache_type != Type::DATABASE) { - try { - $cache = $this->cacheFactory->create($cache_type); - if ($cache instanceof IMemoryCache) { - return new Lock\CacheLock($cache); - } - } catch (\Exception $exception) { - $this->logger->warning('Using Cache driver for locking failed.', ['exception' => $exception]); - } - } - - // 3. Use Database Locking as a Fallback - return new Lock\DatabaseLock($this->dba); - } -} diff --git a/static/dependencies.config.php b/static/dependencies.config.php index fe063c0463..48ed997f33 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -39,7 +39,7 @@ use Friendica\Core\Cache; use Friendica\Core\Config; use Friendica\Core\PConfig; use Friendica\Core\L10n; -use Friendica\Core\Lock\ILock; +use Friendica\Core\Lock; use Friendica\Core\Process; use Friendica\Core\Session\ISession; use Friendica\Core\StorageManager; @@ -101,7 +101,7 @@ return [ ['create', [], Dice::CHAIN_CALL], ], ], - \Friendica\Core\PConfig\IPConfig::class => [ + PConfig\IPConfig::class => [ 'instanceOf' => PConfig\Factory\PConfigFactory::class, 'call' => [ ['create', [], Dice::CHAIN_CALL], @@ -170,8 +170,8 @@ return [ ['create', [], Dice::CHAIN_CALL], ], ], - ILock::class => [ - 'instanceOf' => Factory\LockFactory::class, + Lock\ILock::class => [ + 'instanceOf' => Lock\Factory\LockFactory::class, 'call' => [ ['create', [], Dice::CHAIN_CALL], ], diff --git a/tests/src/Core/Lock/APCuCacheLockTest.php b/tests/src/Core/Lock/APCuCacheLockTest.php index 750748975a..3f13000e03 100644 --- a/tests/src/Core/Lock/APCuCacheLockTest.php +++ b/tests/src/Core/Lock/APCuCacheLockTest.php @@ -22,7 +22,7 @@ namespace Friendica\Test\src\Core\Lock; use Friendica\Core\Cache\Type\APCuCache; -use Friendica\Core\Lock\CacheLock; +use Friendica\Core\Lock\Type\CacheLock; /** * @group APCU @@ -40,6 +40,6 @@ class APCuCacheLockTest extends LockTest protected function getInstance() { - return new CacheLock(new APCuCache('localhost')); + return new \Friendica\Core\Lock\Type\CacheLock(new APCuCache('localhost')); } } diff --git a/tests/src/Core/Lock/ArrayCacheLockTest.php b/tests/src/Core/Lock/ArrayCacheLockTest.php index 93c2c20526..4f6bf3fe91 100644 --- a/tests/src/Core/Lock/ArrayCacheLockTest.php +++ b/tests/src/Core/Lock/ArrayCacheLockTest.php @@ -22,13 +22,13 @@ namespace Friendica\Test\src\Core\Lock; use Friendica\Core\Cache\Type\ArrayCache; -use Friendica\Core\Lock\CacheLock; +use Friendica\Core\Lock\Type\CacheLock; class ArrayCacheLockTest extends LockTest { protected function getInstance() { - return new CacheLock(new ArrayCache('localhost')); + return new \Friendica\Core\Lock\Type\CacheLock(new ArrayCache('localhost')); } /** diff --git a/tests/src/Core/Lock/DatabaseLockDriverTest.php b/tests/src/Core/Lock/DatabaseLockDriverTest.php index 86209d5c7d..e08a0d7ca8 100644 --- a/tests/src/Core/Lock/DatabaseLockDriverTest.php +++ b/tests/src/Core/Lock/DatabaseLockDriverTest.php @@ -21,7 +21,7 @@ namespace Friendica\Test\src\Core\Lock; -use Friendica\Core\Lock\DatabaseLock; +use Friendica\Core\Lock\Type\DatabaseLock; use Friendica\Core\Config\Factory\ConfigFactory; use Friendica\Test\DatabaseTestTrait; use Friendica\Test\Util\Database\StaticDatabase; diff --git a/tests/src/Core/Lock/MemcacheCacheLockTest.php b/tests/src/Core/Lock/MemcacheCacheLockTest.php index efb27dcaba..a3068b6a53 100644 --- a/tests/src/Core/Lock/MemcacheCacheLockTest.php +++ b/tests/src/Core/Lock/MemcacheCacheLockTest.php @@ -24,7 +24,7 @@ namespace Friendica\Test\src\Core\Lock; use Exception; use Friendica\Core\Cache\Type\MemcacheCache; use Friendica\Core\Config\IConfig; -use Friendica\Core\Lock\CacheLock; +use Friendica\Core\Lock\Type\CacheLock; use Mockery; /** @@ -53,7 +53,7 @@ class MemcacheCacheLockTest extends LockTest try { $cache = new MemcacheCache($host, $configMock); - $lock = new CacheLock($cache); + $lock = new \Friendica\Core\Lock\Type\CacheLock($cache); } catch (Exception $e) { static::markTestSkipped('Memcache is not available'); } diff --git a/tests/src/Core/Lock/MemcachedCacheLockTest.php b/tests/src/Core/Lock/MemcachedCacheLockTest.php index f729364847..26cc16f22b 100644 --- a/tests/src/Core/Lock/MemcachedCacheLockTest.php +++ b/tests/src/Core/Lock/MemcachedCacheLockTest.php @@ -24,7 +24,7 @@ namespace Friendica\Test\src\Core\Lock; use Exception; use Friendica\Core\Cache\Type\MemcachedCache; use Friendica\Core\Config\IConfig; -use Friendica\Core\Lock\CacheLock; +use Friendica\Core\Lock\Type\CacheLock; use Mockery; use Psr\Log\NullLogger; diff --git a/tests/src/Core/Lock/RedisCacheLockTest.php b/tests/src/Core/Lock/RedisCacheLockTest.php index 6fdbd1eeb9..fba68b197c 100644 --- a/tests/src/Core/Lock/RedisCacheLockTest.php +++ b/tests/src/Core/Lock/RedisCacheLockTest.php @@ -24,7 +24,7 @@ namespace Friendica\Test\src\Core\Lock; use Exception; use Friendica\Core\Cache\Type\RedisCache; use Friendica\Core\Config\IConfig; -use Friendica\Core\Lock\CacheLock; +use Friendica\Core\Lock\Type\CacheLock; use Mockery; /** @@ -62,7 +62,7 @@ class RedisCacheLockTest extends LockTest try { $cache = new RedisCache($host, $configMock); - $lock = new CacheLock($cache); + $lock = new \Friendica\Core\Lock\Type\CacheLock($cache); } catch (Exception $e) { static::markTestSkipped('Redis is not available. Error: ' . $e->getMessage()); } diff --git a/tests/src/Core/Lock/SemaphoreLockTest.php b/tests/src/Core/Lock/SemaphoreLockTest.php index 89a3ec633c..59b110a333 100644 --- a/tests/src/Core/Lock/SemaphoreLockTest.php +++ b/tests/src/Core/Lock/SemaphoreLockTest.php @@ -25,7 +25,7 @@ use Dice\Dice; use Friendica\App; use Friendica\Core\Config\IConfig; use Friendica\Core\Config\Type\JitConfig; -use Friendica\Core\Lock\SemaphoreLock; +use Friendica\Core\Lock\Type\SemaphoreLock; use Friendica\DI; use Mockery; use Mockery\MockInterface; @@ -56,7 +56,7 @@ class SemaphoreLockTest extends LockTest protected function getInstance() { - return new SemaphoreLock(); + return new \Friendica\Core\Lock\Type\SemaphoreLock(); } /**