Console Lock

WIP
This commit is contained in:
Philipp Holzer 2019-08-13 21:20:41 +02:00
parent 425876316f
commit 41e2031e6b
No known key found for this signature in database
GPG key ID: D8365C3D36B77D90
9 changed files with 420 additions and 25 deletions

185
src/Console/Lock.php Normal file
View 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 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.');
}
}
}

View file

@ -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,

View file

@ -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;
} }
} }

View file

@ -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 `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;
}
} }

View file

@ -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 = '');
} }

View file

@ -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;
} }
} }

View file

@ -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;
}
} }

View file

@ -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
*/ */

View file

@ -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()