diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 871ae71..a05fcbd 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -12,14 +12,14 @@ jobs: strategy: matrix: - php-version: [ '8.1', '8.2', '8.3', '8.4' ] - etcd-version: [ '3.3.27', '3.4.18', '3.5.2' ] + php-version: [ '8.3', '8.4' ] + etcd-version: [ '3.4.36', '3.5.21' ] name: Run tests on PHP v${{ matrix.php-version }} with etcd v${{ matrix.etcd-version }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -47,7 +47,7 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Restore composer from cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} diff --git a/composer.json b/composer.json index 42a7fbf..cfd6f4d 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "require": { "aternos/etcd": "^1.5.0", "ext-json": "*", - "php": ">=8.1" + "php": ">=8.3" }, "require-dev": { "phpunit/phpunit": "^10.5" diff --git a/phpunit.xml b/phpunit.xml index e68aeb1..cc1b233 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,5 +1,6 @@ - + test/ @@ -11,4 +12,4 @@ src - \ No newline at end of file + diff --git a/src/EtcdLock.php b/src/EtcdLock.php deleted file mode 100644 index 17b735f..0000000 --- a/src/EtcdLock.php +++ /dev/null @@ -1,33 +0,0 @@ -lock($exclusive, $time, $wait, $identifier) - * It mainly exists for backwards compatibility reasons. - * - * @param string $key Can be anything, should describe the resource in a unique way - * @param bool $exclusive Should the lock be exclusive (true) or shared (false) - * @param int $time Time until the lock should be released automatically - * @param int $wait Time to wait for an existing lock to get released - * @param string|null $identifier An identifier (if different from Lock::$defaultIdentifier, see Lock::setDefaultIdentifier()) - * @throws InvalidResponseStatusCodeException - * @throws TooManySaveRetriesException - */ - public function __construct(string $key, bool $exclusive = false, int $time = 120, int $wait = 300, ?string $identifier = null) - { - parent::__construct($key); - $this->lock($exclusive, $time, $wait, $identifier); - } -} \ No newline at end of file diff --git a/src/Lock.php b/src/Lock.php index 4c2b6f2..af10155 100644 --- a/src/Lock.php +++ b/src/Lock.php @@ -14,7 +14,7 @@ * * @package Aternos\Lock */ -class Lock +class Lock implements LockInterface { /** * see Lock::setClient() @@ -171,36 +171,27 @@ public static function setDelayPerUnavailableRetry(int $delayPerRetry): void } /** - * Identifier of the current lock - * - * Probably the same as Lock::$defaultIdentifier if not overwritten in Lock::__construct() + * Get an Aternos\Etcd\Client instance * - * @var string|null + * @return ClientInterface */ - protected ?string $identifier = null; + protected static function getClient(): ClientInterface + { + if (static::$client === null) { + static::$client = new Client(); + } - /** - * Unique key for the resource - * - * @var string|null - */ - protected ?string $key = null; + return static::$client; + } /** - * Timeout time of the lock - * - * The lock will be released if this timeout is reached + * Identifier of the current lock * - * @var int|null - */ - protected ?int $time = null; - - /** - * Is this an exclusive lock (true) or shared (false) + * Probably the same as Lock::$defaultIdentifier if not overwritten in Lock::__construct() * - * @var bool + * @var string */ - protected bool $exclusive = false; + protected string $identifier; /** * Full name of the key in etcd (prefix + key) @@ -233,81 +224,78 @@ public static function setDelayPerUnavailableRetry(int $delayPerRetry): void */ protected int $retries = 0; - /** - * Automatically try to break the lock on destruct if possible - * - * @var bool - */ - protected bool $breakOnDestruct = true; - /** * Create a lock * * @param string $key Can be anything, should describe the resource in a unique way * @param string|null $identifier An identifier for this lock, falls back to the default identifier if null - */ - public function __construct(string $key, ?string $identifier = null) + * @param int $time Timeout time of the lock in seconds. The lock will be released if this timeout is reached. + * @param bool $exclusive Is this lock exclusive (true) or shared (false) + * @param int $waitTime Time in seconds to wait for existing locks to be released. + * @param int|null $refreshTime Duration in seconds the timeout should be set to when refreshing the lock. + * If null the initial timeout will be used. + * @param int $refreshThreshold Maximum duration in seconds the existing lock may be valid for to be refreshed. + * If the lock is valid for longer than this time, the lock will not be refreshed. + * @param bool $breakOnDestruct Automatically try to break the lock on destruct if possible + */ + public function __construct( + protected string $key, + ?string $identifier = null, + protected bool $exclusive = false, + protected int $time = 120, + protected int $waitTime = 300, + protected ?int $refreshTime = null, + protected int $refreshThreshold = 30, + protected bool $breakOnDestruct = true, + ) { - $this->key = $key; $this->etcdKey = static::$prefix . $this->key; if (static::$defaultIdentifier === null) { static::setDefaultIdentifier(); } - if ($identifier === null) { - $this->identifier = static::$defaultIdentifier; - } else { - $this->identifier = $identifier; - } + $this->identifier = $identifier ?? static::$defaultIdentifier; } /** * Try to acquire lock * - * @param bool $exclusive - * @param int $time - * @param int $wait - * @param string|null $identifier - * @return bool + * @return bool true if the lock was acquired, false if it was not * @throws InvalidResponseStatusCodeException * @throws TooManySaveRetriesException */ - public function lock(bool $exclusive = false, int $time = 120, int $wait = 300, ?string $identifier = null): bool + public function lock(): bool { - $this->exclusive = $exclusive; - $this->time = $time; - - if ($identifier !== null) { - $this->identifier = $identifier; - } - $this->retries = 0; do { - $this->waitForOtherLocks($wait, $exclusive); + $this->waitForOtherLocks(); $retry = false; if ($this->canLock()) { - $retry = !$this->addOrUpdateLock(); + $retry = !$this->addOrUpdateLock($this->time); } } while ($retry); - return !!$this->isLocked(); + return $this->isLocked(); } /** - * @param int $wait - * @param bool $exclusive + * Wait for other locks to be released. This method will only wait if the timeout of the existing lock ends within + * the specified wait time. If the timeout of the existing lock ends after the wait time, this method will return + * immediately, even though the lock holder might voluntarily break the lock before the timeout. + * + * @param int|null $waitTime maximum time in seconds to wait for other locks * @return bool * @throws InvalidResponseStatusCodeException */ - public function waitForOtherLocks(int $wait = 300, bool $exclusive = false): bool + public function waitForOtherLocks(?int $waitTime = null): bool { + $waitTime ??= $this->waitTime; $startTime = time(); - $this->exclusive = $exclusive; $this->update(); - while (!$this->canLock() && $startTime + $wait > time()) { + while (!$this->canLock() && $startTime + $waitTime > time()) { sleep(static::$waitRetryInterval); $this->update(); } @@ -318,33 +306,53 @@ public function waitForOtherLocks(int $wait = 300, bool $exclusive = false): boo /** * Check if is locked and returns time until lock runs out or false * - * @return bool|int + * @return bool + */ + public function isLocked(): bool + { + return $this->getRemainingLockDuration() > 0; + } + + /** + * Get the time until the lock runs out. This method will return -1 if the lock is not valid or other negative values + * if the lock has already run out. + * + * @return int */ - public function isLocked(): bool|int + public function getRemainingLockDuration(): int { foreach ($this->locks as $lock) { if ($lock->isBy($this->identifier)) { - $remaining = $lock->getRemainingTime(); - return ($remaining > 0) ? $remaining : false; + return $lock->getRemainingTime(); } } - return false; + return -1; + } + + /** + * Get the unique key for the resource + * @return string + */ + public function getKey(): string + { + return $this->key; } /** * Set the identifier for this lock, falls back to the default identifier if null * * @param string|null $identifier - * @return void + * @return $this */ - public function setIdentifier(?string $identifier): void + public function setIdentifier(?string $identifier): static { if ($identifier === null) { $this->identifier = static::$defaultIdentifier; } else { $this->identifier = $identifier; } + return $this; } /** @@ -361,29 +369,132 @@ public function getIdentifier(): ?string * Dis/enable automatic lock break on object destruct * * @param bool $breakOnDestruct + * @return $this */ - public function setBreakOnDestruct(bool $breakOnDestruct): void + public function setBreakOnDestruct(bool $breakOnDestruct): static { $this->breakOnDestruct = $breakOnDestruct; + return $this; + } + + /** + * Set the timeout time of the lock. The lock will be released if this timeout is reached. + * @param int $time time in seconds + * @return $this + */ + public function setTime(int $time): static + { + $this->time = $time; + return $this; + } + + /** + * Get the timeout time of the lock in seconds. The lock will be released if this timeout is reached. + * @return int + */ + public function getTime(): int + { + return $this->time; + } + + /** + * Is this lock exclusive (true) or shared (false) + * @return bool + */ + public function isExclusive(): bool + { + return $this->exclusive; + } + + /** + * Make this lock exclusive (true) or shared (false) + * @param bool $exclusive + * @return $this + */ + public function setExclusive(bool $exclusive): static + { + $this->exclusive = $exclusive; + return $this; + } + + /** + * Get the wait time in seconds to wait for existing locks to be released. + * @return int + */ + public function getWaitTime(): int + { + return $this->waitTime; + } + + /** + * Set the wait time in seconds to wait for existing locks to be released. + * @param int $waitTime + * @return $this + */ + public function setWaitTime(int $waitTime): static + { + $this->waitTime = $waitTime; + return $this; + } + + /** + * Duration in seconds the timeout should be set to when refreshing the lock. + * If null the initial timeout will be used. + * @return int|null + */ + public function getRefreshTime(): ?int + { + return $this->refreshTime; + } + + /** + * Duration in seconds the timeout should be set to when refreshing the lock. + * If null the initial timeout will be used. + * @param int|null $refreshTime + * @return $this + */ + public function setRefreshTime(?int $refreshTime): static + { + $this->refreshTime = $refreshTime; + return $this; + } + + /** + * Maximum duration in seconds the existing lock may be valid for to be refreshed. If the lock is valid for longer + * than this time, the lock will not be refreshed. + * @return int + */ + public function getRefreshThreshold(): int + { + return $this->refreshThreshold; + } + + /** + * Maximum duration in seconds the existing lock may be valid for to be refreshed. If the lock is valid for longer + * than this time, the lock will not be refreshed. + * @param int $refreshThreshold + * @return $this + */ + public function setRefreshThreshold(int $refreshThreshold): static + { + $this->refreshThreshold = $refreshThreshold; + return $this; } /** * Refresh the lock * - * @param int $time Time until the lock should be released automatically - * @param int $remainingThreshold The lock will only be refreshed if the remaining time is below this threshold (0 to disable) * @return boolean * @throws InvalidResponseStatusCodeException * @throws TooManySaveRetriesException */ - public function refresh(int $time = 60, int $remainingThreshold = 30): bool + public function refresh(): bool { - if ($remainingThreshold > 0 && $this->isLocked() > $remainingThreshold) { + if ($this->refreshThreshold > 0 && $this->getRemainingLockDuration() > $this->refreshThreshold) { return true; } $this->update(); - $this->time = $time; $this->retries = 0; do { @@ -391,7 +502,7 @@ public function refresh(int $time = 60, int $remainingThreshold = 30): bool return false; } - $retry = !$this->addOrUpdateLock(); + $retry = !$this->addOrUpdateLock($this->refreshTime ?? $this->time); } while ($retry); return true; } @@ -401,17 +512,19 @@ public function refresh(int $time = 60, int $remainingThreshold = 30): bool * * Should be only used if you have the lock * - * @return boolean + * @return void * @throws InvalidResponseStatusCodeException * @throws TooManySaveRetriesException */ - public function break(): bool + public function break(): void { + if (!$this->isLocked()) { + return; + } + $this->update(); $this->retries = 0; $this->removeLock(); - - return true; } /** @@ -427,11 +540,11 @@ protected function generateLock(): LockEntry /** * Remove a lock from the locking array and save the locks * - * @return bool + * @return void * @throws InvalidResponseStatusCodeException * @throws TooManySaveRetriesException */ - protected function removeLock(): bool + protected function removeLock(): void { do { foreach ($this->locks as $i => $lock) { @@ -441,7 +554,6 @@ protected function removeLock(): bool } $success = $this->saveLocks(); } while ($success === false); - return true; } /** @@ -449,15 +561,16 @@ protected function removeLock(): bool * * A 'false' return value can/should be retried, see Lock::saveLocks() * + * @param int $time * @return bool * @throws InvalidResponseStatusCodeException * @throws TooManySaveRetriesException */ - protected function addOrUpdateLock(): bool + protected function addOrUpdateLock(int $time): bool { foreach ($this->locks as $lock) { if ($lock->isBy($this->identifier)) { - $lock->setRemaining($this->time); + $lock->setRemaining($time); return $this->saveLocks(); } } @@ -544,20 +657,6 @@ protected function saveLocks(): bool } } - /** - * Get an Aternos\Etcd\Client instance - * - * @return ClientInterface - */ - protected function getClient(): ClientInterface - { - if (static::$client === null) { - static::$client = new Client(); - } - - return static::$client; - } - /** * Check if it is possible to lock * @@ -582,8 +681,9 @@ protected function canLock(): bool * Update the locks array from etcd * * @throws InvalidResponseStatusCodeException + * @return $this */ - public function update(): void + public function update(): static { $etcdLockString = false; for ($i = 1; $i <= static::$maxUnavailableRetries; $i++) { @@ -600,15 +700,16 @@ public function update(): void } } - $this->updateFromString($etcdLockString); + return $this->updateFromString($etcdLockString); } /** * Update the locks array from a JSON string * * @param string|bool $lockString + * @return $this */ - protected function updateFromString(string|bool $lockString): void + protected function updateFromString(string|bool $lockString): static { $this->previousLockString = $lockString; @@ -617,6 +718,8 @@ protected function updateFromString(string|bool $lockString): void } else { $this->locks = []; } + + return $this; } /** @@ -627,7 +730,7 @@ protected function updateFromString(string|bool $lockString): void */ public function __destruct() { - if ($this->breakOnDestruct && $this->isLocked()) { + if ($this->breakOnDestruct) { $this->break(); } } @@ -671,4 +774,4 @@ public function isLockedByOtherExclusively(): bool } return false; } -} \ No newline at end of file +} diff --git a/src/LockInterface.php b/src/LockInterface.php index 9beaba2..6cff504 100644 --- a/src/LockInterface.php +++ b/src/LockInterface.php @@ -13,35 +13,45 @@ interface LockInterface * LockInterface constructor. * * @param string $key - * @param bool $exclusive - * @param int $time - * @param int $wait * @param string|null $identifier */ - public function __construct(string $key, bool $exclusive = false, int $time = 60, int $wait = 300, ?string $identifier = null); + public function __construct(string $key, ?string $identifier = null); /** - * Check if is locked and returns time until lock runs out or false + * Try to acquire lock * - * @return bool|int + * @return bool true if lock was acquired, false otherwise */ - public function isLocked(): bool|int; + public function lock(): bool; + + /** + * Check if is locked + * + * @return bool + */ + public function isLocked(): bool; + + /** + * Get the time until the lock runs out. This method will return -1 if the lock is not valid or other negative values + * if the lock has already run out. + * + * @return int + */ + public function getRemainingLockDuration(): int; /** * Refresh the lock * - * @param int $time - * @param int $remainingThreshold * @return bool */ - public function refresh(int $time = 60, int $remainingThreshold = 30): bool; + public function refresh(): bool; /** * Break the lock * * Should be only used if you have the lock * - * @return bool + * @return void */ - public function break(): bool; -} \ No newline at end of file + public function break(): void; +} diff --git a/test/LockTest.php b/test/LockTest.php index 2dd2eab..48ebf63 100644 --- a/test/LockTest.php +++ b/test/LockTest.php @@ -3,10 +3,10 @@ namespace Aternos\Lock\Test; use Aternos\Etcd\Exception\Status\InvalidResponseStatusCodeException; -use Aternos\Lock\EtcdLock; use Aternos\Lock\Lock; use Aternos\Lock\TooManySaveRetriesException; use PHPUnit\Framework\TestCase; +use ReflectionProperty; class LockTest extends TestCase { @@ -24,8 +24,8 @@ public function testCreateLock(): void $key = $this->getRandomString(); $identifier = $this->getRandomString(); - $this->assertTrue(($lock = new Lock($key))->lock(false, 10, 0, $identifier)); - $this->assertTrue($lock->isLocked() >= 8); + $this->assertTrue(($lock = new Lock($key, $identifier, false, 10, 0))->lock()); + $this->assertGreaterThanOrEqual(8, $lock->getRemainingLockDuration()); $lock->break(); } @@ -49,19 +49,10 @@ public function testSetIdentifier(): void $this->assertEquals("identifier", $lock->getIdentifier()); } - /** - * @throws InvalidResponseStatusCodeException - * @throws TooManySaveRetriesException - */ - public function testCreateEtcdLock(): void + public function testGetkey(): void { - $key = $this->getRandomString(); - $identifier = $this->getRandomString(); - - $lock = new EtcdLock($key, false, 10, 0, $identifier); - $this->assertTrue($lock->isLocked() >= 8); - - $lock->break(); + $lock = new Lock("key"); + $this->assertEquals("key", $lock->getKey()); } /** @@ -73,12 +64,35 @@ public function testBreakLock(): void $key = $this->getRandomString(); $identifier = $this->getRandomString(); - $lock = new EtcdLock($key, false, 10, 0, $identifier); - $this->assertTrue($lock->isLocked() > 0); + $lock = new Lock($key, $identifier, false, 10, 0); + $lock->lock(); + $this->assertTrue($lock->isLocked()); $lock->break(); $this->assertFalse($lock->isLocked()); } + public function testBreakLockTwice(): void + { + # Check that break() and isLocked() are called when breakOnDestruct is set to true + $breakingLock = $this->getMockBuilder(Lock::class) + ->setConstructorArgs([$this->getRandomString()]) + ->onlyMethods(['removeLock', 'isLocked']) + ->getMock(); + $breakingLock->setBreakOnDestruct(true); + $breakingLock + ->expects($this->once()) + ->method('removeLock'); + $breakingLock + ->expects($this->exactly(3)) + ->method('isLocked') + ->willReturn(true, true, false); + + $breakingLock->lock(); + + $breakingLock->break(); + $breakingLock->break(); + } + /** * @throws InvalidResponseStatusCodeException * @throws TooManySaveRetriesException @@ -88,8 +102,9 @@ public function testAutoReleaseLock(): void $key = $this->getRandomString(); $identifier = $this->getRandomString(); - $lock = new EtcdLock($key, false, 3, 0, $identifier); - $this->assertTrue($lock->isLocked() > 0); + $lock = new Lock($key, $identifier, false, 3, 0); + $lock->lock(); + $this->assertTrue($lock->isLocked()); sleep(3); $this->assertFalse($lock->isLocked()); @@ -105,13 +120,14 @@ public function testRefreshLock(): void $key = $this->getRandomString(); $identifier = $this->getRandomString(); - $lock = new EtcdLock($key, false, 3, 0, $identifier); - $this->assertTrue($lock->isLocked() > 0); + $lock = new Lock($key, $identifier, false, 3, 0, 5); + $lock->lock(); + $this->assertTrue($lock->isLocked()); sleep(1); - $lock->refresh(5); - $this->assertTrue($lock->isLocked() > 3); + $lock->refresh(); + $this->assertGreaterThan(3, $lock->getRemainingLockDuration()); sleep(2); - $this->assertTrue($lock->isLocked() > 0); + $this->assertTrue($lock->isLocked()); $lock->break(); $this->assertFalse($lock->isLocked()); } @@ -125,15 +141,16 @@ public function testRefreshLockThreshold(): void $key = $this->getRandomString(); $identifier = $this->getRandomString(); - $lock = new EtcdLock($key, false, 10, 0, $identifier); - $this->assertTrue($lock->isLocked() > 0); + $lock = new Lock($key, $identifier, false, 10, 0, refreshThreshold: 5); + $lock->lock(); + $this->assertTrue($lock->isLocked()); sleep(3); - $lock->refresh(10, 5); - $this->assertTrue($lock->isLocked() > 3); - $this->assertTrue($lock->isLocked() < 8); + $lock->refresh(); + $this->assertGreaterThan(3, $lock->getRemainingLockDuration()); + $this->assertLessThan(8, $lock->getRemainingLockDuration()); sleep(3); - $lock->refresh(10, 5); - $this->assertTrue($lock->isLocked() > 8); + $lock->refresh(); + $this->assertGreaterThan(8, $lock->getRemainingLockDuration()); $lock->break(); $this->assertFalse($lock->isLocked()); } @@ -149,20 +166,24 @@ public function testMultipleSharedLocks(): void $identifierB = $this->getRandomString(); $identifierC = $this->getRandomString(); - $lockA = new EtcdLock($key, false, 3, 0, $identifierA); - $lockB = new EtcdLock($key, false, 3, 0, $identifierB); - $lockC = new EtcdLock($key, false, 3, 0, $identifierC); + $lockA = new Lock($key, $identifierA, false, 3, 0, refreshTime: 5); + $lockB = new Lock($key, $identifierB, false, 3, 0); + $lockC = new Lock($key, $identifierC, false, 3, 0); + + $lockA->lock(); + $lockB->lock(); + $lockC->lock(); - $this->assertTrue($lockA->isLocked() > 0); - $this->assertTrue($lockB->isLocked() > 0); - $this->assertTrue($lockC->isLocked() > 0); + $this->assertTrue($lockA->isLocked()); + $this->assertTrue($lockB->isLocked()); + $this->assertTrue($lockC->isLocked()); sleep(1); - $lockA->refresh(5); - $this->assertTrue($lockA->isLocked() > 3); + $lockA->refresh(); + $this->assertGreaterThan(3, $lockA->getRemainingLockDuration()); sleep(2); - $this->assertTrue($lockA->isLocked() > 0); + $this->assertTrue($lockA->isLocked()); $this->assertFalse($lockB->isLocked()); $this->assertFalse($lockC->isLocked()); @@ -182,8 +203,9 @@ public function testExclusiveLock(): void $key = $this->getRandomString(); $identifier = $this->getRandomString(); - $lock = new EtcdLock($key, true, 10, 0, $identifier); - $this->assertTrue($lock->isLocked() > 0); + $lock = new Lock($key, $identifier, true, 10, 0); + $lock->lock(); + $this->assertTrue($lock->isLocked()); $lock->break(); $this->assertFalse($lock->isLocked()); @@ -200,10 +222,13 @@ public function testWaitForExclusiveLockAfterExclusiveLock(): void $identifierA = $this->getRandomString(); $identifierB = $this->getRandomString(); - $lockA = new EtcdLock($key, true, 3, 0, $identifierA); - $this->assertTrue($lockA->isLocked() > 0); - $lockB = new EtcdLock($key, true, 3, 5, $identifierB); - $this->assertTrue($lockB->isLocked() > 0); + $lockA = new Lock($key, $identifierA, true, 3, 0); + $lockA->lock(); + $this->assertTrue($lockA->isLocked()); + + $lockB = new Lock($key, $identifierB, true, 3, 5); + $lockB->lock(); + $this->assertTrue($lockB->isLocked()); $lockA->break(); $lockB->break(); @@ -219,9 +244,11 @@ public function testRejectSharedLockWhileExclusiveLock(): void $identifierA = $this->getRandomString(); $identifierB = $this->getRandomString(); - $lockA = new EtcdLock($key, true, 3, 0, $identifierA); - $this->assertTrue($lockA->isLocked() > 0); - $lockB = new EtcdLock($key, false, 3, 0, $identifierB); + $lockA = new Lock($key, $identifierA, true, 3, 0); + $lockA->lock(); + $this->assertTrue($lockA->isLocked()); + $lockB = new Lock($key, $identifierB, false, 3, 0); + $lockB->lock(); $this->assertFalse($lockB->isLocked()); $lockA->break(); @@ -238,9 +265,11 @@ public function testRejectExclusiveLockWhileSharedLock(): void $identifierA = $this->getRandomString(); $identifierB = $this->getRandomString(); - $lockA = new EtcdLock($key, false, 3, 0, $identifierA); - $this->assertTrue($lockA->isLocked() > 0); - $lockB = new EtcdLock($key, true, 3, 0, $identifierB); + $lockA = new Lock($key, $identifierA, false, 3, 0); + $lockA->lock(); + $this->assertTrue($lockA->isLocked()); + $lockB = new Lock($key, $identifierB, true, 3, 0); + $lockB->lock(); $this->assertFalse($lockB->isLocked()); $lockA->break(); @@ -259,17 +288,22 @@ public function testWaitForExclusiveLockAfterMultipleSharedLocks(): void $identifierC = $this->getRandomString(); $identifierD = $this->getRandomString(); - $lockA = new EtcdLock($key, false, 3, 0, $identifierA); - $lockB = new EtcdLock($key, false, 5, 0, $identifierB); - $lockC = new EtcdLock($key, false, 8, 0, $identifierC); + $lockA = new Lock($key, $identifierA, false, 3, 0); + $lockB = new Lock($key, $identifierB, false, 5, 0); + $lockC = new Lock($key, $identifierC, false, 8, 0); + + $lockA->lock(); + $lockB->lock(); + $lockC->lock(); - $this->assertTrue($lockA->isLocked() > 0); - $this->assertTrue($lockB->isLocked() > 0); - $this->assertTrue($lockC->isLocked() > 0); + $this->assertTrue($lockA->isLocked()); + $this->assertTrue($lockB->isLocked()); + $this->assertTrue($lockC->isLocked()); $time = time(); - $lockD = new EtcdLock($key, true, 3, 10, $identifierD); - $this->assertTrue(time() - $time >= 7); + $lockD = new Lock($key, $identifierD, true, 3, 10); + $lockD->lock(); + $this->assertGreaterThan(7, time() - $time); $lockA->break(); $lockB->break(); @@ -287,24 +321,26 @@ public function testLockWriteConflict(): void $identifierA = $this->getRandomString(); $identifierB = $this->getRandomString(); - $lockA = new PublicLock($key, false, 5, 0, $identifierA); - $lockB = new PublicLock($key, false, 5, 0, $identifierB); + $lockA = new PublicLock($key, $identifierA, false, 5, 0); + $lockA->lock(); + $lockB = new PublicLock($key, $identifierB, false, 5, 0); + $lockB->lock(); - $this->assertTrue($lockA->isLocked() > 0); - $this->assertTrue($lockB->isLocked() > 0); + $this->assertTrue($lockA->isLocked()); + $this->assertTrue($lockB->isLocked()); - $lockA->addOrUpdateLock(); + $lockA->addOrUpdateLock($lockA->getTime()); - $this->assertTrue($lockA->isLocked() > 0); - $this->assertTrue($lockB->isLocked() > 0); + $this->assertTrue($lockA->isLocked()); + $this->assertTrue($lockB->isLocked()); $lockB->update(); - $this->assertTrue($lockB->isLocked() > 0); + $this->assertTrue($lockB->isLocked()); $lockA->update(); - $this->assertTrue($lockA->isLocked() > 0); + $this->assertTrue($lockA->isLocked()); $lockA->break(); $lockB->break(); @@ -320,21 +356,23 @@ public function testLockDeleteConflict(): void $identifierA = $this->getRandomString(); $identifierB = $this->getRandomString(); - $lockA = new PublicLock($key, false, 5, 0, $identifierA); - $lockB = new PublicLock($key, false, 5, 0, $identifierB); + $lockA = new PublicLock($key, $identifierA, false, 5, 0); + $lockA->lock(); + $lockB = new PublicLock($key, $identifierB, false, 5, 0); + $lockB->lock(); - $this->assertTrue($lockA->isLocked() > 0); - $this->assertTrue($lockB->isLocked() > 0); + $this->assertTrue($lockA->isLocked()); + $this->assertTrue($lockB->isLocked()); $lockA->removeLock(); $this->assertFalse($lockA->isLocked()); $lockA->update(); $this->assertFalse($lockA->isLocked()); - $this->assertTrue($lockB->isLocked() > 0); + $this->assertTrue($lockB->isLocked()); $lockB->update(); - $this->assertTrue($lockB->isLocked() > 0); + $this->assertTrue($lockB->isLocked()); $lockA->break(); $lockB->break(); @@ -348,9 +386,8 @@ public function testLockFunctionUsesUniqueDefaultIdentifierIfNoIdentifierParamet { # Default identifier is not set explicitly and its default value is null. # (We have to set it to null manually because other tests might have set the default identifier before.) - $reflection = new \ReflectionProperty(Lock::class, 'defaultIdentifier'); - $reflection->setAccessible(true); - $reflection->setValue(null); + $reflection = new ReflectionProperty(Lock::class, 'defaultIdentifier'); + $reflection->setValue(null, null); $this->assertNull($reflection->getValue()); $lock = new Lock($this->getRandomString()); @@ -388,13 +425,9 @@ public function testBreaksOnDestructWhenBreakOnDestructPropertyIsTrue(): void # Check that break() and isLocked() are called when breakOnDestruct is set to true $breakingLock = $this->getMockBuilder(Lock::class) ->setConstructorArgs([$this->getRandomString()]) - ->onlyMethods(['isLocked', 'break']) + ->onlyMethods(['break']) ->getMock(); $breakingLock->setBreakOnDestruct(true); - $breakingLock - ->expects($this->once()) - ->method('isLocked') - ->willReturn(true); $breakingLock ->expects($this->once()) ->method('break'); @@ -435,10 +468,12 @@ public function testCanLockWithSameKeyTwiceWhenIsNotExclusive(): void $identifierA = $this->getRandomString(); $identifierB = $this->getRandomString(); - $lockA = new PublicLock($key, false, 5, 0, $identifierA); + $lockA = new PublicLock($key, $identifierA, false, 5, 0); + $lockA->lock(); $this->assertFalse($lockA->isLockedByOther()); - $lockB = new PublicLock($key, false, 5, 0, $identifierB); + $lockB = new PublicLock($key, $identifierB, false, 5, 0); + $lockB->lock(); # is not locked by other because it was not updated $this->assertFalse($lockA->isLockedByOther()); # is locked by other because it was updated @@ -468,11 +503,13 @@ public function testCannotLockWithSameKeyTwiceWhenIsExclusive(): void $identifierA = $this->getRandomString(); $identifierB = $this->getRandomString(); - $lockA = new PublicLock($key, true, 999, 0, $identifierA); + $lockA = new PublicLock($key, $identifierA, true, 999, 0); + $lockA->lock(); $this->assertNotFalse($lockA->isLocked()); $this->assertFalse($lockA->isLockedByOtherExclusively()); - $lockB = new PublicLock($key, true, 999, 0, $identifierB); + $lockB = new PublicLock($key, $identifierB, true, 999, 0); + $lockB->lock(); # is not locked because lockA already got the lock $this->assertFalse($lockB->isLocked()); # is locked by other because it was updated @@ -501,15 +538,15 @@ public function testCannotLockWithSameKeyTwiceWhenIsExclusive(): void * * Make some functions public for testing purposes */ -class PublicLock extends EtcdLock +class PublicLock extends Lock { - public function addOrUpdateLock(): bool + public function addOrUpdateLock(int $time): bool { - return parent::addOrUpdateLock(); + return parent::addOrUpdateLock($time); } - public function removeLock(): bool + public function removeLock(): void { - return parent::removeLock(); + parent::removeLock(); } -} \ No newline at end of file +}