-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add async cache implementation (#36)
- Loading branch information
Showing
10 changed files
with
1,224 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
<?php | ||
|
||
namespace Spiral\RoadRunner\KeyValue; | ||
|
||
use DateInterval; | ||
use RoadRunner\KV\DTO\V1\Response; | ||
use Spiral\Goridge\RPC\AsyncRPCInterface; | ||
use Spiral\Goridge\RPC\Exception\RPCException; | ||
use Spiral\Goridge\RPC\Exception\ServiceException; | ||
use Spiral\Goridge\RPC\RPCInterface; | ||
use Spiral\RoadRunner\KeyValue\Exception\KeyValueException; | ||
use Spiral\RoadRunner\KeyValue\Exception\StorageException; | ||
use Spiral\RoadRunner\KeyValue\Serializer\DefaultSerializer; | ||
use Spiral\RoadRunner\KeyValue\Serializer\SerializerInterface; | ||
use function sprintf; | ||
use function str_contains; | ||
use function str_replace; | ||
|
||
/** | ||
* @psalm-suppress PropertyNotSetInConstructor | ||
*/ | ||
class AsyncCache extends Cache implements AsyncStorageInterface | ||
{ | ||
/** | ||
* @var positive-int[] | ||
*/ | ||
protected array $callsInFlight = []; | ||
|
||
/** | ||
* @param AsyncRPCInterface $rpc | ||
* @param non-empty-string $name | ||
*/ | ||
public function __construct( | ||
RPCInterface $rpc, | ||
string $name, | ||
SerializerInterface $serializer = new DefaultSerializer() | ||
) { | ||
parent::__construct($rpc, $name, $serializer); | ||
|
||
// This should result in things like the Symfony ContainerBuilder throwing during build instead of runtime. | ||
assert($this->rpc instanceof AsyncRPCInterface); | ||
} | ||
|
||
/** | ||
* Note: The current PSR-16 implementation always returns true or | ||
* exception on error. | ||
* | ||
* {@inheritDoc} | ||
* | ||
* @throws KeyValueException | ||
* @throws RPCException | ||
*/ | ||
public function deleteAsync(string $key): bool | ||
{ | ||
return $this->deleteMultipleAsync([$key]); | ||
} | ||
|
||
/** | ||
* Note: The current PSR-16 implementation always returns true or | ||
* exception on error. | ||
* | ||
* {@inheritDoc} | ||
* | ||
* @psalm-param iterable<string> $keys | ||
* | ||
* @throws KeyValueException | ||
* @throws RPCException | ||
*/ | ||
public function deleteMultipleAsync(iterable $keys): bool | ||
{ | ||
assert($this->rpc instanceof AsyncRPCInterface); | ||
|
||
// Handle someone never calling commitAsync() | ||
if (count($this->callsInFlight) > 1000) { | ||
$this->commitAsync(); | ||
} | ||
|
||
$this->callsInFlight[] = $this->rpc->callAsync('kv.Delete', $this->requestKeys($keys)); | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
* | ||
* @psalm-param positive-int|\DateInterval|null $ttl | ||
* @psalm-suppress MoreSpecificImplementedParamType | ||
* @throws KeyValueException | ||
* @throws RPCException | ||
*/ | ||
public function setAsync(string $key, mixed $value, null|int|DateInterval $ttl = null): bool | ||
{ | ||
return $this->setMultipleAsync([$key => $value], $ttl); | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
* | ||
* @psalm-param iterable<string, mixed> $values | ||
* @psalm-param positive-int|\DateInterval|null $ttl | ||
* @psalm-suppress MoreSpecificImplementedParamType | ||
* @throws KeyValueException | ||
* @throws RPCException | ||
*/ | ||
public function setMultipleAsync(iterable $values, null|int|DateInterval $ttl = null): bool | ||
{ | ||
assert($this->rpc instanceof AsyncRPCInterface); | ||
|
||
// Handle someone never calling commitAsync() | ||
if (count($this->callsInFlight) > 1000) { | ||
$this->commitAsync(); | ||
} | ||
|
||
$this->callsInFlight[] = $this->rpc->callAsync( | ||
'kv.Set', | ||
$this->requestValues($values, $this->ttlToRfc3339String($ttl)) | ||
); | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* @throws KeyValueException | ||
* @throws RPCException | ||
*/ | ||
public function commitAsync(): bool | ||
{ | ||
assert($this->rpc instanceof AsyncRPCInterface); | ||
|
||
try { | ||
$this->rpc->getResponses($this->callsInFlight, Response::class); | ||
} catch (ServiceException $e) { | ||
$message = str_replace(["\t", "\n"], ' ', $e->getMessage()); | ||
|
||
if (str_contains($message, 'no such storage')) { | ||
throw new StorageException(sprintf(self::ERROR_INVALID_STORAGE, $this->name)); | ||
} | ||
|
||
throw new KeyValueException($message, $e->getCode(), $e); | ||
} finally { | ||
$this->callsInFlight = []; | ||
} | ||
|
||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
<?php | ||
|
||
namespace Spiral\RoadRunner\KeyValue; | ||
|
||
use DateInterval; | ||
use Spiral\RoadRunner\KeyValue\Exception\KeyValueException; | ||
|
||
interface AsyncStorageInterface extends StorageInterface | ||
{ | ||
/** | ||
* Needs to be called to make sure all async calls have completed successfully. | ||
* | ||
* @throws KeyValueException | ||
*/ | ||
public function commitAsync(): bool; | ||
|
||
/** | ||
* Persists a set of key => value pairs in the cache, with an optional TTL. | ||
* | ||
* @param iterable $values A list of key => value pairs for a multiple-set operation. | ||
* @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and | ||
* the driver supports TTL then the library may set a default value | ||
* for it or let the driver take care of that. | ||
* | ||
* @return bool True on success and false on failure. | ||
* | ||
* @throws \Psr\SimpleCache\InvalidArgumentException | ||
* MUST be thrown if $values is neither an array nor a Traversable, | ||
* or if any of the $values are not a legal value. | ||
*/ | ||
public function setMultipleAsync(iterable $values, null|int|DateInterval $ttl = null): bool; | ||
|
||
/** | ||
* Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. | ||
* | ||
* @param string $key The key of the item to store. | ||
* @param mixed $value The value of the item to store, must be serializable. | ||
* @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and | ||
* the driver supports TTL then the library may set a default value | ||
* for it or let the driver take care of that. | ||
* | ||
* @return bool True on success and false on failure. | ||
* | ||
* @throws \Psr\SimpleCache\InvalidArgumentException | ||
* MUST be thrown if the $key string is not a legal value. | ||
*/ | ||
public function setAsync(string $key, mixed $value, null|int|DateInterval $ttl = null): bool; | ||
|
||
/** | ||
* Delete an item from the cache by its unique key. | ||
* | ||
* @param string $key The unique cache key of the item to delete. | ||
* | ||
* @return bool True if the item was successfully removed. False if there was an error. | ||
* | ||
* @throws \Psr\SimpleCache\InvalidArgumentException | ||
* MUST be thrown if the $key string is not a legal value. | ||
*/ | ||
public function deleteAsync(string $key): bool; | ||
|
||
/** | ||
* Deletes multiple cache items in a single operation. | ||
* | ||
* @param iterable<string> $keys A list of string-based keys to be deleted. | ||
* | ||
* @return bool True if the items were successfully removed. False if there was an error. | ||
* | ||
* @throws \Psr\SimpleCache\InvalidArgumentException | ||
* MUST be thrown if $keys is neither an array nor a Traversable, | ||
* or if any of the $keys are not a legal value. | ||
*/ | ||
public function deleteMultipleAsync(iterable $keys): bool; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.