Adding multihost - locking

Adding Unit-Tests for it
This commit is contained in:
Philipp Holzer 2018-07-04 23:37:22 +02:00
parent b07dfbb03f
commit aac94d1d74
No known key found for this signature in database
GPG key ID: 58160D7D6AF942B6
22 changed files with 741 additions and 154 deletions

View file

@ -1,6 +1,7 @@
<?php
namespace Friendica\Core\Lock;
use Friendica\BaseObject;
/**
* Class AbstractLockDriver
@ -9,7 +10,7 @@ namespace Friendica\Core\Lock;
*
* @brief Basic class for Locking with common functions (local acquired locks, releaseAll, ..)
*/
abstract class AbstractLockDriver implements ILockDriver
abstract class AbstractLockDriver extends BaseObject implements ILockDriver
{
/**
* @var array The local acquired locks
@ -23,7 +24,7 @@ abstract class AbstractLockDriver implements ILockDriver
* @return bool Returns true if the lock is set
*/
protected function hasAcquiredLock($key) {
return isset($this->acquireLock[$key]);
return isset($this->acquireLock[$key]) && $this->acquiredLocks[$key] === true;
}
/**
@ -50,7 +51,7 @@ abstract class AbstractLockDriver implements ILockDriver
* @return void
*/
public function releaseAll() {
foreach ($this->acquiredLocks as $acquiredLock) {
foreach ($this->acquiredLocks as $acquiredLock => $hasLock) {
$this->releaseLock($acquiredLock);
}
}

View file

@ -2,7 +2,7 @@
namespace Friendica\Core\Lock;
use Friendica\Core\Cache\ICacheDriver;
use Friendica\Core\Cache\IMemoryCacheDriver;
class CacheLockDriver extends AbstractLockDriver
{
@ -14,9 +14,9 @@ class CacheLockDriver extends AbstractLockDriver
/**
* CacheLockDriver constructor.
*
* @param ICacheDriver $cache The CacheDriver for this type of lock
* @param IMemoryCacheDriver $cache The CacheDriver for this type of lock
*/
public function __construct(ICacheDriver $cache)
public function __construct(IMemoryCacheDriver $cache)
{
$this->cache = $cache;
}
@ -35,32 +35,31 @@ class CacheLockDriver extends AbstractLockDriver
$got_lock = false;
$start = time();
$cachekey = get_app()->get_hostname() . ";lock:" . $key;
$cachekey = self::getCacheKey($key);
do {
$lock = $this->cache->get($cachekey);
if (!is_bool($lock)) {
$pid = (int)$lock;
// When the process id isn't used anymore, we can safely claim the lock for us.
// Or we do want to lock something that was already locked by us.
if (!posix_kill($pid, 0) || ($pid == getmypid())) {
$lock = false;
}
}
if (is_bool($lock)) {
$this->cache->set($cachekey, getmypid(), 300);
// When we do want to lock something that was already locked by us.
if ((int)$lock == getmypid()) {
$got_lock = true;
}
// When we do want to lock something new
if (is_null($lock)) {
// At first initialize it with "0"
$this->cache->add($cachekey, 0);
// Now the value has to be "0" because otherwise the key was used by another process meanwhile
if ($this->cache->compareSet($cachekey, 0, getmypid(), 300)) {
$got_lock = true;
$this->markAcquire($key);
}
}
if (!$got_lock && ($timeout > 0)) {
usleep(rand(10000, 200000));
}
} while (!$got_lock && ((time() - $start) < $timeout));
$this->markAcquire($key);
return $got_lock;
}
@ -68,22 +67,33 @@ class CacheLockDriver extends AbstractLockDriver
* @brief Removes a lock if it was set by us
*
* @param string $key Name of the lock
*
* @return mixed
*/
public function releaseLock($key)
{
$cachekey = get_app()->get_hostname() . ";lock:" . $key;
$lock = $this->cache->get($cachekey);
if (!is_bool($lock)) {
if ((int)$lock == getmypid()) {
$this->cache->delete($cachekey);
}
}
$cachekey = self::getCacheKey($key);
$this->cache->compareDelete($cachekey, getmypid());
$this->markRelease($key);
}
return;
/**
* @brief Checks, if a key is currently locked to a process
*
* @param string $key The name of the lock
* @return bool
*/
public function isLocked($key)
{
$cachekey = self::getCacheKey($key);
$lock = $this->cache->get($cachekey);
return isset($lock) && ($lock !== false);
}
/**
* @param string $key The original key
* @return string The cache key used for the cache
*/
private static function getCacheKey($key) {
return self::getApp()->get_hostname() . ";lock:" . $key;
}
}

View file

@ -4,6 +4,7 @@ namespace Friendica\Core\Lock;
use dba;
use Friendica\Database\DBM;
use Friendica\Util\DateTimeFormat;
/**
* Locking driver that stores the locks in the database
@ -11,12 +12,7 @@ use Friendica\Database\DBM;
class DatabaseLockDriver extends AbstractLockDriver
{
/**
* @brief Sets a lock for a given name
*
* @param string $key The Name of the lock
* @param integer $timeout Seconds until we give up
*
* @return boolean Was the lock successful?
* (@inheritdoc)
*/
public function acquireLock($key, $timeout = 120)
{
@ -25,26 +21,25 @@ class DatabaseLockDriver extends AbstractLockDriver
do {
dba::lock('locks');
$lock = dba::selectFirst('locks', ['locked', 'pid'], ['name' => $key]);
$lock = dba::selectFirst('locks', ['locked', 'pid'], ['`name` = ? AND `expires` >= ?', $key, DateTimeFormat::utcNow()]);
if (DBM::is_result($lock)) {
if ($lock['locked']) {
// When the process id isn't used anymore, we can safely claim the lock for us.
if (!posix_kill($lock['pid'], 0)) {
$lock['locked'] = false;
}
// We want to lock something that was already locked by us? So we got the lock.
if ($lock['pid'] == getmypid()) {
$got_lock = true;
$this->markAcquire($key);
}
}
if (!$lock['locked']) {
dba::update('locks', ['locked' => true, 'pid' => getmypid()], ['name' => $key]);
dba::update('locks', ['locked' => true, 'pid' => getmypid(), 'expires' => DateTimeFormat::utc('now + 300seconds')], ['name' => $key]);
$got_lock = true;
$this->markAcquire($key);
}
} else {
dba::insert('locks', ['name' => $key, 'locked' => true, 'pid' => getmypid()]);
dba::insert('locks', ['name' => $key, 'locked' => true, 'pid' => getmypid(), 'expires' => DateTimeFormat::utc('now + 300seconds')]);
$got_lock = true;
$this->markAcquire($key);
}
dba::unlock();
@ -54,36 +49,42 @@ class DatabaseLockDriver extends AbstractLockDriver
}
} while (!$got_lock && ((time() - $start) < $timeout));
$this->markAcquire($key);
return $got_lock;
}
/**
* @brief Removes a lock if it was set by us
*
* @param string $key Name of the lock
*
* @return mixed
* (@inheritdoc)
*/
public function releaseLock($key)
{
dba::delete('locks', ['locked' => false, 'pid' => 0], ['name' => $key, 'pid' => getmypid()]);
dba::delete('locks', ['name' => $key, 'pid' => getmypid()]);
$this->releaseLock($key);
$this->markRelease($key);
return;
}
/**
* @brief Removes all lock that were set by us
*
* @return void
* (@inheritdoc)
*/
public function releaseAll()
{
dba::delete('locks', ['locked' => false, 'pid' => 0], ['pid' => getmypid()]);
dba::delete('locks', ['pid' => getmypid()]);
$this->acquiredLocks = [];
}
/**
* (@inheritdoc)
*/
public function isLocked($key)
{
$lock = dba::selectFirst('locks', ['locked'], ['`name` = ? AND `expires` >= ?', $key, DateTimeFormat::utcNow()]);
if (DBM::is_result($lock)) {
return $lock['locked'] !== false;
} else {
return false;
}
}
}

