mirror of
https://github.com/friendica/friendica
synced 2024-11-18 06:23:41 +00:00
Merge pull request #7515 from nupplaphil/task/console_lock
New Console Command: Lock
This commit is contained in:
commit
48caf55cff
14 changed files with 743 additions and 41 deletions
185
src/Console/Lock.php
Normal file
185
src/Console/Lock.php
Normal file
|
@ -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 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;
|
||||||
|
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 ($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 {
|
||||||
|
throw new RuntimeException(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 {
|
||||||
|
throw new RuntimeException('Unable to clear the locks.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ class ArrayCache extends Cache implements IMemoryCache
|
||||||
*/
|
*/
|
||||||
public function getAllKeys($prefix = null)
|
public function getAllKeys($prefix = null)
|
||||||
{
|
{
|
||||||
return $this->filterArrayKeysByPrefix($this->cachedData, $prefix);
|
return $this->filterArrayKeysByPrefix(array_keys($this->cachedData), $prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -84,19 +84,19 @@ abstract class Cache implements ICache
|
||||||
* Filters the keys of an array with a given prefix
|
* Filters the keys of an array with a given prefix
|
||||||
* Returns the filtered keys as an new array
|
* 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)
|
* @param string|null $prefix The prefix (if null, all keys will get returned)
|
||||||
*
|
*
|
||||||
* @return array The filtered array with just the keys
|
* @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)) {
|
if (empty($prefix)) {
|
||||||
return array_keys($array);
|
return $keys;
|
||||||
} else {
|
} else {
|
||||||
$result = [];
|
$result = [];
|
||||||
|
|
||||||
foreach (array_keys($array) as $key) {
|
foreach ($keys as $key) {
|
||||||
if (strpos($key, $prefix) === 0) {
|
if (strpos($key, $prefix) === 0) {
|
||||||
array_push($result, $key);
|
array_push($result, $key);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,17 @@ class MemcachedCache extends Cache implements IMemoryCache
|
||||||
*/
|
*/
|
||||||
private $logger;
|
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:
|
* Due to limitations of the INI format, the expected configuration for Memcached servers is the following:
|
||||||
* array {
|
* 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);
|
$this->memcached->addServers($memcached_hosts);
|
||||||
|
|
||||||
if (count($this->memcached->getServerList()) == 0) {
|
if (count($this->memcached->getServerList()) == 0) {
|
||||||
|
@ -70,14 +84,95 @@ class MemcachedCache extends Cache implements IMemoryCache
|
||||||
*/
|
*/
|
||||||
public function getAllKeys($prefix = null)
|
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);
|
||||||
return $this->filterArrayKeysByPrefix($keys, $prefix);
|
}
|
||||||
} else {
|
|
||||||
$this->logger->debug('Memcached \'getAllKeys\' failed', ['result' => $this->memcached->getResultMessage()]);
|
/**
|
||||||
return [];
|
* 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
|
||||||
|
* @see https://stackoverflow.com/a/34724821
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -38,6 +38,7 @@ Commands:
|
||||||
archivecontact Archive a contact when you know that it isn't existing anymore
|
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)
|
help Show help about a command, e.g (bin/console help config)
|
||||||
autoinstall Starts automatic installation of friendica based on values from htconfig.php
|
autoinstall Starts automatic installation of friendica based on values from htconfig.php
|
||||||
|
lock Edit site locks
|
||||||
maintenance Set maintenance mode for this node
|
maintenance Set maintenance mode for this node
|
||||||
newpassword Set a new password for a given user
|
newpassword Set a new password for a given user
|
||||||
php2po Generate a messages.po file from a strings.php file
|
php2po Generate a messages.po file from a strings.php file
|
||||||
|
@ -65,6 +66,7 @@ HELP;
|
||||||
'globalcommunitysilence' => Friendica\Console\GlobalCommunitySilence::class,
|
'globalcommunitysilence' => Friendica\Console\GlobalCommunitySilence::class,
|
||||||
'archivecontact' => Friendica\Console\ArchiveContact::class,
|
'archivecontact' => Friendica\Console\ArchiveContact::class,
|
||||||
'autoinstall' => Friendica\Console\AutomaticInstallation::class,
|
'autoinstall' => Friendica\Console\AutomaticInstallation::class,
|
||||||
|
'lock' => Friendica\Console\Lock::class,
|
||||||
'maintenance' => Friendica\Console\Maintenance::class,
|
'maintenance' => Friendica\Console\Maintenance::class,
|
||||||
'newpassword' => Friendica\Console\NewPassword::class,
|
'newpassword' => Friendica\Console\NewPassword::class,
|
||||||
'php2po' => Friendica\Console\PhpToPo::class,
|
'php2po' => Friendica\Console\PhpToPo::class,
|
||||||
|
|
|
@ -7,6 +7,11 @@ use Friendica\Core\Cache\IMemoryCache;
|
||||||
|
|
||||||
class CacheLock extends Lock
|
class CacheLock extends Lock
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var string The static prefix of all locks inside the cache
|
||||||
|
*/
|
||||||
|
const CACHE_PREFIX = 'lock:';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Friendica\Core\Cache\ICache;
|
* @var \Friendica\Core\Cache\ICache;
|
||||||
*/
|
*/
|
||||||
|
@ -25,7 +30,7 @@ class CacheLock extends Lock
|
||||||
/**
|
/**
|
||||||
* (@inheritdoc)
|
* (@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;
|
$got_lock = false;
|
||||||
$start = time();
|
$start = time();
|
||||||
|
@ -85,6 +90,46 @@ class CacheLock extends Lock
|
||||||
return isset($lock) && ($lock !== false);
|
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
|
* @param string $key The original key
|
||||||
*
|
*
|
||||||
|
@ -92,6 +137,6 @@ class CacheLock extends Lock
|
||||||
*/
|
*/
|
||||||
private static function getLockKey($key)
|
private static function getLockKey($key)
|
||||||
{
|
{
|
||||||
return "lock:" . $key;
|
return self::CACHE_PREFIX . $key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,9 +92,16 @@ class DatabaseLock extends Lock
|
||||||
/**
|
/**
|
||||||
* (@inheritdoc)
|
* (@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 = [];
|
$this->acquiredLocks = [];
|
||||||
|
|
||||||
|
@ -114,4 +121,34 @@ class DatabaseLock extends Lock
|
||||||
return false;
|
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 `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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,25 @@ interface ILock
|
||||||
/**
|
/**
|
||||||
* Releases all lock that were set by us
|
* 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?
|
* @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 = '');
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace Friendica\Core\Lock;
|
namespace Friendica\Core\Lock;
|
||||||
|
|
||||||
|
use Friendica\Core\Cache\Cache;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class AbstractLock
|
* Class AbstractLock
|
||||||
*
|
*
|
||||||
|
@ -11,6 +13,9 @@ namespace Friendica\Core\Lock;
|
||||||
*/
|
*/
|
||||||
abstract class Lock implements ILock
|
abstract class Lock implements ILock
|
||||||
{
|
{
|
||||||
|
const TYPE_DATABASE = Cache::TYPE_DATABASE;
|
||||||
|
const TYPE_SEMAPHORE = 'semaphore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array The local acquired locks
|
* @var array The local acquired locks
|
||||||
*/
|
*/
|
||||||
|
@ -49,16 +54,14 @@ abstract class Lock implements ILock
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Releases all lock that were set by us
|
* {@inheritDoc}
|
||||||
*
|
|
||||||
* @return boolean Was the unlock of all locks successful?
|
|
||||||
*/
|
*/
|
||||||
public function releaseAll()
|
public function releaseAll($override = false)
|
||||||
{
|
{
|
||||||
$return = true;
|
$return = true;
|
||||||
|
|
||||||
foreach ($this->acquiredLocks as $acquiredLock => $hasLock) {
|
foreach ($this->acquiredLocks as $acquiredLock => $hasLock) {
|
||||||
if (!$this->releaseLock($acquiredLock)) {
|
if (!$this->releaseLock($acquiredLock, $override)) {
|
||||||
$return = false;
|
$return = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,7 @@ class SemaphoreLock extends Lock
|
||||||
*/
|
*/
|
||||||
private static function semaphoreKey($key)
|
private static function semaphoreKey($key)
|
||||||
{
|
{
|
||||||
$temp = get_temppath();
|
$file = self::keyToFile($key);
|
||||||
|
|
||||||
$file = $temp . '/' . $key . '.sem';
|
|
||||||
|
|
||||||
if (!file_exists($file)) {
|
if (!file_exists($file)) {
|
||||||
file_put_contents($file, $key);
|
file_put_contents($file, $key);
|
||||||
|
@ -31,10 +29,24 @@ class SemaphoreLock extends Lock
|
||||||
return ftok($file, 'f');
|
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)
|
* (@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));
|
self::$semaphore[$key] = sem_get(self::semaphoreKey($key));
|
||||||
if (self::$semaphore[$key]) {
|
if (self::$semaphore[$key]) {
|
||||||
|
@ -52,14 +64,24 @@ class SemaphoreLock extends Lock
|
||||||
*/
|
*/
|
||||||
public function releaseLock($key, $override = false)
|
public function releaseLock($key, $override = false)
|
||||||
{
|
{
|
||||||
if (empty(self::$semaphore[$key])) {
|
$success = false;
|
||||||
return false;
|
|
||||||
} else {
|
if (!empty(self::$semaphore[$key])) {
|
||||||
$success = @sem_release(self::$semaphore[$key]);
|
try {
|
||||||
unset(self::$semaphore[$key]);
|
$success = @sem_release(self::$semaphore[$key]) &&
|
||||||
$this->markRelease($key);
|
unlink(self::keyToFile($key));
|
||||||
return $success;
|
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]);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
215
tests/src/Console/LockConsoleTest.php
Normal file
215
tests/src/Console/LockConsoleTest.php
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
namespace Friendica\Test\src\Core\Cache;
|
namespace Friendica\Test\src\Core\Cache;
|
||||||
|
|
||||||
use Friendica\Core\Cache\MemcachedCache;
|
|
||||||
use Friendica\Test\MockedTest;
|
use Friendica\Test\MockedTest;
|
||||||
use Friendica\Util\PidFile;
|
use Friendica\Util\PidFile;
|
||||||
|
|
||||||
|
@ -202,10 +201,6 @@ abstract class CacheTest extends MockedTest
|
||||||
*/
|
*/
|
||||||
public function testGetAllKeys($value1, $value2, $value3)
|
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('value1', $value1));
|
||||||
$this->assertTrue($this->instance->set('value2', $value2));
|
$this->assertTrue($this->instance->set('value2', $value2));
|
||||||
$this->assertTrue($this->instance->set('test_value3', $value3));
|
$this->assertTrue($this->instance->set('test_value3', $value3));
|
||||||
|
@ -219,5 +214,7 @@ abstract class CacheTest extends MockedTest
|
||||||
$list = $this->instance->getAllKeys('test');
|
$list = $this->instance->getAllKeys('test');
|
||||||
|
|
||||||
$this->assertContains('test_value3', $list);
|
$this->assertContains('test_value3', $list);
|
||||||
|
$this->assertNotContains('value1', $list);
|
||||||
|
$this->assertNotContains('value2', $list);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,12 @@ abstract class LockTest extends MockedTest
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->instance = $this->getInstance();
|
$this->instance = $this->getInstance();
|
||||||
$this->instance->releaseAll();
|
$this->instance->releaseAll(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function tearDown()
|
protected function tearDown()
|
||||||
{
|
{
|
||||||
$this->instance->releaseAll();
|
$this->instance->releaseAll(true);
|
||||||
parent::tearDown();
|
parent::tearDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,6 +123,46 @@ abstract class LockTest extends MockedTest
|
||||||
$this->assertFalse($this->instance->isLocked('test'));
|
$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
|
* @medium
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -12,8 +12,6 @@ class SemaphoreLockTest extends LockTest
|
||||||
{
|
{
|
||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$dice = \Mockery::mock(Dice::class)->makePartial();
|
$dice = \Mockery::mock(Dice::class)->makePartial();
|
||||||
|
|
||||||
$app = \Mockery::mock(App::class);
|
$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
|
// @todo Because "get_temppath()" is using static methods, we have to initialize the BaseObject
|
||||||
BaseObject::setDependencyInjection($dice);
|
BaseObject::setDependencyInjection($dice);
|
||||||
|
|
||||||
|
parent::setUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getInstance()
|
protected function getInstance()
|
||||||
|
|
Loading…
Reference in a new issue