From 41e2031e6b7b572c00aea53136baaef38cd79c86 Mon Sep 17 00:00:00 2001 From: Philipp Holzer Date: Tue, 13 Aug 2019 21:20:41 +0200 Subject: [PATCH 1/4] Console Lock WIP --- src/Console/Lock.php | 185 ++++++++++++++++++++++ src/Core/Console.php | 2 + src/Core/Lock/CacheLock.php | 49 +++++- src/Core/Lock/DatabaseLock.php | 41 ++++- src/Core/Lock/ILock.php | 20 ++- src/Core/Lock/Lock.php | 13 +- src/Core/Lock/SemaphoreLock.php | 87 ++++++++-- tests/src/Core/Lock/LockTest.php | 44 ++++- tests/src/Core/Lock/SemaphoreLockTest.php | 4 +- 9 files changed, 420 insertions(+), 25 deletions(-) create mode 100644 src/Console/Lock.php diff --git a/src/Console/Lock.php b/src/Console/Lock.php new file mode 100644 index 0000000000..fe9132b7cb --- /dev/null +++ b/src/Console/Lock.php @@ -0,0 +1,185 @@ +, Hypolite Petovan + */ +class Lock extends \Asika\SimpleConsole\Console +{ + protected $helpOptions = ['h', 'help', '?']; + + /** + * @var App\Mode + */ + private $appMode; + + /** + * @var ILock + */ + private $lock; + + protected function getHelp() + { + $help = <<] [-h|--help|-?] [-v] + bin/console lock set [ []] [-h|--help|-?] [-v] + bin/console lock del [-h|--help|-?] [-v] + bin/console lock clear [-h|--help|-?] [-v] + +Description + bin/console lock list [] + List all locks, optionally filtered by a prefix + + bin/console lock set [ []] + Sets manually a lock, optionally with the provided TTL (time to live) with a default of five minutes. + + bin/console lock del + Deletes a lock. + + bin/console lock clear + Clears all locks + +Options + -h|--help|-? Show help information + -v Show more debug information. +HELP; + return $help; + } + + public function __construct(App\Mode $appMode, ILock $lock, array $argv = null) + { + parent::__construct($argv); + + $this->appMode = $appMode; + $this->lock = $lock; + } + + protected function doExecute() + { + if ($this->getOption('v')) { + $this->out('Executable: ' . $this->executable); + $this->out('Class: ' . __CLASS__); + $this->out('Arguments: ' . var_export($this->args, true)); + $this->out('Options: ' . var_export($this->options, true)); + } + + if (!$this->appMode->has(App\Mode::DBCONFIGAVAILABLE)) { + $this->out('Database isn\'t ready or populated yet, database cache won\'t be available'); + } + + if ($this->getOption('v')) { + $this->out('Lock Driver Name: ' . $this->lock->getName()); + $this->out('Lock Driver Class: ' . get_class($this->lock)); + } + + switch ($this->getArgument(0)) { + case 'list': + $this->executeList(); + break; + case 'set': + $this->executeSet(); + break; + case 'del': + $this->executeDel(); + break; + case 'clear': + $this->executeClear(); + break; + } + + if (count($this->args) == 0) { + $this->out($this->getHelp()); + return 0; + } + + return 0; + } + + private function executeList() + { + $prefix = $this->getArgument(1, ''); + $keys = $this->lock->getLocks($prefix); + + if (empty($prefix)) { + $this->out('Listing all Locks:'); + } else { + $this->out('Listing all Locks starting with "' . $prefix . '":'); + } + + $count = 0; + foreach ($keys as $key) { + $this->out($key); + $count++; + } + + $this->out($count . ' locks found'); + } + + private function executeDel() + { + if (count($this->args) >= 2) { + $lock = $this->getArgument(1); + + if ($this->lock->releaseLock($lock, true)){ + $this->out(sprintf('Lock \'%s\' released.', $lock)); + } else { + $this->out(sprintf('Couldn\'t release Lock \'%s\'', $lock)); + } + + } else { + throw new CommandArgsException('Too few arguments for del.'); + } + } + + private function executeSet() + { + if (count($this->args) >= 2) { + $lock = $this->getArgument(1); + $timeout = intval($this->getArgument(2, false)); + $ttl = intval($this->getArgument(3, false)); + + if (is_array($this->lock->isLocked($lock))) { + throw new RuntimeException(sprintf('\'%s\' is already set.', $lock)); + } + + if (!empty($ttl) && !empty($timeout)) { + $result = $this->lock->acquireLock($lock, $timeout, $ttl); + } elseif (!empty($timeout)) { + $result = $this->lock->acquireLock($lock, $timeout); + } else { + $result = $this->lock->acquireLock($lock); + } + + if ($result) { + $this->out(sprintf('Lock \'%s\' acquired.', $lock)); + } else { + $this->out(sprintf('Unable to lock \'%s\'', $lock)); + } + } else { + throw new CommandArgsException('Too few arguments for set.'); + } + } + + private function executeClear() + { + $result = $this->lock->releaseAll(true); + if ($result) { + $this->out('Locks successfully cleared,'); + } else { + $this->out('Unable to clear the locks.'); + } + } +} diff --git a/src/Core/Console.php b/src/Core/Console.php index e1654fbef6..2ca568c2da 100644 --- a/src/Core/Console.php +++ b/src/Core/Console.php @@ -38,6 +38,7 @@ Commands: archivecontact Archive a contact when you know that it isn't existing anymore help Show help about a command, e.g (bin/console help config) autoinstall Starts automatic installation of friendica based on values from htconfig.php + lock Edit site locks maintenance Set maintenance mode for this node newpassword Set a new password for a given user php2po Generate a messages.po file from a strings.php file @@ -65,6 +66,7 @@ HELP; 'globalcommunitysilence' => Friendica\Console\GlobalCommunitySilence::class, 'archivecontact' => Friendica\Console\ArchiveContact::class, 'autoinstall' => Friendica\Console\AutomaticInstallation::class, + 'lock' => Friendica\Console\Lock::class, 'maintenance' => Friendica\Console\Maintenance::class, 'newpassword' => Friendica\Console\NewPassword::class, 'php2po' => Friendica\Console\PhpToPo::class, diff --git a/src/Core/Lock/CacheLock.php b/src/Core/Lock/CacheLock.php index 36a7b4edfb..238beb705c 100644 --- a/src/Core/Lock/CacheLock.php +++ b/src/Core/Lock/CacheLock.php @@ -7,6 +7,11 @@ use Friendica\Core\Cache\IMemoryCache; class CacheLock extends Lock { + /** + * @var string The static prefix of all locks inside the cache + */ + const CACHE_PREFIX = 'lock:'; + /** * @var \Friendica\Core\Cache\ICache; */ @@ -25,7 +30,7 @@ class CacheLock extends Lock /** * (@inheritdoc) */ - public function acquireLock($key, $timeout = 120, $ttl = Cache::FIVE_MINUTES) + public function acquireLock($key, $timeout = 120, $ttl = Cache\Cache::FIVE_MINUTES) { $got_lock = false; $start = time(); @@ -85,6 +90,46 @@ class CacheLock extends Lock 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->releaseLock($lock, $override)) { + $success = false; + } + } + + return $success; + } + /** * @param string $key The original key * @@ -92,6 +137,6 @@ class CacheLock extends Lock */ private static function getLockKey($key) { - return "lock:" . $key; + return self::CACHE_PREFIX . $key; } } diff --git a/src/Core/Lock/DatabaseLock.php b/src/Core/Lock/DatabaseLock.php index e5274b9b9b..2f409cd3d2 100644 --- a/src/Core/Lock/DatabaseLock.php +++ b/src/Core/Lock/DatabaseLock.php @@ -92,9 +92,16 @@ class DatabaseLock extends Lock /** * (@inheritdoc) */ - public function releaseAll() + public function releaseAll($override = false) { - $return = $this->dba->delete('locks', ['pid' => $this->pid]); + $success = parent::releaseAll($override); + + if ($override) { + $where = ['1 = 1']; + } else { + $where = ['pid' => $this->pid]; + } + $return = $this->dba->delete('locks', $where); $this->acquiredLocks = []; @@ -114,4 +121,34 @@ class DatabaseLock extends Lock return false; } } + + /** + * {@inheritDoc} + */ + public function getName() + { + return self::TYPE_DATABASE; + } + + /** + * {@inheritDoc} + */ + public function getLocks(string $prefix = '') + { + if (empty($prefix)) { + $where = ['`expires` >= ?', DateTimeFormat::utcNow()]; + } else { + $where = ['`expires` >= ? AND `k` 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/ILock.php b/src/Core/Lock/ILock.php index 0b91daeb56..d103d99191 100644 --- a/src/Core/Lock/ILock.php +++ b/src/Core/Lock/ILock.php @@ -45,7 +45,25 @@ interface ILock /** * Releases all lock that were set by us * + * @param bool $override Override to release all locks + * * @return boolean Was the unlock of all locks successful? */ - public function releaseAll(); + public function releaseAll($override = false); + + /** + * Returns the name of the current lock + * + * @return string + */ + public function getName(); + + /** + * Lists all locks + * + * @param string prefix optional a prefix to search + * + * @return array Empty if it isn't supported by the cache driver + */ + public function getLocks(string $prefix = ''); } diff --git a/src/Core/Lock/Lock.php b/src/Core/Lock/Lock.php index 4418fee271..f03ffe03d1 100644 --- a/src/Core/Lock/Lock.php +++ b/src/Core/Lock/Lock.php @@ -2,6 +2,8 @@ namespace Friendica\Core\Lock; +use Friendica\Core\Cache\Cache; + /** * Class AbstractLock * @@ -11,6 +13,9 @@ namespace Friendica\Core\Lock; */ abstract class Lock implements ILock { + const TYPE_DATABASE = Cache::TYPE_DATABASE; + const TYPE_SEMAPHORE = 'semaphore'; + /** * @var array The local acquired locks */ @@ -49,16 +54,14 @@ abstract class Lock implements ILock } /** - * Releases all lock that were set by us - * - * @return boolean Was the unlock of all locks successful? + * {@inheritDoc} */ - public function releaseAll() + public function releaseAll($override = false) { $return = true; foreach ($this->acquiredLocks as $acquiredLock => $hasLock) { - if (!$this->releaseLock($acquiredLock)) { + if (!$this->releaseLock($acquiredLock, $override)) { $return = false; } } diff --git a/src/Core/Lock/SemaphoreLock.php b/src/Core/Lock/SemaphoreLock.php index 789c9e8eca..75c7284a5f 100644 --- a/src/Core/Lock/SemaphoreLock.php +++ b/src/Core/Lock/SemaphoreLock.php @@ -20,9 +20,7 @@ class SemaphoreLock extends Lock */ private static function semaphoreKey($key) { - $temp = get_temppath(); - - $file = $temp . '/' . $key . '.sem'; + $file = self::keyToFile($key); if (!file_exists($file)) { file_put_contents($file, $key); @@ -31,10 +29,24 @@ class SemaphoreLock extends Lock return ftok($file, 'f'); } + /** + * Returns the full path to the semaphore file + * + * @param string $key The key of the semaphore + * + * @return string The full path + */ + private static function keyToFile($key) + { + $temp = get_temppath(); + + return $temp . '/' . $key . '.sem'; + } + /** * (@inheritdoc) */ - public function acquireLock($key, $timeout = 120, $ttl = Cache::FIVE_MINUTES) + public function acquireLock($key, $timeout = 120, $ttl = Cache\Cache::FIVE_MINUTES) { self::$semaphore[$key] = sem_get(self::semaphoreKey($key)); if (self::$semaphore[$key]) { @@ -52,14 +64,24 @@ class SemaphoreLock extends Lock */ public function releaseLock($key, $override = false) { - if (empty(self::$semaphore[$key])) { - return false; - } else { - $success = @sem_release(self::$semaphore[$key]); - unset(self::$semaphore[$key]); - $this->markRelease($key); - return $success; + $success = false; + + if (!empty(self::$semaphore[$key])) { + try { + $success = @sem_release(self::$semaphore[$key]) && + unlink(self::keyToFile($key)); + unset(self::$semaphore[$key]); + $this->markRelease($key); + } catch (\Exception $exception) { + $success = false; + } + } else if ($override) { + if ($this->acquireLock($key)) { + $success = $this->releaseLock($key, true); + } } + + return $success; } /** @@ -69,4 +91,47 @@ class SemaphoreLock extends Lock { return isset(self::$semaphore[$key]); } + + /** + * {@inheritDoc} + */ + public function getName() + { + return self::TYPE_SEMAPHORE; + } + + /** + * {@inheritDoc} + */ + public function getLocks(string $prefix = '') + { + $temp = get_temppath(); + $locks = []; + foreach (glob(sprintf('%s/%s*.sem', $temp, $prefix)) as $lock) { + $lock = pathinfo($lock, PATHINFO_FILENAME); + if(sem_get(self::semaphoreKey($lock))) { + $locks[] = $lock; + } + } + + return $locks; + } + + /** + * {@inheritDoc} + */ + public function releaseAll($override = false) + { + $success = parent::releaseAll($override); + + $temp = get_temppath(); + foreach (glob(sprintf('%s/*.sem', $temp)) as $lock) { + $lock = pathinfo($lock, PATHINFO_FILENAME); + if (!$this->releaseLock($lock, true)) { + $success = false; + } + } + + return $success; + } } diff --git a/tests/src/Core/Lock/LockTest.php b/tests/src/Core/Lock/LockTest.php index 0c231713ae..dd38172b38 100644 --- a/tests/src/Core/Lock/LockTest.php +++ b/tests/src/Core/Lock/LockTest.php @@ -23,12 +23,12 @@ abstract class LockTest extends MockedTest parent::setUp(); $this->instance = $this->getInstance(); - $this->instance->releaseAll(); + $this->instance->releaseAll(true); } protected function tearDown() { - $this->instance->releaseAll(); + $this->instance->releaseAll(true); parent::tearDown(); } @@ -123,6 +123,46 @@ abstract class LockTest extends MockedTest $this->assertFalse($this->instance->isLocked('test')); } + /** + * @small + */ + public function testGetLocks() + { + $this->assertTrue($this->instance->acquireLock('foo', 1)); + $this->assertTrue($this->instance->acquireLock('bar', 1)); + $this->assertTrue($this->instance->acquireLock('nice', 1)); + + $this->assertTrue($this->instance->isLocked('foo')); + $this->assertTrue($this->instance->isLocked('bar')); + $this->assertTrue($this->instance->isLocked('nice')); + + $locks = $this->instance->getLocks(); + + $this->assertContains('foo', $locks); + $this->assertContains('bar', $locks); + $this->assertContains('nice', $locks); + } + + /** + * @small + */ + public function testGetLocksWithPrefix() + { + $this->assertTrue($this->instance->acquireLock('foo', 1)); + $this->assertTrue($this->instance->acquireLock('test1', 1)); + $this->assertTrue($this->instance->acquireLock('test2', 1)); + + $this->assertTrue($this->instance->isLocked('foo')); + $this->assertTrue($this->instance->isLocked('test1')); + $this->assertTrue($this->instance->isLocked('test2')); + + $locks = $this->instance->getLocks('test'); + + $this->assertContains('test1', $locks); + $this->assertContains('test2', $locks); + $this->assertNotContains('foo', $locks); + } + /** * @medium */ diff --git a/tests/src/Core/Lock/SemaphoreLockTest.php b/tests/src/Core/Lock/SemaphoreLockTest.php index 7b9b03d728..52c5aaa5b8 100644 --- a/tests/src/Core/Lock/SemaphoreLockTest.php +++ b/tests/src/Core/Lock/SemaphoreLockTest.php @@ -12,8 +12,6 @@ class SemaphoreLockTest extends LockTest { public function setUp() { - parent::setUp(); - $dice = \Mockery::mock(Dice::class)->makePartial(); $app = \Mockery::mock(App::class); @@ -29,6 +27,8 @@ class SemaphoreLockTest extends LockTest // @todo Because "get_temppath()" is using static methods, we have to initialize the BaseObject BaseObject::setDependencyInjection($dice); + + parent::setUp(); } protected function getInstance() From e2e109b8c16f5941b3c3624a50a91335c6172b86 Mon Sep 17 00:00:00 2001 From: Philipp Holzer Date: Thu, 15 Aug 2019 13:58:01 +0200 Subject: [PATCH 2/4] Fix getAllKeys() method for memcache instances --- src/Core/Cache/ArrayCache.php | 2 +- src/Core/Cache/Cache.php | 8 +-- src/Core/Cache/MemcachedCache.php | 106 +++++++++++++++++++++++++++-- src/Core/Lock/DatabaseLock.php | 2 +- tests/src/Core/Cache/CacheTest.php | 7 +- 5 files changed, 108 insertions(+), 17 deletions(-) diff --git a/src/Core/Cache/ArrayCache.php b/src/Core/Cache/ArrayCache.php index 5add98cc2a..c6f3983ee2 100644 --- a/src/Core/Cache/ArrayCache.php +++ b/src/Core/Cache/ArrayCache.php @@ -21,7 +21,7 @@ class ArrayCache extends Cache implements IMemoryCache */ public function getAllKeys($prefix = null) { - return $this->filterArrayKeysByPrefix($this->cachedData, $prefix); + return $this->filterArrayKeysByPrefix(array_keys($this->cachedData), $prefix); } /** diff --git a/src/Core/Cache/Cache.php b/src/Core/Cache/Cache.php index b40c129ae7..cf5b15d052 100644 --- a/src/Core/Cache/Cache.php +++ b/src/Core/Cache/Cache.php @@ -84,19 +84,19 @@ abstract class Cache implements ICache * Filters the keys of an array with a given prefix * Returns the filtered keys as an new array * - * @param array $array The array, which should get filtered + * @param array $keys The keys, which should get filtered * @param string|null $prefix The prefix (if null, all keys will get returned) * * @return array The filtered array with just the keys */ - protected function filterArrayKeysByPrefix($array, $prefix = null) + protected function filterArrayKeysByPrefix(array $keys, string $prefix = null) { if (empty($prefix)) { - return array_keys($array); + return $keys; } else { $result = []; - foreach (array_keys($array) as $key) { + foreach ($keys as $key) { if (strpos($key, $prefix) === 0) { array_push($result, $key); } diff --git a/src/Core/Cache/MemcachedCache.php b/src/Core/Cache/MemcachedCache.php index ac0648a6ce..89685c3f25 100644 --- a/src/Core/Cache/MemcachedCache.php +++ b/src/Core/Cache/MemcachedCache.php @@ -27,6 +27,17 @@ class MemcachedCache extends Cache implements IMemoryCache */ private $logger; + /** + * @var string First server address + */ + + private $firstServer; + + /** + * @var int First server port + */ + private $firstPort; + /** * Due to limitations of the INI format, the expected configuration for Memcached servers is the following: * array { @@ -58,6 +69,9 @@ class MemcachedCache extends Cache implements IMemoryCache } }); + $this->firstServer = $memcached_hosts[0][0] ?? 'localhost'; + $this->firstPort = $memcached_hosts[0][1] ?? 11211; + $this->memcached->addServers($memcached_hosts); if (count($this->memcached->getServerList()) == 0) { @@ -70,14 +84,94 @@ class MemcachedCache extends Cache implements IMemoryCache */ public function getAllKeys($prefix = null) { - $keys = $this->getOriginalKeys($this->memcached->getAllKeys()); + $keys = $this->getOriginalKeys($this->getMemcachedKeys()); - if ($this->memcached->getResultCode() == Memcached::RES_SUCCESS) { - return $this->filterArrayKeysByPrefix($keys, $prefix); - } else { - $this->logger->debug('Memcached \'getAllKeys\' failed', ['result' => $this->memcached->getResultMessage()]); - return []; + return $this->filterArrayKeysByPrefix($keys, $prefix); + } + + /** + * Get all memcached keys. + * Special function because getAllKeys() is broken since memcached 1.4.23. + * + * cleaned up version of code found on Stackoverflow.com by Maduka Jayalath + * + * @return array|int - all retrieved keys (or negative number on error) + */ + private function getMemcachedKeys() + { + $mem = @fsockopen($this->firstServer, $this->firstPort); + if ($mem === false) { + return -1; } + + // retrieve distinct slab + $r = @fwrite($mem, 'stats items' . chr(10)); + if ($r === false) { + return -2; + } + + $slab = []; + while (($l = @fgets($mem, 1024)) !== false) { + // finished? + $l = trim($l); + if ($l == 'END') { + break; + } + + $m = []; + // + $r = preg_match('/^STAT\sitems\:(\d+)\:/', $l, $m); + if ($r != 1) { + return -3; + } + $a_slab = $m[1]; + + if (!array_key_exists($a_slab, $slab)) { + $slab[$a_slab] = []; + } + } + + reset($slab); + foreach ($slab as $a_slab_key => &$a_slab) { + $r = @fwrite($mem, 'stats cachedump ' . $a_slab_key . ' 100' . chr(10)); + if ($r === false) { + return -4; + } + + while (($l = @fgets($mem, 1024)) !== false) { + // finished? + $l = trim($l); + if ($l == 'END') { + break; + } + + $m = []; + // ITEM 42 [118 b; 1354717302 s] + $r = preg_match('/^ITEM\s([^\s]+)\s/', $l, $m); + if ($r != 1) { + return -5; + } + $a_key = $m[1]; + + $a_slab[] = $a_key; + } + } + + // close the connection + @fclose($mem); + unset($mem); + + $keys = []; + reset($slab); + foreach ($slab AS &$a_slab) { + reset($a_slab); + foreach ($a_slab AS &$a_key) { + $keys[] = $a_key; + } + } + unset($slab); + + return $keys; } /** diff --git a/src/Core/Lock/DatabaseLock.php b/src/Core/Lock/DatabaseLock.php index 2f409cd3d2..07cf88c494 100644 --- a/src/Core/Lock/DatabaseLock.php +++ b/src/Core/Lock/DatabaseLock.php @@ -138,7 +138,7 @@ class DatabaseLock extends Lock if (empty($prefix)) { $where = ['`expires` >= ?', DateTimeFormat::utcNow()]; } else { - $where = ['`expires` >= ? AND `k` LIKE CONCAT(?, \'%\')', DateTimeFormat::utcNow(), $prefix]; + $where = ['`expires` >= ? AND `name` LIKE CONCAT(?, \'%\')', DateTimeFormat::utcNow(), $prefix]; } $stmt = $this->dba->select('locks', ['name'], $where); diff --git a/tests/src/Core/Cache/CacheTest.php b/tests/src/Core/Cache/CacheTest.php index 92fdaffa32..9071a55c40 100644 --- a/tests/src/Core/Cache/CacheTest.php +++ b/tests/src/Core/Cache/CacheTest.php @@ -2,7 +2,6 @@ namespace Friendica\Test\src\Core\Cache; -use Friendica\Core\Cache\MemcachedCache; use Friendica\Test\MockedTest; use Friendica\Util\PidFile; @@ -202,10 +201,6 @@ abstract class CacheTest extends MockedTest */ public function testGetAllKeys($value1, $value2, $value3) { - if ($this->cache instanceof MemcachedCache) { - $this->markTestSkipped('Memcached doesn\'t support getAllKeys anymore'); - } - $this->assertTrue($this->instance->set('value1', $value1)); $this->assertTrue($this->instance->set('value2', $value2)); $this->assertTrue($this->instance->set('test_value3', $value3)); @@ -219,5 +214,7 @@ abstract class CacheTest extends MockedTest $list = $this->instance->getAllKeys('test'); $this->assertContains('test_value3', $list); + $this->assertNotContains('value1', $list); + $this->assertNotContains('value2', $list); } } From d95457cf611881a9dde534bacfe368661553e20f Mon Sep 17 00:00:00 2001 From: Philipp Holzer Date: Thu, 15 Aug 2019 14:22:29 +0200 Subject: [PATCH 3/4] Add Lock test --- src/Console/Lock.php | 18 +-- tests/src/Console/LockConsoleTest.php | 215 ++++++++++++++++++++++++++ 2 files changed, 224 insertions(+), 9 deletions(-) create mode 100644 tests/src/Console/LockConsoleTest.php diff --git a/src/Console/Lock.php b/src/Console/Lock.php index fe9132b7cb..46826cc8c9 100644 --- a/src/Console/Lock.php +++ b/src/Console/Lock.php @@ -32,7 +32,7 @@ class Lock extends \Asika\SimpleConsole\Console protected function getHelp() { $help = <<] [-h|--help|-?] [-v] bin/console lock set [ []] [-h|--help|-?] [-v] @@ -131,9 +131,9 @@ HELP; private function executeDel() { if (count($this->args) >= 2) { - $lock = $this->getArgument(1); + $lock = $this->getArgument(1); - if ($this->lock->releaseLock($lock, true)){ + if ($this->lock->releaseLock($lock, true)) { $this->out(sprintf('Lock \'%s\' released.', $lock)); } else { $this->out(sprintf('Couldn\'t release Lock \'%s\'', $lock)); @@ -147,11 +147,11 @@ HELP; private function executeSet() { if (count($this->args) >= 2) { - $lock = $this->getArgument(1); + $lock = $this->getArgument(1); $timeout = intval($this->getArgument(2, false)); - $ttl = intval($this->getArgument(3, false)); + $ttl = intval($this->getArgument(3, false)); - if (is_array($this->lock->isLocked($lock))) { + if ($this->lock->isLocked($lock)) { throw new RuntimeException(sprintf('\'%s\' is already set.', $lock)); } @@ -166,7 +166,7 @@ HELP; if ($result) { $this->out(sprintf('Lock \'%s\' acquired.', $lock)); } else { - $this->out(sprintf('Unable to lock \'%s\'', $lock)); + throw new RuntimeException(sprintf('Unable to lock \'%s\'.', $lock)); } } else { throw new CommandArgsException('Too few arguments for set.'); @@ -177,9 +177,9 @@ HELP; { $result = $this->lock->releaseAll(true); if ($result) { - $this->out('Locks successfully cleared,'); + $this->out('Locks successfully cleared.'); } else { - $this->out('Unable to clear the locks.'); + throw new RuntimeException('Unable to clear the locks.'); } } } diff --git a/tests/src/Console/LockConsoleTest.php b/tests/src/Console/LockConsoleTest.php new file mode 100644 index 0000000000..51c05b5c37 --- /dev/null +++ b/tests/src/Console/LockConsoleTest.php @@ -0,0 +1,215 @@ +setConstantsMap([ + Mode::class => [ + 'DBCONFIGAVAILABLE' => 0 + ] + ]); + + $this->appMode = \Mockery::mock(App\Mode::class); + $this->appMode->shouldReceive('has') + ->andReturn(true); + + $this->lockMock = \Mockery::mock(ILock::class); + } + + public function testList() + { + $this->lockMock + ->shouldReceive('getLocks') + ->andReturn(['test', 'test2']) + ->once(); + + $console = new Lock($this->appMode, $this->lockMock, $this->consoleArgv); + $console->setArgument(0, 'list'); + $txt = $this->dumpExecute($console); + $this->assertEquals("Listing all Locks:\ntest\ntest2\n2 locks found\n", $txt); + } + + public function testListPrefix() + { + $this->lockMock + ->shouldReceive('getLocks') + ->with('test') + ->andReturn(['test', 'test2']) + ->once(); + + $console = new Lock($this->appMode, $this->lockMock, $this->consoleArgv); + $console->setArgument(0, 'list'); + $console->setArgument(1, 'test'); + $txt = $this->dumpExecute($console); + $this->assertEquals("Listing all Locks starting with \"test\":\ntest\ntest2\n2 locks found\n", $txt); + } + + public function testDelLock() + { + $this->lockMock + ->shouldReceive('releaseLock') + ->with('test', true) + ->andReturn(true) + ->once(); + + $console = new Lock($this->appMode, $this->lockMock, $this->consoleArgv); + $console->setArgument(0, 'del'); + $console->setArgument(1, 'test'); + $txt = $this->dumpExecute($console); + $this->assertEquals("Lock 'test' released.\n", $txt); + } + + public function testDelUnknownLock() + { + $this->lockMock + ->shouldReceive('releaseLock') + ->with('test', true) + ->andReturn(false) + ->once(); + + $console = new Lock($this->appMode, $this->lockMock, $this->consoleArgv); + $console->setArgument(0, 'del'); + $console->setArgument(1, 'test'); + $txt = $this->dumpExecute($console); + $this->assertEquals("Couldn't release Lock 'test'\n", $txt); + } + + public function testSetLock() + { + $this->lockMock + ->shouldReceive('isLocked') + ->with('test') + ->andReturn(false) + ->once(); + $this->lockMock + ->shouldReceive('acquireLock') + ->with('test') + ->andReturn(true) + ->once(); + + $console = new Lock($this->appMode, $this->lockMock, $this->consoleArgv); + $console->setArgument(0, 'set'); + $console->setArgument(1, 'test'); + $txt = $this->dumpExecute($console); + $this->assertEquals("Lock 'test' acquired.\n", $txt); + } + + public function testSetLockIsLocked() + { + $this->lockMock + ->shouldReceive('isLocked') + ->with('test') + ->andReturn(true) + ->once(); + + $console = new Lock($this->appMode, $this->lockMock, $this->consoleArgv); + $console->setArgument(0, 'set'); + $console->setArgument(1, 'test'); + $txt = $this->dumpExecute($console); + $this->assertEquals("[Error] 'test' is already set.\n", $txt); + } + + public function testSetLockNotWorking() + { + $this->lockMock + ->shouldReceive('isLocked') + ->with('test') + ->andReturn(false) + ->once(); + $this->lockMock + ->shouldReceive('acquireLock') + ->with('test') + ->andReturn(false) + ->once(); + + $console = new Lock($this->appMode, $this->lockMock, $this->consoleArgv); + $console->setArgument(0, 'set'); + $console->setArgument(1, 'test'); + $txt = $this->dumpExecute($console); + $this->assertEquals("[Error] Unable to lock 'test'.\n", $txt); + } + + public function testReleaseAll() + { + $this->lockMock + ->shouldReceive('releaseAll') + ->andReturn(true) + ->once(); + + $console = new Lock($this->appMode, $this->lockMock, $this->consoleArgv); + $console->setArgument(0, 'clear'); + $txt = $this->dumpExecute($console); + $this->assertEquals("Locks successfully cleared.\n", $txt); + } + + public function testReleaseAllFailed() + { + $this->lockMock + ->shouldReceive('releaseAll') + ->andReturn(false) + ->once(); + + $console = new Lock($this->appMode, $this->lockMock, $this->consoleArgv); + $console->setArgument(0, 'clear'); + $txt = $this->dumpExecute($console); + $this->assertEquals("[Error] Unable to clear the locks.\n", $txt); + } + + public function testGetHelp() + { + // Usable to purposely fail if new commands are added without taking tests into account + $theHelp = <<] [-h|--help|-?] [-v] + bin/console lock set [ []] [-h|--help|-?] [-v] + bin/console lock del [-h|--help|-?] [-v] + bin/console lock clear [-h|--help|-?] [-v] + +Description + bin/console lock list [] + List all locks, optionally filtered by a prefix + + bin/console lock set [ []] + Sets manually a lock, optionally with the provided TTL (time to live) with a default of five minutes. + + bin/console lock del + Deletes a lock. + + bin/console lock clear + Clears all locks + +Options + -h|--help|-? Show help information + -v Show more debug information. + +HELP; + $console = new Lock($this->appMode, $this->lockMock, [$this->consoleArgv]); + $console->setOption('help', true); + + $txt = $this->dumpExecute($console); + + $this->assertEquals($txt, $theHelp); + } +} From 689a2145f6f33772a5106b6debae501af86865ec Mon Sep 17 00:00:00 2001 From: Philipp Holzer Date: Thu, 15 Aug 2019 16:24:11 +0200 Subject: [PATCH 4/4] Add @see for SO link --- src/Core/Cache/MemcachedCache.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Cache/MemcachedCache.php b/src/Core/Cache/MemcachedCache.php index 89685c3f25..69f6b9a0a7 100644 --- a/src/Core/Cache/MemcachedCache.php +++ b/src/Core/Cache/MemcachedCache.php @@ -94,6 +94,7 @@ class MemcachedCache extends Cache implements IMemoryCache * Special function because getAllKeys() is broken since memcached 1.4.23. * * cleaned up version of code found on Stackoverflow.com by Maduka Jayalath + * @see https://stackoverflow.com/a/34724821 * * @return array|int - all retrieved keys (or negative number on error) */