Skip to content

Commit

Permalink
Add mutex abstract class (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
devanych committed Aug 12, 2021
1 parent d021942 commit 22e762a
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 34 deletions.
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -88,8 +88,8 @@ There are some mutex drivers available as separate packages:
- [Redis](https://github.com/yiisoft/mutex-db-redis)

If you want to provide your own driver, you need to implement `MutexFactoryInterface` and `MutexInterface`.
There is ready to extend `MutexFactory` and a `RetryAcquireTrait` that contains `retryAcquire()` method implementing
the "wait for a lock for a certain time" functionality.
There is ready to extend `Mutex`, `MutexFactory` and a `RetryAcquireTrait` that contains `retryAcquire()`
method implementing the "wait for a lock for a certain time" functionality.

When implementing your own drivers, you need to take care of automatic unlocking. For example using a destructor:

Expand Down
91 changes: 91 additions & 0 deletions src/Mutex.php
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Mutex;

use RuntimeException;

use function md5;

/**
* Provides basic functionality for creating drivers {@see MutexFactoryInterface}.
*/
abstract class Mutex implements MutexInterface
{
use RetryAcquireTrait;

private string $lockName;
private string $mutexName;

/**
* @var array<string, true>
*/
private static array $currentProcessLocks = [];

public function __construct(string $driverName, string $mutexName)
{
$this->lockName = md5($driverName . $mutexName);
$this->mutexName = $mutexName;
}

public function __destruct()
{
$this->release();
}

final public function acquire(int $timeout = 0): bool
{
return $this->retryAcquire($timeout, function () use ($timeout): bool {
if (!$this->isCurrentProcessLocked() && $this->acquireLock($timeout)) {
return self::$currentProcessLocks[$this->lockName] = true;
}

return false;
});
}

final public function release(): void
{
if (!$this->isCurrentProcessLocked()) {
return;
}

if (!$this->releaseLock()) {
throw new RuntimeException("Unable to release lock \"$this->mutexName\".");
}

unset(self::$currentProcessLocks[$this->lockName]);
}

/**
* Acquires lock.
*
* This method should be extended by a concrete Mutex implementations.
*
* @param int $timeout Time (in seconds) to wait for lock to be released. Defaults to zero meaning that method
* will return false immediately in case lock was already acquired.
*
* @return bool The acquiring result.
*/
abstract protected function acquireLock(int $timeout = 0): bool;

/**
* Releases lock.
*
* This method should be extended by a concrete Mutex implementations.
*
* @return bool The release result.
*/
abstract protected function releaseLock(): bool;

/**
* Checks whether a lock has been set in the current process.
*
* @return bool Whether a lock has been set in the current process.
*/
private function isCurrentProcessLocked(): bool
{
return isset(self::$currentProcessLocks[$this->lockName]);
}
}
1 change: 1 addition & 0 deletions src/RetryAcquireTrait.php
Expand Up @@ -32,6 +32,7 @@ public function withRetryDelay(int $retryDelay): self
private function retryAcquire(int $timeout, callable $callback): bool
{
$start = microtime(true);

do {
if ($callback()) {
return true;
Expand Down
54 changes: 22 additions & 32 deletions tests/Mocks/Mutex.php
Expand Up @@ -4,9 +4,6 @@

namespace Yiisoft\Mutex\Tests\Mocks;

use Yiisoft\Mutex\MutexInterface;
use Yiisoft\Mutex\RetryAcquireTrait;

use function clearstatcache;
use function fclose;
use function fileinode;
Expand All @@ -16,10 +13,8 @@
use function sys_get_temp_dir;
use function unlink;

final class Mutex implements MutexInterface
final class Mutex extends \Yiisoft\Mutex\Mutex
{
use RetryAcquireTrait;

private string $file;

/**
Expand All @@ -30,48 +25,42 @@ final class Mutex implements MutexInterface
public function __construct(string $name)
{
$this->file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . md5($name) . '.lock';
}

public function __destruct()
{
$this->release();
parent::__construct(self::class, $name);
}

public function getFile(): string
{
return $this->file;
}

public function acquire(int $timeout = 0): bool
public function acquireLock(int $timeout = 0): bool
{
return $this->retryAcquire($timeout, function (): bool {
$resource = fopen($this->file, 'wb+');
$resource = fopen($this->file, 'wb+');

if ($resource === false) {
return false;
}
if ($resource === false) {
return false;
}

if (!flock($resource, LOCK_EX | LOCK_NB)) {
fclose($resource);
return false;
}
if (!flock($resource, LOCK_EX | LOCK_NB)) {
fclose($resource);
return false;
}

if (DIRECTORY_SEPARATOR !== '\\' && fstat($resource)['ino'] !== fileinode($this->file)) {
clearstatcache(true, $this->file);
flock($resource, LOCK_UN);
fclose($resource);
return false;
}
if (DIRECTORY_SEPARATOR !== '\\' && fstat($resource)['ino'] !== fileinode($this->file)) {
clearstatcache(true, $this->file);
flock($resource, LOCK_UN);
fclose($resource);
return false;
}

$this->lockResource = $resource;
return true;
});
$this->lockResource = $resource;
return true;
}

public function release(): void
public function releaseLock(): bool
{
if ($this->lockResource === null) {
return;
return false;
}

if (DIRECTORY_SEPARATOR === '\\') {
Expand All @@ -85,5 +74,6 @@ public function release(): void
}

$this->lockResource = null;
return true;
}
}
18 changes: 18 additions & 0 deletions tests/MutexTest.php
Expand Up @@ -5,8 +5,13 @@
namespace Yiisoft\Mutex\Tests;

use PHPUnit\Framework\TestCase;
use ReflectionObject;
use RuntimeException;
use Yiisoft\Mutex\Tests\Mocks\Mutex;

use function md5;
use function microtime;

final class MutexTest extends TestCase
{
public function testMutexAcquire(): void
Expand Down Expand Up @@ -80,6 +85,19 @@ public function testDestruct(): void
$this->assertFileDoesNotExist($file);
}

public function testReleaseFailure(): void
{
$mutexName = 'testReleaseFailure';
$mutex = $this->createMutex($mutexName);
$reflection = (new ReflectionObject($mutex))->getParentClass();
$reflection->setStaticPropertyValue('currentProcessLocks', [md5(Mutex::class . $mutexName) => true]);

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage("Unable to release lock \"$mutexName\".");

$mutex->release();
}

private function createMutex(string $name): Mutex
{
return new Mutex($name);
Expand Down

0 comments on commit 22e762a

Please sign in to comment.