Console Lock
WIP
This commit is contained in:
parent
425876316f
commit
41e2031e6b
9 changed files with 420 additions and 25 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 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.');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = '');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
$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);
|
||||
return $success;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue