Skip to content

Commit

Permalink
feat: adding storage client (#191)
Browse files Browse the repository at this point in the history
* feat: adding storage client

* chore: add doc comments

* chore: add storage client example script

* chore: hack in support for bytes type

* chore: clean up error message

* chore: add toString for storage get response

* chore: fix type specifier

* chore: support multiple storage data clients

* chore: regenerate protos

* chore: mostly finished storage client

* chore: 'not found' handling

* chore: remove refs to deleted 'put' func and deprecated not found error

* chore: better handling of not found errors

* chore: PR feedback cleanup

* chore: split out grpc config and transport strategies for storage

* chore: return nil for type and found on error
  • Loading branch information
pgautier404 authored Jun 21, 2024
1 parent 65abdd8 commit d6bcc98
Show file tree
Hide file tree
Showing 86 changed files with 2,757 additions and 4,654 deletions.
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@
"Leaderboard\\": "types/Leaderboard/",
"Permission_messages\\": "types/Permission_messages/",
"Token\\": "types/Token/",
"Webhook\\": "types/Webhook/"
"Webhook\\": "types/Webhook/",
"Store\\": "types/Store/"
},
"files": [
"src/Utilities/_DataValidation.php"
],
"classmap": [
"src/Cache/CacheOperationTypes/CacheOperationTypes.php",
"src/Storage/StorageOperationTypes/StorageOperationTypes.php",
"src/Cache/Errors/Errors.php"
]
},
Expand Down
2 changes: 1 addition & 1 deletion examples/composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"require": {
"momentohq/client-sdk-php": "1.4.0",
"momentohq/client-sdk-php": "1.9.1",
"monolog/monolog": "^2.5"
}
}
125 changes: 125 additions & 0 deletions examples/storage-example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
require "vendor/autoload.php";

use Momento\Auth\CredentialProvider;
use Momento\Config\Configurations\StorageLaptop;
use Momento\Logging\StderrLoggerFactory;
use Psr\Log\LoggerInterface;
use Momento\Storage\PreviewStorageClient;
use Momento\Storage\StorageOperationTypes\StorageValueType;

$STORE_NAME = uniqid("php-storage-example-");
$VALUES = [
"str"=> "StringValue",
"int" => 123,
"double" => 123.456,
"fakeout" => "123"
];

// Setup
$authProvider = CredentialProvider::fromEnvironmentVariable("MOMENTO_AUTH_TOKEN");
$configuration = StorageLaptop::latest(new StderrLoggerFactory());
$client = new PreviewStorageClient($configuration, $authProvider);
$logger = $configuration->getLoggerFactory()->getLogger("ex:");

function printBanner(string $message, LoggerInterface $logger): void
{
$line = "**************************************************************************";
$logger->info($line);
$logger->info($message);
$logger->info($line);
}

// Used to tear down temporary store after failure or completion of script
function deleteStore(string $storeName, LoggerInterface $logger, PreviewStorageClient $client): void
{
$logger->info("Deleting store $storeName\n");
$response = $client->deleteStore($storeName);
if ($response->asError()) {
$logger->info("Error deleting store: " . $response->asError()->message() . "\n");
exit(1);
}
}

printBanner("* Momento Storage Example Start *", $logger);

// Ensure test store exists
$response = $client->createStore($STORE_NAME);
if ($response->asSuccess()) {
$logger->info("Created store " . $STORE_NAME . "\n");
} elseif ($response->asError()) {
$logger->info("Error creating store: " . $response->asError()->message() . "\n");
exit(1);
} elseif ($response->asAlreadyExists()) {
$logger->info("Store " . $STORE_NAME . " already exists.\n");
}

// List stores
$response = $client->listStores();
if ($response->asSuccess()) {
$logger->info("SUCCESS: List stores: \n");
foreach ($response->asSuccess()->stores() as $store) {
$storeName = $store->name();
$logger->info("$storeName\n");
}
$logger->info("\n");
} elseif ($response->asError()) {
$logger->info("Error listing store: " . $response->asError()->message() . "\n");
deleteStore($STORE_NAME, $logger, $client);
exit(1);
}

// Set
foreach ($VALUES as $key => $value) {
$logger->info("Setting key: '$key' to value: '$value', type = " . get_class($value) . "\n");
$response = $client->set($STORE_NAME, $key, $value);
if ($response->asSuccess()) {
$logger->info("SUCCESS\n");
} elseif ($response->asError()) {
$logger->info("Error setting key: " . $response->asError()->message() . "\n");
deleteStore($STORE_NAME, $logger, $client);
exit(1);
}
}

// Get
foreach ($VALUES as $key => $value) {
$logger->info("Getting value for key: '$key'\n");
$response = $client->get($STORE_NAME, $key);
if ($response->asSuccess()) {
$logger->info("SUCCESS\n");
$valueType = $response->asSuccess()->type();
if ($valueType == StorageValueType::STRING) {
print("Got string value: " . $response->asSuccess()->tryGetString() . "\n");
} elseif ($valueType == StorageValueType::INTEGER) {
print("Got integer value: " . $response->asSuccess()->tryGetInteger() . "\n");
} elseif ($valueType == StorageValueType::DOUBLE) {
print("Got double value: " . $response->asSuccess()->tryGetDouble() . "\n");
} elseif ($valueType == StorageValueType::BYTES) {
// This case is not expected in this example as PHP doesn't have a native byte type
print("Got bytes value: " . $response->asSuccess()->tryGetBytes() . "\n");
}
} elseif ($response->asError()) {
$logger->info("Error getting key: " . $response->asError()->message() . "\n");
deleteStore($STORE_NAME, $logger, $client);
exit(1);
}
}

// Delete
foreach (array_keys($VALUES) as $key) {
$logger->info("Deleting key: '$key'\n");
$response = $client->delete($STORE_NAME, $key);
if ($response->asSuccess()) {
$logger->info("SUCCESS\n");
} elseif ($response->asError()) {
$logger->info("Error deleting key: " . $response->asError()->message() . "\n");
exit(1);
}
}

// Delete store
deleteStore($STORE_NAME, $logger, $client);

printBanner("* Momento Storage Example End *", $logger);
2 changes: 2 additions & 0 deletions src/Auth/AuthUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public static function parseV1Token(string $authToken): object {
$payload = new \stdClass();
$payload->c = "cache.{$tokenData->endpoint}";
$payload->cp = "control.{$tokenData->endpoint}";
$payload->storage = "storage.{$tokenData->endpoint}";
$payload->authToken = $tokenData->api_key;
return $payload;
}
Expand All @@ -47,6 +48,7 @@ public static function parseJwtToken(string $authToken): object
try {
$payload = $exploded[1];
$payload = JWT::jsonDecode(JWT::urlsafeB64Decode($payload));
$payload->storage = null;
$payload->authToken = $authToken;
} catch (\Exception $e) {
self::throwBadAuthToken();
Expand Down
3 changes: 2 additions & 1 deletion src/Auth/EnvMomentoTokenProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public function __construct(
string $envVariableName,
?string $controlEndpoint = null,
?string $cacheEndpoint = null,
?string $storageEndpoint = null,
?string $trustedControlEndpointCertificateName = null,
?string $trustedCacheEndpointCertificateName = null
)
Expand All @@ -23,6 +24,6 @@ public function __construct(
throw new InvalidArgumentError("Environment variable $envVariableName is empty or null.");
}
$authToken = $_SERVER[$envVariableName];
parent::__construct($authToken, $controlEndpoint, $cacheEndpoint, $trustedControlEndpointCertificateName, $trustedCacheEndpointCertificateName);
parent::__construct($authToken, $controlEndpoint, $cacheEndpoint, $storageEndpoint, $trustedControlEndpointCertificateName, $trustedCacheEndpointCertificateName);
}
}
14 changes: 14 additions & 0 deletions src/Auth/StringMomentoTokenProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ class StringMomentoTokenProvider extends CredentialProvider
protected string $authToken;
protected ?string $controlEndpoint = null;
protected ?string $cacheEndpoint = null;
protected ?string $storageEndpoint = null;
protected ?string $trustedControlEndpointCertificateName = null;
protected ?string $trustedCacheEndpointCertificateName = null;

public function __construct(
string $authToken,
?string $controlEndpoint = null,
?string $cacheEndpoint = null,
// TODO: adding this arg would be a breaking change for anyone currently passing in
// endpointCertificateName arguments. I am pretty sure that is 0 people, but wanted
// to call it out.
?string $storageEndpoint = null,
?string $trustedControlEndpointCertificateName = null,
?string $trustedCacheEndpointCertificateName = null
)
Expand All @@ -38,6 +43,7 @@ public function __construct(
$this->authToken = $payload->authToken;
$this->controlEndpoint = $controlEndpoint ?? $payload->cp;
$this->cacheEndpoint = $cacheEndpoint ?? $payload->c;
$this->storageEndpoint = $storageEndpoint ?? $payload->storage;
$this->trustedControlEndpointCertificateName = $trustedControlEndpointCertificateName;
$this->trustedCacheEndpointCertificateName = $trustedCacheEndpointCertificateName;
}
Expand Down Expand Up @@ -66,6 +72,14 @@ public function getControlEndpoint(): string
return $this->controlEndpoint;
}

/**
* @return string|null The host which the Momento client will connect to for Momento storage operations.
*/
public function getStorageEndpoint(): ?string
{
return $this->storageEndpoint;
}

/**
* @return string|null Used for routing gRPC calls through a proxy server
*/
Expand Down
41 changes: 41 additions & 0 deletions src/Cache/Errors/Errors.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,21 @@ abstract class MomentoErrorCode
public const ALREADY_EXISTS_ERROR = "ALREADY_EXISTS_ERROR";
/**
* Cache with specified name doesn't exist
* @deprecated Use CacheNotFoundError instead
*/
public const NOT_FOUND_ERROR = "NOT_FOUND_ERROR";
/**
* Cache with specified name doesn't exist
*/
public const CACHE_NOT_FOUND_ERROR = "NOT_FOUND_ERROR";
/**
* Store with specified name doesn't exist
*/
public const STORE_NOT_FOUND_ERROR = "STORE_NOT_FOUND_ERROR";
/**
* Item with specified name doesn't exist
*/
public const ITEM_NOT_FOUND_ERROR = "ITEM_NOT_FOUND_ERROR";
/**
* An unexpected error occurred while trying to fulfill the request
*/
Expand Down Expand Up @@ -216,13 +229,41 @@ class LimitExceededError extends SdkError

/**
* Cache with specified name doesn't exist
* @deprecated Use CacheNotFoundError instead
*/
class NotFoundError extends SdkError
{
public string $errorCode = MomentoErrorCode::NOT_FOUND_ERROR;
public string $messageWrapper = 'A cache with the specified name does not exist. To resolve this error, make sure you have created the cache before attempting to use it';
}

/**
* Cache with specified name doesn't exist
*/
class CacheNotFoundError extends SdkError
{
public string $errorCode = MomentoErrorCode::CACHE_NOT_FOUND_ERROR;
public string $messageWrapper = 'A cache with the specified name does not exist. To resolve this error, make sure you have created the cache before attempting to use it';
}

/**
* Store with specified name doesn't exist
*/
class StoreNotFoundError extends SdkError
{
public string $errorCode = MomentoErrorCode::STORE_NOT_FOUND_ERROR;
public string $messageWrapper = 'A store with the specified name does not exist. To resolve this error, make sure you have created the store before attempting to use it';
}

/**
* Item with specified name doesn't exist
*/
class ItemNotFoundError extends SdkError
{
public string $errorCode = MomentoErrorCode::ITEM_NOT_FOUND_ERROR;
public string $messageWrapper = 'An item with the specified name does not exist. To resolve this error, make sure you have created the item before attempting to use it';
}

/**
* Insufficient permissions to perform operation
*/
Expand Down
54 changes: 54 additions & 0 deletions src/Cache/Internal/IdleStorageDataClientWrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);

namespace Momento\Cache\Internal;

use Momento\Config\IConfiguration;
use Momento\Config\IStorageConfiguration;
use Momento\Storage\Internal\StorageDataClient;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;

class IdleStorageDataClientWrapper implements LoggerAwareInterface {

private StorageDataClient $client;
private LoggerInterface $logger;
private object $clientFactory;
private ?int $maxIdleMillis;
private int $lastAccessTime;

public function __construct(object $clientFactory, IStorageConfiguration $configuration) {
$this->clientFactory = $clientFactory;
$this->client = ($clientFactory->callback)();
$this->logger = $configuration->getLoggerFactory()->getLogger(get_class($this));
$this->maxIdleMillis = $configuration->getTransportStrategy()->getMaxIdleMillis();
$this->lastAccessTime = $this->getMilliseconds();
}

public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}

public function getClient(): StorageDataClient {
if ($this->maxIdleMillis === null) {
return $this->client;
}
$this->logger->debug("Checking to see if client has been idle for more than {$this->maxIdleMillis}");
if ($this->getMilliseconds() - $this->lastAccessTime > $this->maxIdleMillis) {
$this->logger->debug("Client has been idle for more than {$this->maxIdleMillis}; reconnecting");
$this->client->close();
$this->client = ($this->clientFactory->callback)();
}
$this->lastAccessTime = $this->getMilliseconds();
return $this->client;
}

public function close(): void {
$this->client->close();
}

private function getMilliseconds(): int {
return (int)(gettimeofday(true) * 1000);
}
}
Loading

0 comments on commit d6bcc98

Please sign in to comment.