Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/AbstractLock.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Aternos\Lock;

/**
* Abstract base class for locks that contains getters and setters for the lock properties.
*/
abstract class AbstractLock implements LockInterface
{
/**
Expand Down
99 changes: 46 additions & 53 deletions src/Lock.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,23 @@

namespace Aternos\Lock;

use Aternos\Etcd\Client;
use Aternos\Etcd\ClientInterface;
use Aternos\Etcd\Exception\Status\DeadlineExceededException;
use Aternos\Etcd\Exception\Status\InvalidResponseStatusCodeException;
use Aternos\Etcd\Exception\Status\UnavailableException;
use Aternos\Etcd\Exception\Status\UnknownException;
use Aternos\Lock\Storage\EtcdStorage;
use Aternos\Lock\Storage\StorageException;
use Aternos\Lock\Storage\StorageInterface;
use Exception;

/**
* Class Lock
* LockInterface implementation using etcd-like storage
*
* @package Aternos\Lock
*/
class Lock extends AbstractLock
{
/**
* see Lock::setClient()
*
* @var ClientInterface|null
* @see static::setStorage()
* @var StorageInterface|null
*/
protected static ?ClientInterface $client = null;
protected static ?StorageInterface $storage = null;

/**
* see Lock::setPrefix()
Expand Down Expand Up @@ -66,15 +63,22 @@ class Lock extends AbstractLock
protected static int $delayPerUnavailableRetry = 1;

/**
* Set the etcd client (Aternos\Etcd\Client)
*
* Uses a localhost client if not set
*
* @param ClientInterface $client
* Set the storage interface used to store locks. If not set, {@link EtcdStorage} is used.
* @param StorageInterface $storage
* @return void
*/
public static function setStorage(StorageInterface $storage): void
{
static::$storage = $storage;
}

/**
* Get the storage interface used to store locks. If not set, {@link EtcdStorage} is used.
* @return StorageInterface
*/
public static function setClient(ClientInterface $client): void
protected static function getStorage(): StorageInterface
{
static::$client = $client;
return static::$storage ??= new EtcdStorage();
}

/**
Expand Down Expand Up @@ -110,12 +114,13 @@ public static function setMaxSaveRetries(int $retries): void
}

/**
* Set the maximum delay in microseconds (1,000,000 microseconds = 1 second) that should used for the random delay between retries
* Set the maximum delay in microseconds (1,000,000 microseconds = 1 second) that should be used for the random
* delay between retries.
*
* The delay is random and calculated like this: rand(0, $retries * $delayPerRetry)
*
* Lower value = faster retries (probably more retries necessary)
* Higher value = slower retries (probably less retries necessary)
* Higher value = slower retries (probably fewer retries necessary)
*
* @param int $delayPerRetry
*/
Expand Down Expand Up @@ -144,20 +149,6 @@ public static function setDelayPerUnavailableRetry(int $delayPerRetry): void
static::$delayPerUnavailableRetry = $delayPerRetry;
}

/**
* Get an Aternos\Etcd\Client instance
*
* @return ClientInterface
*/
protected static function getClient(): ClientInterface
{
if (static::$client === null) {
static::$client = new Client();
}

return static::$client;
}

/**
* Full name of the key in etcd (prefix + key)
*
Expand All @@ -171,9 +162,9 @@ protected static function getClient(): ClientInterface
* Will be used in deleteIf and putIf requests to check
* if there was no change in etcd while processing the lock
*
* @var string|bool
* @var string
*/
protected string|bool $previousLockString = false;
protected ?string $previousLockString = null;

/**
* Current parsed locks
Expand Down Expand Up @@ -222,7 +213,7 @@ public function __construct(
* Try to acquire lock
*
* @return bool true if the lock was acquired, false if it was not
* @throws InvalidResponseStatusCodeException
* @throws StorageException
* @throws TooManySaveRetriesException
*/
public function lock(): bool
Expand All @@ -247,7 +238,7 @@ public function lock(): bool
*
* @param int|null $waitTime maximum time in seconds to wait for other locks
* @return bool
* @throws InvalidResponseStatusCodeException
* @throws StorageException
*/
public function waitForOtherLocks(?int $waitTime = null): bool
{
Expand Down Expand Up @@ -284,7 +275,7 @@ public function getRemainingLockDuration(): int
* Refresh the lock
*
* @return boolean
* @throws InvalidResponseStatusCodeException
* @throws StorageException
* @throws TooManySaveRetriesException
*/
public function refresh(): bool
Expand Down Expand Up @@ -312,7 +303,7 @@ public function refresh(): bool
* Should be only used if you have the lock
*
* @return void
* @throws InvalidResponseStatusCodeException
* @throws StorageException
* @throws TooManySaveRetriesException
*/
public function break(): void
Expand Down Expand Up @@ -340,7 +331,7 @@ protected function generateLock(): LockEntry
* Remove a lock from the locking array and save the locks
*
* @return void
* @throws InvalidResponseStatusCodeException
* @throws StorageException
* @throws TooManySaveRetriesException
*/
protected function removeLock(): void
Expand All @@ -362,7 +353,7 @@ protected function removeLock(): void
*
* @param int $time
* @return bool
* @throws InvalidResponseStatusCodeException
* @throws StorageException
* @throws TooManySaveRetriesException
*/
protected function addOrUpdateLock(int $time): bool
Expand All @@ -389,8 +380,9 @@ protected function addOrUpdateLock(int $time): bool
* changed since the last update, they will be updated by this function again.
*
* @return bool
* @throws InvalidResponseStatusCodeException
* @throws StorageException
* @throws TooManySaveRetriesException
* @throws Exception
*/
protected function saveLocks(): bool
{
Expand All @@ -408,9 +400,9 @@ protected function saveLocks(): bool
if (count($this->locks) === 0) {
for ($i = 1; $i <= static::$maxUnavailableRetries; $i++) {
try {
$result = static::getClient()->deleteIf($this->etcdKey, $previousLocks, !$delayRetry);
$result = static::getStorage()->deleteIf($this->etcdKey, $previousLocks, !$delayRetry);
break;
} catch (UnavailableException | DeadlineExceededException | UnknownException $e) {
} catch (StorageException $e) {
if ($i === static::$maxUnavailableRetries) {
throw $e;
} else {
Expand All @@ -424,9 +416,9 @@ protected function saveLocks(): bool

for ($i = 1; $i <= static::$maxUnavailableRetries; $i++) {
try {
$result = static::getClient()->putIf($this->etcdKey, $lockString, $previousLocks, !$delayRetry);
$result = static::getStorage()->putIf($this->etcdKey, $lockString, $previousLocks, !$delayRetry);
break;
} catch (UnavailableException | DeadlineExceededException | UnknownException $e) {
} catch (StorageException $e) {
if ($i === static::$maxUnavailableRetries) {
throw $e;
} else {
Expand Down Expand Up @@ -479,17 +471,18 @@ protected function canLock(): bool
/**
* Update the locks array from etcd
*
* @throws InvalidResponseStatusCodeException
* @return $this
* @throws StorageException
* @throws Exception
*/
public function update(): static
{
$etcdLockString = false;
for ($i = 1; $i <= static::$maxUnavailableRetries; $i++) {
try {
$etcdLockString = static::getClient()->get($this->etcdKey);
$etcdLockString = static::getStorage()->get($this->etcdKey);
break;
} catch (UnavailableException | DeadlineExceededException | UnknownException $e) {
} catch (StorageException $e) {
if ($i === static::$maxUnavailableRetries) {
throw $e;
} else {
Expand All @@ -505,10 +498,10 @@ public function update(): static
/**
* Update the locks array from a JSON string
*
* @param string|bool $lockString
* @param ?string $lockString
* @return $this
*/
protected function updateFromString(string|bool $lockString): static
protected function updateFromString(?string $lockString): static
{
$this->previousLockString = $lockString;

Expand All @@ -524,7 +517,7 @@ protected function updateFromString(string|bool $lockString): static
/**
* Break the lock on destruction of this object
*
* @throws InvalidResponseStatusCodeException
* @throws StorageException
* @throws TooManySaveRetriesException
*/
public function __destruct()
Expand Down
53 changes: 53 additions & 0 deletions src/Storage/EtcdStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Aternos\Lock\Storage;

use Aternos\Etcd\Client;
use Aternos\Etcd\ClientInterface;
use Aternos\Etcd\Exception\Status\DeadlineExceededException;
use Aternos\Etcd\Exception\Status\UnavailableException;
use Aternos\Etcd\Exception\Status\UnknownException;

class EtcdStorage implements StorageInterface
{
protected ClientInterface $client;

public function __construct(?ClientInterface $client = null)
{
$this->client = $client ?? new Client();
}

public function putIf(string $key, string $value, ?string $previousValue, bool $returnNewValueOnFail): bool|string
{
try {
return $this->client->putIf($key, $value, $previousValue ?? false, $returnNewValueOnFail);
} catch (UnavailableException | DeadlineExceededException | UnknownException $e) {
throw new StorageException($e->getMessage(), $e->getCode(), $e);
}
}

public function deleteIf(string $key, ?string $previousValue, bool $returnNewValueOnFail = false): bool|string
{
try {
return $this->client->deleteIf($key, $previousValue ?? false, $returnNewValueOnFail);
} catch (UnavailableException | DeadlineExceededException | UnknownException $e) {
throw new StorageException($e->getMessage(), $e->getCode(), $e);
}
}


public function get(string $key): ?string
{
try {
$value = $this->client->get($key);
} catch (UnavailableException | DeadlineExceededException | UnknownException $e) {
throw new StorageException($e->getMessage(), $e->getCode(), $e);
}

if ($value === false) {
return null;
}

return $value;
}
}
13 changes: 13 additions & 0 deletions src/Storage/StorageException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Aternos\Lock\Storage;

use Exception;

/**
* An exception thrown by a storage client. This should only be used for known exceptions as the operation will be retried by default.
*/
class StorageException extends Exception
{

}
49 changes: 49 additions & 0 deletions src/Storage/StorageInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Aternos\Lock\Storage;

use Exception;

/**
* Interface for a storage that can be used to perform etcd-like operations.
*/
interface StorageInterface
{
/**
* Put `$value` if `$key` value matches `$previousValue` and return true. If the new value does not match and
* `$returnNewValueOnFail` is true return the new value, otherwise return false.
* @param string $key
* @param string $value The new value to set
* @param string|null $previousValue The previous value to compare against. If null is provided, the comparison
* should check that the key does not exist yet.
* @param bool $returnNewValueOnFail if true the new value of the key should be returned if the operation fails
* @return bool|string true if the operation succeeded, false if it failed and `$returnNewValueOnFail` is false,
* otherwise the new value of the key
* @throws StorageException a known, retryable error occurred
* @throws Exception an unknown error occurred
*/
public function putIf(string $key, string $value, ?string $previousValue, bool $returnNewValueOnFail): bool|string;

/**
* Delete if $key value matches $previous value otherwise $returnNewValueOnFail
*
* @param string $key
* @param string|null $previousValue The previous value to compare against. If null is provided, the comparison
* should check that the key does not exist yet.
* @param bool $returnNewValueOnFail
* @return bool|string true if the operation succeeded, false if it failed and `$returnNewValueOnFail` is false,
* otherwise the new value of the key
* @throws StorageException a known, retryable error occurred
* @throws Exception an unknown error occurred
*/
public function deleteIf(string $key, ?string $previousValue, bool $returnNewValueOnFail = false): bool|string;

/**
* Get the value of a key
* @param string $key
* @return string|null the value of the key or null if the key does not exist
* @throws StorageException a known, retryable error occurred
* @throws Exception an unknown error occurred
*/
public function get(string $key): ?string;
}
Loading
Loading