Skip to content

Commit

Permalink
Merge pull request #2 from roadrunner-php/wip
Browse files Browse the repository at this point in the history
Beta
  • Loading branch information
butschster authored Sep 15, 2023
2 parents 2dd3cb4 + e5885a3 commit 6b11b36
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 32 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "roadrunner-php/symfony-lock-driver",
"type": "library",
"description": "RoadRunner: HTTP and PSR-7 worker",
"description": "RoadRunner: symfony/lock bridge",
"license": "MIT",
"authors": [
{
Expand Down
62 changes: 46 additions & 16 deletions src/RoadRunnerStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,74 @@

namespace Spiral\RoadRunner\Symfony\Lock;

use RoadRunner\Lock\LockInterface as RrLockInterface;
use RoadRunner\Lock as RR;
use Spiral\Goridge\RPC\Exception\RPCException;
use Symfony\Component\Lock\Exception\LockAcquiringException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockReleasingException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\PersistingStoreInterface;
use Symfony\Component\Lock\SharedLockStoreInterface;

final class RoadRunnerStore implements SharedLockStoreInterface
{
private string $processId;

/**
* @param RR\LockInterface $rrLock
* @param float $initialTtl The time-to-live of the lock, in seconds. Defaults to 0 (forever).
* @param float $initialWaitTtl How long to wait to acquire lock until returning false.
*/
public function __construct(
private readonly RrLockInterface $rrLock,
?string $processId = null
private readonly RR\LockInterface $rrLock,
private readonly float $initialTtl = 300.0,
private readonly float $initialWaitTtl = 0,
) {
$this->processId = $processId ?? '*';
\assert($this->initialTtl >= 0);
\assert($this->initialWaitTtl >= 0);
}

public function save(Key $key): void
{
$this->rrLock->lock((string) $key, $this->processId);
\assert(false === $key->hasState(__CLASS__));
try {
$lockId = $this->rrLock->lock((string) $key, null, $this->initialTtl, $this->initialWaitTtl);
if (false === $lockId) {
throw new LockConflictedException('RoadRunner. Failed to make lock');
}
$key->setState(__CLASS__, $lockId);
} catch (RPCException $e) {
throw new LockAcquiringException(message: 'RoadRunner. RPC call error', previous: $e);
}
}

public function exists(Key $key): bool
public function saveRead(Key $key): void
{
return $this->rrLock->exists((string) $key, $this->processId);
\assert(false === $key->hasState(__CLASS__));
$lockId = $this->rrLock->lockRead((string)$key, null, $this->initialTtl, $this->initialWaitTtl);
if (false === $lockId) {
throw new LockConflictedException('RoadRunner. Failed to make read lock');
}
$key->setState(__CLASS__, $lockId);
}

public function putOffExpiration(Key $key, float $ttl): void
public function exists(Key $key): bool
{
$this->rrLock->updateTTL((string) $key, $this->processId, $ttl);
\assert($key->hasState(__CLASS__));
return $this->rrLock->exists((string) $key, $key->getState(__CLASS__));
}

public function delete(Key $key): void
public function putOffExpiration(Key $key, float $ttl): void
{
$this->rrLock->release((string) $key, $this->processId);
\assert($key->hasState(__CLASS__));
\assert($ttl > 0);
if (false === $this->rrLock->updateTTL((string) $key, $key->getState(__CLASS__), $ttl)) {
throw new LockConflictedException('RoadRunner. Failed to update lock ttl');
}
}

public function saveRead(Key $key): void
public function delete(Key $key): void
{
$this->rrLock->lockRead((string) $key, $this->processId);
\assert($key->hasState(__CLASS__));
if (false === $this->rrLock->release((string) $key, $key->getState(__CLASS__))) {
throw new LockReleasingException('RoadRunner. Failed to release lock');
}
}
}
55 changes: 55 additions & 0 deletions tests/IntegrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Spiral\RoadRunner\Symfony\Lock\Tests;

use PHPUnit\Framework\TestCase;
use RoadRunner\Lock as RR;
use Spiral\RoadRunner\Symfony\Lock\RoadRunnerStore;
use Symfony\Component\Lock\LockFactory;

final class IntegrationTest extends TestCase
{
public function testLock(): void
{
$responseList = [
'test-lock' => [
'uuid1',
false,
'uuid2'
],
];
$rrLock = $this->createMock(RR\LockInterface::class);
$rrLock
->expects(self::exactly(3))
->method('lock')
->willReturnCallback(function (string $name) use (&$responseList) {
$array_shift = \array_shift($responseList[$name]);
return $array_shift;
});
$rrLock
->expects(self::exactly(2))
->method('updateTTL')
->willReturn(true);

$rrLock
->expects(self::exactly(2))
->method('release')
->willReturn(true);

// lock
$factory = new LockFactory(new RoadRunnerStore($rrLock));
$lock1 = $factory->createLock('test-lock');
self::assertTrue($lock1->acquire());

$lock2 = $factory->createLock('test-lock');
self::assertFalse($lock2->acquire());

$lock1->release();

// lock 2
self::assertTrue($lock2->acquire());
$lock2->release();
}
}
120 changes: 105 additions & 15 deletions tests/RoadRunnerStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,57 +7,147 @@
use PHPUnit\Framework\TestCase;
use RoadRunner\Lock\LockInterface as RrLock;
use Spiral\RoadRunner\Symfony\Lock\RoadRunnerStore;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockReleasingException;
use Symfony\Component\Lock\Key;

final class RoadRunnerStoreTest extends TestCase
{
public function testSave(): void
public function testSaveSuccess(): void
{
$rrLock = $this->createMock(RrLock::class);
$rrLock->expects(self::once())
->method('lock')
->with('resource-name', '*');
->with('resource-name', null)
->willReturn('lock-id');
$store = new RoadRunnerStore($rrLock);
$store->save(new Key('resource-name'));
$key = new Key('resource-name');
$store->save($key);
self::assertTrue($key->hasState(RoadRunnerStore::class));
self::assertSame('lock-id', $key->getState(RoadRunnerStore::class));
}

public function testExists(): void
public function testSaveReadSuccess(): void
{
$rrLock = $this->createMock(RrLock::class);
$rrLock->expects(self::once())
->method('lockRead')
->with('resource-name', null)
->willReturn('lock-id');
$store = new RoadRunnerStore($rrLock);
$key = new Key('resource-name');
$store->saveRead($key);
}

public function testExistsSuccess(): void
{
$rrLock = $this->createMock(RrLock::class);
$rrLock->expects(self::once())
->method('exists')
->with('resource-name', '*');
->with('resource-name')
->willReturn(true);
$store = new RoadRunnerStore($rrLock);
$store->exists(new Key('resource-name'));
$key = new Key('resource-name');
$key->setState(RoadRunnerStore::class, 'lock-id');
$store->exists($key);
}

public function testPutOffExpiration(): void
public function testPutOffExpirationSuccess(): void
{
$rrLock = $this->createMock(RrLock::class);
$rrLock->expects(self::once())
->method('updateTTL')
->with('resource-name', '*', 3600.0);
->with('resource-name', 'lock-id', 3600.0)
->willReturn(true);
$store = new RoadRunnerStore($rrLock);
$store->putOffExpiration(new Key('resource-name'), 3600.0);
$key = new Key('resource-name');
$key->setState(RoadRunnerStore::class, 'lock-id');
$store->putOffExpiration($key, 3600.0);
}

public function testDelete(): void
public function testDeleteSuccess(): void
{
$rrLock = $this->createMock(RrLock::class);
$rrLock->expects(self::once())
->method('release')
->with('resource-name', '*');
->with('resource-name')
->willReturn(true);
$store = new RoadRunnerStore($rrLock);
$store->delete(new Key('resource-name'));
$key = new Key('resource-name');
$key->setState(RoadRunnerStore::class, 'lock-id');
$store->delete($key);
}

public function testSaveRead(): void
public function testSaveFail(): void
{
self::expectException(LockConflictedException::class);
self::expectExceptionMessage('RoadRunner. Failed to make lock');

$rrLock = $this->createMock(RrLock::class);
$rrLock->expects(self::once())
->method('lock')
->with('resource-name', null)
->willReturn(false);
$store = new RoadRunnerStore($rrLock);
$store->save(new Key('resource-name'));
}

public function testSaveReadFail(): void
{
self::expectException(LockConflictedException::class);
self::expectExceptionMessage('RoadRunner. Failed to make read lock');

$rrLock = $this->createMock(RrLock::class);
$rrLock->expects(self::once())
->method('lockRead')
->with('resource-name', '*');
->with('resource-name', null);
$store = new RoadRunnerStore($rrLock);
$key = new Key('resource-name');
$store->saveRead($key);
}

public function testExistsFail(): void
{
$rrLock = $this->createMock(RrLock::class);
$rrLock->expects(self::once())
->method('exists')
->with('resource-name')
->willReturn(false);
$store = new RoadRunnerStore($rrLock);
$key = new Key('resource-name');
$key->setState(RoadRunnerStore::class, 'lock-id');
self::assertFalse($store->exists($key));
}

public function testPutOffExpirationFail(): void
{
self::expectException(LockConflictedException::class);
self::expectExceptionMessage('RoadRunner. Failed to update lock ttl');

$rrLock = $this->createMock(RrLock::class);
$rrLock->expects(self::once())
->method('updateTTL')
->with('resource-name', 'lock-id', 3600.0)
->willReturn(false);
$store = new RoadRunnerStore($rrLock);
$key = new Key('resource-name');
$key->setState(RoadRunnerStore::class, 'lock-id');
$store->putOffExpiration($key, 3600.0);
}

public function testDeleteFail(): void
{
self::expectException(LockReleasingException::class);
self::expectExceptionMessage('RoadRunner. Failed to release lock');

$rrLock = $this->createMock(RrLock::class);
$rrLock->expects(self::once())
->method('release')
->with('resource-name')
->willReturn(false);
$store = new RoadRunnerStore($rrLock);
$store->saveRead(new Key('resource-name'));
$key = new Key('resource-name');
$key->setState(RoadRunnerStore::class, 'lock-id');
$store->delete($key);
}
}

0 comments on commit 6b11b36

Please sign in to comment.