From 41e2031e6b7b572c00aea53136baaef38cd79c86 Mon Sep 17 00:00:00 2001
From: Philipp Holzer <admin+github@philipp.info>
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 @@
+<?php
+
+namespace Friendica\Console;
+
+use Asika\SimpleConsole\CommandArgsException;
+use Friendica\App;
+use Friendica\Core\Lock\ILock;
+use RuntimeException;
+
+/**
+ * @brief tool to access the locks from the CLI
+ *
+ * With this script you can access the locks of your node from the CLI.
+ * You can read current locks and set/remove locks.
+ *
+ * @author Philipp Holzer <admin@philipp.info>, Hypolite Petovan <hypolite@mrpetovan.com>
+ */
+class Lock extends \Asika\SimpleConsole\Console
+{
+	protected $helpOptions = ['h', 'help', '?'];
+
+	/**
+	 * @var App\Mode
+	 */
+	private $appMode;
+
+	/**
+	 * @var ILock
+	 */
+	private $lock;
+
+	protected function getHelp()
+	{
+		$help = <<<HELP
+console cache - Manage node cache
+Synopsis
+	bin/console lock list [<prefix>] [-h|--help|-?] [-v]
+	bin/console lock set <lock> [<timeout> [<ttl>]] [-h|--help|-?] [-v]
+	bin/console lock del <lock> [-h|--help|-?] [-v]
+	bin/console lock clear [-h|--help|-?] [-v]
+
+Description
+	bin/console lock list [<prefix>]
+		List all locks, optionally filtered by a prefix
+
+	bin/console lock set <lock> [<timeout> [<ttl>]]
+		Sets manually a lock, optionally with the provided TTL (time to live) with a default of five minutes.
+
+	bin/console lock del <lock>
+		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 <admin+github@philipp.info>
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 = [];
+			// <STAT items:22:evicted_nonzero 0>
+			$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 <admin+github@philipp.info>
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 = <<<HELP
-console cache - Manage node cache
+console lock - Manage node locks
 Synopsis
 	bin/console lock list [<prefix>] [-h|--help|-?] [-v]
 	bin/console lock set <lock> [<timeout> [<ttl>]] [-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 @@
+<?php
+
+namespace Friendica\Test\src\Console;
+
+use Friendica\App;
+use Friendica\App\Mode;
+use Friendica\Console\Lock;
+use Friendica\Core\Lock\ILock;
+use Mockery\MockInterface;
+
+class LockConsoleTest extends ConsoleTest
+{
+	/**
+	 * @var App\Mode|MockInterface $appMode
+	 */
+	private $appMode;
+
+	/**
+	 * @var ILock|MockInterface
+	 */
+	private $lockMock;
+
+	protected function setUp()
+	{
+		parent::setUp();
+
+		\Mockery::getConfiguration()->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 = <<<HELP
+console lock - Manage node locks
+Synopsis
+	bin/console lock list [<prefix>] [-h|--help|-?] [-v]
+	bin/console lock set <lock> [<timeout> [<ttl>]] [-h|--help|-?] [-v]
+	bin/console lock del <lock> [-h|--help|-?] [-v]
+	bin/console lock clear [-h|--help|-?] [-v]
+
+Description
+	bin/console lock list [<prefix>]
+		List all locks, optionally filtered by a prefix
+
+	bin/console lock set <lock> [<timeout> [<ttl>]]
+		Sets manually a lock, optionally with the provided TTL (time to live) with a default of five minutes.
+
+	bin/console lock del <lock>
+		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 <admin+github@philipp.info>
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)
 	 */