View file

@ -9,6 +9,14 @@ namespace Friendica\Core\Lock;
*/
interface ILockDriver
{
/**
* @brief Checks, if a key is currently locked to a or my process
*
* @param string $key The name of the lock
* @return bool
*/
public function isLocked($key);
/**
*
* @brief Acquires a lock for a given name

View file

@ -4,6 +4,8 @@ namespace Friendica\Core\Lock;
class SemaphoreLockDriver extends AbstractLockDriver
{
private static $semaphore = [];
public function __construct()
{
if (!function_exists('sem_get')) {
@ -42,10 +44,15 @@ class SemaphoreLockDriver extends AbstractLockDriver
*/
public function acquireLock($key, $timeout = 120)
{
$this->acquiredLocks[$key] = sem_get(self::semaphoreKey($key));
if ($this->acquiredLocks[$key]) {
return sem_acquire($this->acquiredLocks[$key], ($timeout == 0));
self::$semaphore[$key] = sem_get(self::semaphoreKey($key));
if (self::$semaphore[$key]) {
if (sem_acquire(self::$semaphore[$key], ($timeout == 0))) {
$this->markAcquire($key);
return true;
}
}
return false;
}
/**
@ -57,12 +64,24 @@ class SemaphoreLockDriver extends AbstractLockDriver
*/
public function releaseLock($key)
{
if (empty($this->acquiredLocks[$key])) {
if (empty(self::$semaphore[$key])) {
return false;
} else {
$success = @sem_release($this->acquiredLocks[$key]);
unset($this->acquiredLocks[$key]);
$success = @sem_release(self::$semaphore[$key]);
unset(self::$semaphore[$key]);
$this->markRelease($key);
return $success;
}
}
/**
* @brief Checks, if a key is currently locked to a process
*
* @param string $key The name of the lock
* @return bool
*/
public function isLocked($key)
{
return @sem_get(self::$semaphore[$key]) !== false;
}
}