diff --git a/momento-sdk/src/intTest/java/momento/sdk/BaseTestClass.java b/momento-sdk/src/intTest/java/momento/sdk/BaseTestClass.java new file mode 100644 index 00000000..5ef3407d --- /dev/null +++ b/momento-sdk/src/intTest/java/momento/sdk/BaseTestClass.java @@ -0,0 +1,30 @@ +package momento.sdk; + +import momento.sdk.exceptions.CacheAlreadyExistsException; +import org.junit.jupiter.api.BeforeAll; + +class BaseTestClass { + + @BeforeAll + static void beforeAll() { + if (System.getenv("TEST_AUTH_TOKEN") == null) { + throw new IllegalArgumentException( + "Integration tests require TEST_AUTH_TOKEN env var; see README for more details."); + } + if (System.getenv("TEST_CACHE_NAME") == null) { + throw new IllegalArgumentException( + "Integration tests require TEST_CACHE_NAME env var; see README for more details."); + } + ensureTestCacheExists(); + } + + private static void ensureTestCacheExists() { + SimpleCacheClient client = + SimpleCacheClient.builder(System.getenv("TEST_AUTH_TOKEN"), 10).build(); + try { + client.createCache(System.getenv("TEST_CACHE_NAME")); + } catch (CacheAlreadyExistsException e) { + // do nothing. Cache already exists. + } + } +} diff --git a/momento-sdk/src/intTest/java/momento/sdk/CacheTest.java b/momento-sdk/src/intTest/java/momento/sdk/CacheTest.java deleted file mode 100644 index 7f7ec76a..00000000 --- a/momento-sdk/src/intTest/java/momento/sdk/CacheTest.java +++ /dev/null @@ -1,434 +0,0 @@ -package momento.sdk; - -import static momento.sdk.TestHelpers.DEFAULT_CACHE_ENDPOINT; -import static org.junit.jupiter.api.Assertions.*; - -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; -import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import momento.sdk.exceptions.CacheNotFoundException; -import momento.sdk.exceptions.ClientSdkException; -import momento.sdk.exceptions.PermissionDeniedException; -import momento.sdk.messages.CacheGetResponse; -import momento.sdk.messages.CacheSetResponse; -import momento.sdk.messages.MomentoCacheResult; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -final class CacheTest { - - private Cache cache; - private static final int DEFAULT_ITEM_TTL_SECONDS = 60; - - @BeforeAll - static void beforeAll() { - if (System.getenv("TEST_AUTH_TOKEN") == null) { - throw new IllegalArgumentException( - "Integration tests require TEST_AUTH_TOKEN env var; see README for more details."); - } - if (System.getenv("TEST_CACHE_NAME") == null) { - throw new IllegalArgumentException( - "Integration tests require TEST_CACHE_NAME env var; see README for more details."); - } - } - - @BeforeEach - void setUp() { - cache = getCache(Optional.empty(), DEFAULT_ITEM_TTL_SECONDS); - } - - Cache getCache(Optional openTelemetry, int defaultItemTtlSeconds) { - return getCache( - System.getenv("TEST_AUTH_TOKEN"), - System.getenv("TEST_CACHE_NAME"), - openTelemetry, - defaultItemTtlSeconds) - .connect(); - } - - Cache getCache( - String authToken, - String cacheName, - Optional openTelemetry, - int defaultItemTtlSeconds) { - String endpoint = System.getenv("TEST_ENDPOINT"); - if (endpoint == null) { - endpoint = DEFAULT_CACHE_ENDPOINT; - } - - return new Cache( - authToken, - cacheName, - openTelemetry, - endpoint, - defaultItemTtlSeconds, - System.getenv("TEST_SSL_INSECURE") != null) - .connect(); - } - - @AfterEach - void tearDown() throws Exception { - stopIntegrationTestOtel(); - } - - @Test - void testBlockingClientHappyPath() { - testHappyPath(cache); - } - - @Test - void testBlockingClientHappyPathWithTracing() throws Exception { - startIntegrationTestOtel(); - OpenTelemetrySdk openTelemetry = setOtelSDK(); - Cache client = getCache(Optional.of(openTelemetry), DEFAULT_ITEM_TTL_SECONDS); - testHappyPath(client); - // To accommodate for delays in tracing logs to appear in docker - Thread.sleep(1000); - verifySetTrace("1"); - verifyGetTrace("1"); - } - - private static void testHappyPath(Cache cache) { - String key = UUID.randomUUID().toString(); - - // Set Key sync - CacheSetResponse setRsp = - cache.set(key, ByteBuffer.wrap("bar".getBytes(StandardCharsets.UTF_8)), 2); - assertEquals(MomentoCacheResult.Ok, setRsp.result()); - - // Get Key that was just set - CacheGetResponse rsp = cache.get(key); - assertEquals(MomentoCacheResult.Hit, rsp.result()); - assertEquals("bar", rsp.string().get()); - } - - @Test - void testAsyncClientHappyPath() throws Exception { - testAsyncHappyPath(cache); - } - - @Test - void testAsyncClientHappyPathWithTracing() throws Exception { - startIntegrationTestOtel(); - OpenTelemetrySdk openTelemetry = setOtelSDK(); - Cache client = getCache(Optional.of(openTelemetry), DEFAULT_ITEM_TTL_SECONDS); - testAsyncHappyPath(client); - // To accommodate for delays in tracing logs to appear in docker - Thread.sleep(1000); - verifySetTrace("1"); - verifyGetTrace("1"); - } - - private static void testAsyncHappyPath(Cache client) throws Exception { - String key = UUID.randomUUID().toString(); - // Set Key Async - CompletableFuture setRsp = - client.setAsync(key, ByteBuffer.wrap("bar".getBytes(StandardCharsets.UTF_8)), 10); - assertEquals(MomentoCacheResult.Ok, setRsp.get().result()); - - // Get Key Async - CacheGetResponse rsp = client.getAsync(key).get(); - - assertEquals(MomentoCacheResult.Hit, rsp.result()); - assertEquals("bar", rsp.string().get()); - } - - @Test - void testTtlHappyPath() throws Exception { - testTtlHappyPath(cache); - } - - @Test - void testTtlHappyPathWithTracing() throws Exception { - startIntegrationTestOtel(); - OpenTelemetrySdk openTelemetry = setOtelSDK(); - Cache client = getCache(Optional.of(openTelemetry), DEFAULT_ITEM_TTL_SECONDS); - testTtlHappyPath(client); - // To accommodate for delays in tracing logs to appear in docker - Thread.sleep(1000); - verifySetTrace("1"); - verifyGetTrace("1"); - } - - private static void testTtlHappyPath(Cache client) throws Exception { - String key = UUID.randomUUID().toString(); - - // Set Key sync - CacheSetResponse setRsp = - client.set(key, ByteBuffer.wrap("bar".getBytes(StandardCharsets.UTF_8)), 1); - assertEquals(MomentoCacheResult.Ok, setRsp.result()); - - Thread.sleep(1500); - - // Get Key that was just set - CacheGetResponse rsp = client.get(key); - assertEquals(MomentoCacheResult.Miss, rsp.result()); - assertFalse(rsp.inputStream().isPresent()); - } - - @Test - void testMissHappyPath() { - testMissHappyPathInternal(cache); - } - - @Test - void testMissHappyPathWithTracing() throws Exception { - startIntegrationTestOtel(); - OpenTelemetrySdk openTelemetry = setOtelSDK(); - Cache client = getCache(Optional.of(openTelemetry), DEFAULT_ITEM_TTL_SECONDS); - testMissHappyPathInternal(client); - // To accommodate for delays in tracing logs to appear in docker - Thread.sleep(1000); - verifySetTrace("0"); - verifyGetTrace("1"); - } - - private static void testMissHappyPathInternal(Cache client) { - // Get Key that was just set - CacheGetResponse rsp = client.get(UUID.randomUUID().toString()); - - assertEquals(MomentoCacheResult.Miss, rsp.result()); - assertFalse(rsp.inputStream().isPresent()); - assertFalse(rsp.byteArray().isPresent()); - assertFalse(rsp.byteBuffer().isPresent()); - assertFalse(rsp.string().isPresent()); - assertFalse(rsp.string(Charset.defaultCharset()).isPresent()); - } - - @Test - void testBadAuthToken() { - assertThrows( - PermissionDeniedException.class, - () -> getCache("BAD_TOKEN", "dummy", Optional.empty(), DEFAULT_ITEM_TTL_SECONDS)); - } - - @Test - public void unreachableEndpoint_ThrowsException() { - assertThrows( - ClientSdkException.class, - () -> - new Cache( - System.getenv("TEST_AUTH_TOKEN"), - System.getenv("TEST_CACHE_NAME"), - "nonexistent.preprod.a.momentohq.com", - DEFAULT_ITEM_TTL_SECONDS) - .connect()); - } - - @Test - public void invalidCache_ThrowsNotFoundException() { - assertThrows( - CacheNotFoundException.class, - () -> - getCache( - System.getenv("TEST_AUTH_TOKEN"), - UUID.randomUUID().toString(), - Optional.empty(), - DEFAULT_ITEM_TTL_SECONDS)); - } - - @Test - public void setAndGetWithByteKeyValuesMustSucceed() { - byte[] key = {0x01, 0x02, 0x03, 0x04}; - byte[] value = {0x05, 0x06, 0x07, 0x08}; - - CacheSetResponse setResponse = cache.set(key, value, 3); - assertEquals(MomentoCacheResult.Ok, setResponse.result()); - - CacheGetResponse getResponse = cache.get(key); - assertEquals(MomentoCacheResult.Hit, getResponse.result()); - assertArrayEquals(value, getResponse.byteArray().get()); - } - - @Test - public void nullKeyGet_throwsException() { - String nullKeyString = null; - assertThrows(ClientSdkException.class, () -> cache.get(nullKeyString)); - assertThrows(ClientSdkException.class, () -> cache.getAsync(nullKeyString)); - - byte[] nullByteKey = null; - assertThrows(ClientSdkException.class, () -> cache.get(nullByteKey)); - assertThrows(ClientSdkException.class, () -> cache.getAsync(nullByteKey)); - } - - @Test - public void nullKeySet_throwsException() { - String nullKeyString = null; - // Blocking String key set - assertThrows(ClientSdkException.class, () -> cache.set(nullKeyString, "hello", 10)); - assertThrows( - ClientSdkException.class, () -> cache.set(nullKeyString, ByteBuffer.allocate(1), 10)); - // Async String key set - assertThrows(ClientSdkException.class, () -> cache.setAsync(nullKeyString, "hello", 10)); - assertThrows( - ClientSdkException.class, () -> cache.setAsync(nullKeyString, ByteBuffer.allocate(1), 10)); - - byte[] nullByteKey = null; - assertThrows(ClientSdkException.class, () -> cache.set(nullByteKey, new byte[] {0x00}, 10)); - assertThrows( - ClientSdkException.class, () -> cache.setAsync(nullByteKey, new byte[] {0x00}, 10)); - } - - @Test - public void nullValueSet_throwsException() { - assertThrows(ClientSdkException.class, () -> cache.set("hello", (String) null, 10)); - assertThrows(ClientSdkException.class, () -> cache.set("hello", (ByteBuffer) null, 10)); - assertThrows(ClientSdkException.class, () -> cache.set(new byte[] {}, null, 10)); - - assertThrows(ClientSdkException.class, () -> cache.setAsync("hello", (String) null, 10)); - assertThrows(ClientSdkException.class, () -> cache.setAsync("hello", (ByteBuffer) null, 10)); - assertThrows(ClientSdkException.class, () -> cache.setAsync(new byte[] {}, null, 10)); - } - - @Test - public void ttlMustBePositive_throwsException() { - for (int i = -1; i <= 0; i++) { - final int j = i; - assertThrows(ClientSdkException.class, () -> cache.set("hello", "world", j)); - assertThrows(ClientSdkException.class, () -> cache.set("hello", ByteBuffer.allocate(1), j)); - assertThrows(ClientSdkException.class, () -> cache.set(new byte[] {}, new byte[] {}, j)); - } - - for (int i = -1; i <= 0; i++) { - final int j = i; - - assertThrows(ClientSdkException.class, () -> cache.setAsync("hello", "", j)); - assertThrows( - ClientSdkException.class, () -> cache.setAsync("hello", ByteBuffer.allocate(1), j)); - assertThrows(ClientSdkException.class, () -> cache.setAsync(new byte[] {}, new byte[] {}, j)); - } - } - - /** ================ HELPER FUNCTIONS ====================================== */ - OpenTelemetrySdk setOtelSDK() { - String otelGwUrl = "0.0.0.0"; - // this is due to the cfn export format we are using for the vpc endpoints - String serviceUrl = "http://" + otelGwUrl + ":" + "4317"; - OtlpGrpcSpanExporter spanExporter = - OtlpGrpcSpanExporter.builder() - .setEndpoint(serviceUrl) - .setTimeout(2, TimeUnit.SECONDS) - .build(); - BatchSpanProcessor spanProcessor = - BatchSpanProcessor.builder(spanExporter).setScheduleDelay(1, TimeUnit.MILLISECONDS).build(); - SdkTracerProvider sdkTracerProvider = - SdkTracerProvider.builder().addSpanProcessor(spanProcessor).build(); - OpenTelemetrySdk openTelemetry = - OpenTelemetrySdk.builder() - .setTracerProvider(sdkTracerProvider) - .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) - .build(); - Runtime.getRuntime().addShutdownHook(new Thread(sdkTracerProvider::shutdown)); - return openTelemetry; - } - - void startIntegrationTestOtel() throws Exception { - stopIntegrationTestOtel(); - shellex( - "docker run -p 4317:4317 --name integtest_otelcol " - + " -v ${PWD}/otel-collector-config.yaml:/etc/otel-collector-config.yaml " - + " -d otel/opentelemetry-collector:latest " - + " --config=/etc/otel-collector-config.yaml", - true, - "started local otel container for integration testing", - "failed to bootstrap integration test local otel container"); - shellExpect( - 5, - "docker logs integtest_otelcol 2>& 1", - ".*Everything is ready. Begin running and processing data.*"); - } - - void stopIntegrationTestOtel() throws Exception { - shellex( - "docker stop integtest_otelcol && docker rm integtest_otelcol", - false, - "successfully stopped otelcol test container", - "it is okay for this to fail because maybe it is not present"); - } - - void verifySetTrace(String expectedCount) throws Exception { - String count = - shellex( - "docker logs integtest_otelcol 2>& 1 | grep Name | grep java-sdk-set-request | wc -l", - true, - "Verify set trace", - "failed to verify set trace"); - assertEquals(expectedCount, count.trim()); - } - - void verifyGetTrace(String expectedCount) throws Exception { - String count = - shellex( - "docker logs integtest_otelcol 2>& 1 | grep Name | grep java-sdk-get-request | wc -l", - true, - "Verify get trace", - "failed to verify get trace"); - assertEquals(expectedCount, count.trim()); - } - - // Polls a command until the expected result comes back - private void shellExpect(double timeoutSeconds, String command, String outputRegex) - throws Exception { - long start = System.currentTimeMillis(); - String lastOutput = ""; - - while ((System.currentTimeMillis() - start) / 1000.0 < timeoutSeconds) { - lastOutput = shellex(command, false, null, null); - if (lastOutput.matches(outputRegex)) { - return; - } - } - - throw new InternalError(String.format("Never got expected output. Last:\n%s", lastOutput)); - } - - private String shellex( - String command, Boolean expectSuccess, String successDescription, String failDescription) - throws Exception { - ProcessBuilder processBuilder = new ProcessBuilder(); - processBuilder.command("sh", "-c", command); - Process process = processBuilder.start(); - - int exitCode = process.waitFor(); - - String output = ""; - try (BufferedReader reader = - new BufferedReader(new InputStreamReader(process.getInputStream()))) { - - String line; - while ((line = reader.readLine()) != null) { - System.out.println(line); - output += line; - } - } - - if (exitCode == 0) { - if (successDescription != null) { - System.out.println(successDescription); - } - } else if (expectSuccess) { - throw new InternalError(failDescription + "exit_code:" + exitCode); - } else { - if (failDescription != null) { - System.out.println(failDescription); - } - } - - return output; - } -} diff --git a/momento-sdk/src/intTest/java/momento/sdk/MomentoTest.java b/momento-sdk/src/intTest/java/momento/sdk/MomentoTest.java deleted file mode 100644 index 22facbd4..00000000 --- a/momento-sdk/src/intTest/java/momento/sdk/MomentoTest.java +++ /dev/null @@ -1,156 +0,0 @@ -package momento.sdk; - -import static momento.sdk.TestHelpers.DEFAULT_MOMENTO_HOSTED_ZONE_ENDPOINT; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.UUID; -import java.util.stream.Collectors; -import momento.sdk.exceptions.CacheAlreadyExistsException; -import momento.sdk.exceptions.CacheNotFoundException; -import momento.sdk.exceptions.ClientSdkException; -import momento.sdk.exceptions.InvalidArgumentException; -import momento.sdk.messages.CacheGetResponse; -import momento.sdk.messages.CacheInfo; -import momento.sdk.messages.CacheSetResponse; -import momento.sdk.messages.ListCachesRequest; -import momento.sdk.messages.ListCachesResponse; -import momento.sdk.messages.MomentoCacheResult; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -final class MomentoTest { - - private String authToken; - private String cacheName; - - @BeforeAll - static void beforeAll() { - if (System.getenv("TEST_AUTH_TOKEN") == null) { - throw new IllegalArgumentException( - "Integration tests require TEST_AUTH_TOKEN env var; see README for more details."); - } - if (System.getenv("TEST_CACHE_NAME") == null) { - throw new IllegalArgumentException( - "Integration tests require TEST_CACHE_NAME env var; see README for more details."); - } - } - - @BeforeEach - void setup() { - this.authToken = System.getenv("TEST_AUTH_TOKEN"); - this.cacheName = System.getenv("TEST_CACHE_NAME"); - } - - @Test - void testHappyPath() { - Momento momento = Momento.builder(authToken).build(); - runHappyPathTest(momento, cacheName); - } - - @Test - void recreatingCacheWithSameName_throwsAlreadyExists() { - Momento momento = Momento.builder(authToken).build(); - momento.cacheBuilder(cacheName, 2).createCacheIfDoesntExist().build(); - assertThrows(CacheAlreadyExistsException.class, () -> momento.createCache(cacheName)); - } - - @Test - void testInvalidCacheName() { - Momento momento = - Momento.builder(authToken).endpointOverride(DEFAULT_MOMENTO_HOSTED_ZONE_ENDPOINT).build(); - - assertThrows(InvalidArgumentException.class, () -> momento.createCache(" ")); - assertThrows( - InvalidArgumentException.class, - () -> momento.cacheBuilder(" ", 2).createCacheIfDoesntExist().build()); - } - - @Test - void clientWithEndpointOverride_succeeds() { - Momento momentoWithEndpointOverride = - Momento.builder(authToken).endpointOverride(DEFAULT_MOMENTO_HOSTED_ZONE_ENDPOINT).build(); - runHappyPathTest(momentoWithEndpointOverride, cacheName); - } - - @Test - void deleteCacheTest_succeeds() { - String cacheName = "deleteCacheTest_succeeds-" + Math.random(); - Momento momento = Momento.builder(authToken).build(); - momento.createCache(cacheName); - momento.cacheBuilder(cacheName, 2).build(); - momento.deleteCache(cacheName); - } - - @Test - void deleteForNonExistentCache_throwsNotFound() { - String cacheName = "deleteCacheTest_failure-" + Math.random(); - Momento momento = Momento.builder(authToken).build(); - assertThrows(CacheNotFoundException.class, () -> momento.deleteCache(cacheName)); - } - - @Test - void nonPositiveTtl_throwsException() { - Momento momento = Momento.builder(authToken).build(); - assertThrows(ClientSdkException.class, () -> momento.cacheBuilder(cacheName, -1).build()); - } - - @Test - void buildingCacheClientForNonExistentCache_throwsException() { - Momento momento = Momento.builder(authToken).build(); - assertThrows( - CacheNotFoundException.class, - () -> momento.cacheBuilder(UUID.randomUUID().toString(), 60).build()); - } - - @Test - void createCacheViaBuilder_succeeds() { - Momento momento = Momento.builder(authToken).build(); - String cacheName = UUID.randomUUID().toString(); - Cache cache = momento.cacheBuilder(cacheName, 60).createCacheIfDoesntExist().build(); - cache.set("key", "value"); - assertEquals("value", cache.get("key").string().get()); - momento.deleteCache(cacheName); - } - - @Test - void listCachesMustIncludeCreatedCache() { - Momento momento = Momento.builder(authToken).build(); - String cacheName = UUID.randomUUID().toString(); - momento.createCache(cacheName); - try { - ListCachesResponse response = momento.listCaches(ListCachesRequest.builder().build()); - assertTrue(response.caches().size() >= 1); - assertTrue( - response.caches().stream() - .map(CacheInfo::name) - .collect(Collectors.toSet()) - .contains(cacheName)); - assertFalse(response.nextPageToken().isPresent()); - } finally { - // cleanup - momento.deleteCache(cacheName); - } - } - - private static void runHappyPathTest(Momento momento, String cacheName) { - Cache cache = momento.cacheBuilder(cacheName, 2).createCacheIfDoesntExist().build(); - - String key = java.util.UUID.randomUUID().toString(); - - // Set Key sync - CacheSetResponse setRsp = - cache.set(key, ByteBuffer.wrap("bar".getBytes(StandardCharsets.UTF_8)), 2); - assertEquals(MomentoCacheResult.Ok, setRsp.result()); - - // Get Key that was just set - CacheGetResponse rsp = cache.get(key); - assertEquals(MomentoCacheResult.Hit, rsp.result()); - assertEquals("bar", rsp.string().get()); - } -} diff --git a/momento-sdk/src/intTest/java/momento/sdk/OtelTestHelpers.java b/momento-sdk/src/intTest/java/momento/sdk/OtelTestHelpers.java new file mode 100644 index 00000000..39b99492 --- /dev/null +++ b/momento-sdk/src/intTest/java/momento/sdk/OtelTestHelpers.java @@ -0,0 +1,135 @@ +package momento.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.concurrent.TimeUnit; + +final class OtelTestHelpers { + + private OtelTestHelpers() {} + + static OpenTelemetrySdk setOtelSDK() { + String otelGwUrl = "0.0.0.0"; + // this is due to the cfn export format we are using for the vpc endpoints + String serviceUrl = "http://" + otelGwUrl + ":" + "4317"; + OtlpGrpcSpanExporter spanExporter = + OtlpGrpcSpanExporter.builder() + .setEndpoint(serviceUrl) + .setTimeout(2, TimeUnit.SECONDS) + .build(); + BatchSpanProcessor spanProcessor = + BatchSpanProcessor.builder(spanExporter).setScheduleDelay(1, TimeUnit.MILLISECONDS).build(); + SdkTracerProvider sdkTracerProvider = + SdkTracerProvider.builder().addSpanProcessor(spanProcessor).build(); + OpenTelemetrySdk openTelemetry = + OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .build(); + Runtime.getRuntime().addShutdownHook(new Thread(sdkTracerProvider::shutdown)); + return openTelemetry; + } + + static void startIntegrationTestOtel() throws Exception { + stopIntegrationTestOtel(); + shellex( + "docker run -p 4317:4317 --name integtest_otelcol " + + " -v ${PWD}/otel-collector-config.yaml:/etc/otel-collector-config.yaml " + + " -d otel/opentelemetry-collector:latest " + + " --config=/etc/otel-collector-config.yaml", + true, + "started local otel container for integration testing", + "failed to bootstrap integration test local otel container"); + shellExpect( + 5, + "docker logs integtest_otelcol 2>& 1", + ".*Everything is ready. Begin running and processing data.*"); + } + + static void stopIntegrationTestOtel() throws Exception { + shellex( + "docker stop integtest_otelcol && docker rm integtest_otelcol", + false, + "successfully stopped otelcol test container", + "it is okay for this to fail because maybe it is not present"); + } + + static void verifySetTrace(String expectedCount) throws Exception { + String count = + shellex( + "docker logs integtest_otelcol 2>& 1 | grep Name | grep java-sdk-set-request | wc -l", + true, + "Verify set trace", + "failed to verify set trace"); + assertEquals(expectedCount, count.trim()); + } + + static void verifyGetTrace(String expectedCount) throws Exception { + String count = + shellex( + "docker logs integtest_otelcol 2>& 1 | grep Name | grep java-sdk-get-request | wc -l", + true, + "Verify get trace", + "failed to verify get trace"); + assertEquals(expectedCount, count.trim()); + } + + // Polls a command until the expected result comes back + private static void shellExpect(double timeoutSeconds, String command, String outputRegex) + throws Exception { + long start = System.currentTimeMillis(); + String lastOutput = ""; + + while ((System.currentTimeMillis() - start) / 1000.0 < timeoutSeconds) { + lastOutput = shellex(command, false, null, null); + if (lastOutput.matches(outputRegex)) { + return; + } + } + + throw new InternalError(String.format("Never got expected output. Last:\n%s", lastOutput)); + } + + private static String shellex( + String command, Boolean expectSuccess, String successDescription, String failDescription) + throws Exception { + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.command("sh", "-c", command); + Process process = processBuilder.start(); + + int exitCode = process.waitFor(); + + String output = ""; + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(process.getInputStream()))) { + + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); + output += line; + } + } + + if (exitCode == 0) { + if (successDescription != null) { + System.out.println(successDescription); + } + } else if (expectSuccess) { + throw new InternalError(failDescription + "exit_code:" + exitCode); + } else { + if (failDescription != null) { + System.out.println(failDescription); + } + } + + return output; + } +} diff --git a/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheClientTest.java b/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheClientTest.java new file mode 100644 index 00000000..f49f692a --- /dev/null +++ b/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheClientTest.java @@ -0,0 +1,59 @@ +package momento.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.UUID; +import momento.sdk.exceptions.ValidationException; +import momento.sdk.messages.CacheGetResponse; +import momento.sdk.messages.CacheSetResponse; +import momento.sdk.messages.MomentoCacheResult; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Just includes a happy test path that interacts with both control and data plane clients. */ +final class SimpleCacheClientTest extends BaseTestClass { + + private static final int DEFAULT_TTL_SECONDS = 60; + + private SimpleCacheClient target; + + @BeforeEach + void setup() { + target = + SimpleCacheClient.builder(System.getenv("TEST_AUTH_TOKEN"), DEFAULT_TTL_SECONDS).build(); + } + + @AfterEach + void teardown() { + target.close(); + } + + @Test + public void createCacheGetSetValuesAndDeleteCache() { + String cacheName = UUID.randomUUID().toString(); + String key = UUID.randomUUID().toString(); + String value = UUID.randomUUID().toString(); + + target.createCache(cacheName); + CacheSetResponse response = target.set(cacheName, key, value); + assertEquals(MomentoCacheResult.Ok, response.result()); + + CacheGetResponse getResponse = target.get(cacheName, key); + assertEquals(MomentoCacheResult.Hit, getResponse.result()); + assertEquals(value, getResponse.string().get()); + + CacheGetResponse getForKeyInSomeOtherCache = target.get(System.getenv("TEST_CACHE_NAME"), key); + assertEquals(MomentoCacheResult.Miss, getForKeyInSomeOtherCache.result()); + + target.deleteCache(cacheName); + } + + @Test + public void throwsExceptionWhenClientUsesNegativeDefaultTtl() { + assertThrows( + ValidationException.class, + () -> SimpleCacheClient.builder(System.getenv("TEST_AUTH_TOKEN"), -1).build()); + } +} diff --git a/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheControlPlaneTest.java b/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheControlPlaneTest.java new file mode 100644 index 00000000..991d1666 --- /dev/null +++ b/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheControlPlaneTest.java @@ -0,0 +1,102 @@ +package momento.sdk; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.UUID; +import java.util.stream.Collectors; +import momento.sdk.exceptions.CacheAlreadyExistsException; +import momento.sdk.exceptions.CacheNotFoundException; +import momento.sdk.exceptions.InvalidArgumentException; +import momento.sdk.exceptions.PermissionDeniedException; +import momento.sdk.exceptions.ValidationException; +import momento.sdk.messages.CacheInfo; +import momento.sdk.messages.ListCachesRequest; +import momento.sdk.messages.ListCachesResponse; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +final class SimpleCacheControlPlaneTest extends BaseTestClass { + + private static final int DEFAULT_TTL_SECONDS = 60; + + private SimpleCacheClient target; + + @BeforeEach + void setup() { + target = + SimpleCacheClient.builder(System.getenv("TEST_AUTH_TOKEN"), DEFAULT_TTL_SECONDS).build(); + } + + @AfterEach + void tearDown() { + target.close(); + } + + @Test + public void throwsAlreadyExistsWhenCreatingExistingCache() { + String existingCache = System.getenv("TEST_CACHE_NAME"); + assertThrows(CacheAlreadyExistsException.class, () -> target.createCache(existingCache)); + } + + @Test + public void throwsNotFoundWhenDeletingUnknownCache() { + String doesNotExistCache = UUID.randomUUID().toString(); + assertThrows(CacheNotFoundException.class, () -> target.deleteCache(doesNotExistCache)); + } + + @Test + public void listsCachesSuccessfully() { + String cacheName = UUID.randomUUID().toString(); + target.createCache(cacheName); + try { + ListCachesResponse response = target.listCaches(ListCachesRequest.builder().build()); + assertTrue(response.caches().size() >= 1); + assertTrue( + response.caches().stream() + .map(CacheInfo::name) + .collect(Collectors.toSet()) + .contains(cacheName)); + assertFalse(response.nextPageToken().isPresent()); + } finally { + // cleanup + target.deleteCache(cacheName); + } + } + + @Test + public void throwsInvalidArgumentForEmptyCacheName() { + assertThrows(InvalidArgumentException.class, () -> target.createCache(" ")); + } + + @Test + public void throwsValidationExceptionForNullCacheName() { + assertThrows(ValidationException.class, () -> target.createCache(null)); + assertThrows(ValidationException.class, () -> target.deleteCache(null)); + } + + @Test + public void deleteSucceeds() { + String cacheName = UUID.randomUUID().toString(); + target.createCache(cacheName); + assertThrows(CacheAlreadyExistsException.class, () -> target.createCache(cacheName)); + target.deleteCache(cacheName); + assertThrows(CacheNotFoundException.class, () -> target.deleteCache(cacheName)); + } + + @Test + public void throwsPemissionDeniedForBadToken() { + String cacheName = UUID.randomUUID().toString(); + String badToken = + "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJpbnRlZ3JhdGlvbiIsImNwIjoiY29udHJvbC5jZWxsLWFscGhhLWRldi5wcmVwcm9kLmEubW9tZW50b2hxLmNvbSIsImMiOiJjYWNoZS5jZWxsLWFscGhhLWRldi5wcmVwcm9kLmEubW9tZW50b2hxLmNvbSJ9.gdghdjjfjyehhdkkkskskmmls76573jnajhjjjhjdhnndy"; + SimpleCacheClient target = SimpleCacheClient.builder(badToken, 10).build(); + assertThrows(PermissionDeniedException.class, () -> target.createCache(cacheName)); + + assertThrows(PermissionDeniedException.class, () -> target.deleteCache(cacheName)); + assertThrows( + PermissionDeniedException.class, + () -> target.listCaches(ListCachesRequest.builder().build())); + } +} diff --git a/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheDataPlaneAsyncTest.java b/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheDataPlaneAsyncTest.java new file mode 100644 index 00000000..e40c0e2b --- /dev/null +++ b/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheDataPlaneAsyncTest.java @@ -0,0 +1,171 @@ +package momento.sdk; + +import static momento.sdk.OtelTestHelpers.setOtelSDK; +import static momento.sdk.OtelTestHelpers.startIntegrationTestOtel; +import static momento.sdk.OtelTestHelpers.stopIntegrationTestOtel; +import static momento.sdk.OtelTestHelpers.verifyGetTrace; +import static momento.sdk.OtelTestHelpers.verifySetTrace; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.nio.charset.Charset; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import momento.sdk.exceptions.CacheNotFoundException; +import momento.sdk.exceptions.PermissionDeniedException; +import momento.sdk.messages.CacheGetResponse; +import momento.sdk.messages.CacheSetResponse; +import momento.sdk.messages.MomentoCacheResult; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Tests with Async APIs. */ +final class SimpleCacheDataPlaneAsyncTest extends BaseTestClass { + + private static final int DEFAULT_ITEM_TTL_SECONDS = 60; + private String authToken; + private String cacheName; + + @BeforeEach + void setup() { + authToken = System.getenv("TEST_AUTH_TOKEN"); + cacheName = System.getenv("TEST_CACHE_NAME"); + } + + @AfterEach + void tearDown() throws Exception { + stopIntegrationTestOtel(); + } + + @Test + void getReturnsHitAfterSet() throws Exception { + runSetAndGetWithHitTest(SimpleCacheClient.builder(authToken, DEFAULT_ITEM_TTL_SECONDS).build()); + } + + @Test + void getReturnsHitAfterSetWithTracing() throws Exception { + startIntegrationTestOtel(); + OpenTelemetrySdk openTelemetry = setOtelSDK(); + + runSetAndGetWithHitTest( + new SimpleCacheClient(authToken, DEFAULT_ITEM_TTL_SECONDS, Optional.of(openTelemetry))); + + // To accommodate for delays in tracing logs to appear in docker + Thread.sleep(1000); + verifySetTrace("1"); + verifyGetTrace("1"); + } + + @Test + void cacheMissSuccess() throws Exception { + runMissTest(SimpleCacheClient.builder(authToken, DEFAULT_ITEM_TTL_SECONDS).build()); + } + + @Test + void cacheMissSuccessWithTracing() throws Exception { + startIntegrationTestOtel(); + OpenTelemetrySdk openTelemetry = setOtelSDK(); + + runMissTest( + new SimpleCacheClient(authToken, DEFAULT_ITEM_TTL_SECONDS, Optional.of(openTelemetry))); + + // To accommodate for delays in tracing logs to appear in docker + Thread.sleep(1000); + verifySetTrace("0"); + verifyGetTrace("1"); + } + + @Test + void itemDroppedAfterTtlExpires() throws Exception { + runTtlTest(SimpleCacheClient.builder(authToken, DEFAULT_ITEM_TTL_SECONDS).build()); + } + + @Test + void itemDroppedAfterTtlExpiresWithTracing() throws Exception { + startIntegrationTestOtel(); + OpenTelemetrySdk openTelemetry = setOtelSDK(); + + runTtlTest( + new SimpleCacheClient(authToken, DEFAULT_ITEM_TTL_SECONDS, Optional.of(openTelemetry))); + + // To accommodate for delays in tracing logs to appear in docker + Thread.sleep(1000); + verifySetTrace("1"); + verifyGetTrace("1"); + } + + @Test + void badTokenThrowsPermissionDenied() { + String badToken = + "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJpbnRlZ3JhdGlvbiIsImNwIjoiY29udHJvbC5jZWxsLWFscGhhLWRldi5wcmVwcm9kLmEubW9tZW50b2hxLmNvbSIsImMiOiJjYWNoZS5jZWxsLWFscGhhLWRldi5wcmVwcm9kLmEubW9tZW50b2hxLmNvbSJ9.gdghdjjfjyehhdkkkskskmmls76573jnajhjjjhjdhnndy"; + SimpleCacheClient target = + SimpleCacheClient.builder(badToken, DEFAULT_ITEM_TTL_SECONDS).build(); + ExecutionException e = + assertThrows(ExecutionException.class, () -> target.getAsync(cacheName, "").get()); + assertTrue(e.getCause() instanceof PermissionDeniedException); + } + + @Test + public void nonExistentCacheNameThrowsNotFoundOnGetOrSet() { + SimpleCacheClient target = + SimpleCacheClient.builder(authToken, DEFAULT_ITEM_TTL_SECONDS).build(); + String cacheName = UUID.randomUUID().toString(); + + ExecutionException setException = + assertThrows(ExecutionException.class, () -> target.getAsync(cacheName, "").get()); + assertTrue(setException.getCause() instanceof CacheNotFoundException); + + ExecutionException getException = + assertThrows(ExecutionException.class, () -> target.setAsync(cacheName, "", "", 10).get()); + assertTrue(getException.getCause() instanceof CacheNotFoundException); + } + + private void runSetAndGetWithHitTest(SimpleCacheClient target) throws Exception { + String key = UUID.randomUUID().toString(); + String value = UUID.randomUUID().toString(); + + // Successful Set + CompletableFuture setResponse = target.setAsync(cacheName, key, value); + assertEquals(MomentoCacheResult.Ok, setResponse.get().result()); + + // Successful Get with Hit + CompletableFuture getResponse = target.getAsync(cacheName, key); + assertEquals(MomentoCacheResult.Hit, getResponse.get().result()); + assertEquals(value, getResponse.get().string().get()); + } + + private void runTtlTest(SimpleCacheClient target) throws Exception { + String key = UUID.randomUUID().toString(); + + // Set Key sync + CompletableFuture setRsp = target.setAsync(cacheName, key, "", 1); + assertEquals(MomentoCacheResult.Ok, setRsp.get().result()); + + Thread.sleep(1500); + + // Get Key that was just set + CompletableFuture rsp = target.getAsync(cacheName, key); + assertEquals(MomentoCacheResult.Miss, rsp.get().result()); + assertFalse(rsp.get().string().isPresent()); + } + + private void runMissTest(SimpleCacheClient target) throws Exception { + // Get Key that was just set + CompletableFuture rsFuture = + target.getAsync(cacheName, UUID.randomUUID().toString()); + + CacheGetResponse rsp = rsFuture.get(); + assertEquals(MomentoCacheResult.Miss, rsp.result()); + assertFalse(rsp.inputStream().isPresent()); + assertFalse(rsp.byteArray().isPresent()); + assertFalse(rsp.byteBuffer().isPresent()); + assertFalse(rsp.string().isPresent()); + assertFalse(rsp.string(Charset.defaultCharset()).isPresent()); + } +} diff --git a/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheDataPlaneBlockingTest.java b/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheDataPlaneBlockingTest.java new file mode 100644 index 00000000..5915a1c5 --- /dev/null +++ b/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheDataPlaneBlockingTest.java @@ -0,0 +1,175 @@ +package momento.sdk; + +import static momento.sdk.OtelTestHelpers.setOtelSDK; +import static momento.sdk.OtelTestHelpers.startIntegrationTestOtel; +import static momento.sdk.OtelTestHelpers.stopIntegrationTestOtel; +import static momento.sdk.OtelTestHelpers.verifyGetTrace; +import static momento.sdk.OtelTestHelpers.verifySetTrace; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.nio.charset.Charset; +import java.util.Optional; +import java.util.UUID; +import momento.sdk.exceptions.CacheNotFoundException; +import momento.sdk.exceptions.PermissionDeniedException; +import momento.sdk.messages.CacheGetResponse; +import momento.sdk.messages.CacheSetResponse; +import momento.sdk.messages.MomentoCacheResult; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Tests with Blocking APIs. */ +final class SimpleCacheDataPlaneBlockingTest extends BaseTestClass { + + private static final int DEFAULT_ITEM_TTL_SECONDS = 60; + private String authToken; + private String cacheName; + + @BeforeEach + void setup() { + authToken = System.getenv("TEST_AUTH_TOKEN"); + cacheName = System.getenv("TEST_CACHE_NAME"); + } + + @AfterEach + void tearDown() throws Exception { + stopIntegrationTestOtel(); + } + + @Test + void getReturnsHitAfterSet() { + runSetAndGetWithHitTest(SimpleCacheClient.builder(authToken, DEFAULT_ITEM_TTL_SECONDS).build()); + } + + @Test + void getReturnsHitAfterSetWithTracing() throws Exception { + startIntegrationTestOtel(); + OpenTelemetrySdk openTelemetry = setOtelSDK(); + + runSetAndGetWithHitTest( + new SimpleCacheClient(authToken, DEFAULT_ITEM_TTL_SECONDS, Optional.of(openTelemetry))); + + // To accommodate for delays in tracing logs to appear in docker + Thread.sleep(1000); + verifySetTrace("1"); + verifyGetTrace("1"); + } + + @Test + void cacheMissSuccess() { + runMissTest(SimpleCacheClient.builder(authToken, DEFAULT_ITEM_TTL_SECONDS).build()); + } + + @Test + void cacheMissSuccessWithTracing() throws Exception { + startIntegrationTestOtel(); + OpenTelemetrySdk openTelemetry = setOtelSDK(); + + runMissTest( + new SimpleCacheClient(authToken, DEFAULT_ITEM_TTL_SECONDS, Optional.of(openTelemetry))); + + // To accommodate for delays in tracing logs to appear in docker + Thread.sleep(1000); + verifySetTrace("0"); + verifyGetTrace("1"); + } + + @Test + void itemDroppedAfterTtlExpires() throws Exception { + runTtlTest(SimpleCacheClient.builder(authToken, DEFAULT_ITEM_TTL_SECONDS).build()); + } + + @Test + void itemDroppedAfterTtlExpiresWithTracing() throws Exception { + startIntegrationTestOtel(); + OpenTelemetrySdk openTelemetry = setOtelSDK(); + + runTtlTest( + new SimpleCacheClient(authToken, DEFAULT_ITEM_TTL_SECONDS, Optional.of(openTelemetry))); + + // To accommodate for delays in tracing logs to appear in docker + Thread.sleep(1000); + verifySetTrace("1"); + verifyGetTrace("1"); + } + + @Test + public void badTokenThrowsPermissionDenied() { + String badToken = + "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJpbnRlZ3JhdGlvbiIsImNwIjoiY29udHJvbC5jZWxsLWFscGhhLWRldi5wcmVwcm9kLmEubW9tZW50b2hxLmNvbSIsImMiOiJjYWNoZS5jZWxsLWFscGhhLWRldi5wcmVwcm9kLmEubW9tZW50b2hxLmNvbSJ9.gdghdjjfjyehhdkkkskskmmls76573jnajhjjjhjdhnndy"; + SimpleCacheClient target = + SimpleCacheClient.builder(badToken, DEFAULT_ITEM_TTL_SECONDS).build(); + assertThrows(PermissionDeniedException.class, () -> target.get(cacheName, "")); + } + + @Test + public void nonExistentCacheNameThrowsNotFoundOnGetOrSet() { + SimpleCacheClient target = + SimpleCacheClient.builder(authToken, DEFAULT_ITEM_TTL_SECONDS).build(); + String cacheName = UUID.randomUUID().toString(); + + assertThrows(CacheNotFoundException.class, () -> target.get(cacheName, "")); + + assertThrows(CacheNotFoundException.class, () -> target.set(cacheName, "", "", 10)); + } + + @Test + public void setAndGetWithByteKeyValuesMustSucceed() { + byte[] key = {0x01, 0x02, 0x03, 0x04}; + byte[] value = {0x05, 0x06, 0x07, 0x08}; + SimpleCacheClient cache = + SimpleCacheClient.builder(authToken, DEFAULT_ITEM_TTL_SECONDS).build(); + CacheSetResponse setResponse = cache.set(cacheName, key, value, 60); + assertEquals(MomentoCacheResult.Ok, setResponse.result()); + + CacheGetResponse getResponse = cache.get(cacheName, key); + assertEquals(MomentoCacheResult.Hit, getResponse.result()); + assertArrayEquals(value, getResponse.byteArray().get()); + } + + private void runSetAndGetWithHitTest(SimpleCacheClient target) { + String key = UUID.randomUUID().toString(); + String value = UUID.randomUUID().toString(); + + // Successful Set + CacheSetResponse setResponse = target.set(cacheName, key, value); + assertEquals(MomentoCacheResult.Ok, setResponse.result()); + + // Successful Get with Hit + CacheGetResponse getResponse = target.get(cacheName, key); + assertEquals(MomentoCacheResult.Hit, getResponse.result()); + assertEquals(value, getResponse.string().get()); + } + + private void runTtlTest(SimpleCacheClient target) throws Exception { + String key = UUID.randomUUID().toString(); + + // Set Key sync + CacheSetResponse setRsp = target.set(cacheName, key, "", 1); + assertEquals(MomentoCacheResult.Ok, setRsp.result()); + + Thread.sleep(1500); + + // Get Key that was just set + CacheGetResponse rsp = target.get(cacheName, key); + assertEquals(MomentoCacheResult.Miss, rsp.result()); + assertFalse(rsp.string().isPresent()); + } + + private void runMissTest(SimpleCacheClient target) { + // Get Key that was just set + CacheGetResponse rsp = target.get(cacheName, UUID.randomUUID().toString()); + + assertEquals(MomentoCacheResult.Miss, rsp.result()); + assertFalse(rsp.inputStream().isPresent()); + assertFalse(rsp.byteArray().isPresent()); + assertFalse(rsp.byteBuffer().isPresent()); + assertFalse(rsp.string().isPresent()); + assertFalse(rsp.string(Charset.defaultCharset()).isPresent()); + } +} diff --git a/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheDataPlaneClientSideTest.java b/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheDataPlaneClientSideTest.java new file mode 100644 index 00000000..467bb8a7 --- /dev/null +++ b/momento-sdk/src/intTest/java/momento/sdk/SimpleCacheDataPlaneClientSideTest.java @@ -0,0 +1,105 @@ +package momento.sdk; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.ByteBuffer; +import momento.sdk.exceptions.ClientSdkException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Tests client side exceptions */ +final class SimpleCacheDataPlaneClientSideTest extends BaseTestClass { + + private static final int DEFAULT_ITEM_TTL_SECONDS = 60; + private String authToken; + private String cacheName; + private SimpleCacheClient client; + + @BeforeEach + void setup() { + authToken = System.getenv("TEST_AUTH_TOKEN"); + cacheName = System.getenv("TEST_CACHE_NAME"); + client = SimpleCacheClient.builder(authToken, DEFAULT_ITEM_TTL_SECONDS).build(); + } + + @AfterEach + void teardown() { + client.close(); + } + + @Test + public void nullKeyGetThrowsException() { + String nullKeyString = null; + assertThrows(ClientSdkException.class, () -> client.get(cacheName, nullKeyString)); + assertThrows(ClientSdkException.class, () -> client.getAsync(cacheName, nullKeyString)); + + byte[] nullByteKey = null; + assertThrows(ClientSdkException.class, () -> client.get(cacheName, nullByteKey)); + assertThrows(ClientSdkException.class, () -> client.getAsync(cacheName, nullByteKey)); + } + + @Test + public void nullKeySetThrowsException() { + String nullKeyString = null; + // Blocking String key set + assertThrows(ClientSdkException.class, () -> client.set(cacheName, nullKeyString, "hello", 10)); + assertThrows( + ClientSdkException.class, + () -> client.set(cacheName, nullKeyString, ByteBuffer.allocate(1), 10)); + // Async String key set + assertThrows( + ClientSdkException.class, () -> client.setAsync(cacheName, nullKeyString, "hello", 10)); + assertThrows( + ClientSdkException.class, + () -> client.setAsync(cacheName, nullKeyString, ByteBuffer.allocate(1), 10)); + + byte[] nullByteKey = null; + assertThrows( + ClientSdkException.class, () -> client.set(cacheName, nullByteKey, new byte[] {0x00}, 10)); + assertThrows( + ClientSdkException.class, + () -> client.setAsync(cacheName, nullByteKey, new byte[] {0x00}, 10)); + } + + @Test + public void nullValueSetThrowsException() { + assertThrows(ClientSdkException.class, () -> client.set(cacheName, "hello", (String) null, 10)); + assertThrows( + ClientSdkException.class, () -> client.set(cacheName, "hello", (ByteBuffer) null, 10)); + assertThrows(ClientSdkException.class, () -> client.set(cacheName, new byte[] {}, null, 10)); + + assertThrows( + ClientSdkException.class, () -> client.setAsync(cacheName, "hello", (String) null, 10)); + assertThrows( + ClientSdkException.class, () -> client.setAsync(cacheName, "hello", (ByteBuffer) null, 10)); + assertThrows( + ClientSdkException.class, () -> client.setAsync(cacheName, new byte[] {}, null, 10)); + } + + @Test + public void ttlMustNotBeNegativeThrowsException() { + assertThrows(ClientSdkException.class, () -> client.set(cacheName, "hello", "world", -1)); + assertThrows( + ClientSdkException.class, () -> client.set(cacheName, "hello", ByteBuffer.allocate(1), -1)); + assertThrows( + ClientSdkException.class, () -> client.set(cacheName, new byte[] {}, new byte[] {}, -1)); + + assertThrows(ClientSdkException.class, () -> client.setAsync(cacheName, "hello", "", -1)); + assertThrows( + ClientSdkException.class, + () -> client.setAsync(cacheName, "hello", ByteBuffer.allocate(1), -1)); + assertThrows( + ClientSdkException.class, + () -> client.setAsync(cacheName, new byte[] {}, new byte[] {}, -1)); + } + + @Test + public void nullCacheNameThrowsException() { + assertThrows(ClientSdkException.class, () -> client.get(null, "")); + assertThrows(ClientSdkException.class, () -> client.set(null, "", "", 10)); + + assertThrows(ClientSdkException.class, () -> client.getAsync(null, "").get()); + assertThrows(ClientSdkException.class, () -> client.setAsync(null, "", "", 10).get()); + } +} diff --git a/momento-sdk/src/intTest/java/momento/sdk/TestHelpers.java b/momento-sdk/src/intTest/java/momento/sdk/TestHelpers.java deleted file mode 100644 index 7d1ef23e..00000000 --- a/momento-sdk/src/intTest/java/momento/sdk/TestHelpers.java +++ /dev/null @@ -1,11 +0,0 @@ -package momento.sdk; - -public final class TestHelpers { - - private TestHelpers() {} - - public static final String DEFAULT_MOMENTO_HOSTED_ZONE_ENDPOINT = - "cell-alpha-dev.preprod.a.momentohq.com"; - public static final String DEFAULT_CACHE_ENDPOINT = - "cache." + DEFAULT_MOMENTO_HOSTED_ZONE_ENDPOINT; -} diff --git a/momento-sdk/src/main/java/momento/sdk/Cache.java b/momento-sdk/src/main/java/momento/sdk/Cache.java deleted file mode 100644 index 26a310b0..00000000 --- a/momento-sdk/src/main/java/momento/sdk/Cache.java +++ /dev/null @@ -1,664 +0,0 @@ -package momento.sdk; - -import static java.time.Instant.now; - -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.protobuf.ByteString; -import grpc.cache_client.GetRequest; -import grpc.cache_client.GetResponse; -import grpc.cache_client.ScsGrpc; -import grpc.cache_client.SetRequest; -import grpc.cache_client.SetResponse; -import io.grpc.ClientInterceptor; -import io.grpc.ManagedChannel; -import io.grpc.netty.GrpcSslContexts; -import io.grpc.netty.NettyChannelBuilder; -import io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.api.trace.StatusCode; -import io.opentelemetry.api.trace.Tracer; -import io.opentelemetry.context.ImplicitContextKeyed; -import io.opentelemetry.context.Scope; -import java.io.Closeable; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import javax.net.ssl.SSLException; -import momento.sdk.exceptions.CacheServiceExceptionMapper; -import momento.sdk.exceptions.ClientSdkException; -import momento.sdk.messages.CacheGetResponse; -import momento.sdk.messages.CacheSetResponse; - -/** Client to perform operations on cache. */ -public final class Cache implements Closeable { - private final ScsGrpc.ScsBlockingStub blockingStub; - private final ScsGrpc.ScsFutureStub futureStub; - private final ManagedChannel channel; - private final Optional tracer; - private final int itemDefaultTtlSeconds; - - Cache(String authToken, String cacheName, String endpoint, int itemDefaultTtlSeconds) { - this(authToken, cacheName, Optional.empty(), endpoint, itemDefaultTtlSeconds); - } - - Cache( - String authToken, - String cacheName, - Optional openTelemetry, - String endpoint, - int itemDefaultTtlSeconds) { - this(authToken, cacheName, openTelemetry, endpoint, itemDefaultTtlSeconds, false); - } - - Cache( - String authToken, - String cacheName, - Optional openTelemetry, - String endpoint, - int itemDefaultTtlSeconds, - boolean insecureSsl) { - this.channel = setupChannel(endpoint, authToken, cacheName, insecureSsl, openTelemetry); - this.blockingStub = ScsGrpc.newBlockingStub(channel); - this.futureStub = ScsGrpc.newFutureStub(channel); - this.tracer = openTelemetry.map(ot -> ot.getTracer("momento-java-scs-client", "1.0.0")); - this.itemDefaultTtlSeconds = itemDefaultTtlSeconds; - } - - private ManagedChannel setupChannel( - String endpoint, - String authToken, - String cacheName, - boolean insecureSsl, - Optional openTelemetry) { - NettyChannelBuilder channelBuilder = NettyChannelBuilder.forAddress(endpoint, 443); - if (insecureSsl) { - try { - channelBuilder.sslContext( - GrpcSslContexts.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build()); - } catch (SSLException e) { - throw new RuntimeException("Unable to use insecure trust manager", e); - } - } - channelBuilder.useTransportSecurity(); - channelBuilder.disableRetry(); - List clientInterceptors = new ArrayList<>(); - clientInterceptors.add(new AuthInterceptor(authToken)); - clientInterceptors.add(new CacheNameInterceptor(cacheName)); - openTelemetry.ifPresent( - theOpenTelemetry -> - clientInterceptors.add(new OpenTelemetryClientInterceptor(theOpenTelemetry))); - channelBuilder.intercept(clientInterceptors); - ManagedChannel channel = channelBuilder.build(); - return channel; - } - - // Runs a get using the provided cache name and the auth token. - // - // An alternate approach would be to make this call during construction itself. That however, may - // cause Cache object construction to fail and leave behind open grpc channels. Eventually those - // would be garbage collected. - // - // The separation between opening a grpc channel vs performing operations against the Momento - // Cache construct allows SDK builders a better control to manage objects. This is particularly - // useful for getOrCreateCache calls. Doing a get first is desirable as our data plane can take - // more load as compared to the control plane. However, if a cache doesn't exist the constructor - // may end up failing and then upon cache creation using the control plane a new server connection - // would have to establish. This paradigm is a minor but desirable optimization to prevent opening - // multiple channels and incurring the cost. - Cache connect() { - this.testConnection(); - return this; - } - - private void testConnection() { - try { - this.blockingStub.get(buildGetRequest(convert("000"))); - } catch (Exception e) { - throw CacheServiceExceptionMapper.convert(e); - } - } - - /** - * Get the cache value stored for the given key. - * - * @param key The key to get - * @return {@link CacheGetResponse} containing the status of the get operation and the associated - * value data. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key is null - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#getAsync(String) - */ - public CacheGetResponse get(String key) { - ensureValidKey(key); - return sendGet(convert(key)); - } - - /** - * Get the cache value stored for the given key. - * - * @param key The key to get - * @return {@link CacheGetResponse} containing the status of the get operation and the associated - * value data. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key is null - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#getAsync(byte[]) - */ - public CacheGetResponse get(byte[] key) { - ensureValidKey(key); - return sendGet(convert(key)); - } - - private CacheGetResponse sendGet(ByteString key) { - Optional span = buildSpan("java-sdk-get-request"); - try (Scope ignored = (span.map(ImplicitContextKeyed::makeCurrent).orElse(null))) { - GetResponse rsp = blockingStub.get(buildGetRequest(key)); - CacheGetResponse cacheGetResponse = new CacheGetResponse(rsp.getResult(), rsp.getCacheBody()); - span.ifPresent(theSpan -> theSpan.setStatus(StatusCode.OK)); - return cacheGetResponse; - } catch (Exception e) { - span.ifPresent( - theSpan -> { - theSpan.recordException(e); - theSpan.setStatus(StatusCode.ERROR); - }); - throw CacheServiceExceptionMapper.convert(e); - } finally { - span.ifPresent(theSpan -> theSpan.end(now())); - } - } - - /** - * Sets the value in cache with a given Time To Live (TTL) seconds. - * - *

If a value for this key is already present it will be replaced by the new value. - * - * @param key The key under which the value is to be added. - * @param value The value to be stored. - * @param ttlSeconds Time to Live for the item in Cache. This ttl takes precedence over the TTL - * used when building a cache client {@link Momento#cacheBuilder(String, int)} - * @return Result of the set operation. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key, value is null or if ttlSeconds is less than or equal to zero - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#set(String, ByteBuffer) - * @see Cache#setAsync(String, ByteBuffer, int) - */ - public CacheSetResponse set(String key, ByteBuffer value, int ttlSeconds) { - ensureValid(key, value, ttlSeconds); - return sendSet(convert(key), convert(value), ttlSeconds); - } - - /** - * Sets the value in the cache. If a value for this key is already present it will be replaced by - * the new value. - * - *

The Time to Live (TTL) seconds defaults to the parameter used when building this Cache - * client - {@link Momento#cacheBuilder(String, int)} - * - * @param key The key under which the value is to be added. - * @param value The value to be stored. - * @return Result of the set operation. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key or value is null - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#set(String, ByteBuffer, int) - * @see Cache#setAsync(String, ByteBuffer) - */ - public CacheSetResponse set(String key, ByteBuffer value) { - return set(key, value, itemDefaultTtlSeconds); - } - - /** - * Sets the value in cache with a given Time To Live (TTL) seconds. - * - *

If a value for this key is already present it will be replaced by the new value. - * - * @param key The key under which the value is to be added. - * @param value The value to be stored. - * @param ttlSeconds Time to Live for the item in Cache. This ttl takes precedence over the TTL - * used when building a cache client {@link Momento#cacheBuilder(String, int)} - * @return Result of the set operation. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key or value is null or ttlSeconds is less than or equal to zero - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#set(String, String) - * @see Cache#setAsync(String, String, int) - */ - public CacheSetResponse set(String key, String value, int ttlSeconds) { - ensureValid(key, value, ttlSeconds); - return sendSet(convert(key), convert(value), ttlSeconds); - } - - /** - * Sets the value in the cache. If a value for this key is already present it will be replaced by - * the new value. - * - *

The Time to Live (TTL) seconds defaults to the parameter used when building this Cache - * client - {@link Momento#cacheBuilder(String, int)} - * - * @param key The key under which the value is to be added. - * @param value The value to be stored. - * @return Result of the set operation. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key or value is null - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#set(String, String, int) - * @see Cache#setAsync(String, String) - */ - public CacheSetResponse set(String key, String value) { - return set(key, value, itemDefaultTtlSeconds); - } - - /** - * Sets the value in cache with a given Time To Live (TTL) seconds. - * - *

If a value for this key is already present it will be replaced by the new value. - * - * @param key The key under which the value is to be added. - * @param value The value to be stored. - * @param ttlSeconds Time to Live for the item in Cache. This ttl takes precedence over the TTL - * used when building a cache client {@link Momento#cacheBuilder(String, int)} - * @return Result of the set operation. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key or value is null or ttlSeconds is less than or equal to zero - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#set(byte[], byte[]) - * @see Cache#setAsync(byte[], byte[], int) - */ - public CacheSetResponse set(byte[] key, byte[] value, int ttlSeconds) { - ensureValid(key, value, ttlSeconds); - return sendSet(convert(key), convert(value), ttlSeconds); - } - - /** - * Sets the value in the cache. If a value for this key is already present it will be replaced by - * the new value. - * - *

The Time to Live (TTL) seconds defaults to the parameter used when building this Cache - * client - {@link Momento#cacheBuilder(String, int)} - * - * @param key The key under which the value is to be added. - * @param value The value to be stored. - * @return Result of the set operation. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key or value is null - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#set(byte[], byte[], int) - * @see Cache#setAsync(byte[], byte[]) - */ - public CacheSetResponse set(byte[] key, byte[] value) { - return set(key, value, itemDefaultTtlSeconds); - } - - // Having this method named as set causes client side compilation issues, where the compiler - // requires a dependency - // on com.google.protobuf.ByteString - private CacheSetResponse sendSet(ByteString key, ByteString value, int ttlSeconds) { - Optional span = buildSpan("java-sdk-set-request"); - try (Scope ignored = (span.map(ImplicitContextKeyed::makeCurrent).orElse(null))) { - SetResponse rsp = blockingStub.set(buildSetRequest(key, value, ttlSeconds * 1000)); - - CacheSetResponse response = new CacheSetResponse(rsp.getResult()); - span.ifPresent(theSpan -> theSpan.setStatus(StatusCode.OK)); - return response; - } catch (Exception e) { - span.ifPresent( - theSpan -> { - theSpan.recordException(e); - theSpan.setStatus(StatusCode.ERROR); - }); - throw CacheServiceExceptionMapper.convert(e); - } finally { - span.ifPresent(theSpan -> theSpan.end(now())); - } - } - - /** - * Get the cache value stored for the given key. - * - * @param key The key to get - * @return Future with {@link CacheGetResponse} containing the status of the get operation and the - * associated value data. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key is null - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#get(byte[]) - */ - public CompletableFuture getAsync(byte[] key) { - ensureValidKey(key); - return sendAsyncGet(convert(key)); - } - - /** - * Get the cache value stored for the given key. - * - * @param key The key to get - * @return Future with {@link CacheGetResponse} containing the status of the get operation and the - * associated value data. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key is null - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#get(String) - */ - public CompletableFuture getAsync(String key) { - ensureValidKey(key); - return sendAsyncGet(convert(key)); - } - - private CompletableFuture sendAsyncGet(ByteString key) { - Optional span = buildSpan("java-sdk-get-request"); - Optional scope = (span.map(ImplicitContextKeyed::makeCurrent)); - // Submit request to non-blocking stub - ListenableFuture rspFuture = futureStub.get(buildGetRequest(key)); - - // Build a CompletableFuture to return to caller - CompletableFuture returnFuture = - new CompletableFuture() { - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - // propagate cancel to the listenable future if called on returned completable future - boolean result = rspFuture.cancel(mayInterruptIfRunning); - super.cancel(mayInterruptIfRunning); - return result; - } - }; - - // Convert returned ListenableFuture to CompletableFuture - Futures.addCallback( - rspFuture, - new FutureCallback() { - @Override - public void onSuccess(GetResponse rsp) { - returnFuture.complete(new CacheGetResponse(rsp.getResult(), rsp.getCacheBody())); - span.ifPresent( - theSpan -> { - theSpan.setStatus(StatusCode.OK); - theSpan.end(now()); - }); - scope.ifPresent(Scope::close); - } - - @Override - public void onFailure(Throwable e) { - returnFuture.completeExceptionally(CacheServiceExceptionMapper.convert(e)); - span.ifPresent( - theSpan -> { - theSpan.setStatus(StatusCode.ERROR); - theSpan.recordException(e); - theSpan.end(now()); - }); - scope.ifPresent(Scope::close); - } - }, - MoreExecutors - .directExecutor()); // Execute on same thread that called execute on CompletionStage - // returned - - return returnFuture; - } - - /** - * Sets the value in cache with a given Time To Live (TTL) seconds. - * - *

If a value for this key is already present it will be replaced by the new value. - * - * @param key The key under which the value is to be added. - * @param value The value to be stored. - * @param ttlSeconds Time to Live for the item in Cache. This ttl takes precedence over the TTL - * used when building a cache client {@link Momento#cacheBuilder(String, int)} - * @return Future containing the result of the set operation. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key or value is null or ttlSeconds is less than or equal to zero - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#setAsync(String, ByteBuffer) - * @see Cache#set(String, ByteBuffer, int) - */ - public CompletableFuture setAsync( - String key, ByteBuffer value, int ttlSeconds) { - ensureValid(key, value, ttlSeconds); - return sendSetAsync(convert(key), convert(value), ttlSeconds); - } - - /** - * Sets the value in the cache. If a value for this key is already present it will be replaced by - * the new value. - * - *

The Time to Live (TTL) seconds defaults to the parameter used when building this Cache - * client - {@link Momento#cacheBuilder(String, int)} - * - * @param key The key under which the value is to be added. - * @param value The value to be stored. - * @return Future containing the result of the set operation. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key or value is null - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#set(String, ByteBuffer) - * @see Cache#setAsync(String, ByteBuffer, int) - */ - public CompletableFuture setAsync(String key, ByteBuffer value) { - return setAsync(key, value, itemDefaultTtlSeconds); - } - - /** - * Sets the value in cache with a given Time To Live (TTL) seconds. - * - *

If a value for this key is already present it will be replaced by the new value. - * - * @param key The key under which the value is to be added. - * @param value The value to be stored. - * @param ttlSeconds Time to Live for the item in Cache. This ttl takes precedence over the TTL - * used when building a cache client {@link Momento#cacheBuilder(String, int)} - * @return Future containing the result of the set operation. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key or value is null or ttlSeconds is less than or equal to zero - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#setAsync(byte[], byte[]) - * @see Cache#set(byte[], byte[], int) - */ - public CompletableFuture setAsync(byte[] key, byte[] value, int ttlSeconds) { - ensureValid(key, value, ttlSeconds); - return sendSetAsync(convert(key), convert(value), ttlSeconds); - } - - /** - * Sets the value in the cache. If a value for this key is already present it will be replaced by - * the new value. - * - *

The Time to Live (TTL) seconds defaults to the parameter used when building this Cache - * client - {@link Momento#cacheBuilder(String, int)} - * - * @param key The key under which the value is to be added. - * @param value The value to be stored. - * @return Future containing the result of the set operation. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key or value is null - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#setAsync(byte[], byte[], int) - * @see Cache#set(byte[], byte[]) - */ - public CompletableFuture setAsync(byte[] key, byte[] value) { - return setAsync(key, value, itemDefaultTtlSeconds); - } - - /** - * Sets the value in cache with a given Time To Live (TTL) seconds. - * - *

If a value for this key is already present it will be replaced by the new value. - * - * @param key The key under which the value is to be added. - * @param value The value to be stored. - * @param ttlSeconds Time to Live for the item in Cache. This ttl takes precedence over the TTL - * used when building a cache client {@link Momento#cacheBuilder(String, int)} - * @return Future containing the result of the set operation. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key or value is null or ttlSeconds is less than or equal to zero - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#setAsync(String, String) - * @see Cache#set(String, String, int) - */ - public CompletableFuture setAsync(String key, String value, int ttlSeconds) { - ensureValid(key, value, ttlSeconds); - return sendSetAsync(convert(key), convert(value), ttlSeconds); - } - - /** - * Sets the value in the cache. If a value for this key is already present it will be replaced by - * the new value. - * - *

The Time to Live (TTL) seconds defaults to the parameter used when building this Cache - * client - {@link Momento#cacheBuilder(String, int)} - * - * @param key The key under which the value is to be added. - * @param value The value to be stored. - * @return Future containing the result of the set operation. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws ClientSdkException if key or value is null - * @throws momento.sdk.exceptions.CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @see Cache#setAsync(String, String, int) - * @see Cache#set(String, String) - */ - public CompletableFuture setAsync(String key, String value) { - return setAsync(key, value, itemDefaultTtlSeconds); - } - - private CompletableFuture sendSetAsync( - ByteString key, ByteString value, int ttlSeconds) { - - Optional span = buildSpan("java-sdk-set-request"); - Optional scope = (span.map(ImplicitContextKeyed::makeCurrent)); - - // Submit request to non-blocking stub - ListenableFuture rspFuture = - futureStub.set(buildSetRequest(key, value, ttlSeconds * 1000)); - - // Build a CompletableFuture to return to caller - CompletableFuture returnFuture = - new CompletableFuture() { - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - // propagate cancel to the listenable future if called on returned completable future - boolean result = rspFuture.cancel(mayInterruptIfRunning); - super.cancel(mayInterruptIfRunning); - return result; - } - }; - - // Convert returned ListenableFuture to CompletableFuture - Futures.addCallback( - rspFuture, - new FutureCallback() { - @Override - public void onSuccess(SetResponse rsp) { - returnFuture.complete(new CacheSetResponse(rsp.getResult())); - span.ifPresent( - theSpan -> { - theSpan.setStatus(StatusCode.OK); - theSpan.end(now()); - }); - scope.ifPresent(Scope::close); - } - - @Override - public void onFailure(Throwable e) { - returnFuture.completeExceptionally( - CacheServiceExceptionMapper.convert(e)); // bubble all errors up - span.ifPresent( - theSpan -> { - theSpan.setStatus(StatusCode.ERROR); - theSpan.recordException(e); - theSpan.end(now()); - }); - scope.ifPresent(Scope::close); - } - }, - MoreExecutors - .directExecutor()); // Execute on same thread that called execute on CompletionStage - // returned - - return returnFuture; - } - - /** Shutdown the client. */ - public void close() { - this.channel.shutdown(); - } - - private GetRequest buildGetRequest(ByteString key) { - return GetRequest.newBuilder().setCacheKey(key).build(); - } - - private SetRequest buildSetRequest(ByteString key, ByteString value, int ttl) { - return SetRequest.newBuilder() - .setCacheKey(key) - .setCacheBody(value) - .setTtlMilliseconds(ttl) - .build(); - } - - private static void ensureValid(Object key, Object value, int ttlSeconds) { - - ensureValidKey(key); - - if (value == null) { - throw new ClientSdkException("A non-null value is required."); - } - - if (ttlSeconds <= 0) { - throw new ClientSdkException("Item's time to live in Cache must be a positive integer."); - } - } - - private static void ensureValidKey(Object key) { - if (key == null) { - throw new ClientSdkException("A non-null Key is required."); - } - } - - private ByteString convert(String stringToEncode) { - return ByteString.copyFromUtf8(stringToEncode); - } - - private ByteString convert(byte[] bytes) { - return ByteString.copyFrom(bytes); - } - - private ByteString convert(ByteBuffer byteBuffer) { - return ByteString.copyFrom(byteBuffer); - } - - private Optional buildSpan(String spanName) { - // TODO - We should change this logic so can pass in parent span so returned span becomes a sub - // span of a parent span. - return tracer.map( - t -> - t.spanBuilder(spanName) - .setSpanKind(SpanKind.CLIENT) - .setStartTimestamp(now()) - .startSpan()); - } -} diff --git a/momento-sdk/src/main/java/momento/sdk/CacheClientBuilder.java b/momento-sdk/src/main/java/momento/sdk/CacheClientBuilder.java deleted file mode 100644 index 1ccc70a9..00000000 --- a/momento-sdk/src/main/java/momento/sdk/CacheClientBuilder.java +++ /dev/null @@ -1,57 +0,0 @@ -package momento.sdk; - -import momento.sdk.exceptions.CacheNotFoundException; -import momento.sdk.exceptions.ClientSdkException; - -/** Build a {@link Cache} */ -public final class CacheClientBuilder { - - private final Momento momento; - private final String authToken; - private final String cacheName; - private final int defaultItemTtlSeconds; - private final String endpoint; - private boolean createIfDoesntExist; - - CacheClientBuilder( - Momento momento, - String authToken, - String cacheName, - int defaultItemTtlSeconds, - String endpoint) { - this.momento = momento; - this.authToken = authToken; - this.cacheName = cacheName; - this.defaultItemTtlSeconds = defaultItemTtlSeconds; - this.endpoint = endpoint; - } - - /** Signal the builder to create a new Cache if one with the given name doesn't exist. */ - public CacheClientBuilder createCacheIfDoesntExist() { - this.createIfDoesntExist = true; - return this; - } - - /** Builds {@link Cache} client based on the properties set on the builder. */ - public Cache build() { - if (defaultItemTtlSeconds <= 0) { - throw new ClientSdkException("Item's time to live in Cache must be a positive integer."); - } - - Momento.checkCacheNameValid(cacheName); - Cache cache = null; - try { - cache = new Cache(authToken, cacheName, endpoint, defaultItemTtlSeconds); - return cache.connect(); - } catch (CacheNotFoundException e) { - if (!createIfDoesntExist) { - throw e; - } - } - - // Create since the cache is not found and the request is to create it. - momento.createCache(cacheName); - // Use the same cache object as previously constructed. - return cache.connect(); - } -} diff --git a/momento-sdk/src/main/java/momento/sdk/CacheNameInterceptor.java b/momento-sdk/src/main/java/momento/sdk/CacheNameInterceptor.java deleted file mode 100644 index 0b1192b0..00000000 --- a/momento-sdk/src/main/java/momento/sdk/CacheNameInterceptor.java +++ /dev/null @@ -1,35 +0,0 @@ -package momento.sdk; - -import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; - -import io.grpc.CallOptions; -import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.ClientInterceptor; -import io.grpc.ForwardingClientCall; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; - -final class CacheNameInterceptor implements ClientInterceptor { - - private static final Metadata.Key CACHE_NAME_KEY = - Metadata.Key.of("cache", ASCII_STRING_MARSHALLER); - private final String cacheName; - - CacheNameInterceptor(String inputCacheName) { - cacheName = inputCacheName; - } - - @Override - public ClientCall interceptCall( - MethodDescriptor methodDescriptor, CallOptions callOptions, Channel channel) { - return new ForwardingClientCall.SimpleForwardingClientCall( - channel.newCall(methodDescriptor, callOptions)) { - @Override - public void start(Listener listener, Metadata metadata) { - metadata.put(CACHE_NAME_KEY, cacheName); - super.start(listener, metadata); - } - }; - } -} diff --git a/momento-sdk/src/main/java/momento/sdk/Momento.java b/momento-sdk/src/main/java/momento/sdk/Momento.java deleted file mode 100644 index 7dd21780..00000000 --- a/momento-sdk/src/main/java/momento/sdk/Momento.java +++ /dev/null @@ -1,215 +0,0 @@ -package momento.sdk; - -import grpc.control_client.CreateCacheRequest; -import grpc.control_client.DeleteCacheRequest; -import grpc.control_client.ScsControlGrpc; -import grpc.control_client.ScsControlGrpc.ScsControlBlockingStub; -import io.grpc.ClientInterceptor; -import io.grpc.ManagedChannel; -import io.grpc.Status; -import io.grpc.netty.NettyChannelBuilder; -import java.io.Closeable; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import momento.sdk.exceptions.CacheAlreadyExistsException; -import momento.sdk.exceptions.CacheNotFoundException; -import momento.sdk.exceptions.CacheServiceExceptionMapper; -import momento.sdk.exceptions.ClientSdkException; -import momento.sdk.messages.CacheInfo; -import momento.sdk.messages.CreateCacheResponse; -import momento.sdk.messages.DeleteCacheResponse; -import momento.sdk.messages.ListCachesRequest; -import momento.sdk.messages.ListCachesResponse; -import org.apache.commons.lang3.StringUtils; - -/** Client to interact with Momento services. */ -public final class Momento implements Closeable { - - private final String authToken; - private final ScsControlBlockingStub blockingStub; - private final ManagedChannel channel; - private final MomentoEndpointsResolver.MomentoEndpoints momentoEndpoints; - - private Momento(String authToken, Optional hostedZoneOverride) { - this.authToken = authToken; - this.momentoEndpoints = MomentoEndpointsResolver.resolve(authToken, hostedZoneOverride); - this.channel = setupConnection(momentoEndpoints, authToken); - this.blockingStub = ScsControlGrpc.newBlockingStub(channel); - } - - private static ManagedChannel setupConnection( - MomentoEndpointsResolver.MomentoEndpoints momentoEndpoints, String authToken) { - NettyChannelBuilder channelBuilder = - NettyChannelBuilder.forAddress(momentoEndpoints.controlEndpoint(), 443); - channelBuilder.useTransportSecurity(); - channelBuilder.disableRetry(); - List clientInterceptors = new ArrayList<>(); - clientInterceptors.add(new AuthInterceptor(authToken)); - channelBuilder.intercept(clientInterceptors); - return channelBuilder.build(); - } - - /** - * Creates a cache with provided name - * - * @param cacheName Name of the cache to be created. - * @return The result of the create cache operation - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws momento.sdk.exceptions.InvalidArgumentException - * @throws CacheAlreadyExistsException - * @throws momento.sdk.exceptions.InternalServerException - * @throws ClientSdkException when cacheName is null - */ - public CreateCacheResponse createCache(String cacheName) { - checkCacheNameValid(cacheName); - try { - this.blockingStub.createCache(buildCreateCacheRequest(cacheName)); - return new CreateCacheResponse(); - } catch (io.grpc.StatusRuntimeException e) { - if (e.getStatus().getCode() == Status.Code.ALREADY_EXISTS) { - throw new CacheAlreadyExistsException( - String.format("Cache with name %s already exists", cacheName)); - } - throw CacheServiceExceptionMapper.convert(e); - } catch (Exception e) { - throw CacheServiceExceptionMapper.convert(e); - } - } - - /** - * Deletes a cache - * - * @param cacheName The name of the cache to be deleted. - * @return The result of the cache deletion operation. - * @throws momento.sdk.exceptions.PermissionDeniedException - * @throws CacheNotFoundException - * @throws momento.sdk.exceptions.InternalServerException - * @throws ClientSdkException if the {@code cacheName} is null. - */ - public DeleteCacheResponse deleteCache(String cacheName) { - checkCacheNameValid(cacheName); - try { - this.blockingStub.deleteCache(buildDeleteCacheRequest(cacheName)); - return new DeleteCacheResponse(); - } catch (io.grpc.StatusRuntimeException e) { - if (e.getStatus().getCode() == Status.Code.NOT_FOUND) { - throw new CacheNotFoundException( - String.format("Cache with name %s doesn't exist", cacheName)); - } - throw CacheServiceExceptionMapper.convert(e); - } catch (Exception e) { - throw CacheServiceExceptionMapper.convert(e); - } - } - - /** Lists all caches for the provided auth token. */ - public ListCachesResponse listCaches(ListCachesRequest request) { - try { - return convert(this.blockingStub.listCaches(convert(request))); - } catch (Exception e) { - throw CacheServiceExceptionMapper.convert(e); - } - } - - /** - * Creates a builder to make a Cache client. - * - * @param cacheName - Name of the cache for the which the client will be built. - * @param defaultItemTtlSeconds - The default Time to live in seconds for the items that will be - * stored in Cache. Default TTL can be overridden at individual items level at the time of - * storing them in the cache. - * @return {@link CacheClientBuilder} to further build the {@link Cache} client. - */ - public CacheClientBuilder cacheBuilder(String cacheName, int defaultItemTtlSeconds) { - return new CacheClientBuilder( - this, authToken, cacheName, defaultItemTtlSeconds, momentoEndpoints.cacheEndpoint()); - } - - private CreateCacheRequest buildCreateCacheRequest(String cacheName) { - return CreateCacheRequest.newBuilder().setCacheName(cacheName).build(); - } - - private DeleteCacheRequest buildDeleteCacheRequest(String cacheName) { - return DeleteCacheRequest.newBuilder().setCacheName(cacheName).build(); - } - - private grpc.control_client.ListCachesRequest convert(ListCachesRequest request) { - return grpc.control_client.ListCachesRequest.newBuilder() - .setNextToken(request.nextPageToken().orElse("")) - .build(); - } - - private ListCachesResponse convert(grpc.control_client.ListCachesResponse response) { - List caches = new ArrayList<>(); - for (grpc.control_client.Cache cache : response.getCacheList()) { - caches.add(convert(cache)); - } - Optional nextPageToken = - StringUtils.isEmpty(response.getNextToken()) - ? Optional.empty() - : Optional.of(response.getNextToken()); - return new ListCachesResponse(caches, nextPageToken); - } - - private CacheInfo convert(grpc.control_client.Cache cache) { - return new CacheInfo(cache.getCacheName()); - } - - static void checkCacheNameValid(String cacheName) { - if (cacheName == null) { - throw new ClientSdkException("Cache Name is required."); - } - } - - /** Shuts down the client. */ - public void close() { - this.channel.shutdown(); - } - - /** - * Builder to create a {@link Momento} client. - * - * @param authToken The authentication token required to authenticate with Momento Services. - * @return A builder to build the Momento Client - */ - public static MomentoBuilder builder(String authToken) { - return new MomentoBuilder(authToken); - } - - /** Builder for {@link Momento} client */ - public static class MomentoBuilder { - private String authToken; - private Optional endpointOverride = Optional.empty(); - - private MomentoBuilder(String authToken) { - this.authToken = authToken; - } - - /** - * Override the endpoints used to perform operations. - * - *

This parameter should only be set when Momento services team advises to. Any invalid - * values here will result in application failures. - * - * @param endpointOverride Endpoint for momento services. - */ - public MomentoBuilder endpointOverride(String endpointOverride) { - this.endpointOverride = Optional.ofNullable(endpointOverride); - return this; - } - - /** - * Creates a {@link momento.sdk.Momento} client. - * - * @throws ClientSdkException for malformed auth tokens or other invalid data provided to - * initialize the client. - */ - public Momento build() { - if (StringUtils.isEmpty(authToken)) { - throw new ClientSdkException("Auth Token is required"); - } - return new Momento(authToken, endpointOverride); - } - } -} diff --git a/momento-sdk/src/main/java/momento/sdk/ScsControlClient.java b/momento-sdk/src/main/java/momento/sdk/ScsControlClient.java new file mode 100644 index 00000000..fa0dfb74 --- /dev/null +++ b/momento-sdk/src/main/java/momento/sdk/ScsControlClient.java @@ -0,0 +1,110 @@ +package momento.sdk; + +import grpc.control_client.CreateCacheRequest; +import grpc.control_client.DeleteCacheRequest; +import io.grpc.Status; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import momento.sdk.exceptions.CacheAlreadyExistsException; +import momento.sdk.exceptions.CacheNotFoundException; +import momento.sdk.exceptions.CacheServiceExceptionMapper; +import momento.sdk.exceptions.ValidationException; +import momento.sdk.messages.CacheInfo; +import momento.sdk.messages.CreateCacheResponse; +import momento.sdk.messages.DeleteCacheResponse; +import momento.sdk.messages.ListCachesRequest; +import momento.sdk.messages.ListCachesResponse; +import org.apache.commons.lang3.StringUtils; + +/** Client for interacting with Scs Control Plane. */ +final class ScsControlClient implements Closeable { + + private final ScsControlGrpcStubsManager controlGrpcStubsManager; + + ScsControlClient(String authToken, String endpoint) { + this.controlGrpcStubsManager = new ScsControlGrpcStubsManager(authToken, endpoint); + } + + CreateCacheResponse createCache(String cacheName) { + checkCacheNameValid(cacheName); + try { + controlGrpcStubsManager.getBlockingStub().createCache(buildCreateCacheRequest(cacheName)); + return new CreateCacheResponse(); + } catch (io.grpc.StatusRuntimeException e) { + if (e.getStatus().getCode() == Status.Code.ALREADY_EXISTS) { + throw new CacheAlreadyExistsException( + String.format("Cache with name %s already exists", cacheName)); + } + throw CacheServiceExceptionMapper.convert(e); + } catch (Exception e) { + throw CacheServiceExceptionMapper.convert(e); + } + } + + DeleteCacheResponse deleteCache(String cacheName) { + checkCacheNameValid(cacheName); + try { + controlGrpcStubsManager.getBlockingStub().deleteCache(buildDeleteCacheRequest(cacheName)); + return new DeleteCacheResponse(); + } catch (io.grpc.StatusRuntimeException e) { + if (e.getStatus().getCode() == Status.Code.NOT_FOUND) { + throw new CacheNotFoundException( + String.format("Cache with name %s doesn't exist", cacheName)); + } + throw CacheServiceExceptionMapper.convert(e); + } catch (Exception e) { + throw CacheServiceExceptionMapper.convert(e); + } + } + + ListCachesResponse listCaches(ListCachesRequest request) { + try { + return convert(controlGrpcStubsManager.getBlockingStub().listCaches(convert(request))); + } catch (Exception e) { + throw CacheServiceExceptionMapper.convert(e); + } + } + + private static CreateCacheRequest buildCreateCacheRequest(String cacheName) { + return CreateCacheRequest.newBuilder().setCacheName(cacheName).build(); + } + + private static DeleteCacheRequest buildDeleteCacheRequest(String cacheName) { + return DeleteCacheRequest.newBuilder().setCacheName(cacheName).build(); + } + + private static grpc.control_client.ListCachesRequest convert(ListCachesRequest request) { + return grpc.control_client.ListCachesRequest.newBuilder() + .setNextToken(request.nextPageToken().orElse("")) + .build(); + } + + private static ListCachesResponse convert(grpc.control_client.ListCachesResponse response) { + List caches = new ArrayList<>(); + for (grpc.control_client.Cache cache : response.getCacheList()) { + caches.add(convert(cache)); + } + Optional nextPageToken = + StringUtils.isEmpty(response.getNextToken()) + ? Optional.empty() + : Optional.of(response.getNextToken()); + return new ListCachesResponse(caches, nextPageToken); + } + + private static CacheInfo convert(grpc.control_client.Cache cache) { + return new CacheInfo(cache.getCacheName()); + } + + private static void checkCacheNameValid(String cacheName) { + if (cacheName == null) { + throw new ValidationException("Cache Name is required."); + } + } + + @Override + public void close() { + controlGrpcStubsManager.close(); + } +} diff --git a/momento-sdk/src/main/java/momento/sdk/ScsControlGrpcStubsManager.java b/momento-sdk/src/main/java/momento/sdk/ScsControlGrpcStubsManager.java new file mode 100644 index 00000000..a09c8ce3 --- /dev/null +++ b/momento-sdk/src/main/java/momento/sdk/ScsControlGrpcStubsManager.java @@ -0,0 +1,46 @@ +package momento.sdk; + +import grpc.control_client.ScsControlGrpc; +import io.grpc.ClientInterceptor; +import io.grpc.ManagedChannel; +import io.grpc.netty.NettyChannelBuilder; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.List; + +/** + * Manager responsible for GRPC channels and stubs for the Control Plane. + * + *

The business layer, will get request stubs from this layer. This keeps the two layers + * independent and any future pooling of channels can happen exclusively in the manager without + * impacting the API business logic. + */ +final class ScsControlGrpcStubsManager implements Closeable { + + private final ManagedChannel channel; + private final ScsControlGrpc.ScsControlBlockingStub controlBlockingStub; + + ScsControlGrpcStubsManager(String authToken, String endpoint) { + this.channel = setupConnection(authToken, endpoint); + this.controlBlockingStub = ScsControlGrpc.newBlockingStub(channel); + } + + private static ManagedChannel setupConnection(String authToken, String endpoint) { + NettyChannelBuilder channelBuilder = NettyChannelBuilder.forAddress(endpoint, 443); + channelBuilder.useTransportSecurity(); + channelBuilder.disableRetry(); + List clientInterceptors = new ArrayList<>(); + clientInterceptors.add(new AuthInterceptor(authToken)); + channelBuilder.intercept(clientInterceptors); + return channelBuilder.build(); + } + + ScsControlGrpc.ScsControlBlockingStub getBlockingStub() { + return controlBlockingStub; + } + + @Override + public void close() { + channel.shutdown(); + } +} diff --git a/momento-sdk/src/main/java/momento/sdk/ScsDataClient.java b/momento-sdk/src/main/java/momento/sdk/ScsDataClient.java new file mode 100644 index 00000000..2ffcad4d --- /dev/null +++ b/momento-sdk/src/main/java/momento/sdk/ScsDataClient.java @@ -0,0 +1,340 @@ +package momento.sdk; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; +import static java.time.Instant.now; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.ByteString; +import grpc.cache_client.GetRequest; +import grpc.cache_client.GetResponse; +import grpc.cache_client.ScsGrpc; +import grpc.cache_client.SetRequest; +import grpc.cache_client.SetResponse; +import io.grpc.Metadata; +import io.grpc.stub.MetadataUtils; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.ImplicitContextKeyed; +import io.opentelemetry.context.Scope; +import java.io.Closeable; +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import momento.sdk.exceptions.CacheServiceExceptionMapper; +import momento.sdk.exceptions.SdkException; +import momento.sdk.exceptions.ValidationException; +import momento.sdk.messages.CacheGetResponse; +import momento.sdk.messages.CacheSetResponse; + +/** Client for interacting with Scs Data plane. */ +final class ScsDataClient implements Closeable { + + private static final Metadata.Key CACHE_NAME_KEY = + Metadata.Key.of("cache", ASCII_STRING_MARSHALLER); + + private final Optional tracer; + private int itemDefaultTtlSeconds; + private ScsDataGrpcStubsManager scsDataGrpcStubsManager; + + ScsDataClient( + String authToken, + String endpoint, + int defaultTtlSeconds, + Optional openTelemetry) { + this.tracer = openTelemetry.map(ot -> ot.getTracer("momento-java-scs-client", "1.0.0")); + this.itemDefaultTtlSeconds = defaultTtlSeconds; + this.scsDataGrpcStubsManager = new ScsDataGrpcStubsManager(authToken, endpoint, openTelemetry); + } + + CacheGetResponse get(String cacheName, String key) { + ensureValidKey(key); + return sendBlockingGet(cacheName, convert(key)); + } + + CacheGetResponse get(String cacheName, byte[] key) { + ensureValidKey(key); + return sendBlockingGet(cacheName, convert(key)); + } + + CacheSetResponse set(String cacheName, String key, ByteBuffer value, int ttlSeconds) { + ensureValid(key, value, ttlSeconds); + return sendBlockingSet(cacheName, convert(key), convert(value), ttlSeconds); + } + + CacheSetResponse set(String cacheName, String key, ByteBuffer value) { + return set(cacheName, key, value, itemDefaultTtlSeconds); + } + + CacheSetResponse set(String cacheName, String key, String value, int ttlSeconds) { + ensureValid(key, value, ttlSeconds); + return sendBlockingSet(cacheName, convert(key), convert(value), ttlSeconds); + } + + CacheSetResponse set(String cacheName, String key, String value) { + return set(cacheName, key, value, itemDefaultTtlSeconds); + } + + CacheSetResponse set(String cacheName, byte[] key, byte[] value, int ttlSeconds) { + ensureValid(key, value, ttlSeconds); + return sendBlockingSet(cacheName, convert(key), convert(value), ttlSeconds); + } + + CacheSetResponse set(String cacheName, byte[] key, byte[] value) { + return set(cacheName, key, value, itemDefaultTtlSeconds); + } + + CompletableFuture getAsync(String cacheName, byte[] key) { + ensureValidKey(key); + return sendGet(cacheName, convert(key)); + } + + CompletableFuture getAsync(String cacheName, String key) { + ensureValidKey(key); + return sendGet(cacheName, convert(key)); + } + + CompletableFuture setAsync( + String cacheName, String key, ByteBuffer value, int ttlSeconds) { + ensureValid(key, value, ttlSeconds); + return sendSet(cacheName, convert(key), convert(value), ttlSeconds); + } + + CompletableFuture setAsync(String cacheName, String key, ByteBuffer value) { + return setAsync(cacheName, key, value, itemDefaultTtlSeconds); + } + + CompletableFuture setAsync( + String cacheName, byte[] key, byte[] value, int ttlSeconds) { + ensureValid(key, value, ttlSeconds); + return sendSet(cacheName, convert(key), convert(value), ttlSeconds); + } + + CompletableFuture setAsync(String cacheName, byte[] key, byte[] value) { + return setAsync(cacheName, key, value, itemDefaultTtlSeconds); + } + + CompletableFuture setAsync( + String cacheName, String key, String value, int ttlSeconds) { + ensureValid(key, value, ttlSeconds); + return sendSet(cacheName, convert(key), convert(value), ttlSeconds); + } + + CompletableFuture setAsync(String cacheName, String key, String value) { + return setAsync(cacheName, key, value, itemDefaultTtlSeconds); + } + + private static void ensureValid(Object key, Object value, int ttlSeconds) { + + ensureValidKey(key); + + if (value == null) { + throw new ValidationException("A non-null value is required."); + } + + if (ttlSeconds < 0) { + throw new ValidationException("Item's time to live in Cache cannot be negative."); + } + } + + private static void ensureValidKey(Object key) { + if (key == null) { + throw new ValidationException("A non-null Key is required."); + } + } + + private ByteString convert(String stringToEncode) { + return ByteString.copyFromUtf8(stringToEncode); + } + + private ByteString convert(byte[] bytes) { + return ByteString.copyFrom(bytes); + } + + private ByteString convert(ByteBuffer byteBuffer) { + return ByteString.copyFrom(byteBuffer); + } + + private static SdkException handleExceptionally(Throwable t) { + if (t instanceof ExecutionException) { + return CacheServiceExceptionMapper.convert(t.getCause()); + } + return CacheServiceExceptionMapper.convert(t); + } + + private CacheGetResponse sendBlockingGet(String cacheName, ByteString key) { + try { + return sendGet(cacheName, key).get(); + } catch (Throwable t) { + throw handleExceptionally(t); + } + } + + private CacheSetResponse sendBlockingSet( + String cacheName, ByteString key, ByteString value, int itemTtlSeconds) { + try { + return sendSet(cacheName, key, value, itemTtlSeconds).get(); + } catch (Throwable t) { + throw handleExceptionally(t); + } + } + + private CompletableFuture sendGet(String cacheName, ByteString key) { + checkCacheNameValid(cacheName); + Optional span = buildSpan("java-sdk-get-request"); + Optional scope = (span.map(ImplicitContextKeyed::makeCurrent)); + // Submit request to non-blocking stub + ListenableFuture rspFuture = + withCacheNameHeader(scsDataGrpcStubsManager.getStub(), cacheName).get(buildGetRequest(key)); + + // Build a CompletableFuture to return to caller + CompletableFuture returnFuture = + new CompletableFuture() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + // propagate cancel to the listenable future if called on returned completable future + boolean result = rspFuture.cancel(mayInterruptIfRunning); + super.cancel(mayInterruptIfRunning); + return result; + } + }; + + // Convert returned ListenableFuture to CompletableFuture + Futures.addCallback( + rspFuture, + new FutureCallback() { + @Override + public void onSuccess(GetResponse rsp) { + returnFuture.complete(new CacheGetResponse(rsp.getResult(), rsp.getCacheBody())); + span.ifPresent( + theSpan -> { + theSpan.setStatus(StatusCode.OK); + theSpan.end(now()); + }); + scope.ifPresent(Scope::close); + } + + @Override + public void onFailure(Throwable e) { + returnFuture.completeExceptionally(CacheServiceExceptionMapper.convert(e)); + span.ifPresent( + theSpan -> { + theSpan.setStatus(StatusCode.ERROR); + theSpan.recordException(e); + theSpan.end(now()); + }); + scope.ifPresent(Scope::close); + } + }, + MoreExecutors + .directExecutor()); // Execute on same thread that called execute on CompletionStage + // returned + + return returnFuture; + } + + private CompletableFuture sendSet( + String cacheName, ByteString key, ByteString value, int ttlSeconds) { + checkCacheNameValid(cacheName); + Optional span = buildSpan("java-sdk-set-request"); + Optional scope = (span.map(ImplicitContextKeyed::makeCurrent)); + + // Submit request to non-blocking stub + ListenableFuture rspFuture = + withCacheNameHeader(scsDataGrpcStubsManager.getStub(), cacheName) + .set(buildSetRequest(key, value, ttlSeconds * 1000)); + + // Build a CompletableFuture to return to caller + CompletableFuture returnFuture = + new CompletableFuture() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + // propagate cancel to the listenable future if called on returned completable future + boolean result = rspFuture.cancel(mayInterruptIfRunning); + super.cancel(mayInterruptIfRunning); + return result; + } + }; + + // Convert returned ListenableFuture to CompletableFuture + Futures.addCallback( + rspFuture, + new FutureCallback() { + @Override + public void onSuccess(SetResponse rsp) { + returnFuture.complete(new CacheSetResponse(rsp.getResult())); + span.ifPresent( + theSpan -> { + theSpan.setStatus(StatusCode.OK); + theSpan.end(now()); + }); + scope.ifPresent(Scope::close); + } + + @Override + public void onFailure(Throwable e) { + returnFuture.completeExceptionally( + CacheServiceExceptionMapper.convert(e)); // bubble all errors up + span.ifPresent( + theSpan -> { + theSpan.setStatus(StatusCode.ERROR); + theSpan.recordException(e); + theSpan.end(now()); + }); + scope.ifPresent(Scope::close); + } + }, + MoreExecutors + .directExecutor()); // Execute on same thread that called execute on CompletionStage + // returned + + return returnFuture; + } + + private static ScsGrpc.ScsFutureStub withCacheNameHeader( + ScsGrpc.ScsFutureStub stub, String cacheName) { + Metadata header = new Metadata(); + header.put(CACHE_NAME_KEY, cacheName); + return MetadataUtils.attachHeaders(stub, header); + } + + private GetRequest buildGetRequest(ByteString key) { + return GetRequest.newBuilder().setCacheKey(key).build(); + } + + private SetRequest buildSetRequest(ByteString key, ByteString value, int ttl) { + return SetRequest.newBuilder() + .setCacheKey(key) + .setCacheBody(value) + .setTtlMilliseconds(ttl) + .build(); + } + + private Optional buildSpan(String spanName) { + // TODO - We should change this logic so can pass in parent span so returned span becomes a sub + // span of a parent span. + return tracer.map( + t -> + t.spanBuilder(spanName) + .setSpanKind(SpanKind.CLIENT) + .setStartTimestamp(now()) + .startSpan()); + } + + private static void checkCacheNameValid(String cacheName) { + if (cacheName == null) { + throw new ValidationException("Cache Name is required."); + } + } + + @Override + public void close() { + scsDataGrpcStubsManager.close(); + } +} diff --git a/momento-sdk/src/main/java/momento/sdk/ScsDataGrpcStubsManager.java b/momento-sdk/src/main/java/momento/sdk/ScsDataGrpcStubsManager.java new file mode 100644 index 00000000..81116a54 --- /dev/null +++ b/momento-sdk/src/main/java/momento/sdk/ScsDataGrpcStubsManager.java @@ -0,0 +1,54 @@ +package momento.sdk; + +import grpc.cache_client.ScsGrpc; +import io.grpc.ClientInterceptor; +import io.grpc.ManagedChannel; +import io.grpc.netty.NettyChannelBuilder; +import io.opentelemetry.api.OpenTelemetry; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Manager responsible for GRPC channels and stubs for the Data Plane. + * + *

The business layer, will get request stubs from this layer. This keeps the two layers + * independent and any future pooling of channels can happen exclusively in the manager without + * impacting the API business logic. + */ +final class ScsDataGrpcStubsManager implements Closeable { + + private final ManagedChannel channel; + private final ScsGrpc.ScsFutureStub futureStub; + + ScsDataGrpcStubsManager( + String authToken, String endpoint, Optional openTelemetry) { + this.channel = setupChannel(authToken, endpoint, openTelemetry); + this.futureStub = ScsGrpc.newFutureStub(channel); + } + + private static ManagedChannel setupChannel( + String authToken, String endpoint, Optional openTelemetry) { + NettyChannelBuilder channelBuilder = NettyChannelBuilder.forAddress(endpoint, 443); + channelBuilder.useTransportSecurity(); + channelBuilder.disableRetry(); + List clientInterceptors = new ArrayList<>(); + clientInterceptors.add(new AuthInterceptor(authToken)); + openTelemetry.ifPresent( + theOpenTelemetry -> + clientInterceptors.add(new OpenTelemetryClientInterceptor(theOpenTelemetry))); + channelBuilder.intercept(clientInterceptors); + ManagedChannel channel = channelBuilder.build(); + return channel; + } + + ScsGrpc.ScsFutureStub getStub() { + return futureStub; + } + + @Override + public void close() { + channel.shutdown(); + } +} diff --git a/momento-sdk/src/main/java/momento/sdk/SimpleCacheClient.java b/momento-sdk/src/main/java/momento/sdk/SimpleCacheClient.java new file mode 100644 index 00000000..7e1e5105 --- /dev/null +++ b/momento-sdk/src/main/java/momento/sdk/SimpleCacheClient.java @@ -0,0 +1,383 @@ +package momento.sdk; + +import io.opentelemetry.api.OpenTelemetry; +import java.io.Closeable; +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import momento.sdk.exceptions.CacheAlreadyExistsException; +import momento.sdk.exceptions.CacheNotFoundException; +import momento.sdk.exceptions.ClientSdkException; +import momento.sdk.messages.CacheGetResponse; +import momento.sdk.messages.CacheSetResponse; +import momento.sdk.messages.CreateCacheResponse; +import momento.sdk.messages.DeleteCacheResponse; +import momento.sdk.messages.ListCachesRequest; +import momento.sdk.messages.ListCachesResponse; + +/** Client to perform operations against the Simple Cache Service */ +public final class SimpleCacheClient implements Closeable { + + private final ScsControlClient scsControlClient; + private final ScsDataClient scsDataClient; + + SimpleCacheClient( + String authToken, int itemDefaultTtlSeconds, Optional telemetryOptional) { + MomentoEndpointsResolver.MomentoEndpoints endpoints = + MomentoEndpointsResolver.resolve(authToken, Optional.empty()); + this.scsControlClient = new ScsControlClient(authToken, endpoints.controlEndpoint()); + this.scsDataClient = + new ScsDataClient( + authToken, endpoints.cacheEndpoint(), itemDefaultTtlSeconds, telemetryOptional); + } + + public static SimpleCacheClientBuilder builder(String authToken, int itemDefaultTtlSeconds) { + return new SimpleCacheClientBuilder(authToken, itemDefaultTtlSeconds); + } + + /** + * Creates a cache with provided name + * + * @param cacheName Name of the cache to be created. + * @return The result of the create cache operation + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws momento.sdk.exceptions.InvalidArgumentException + * @throws CacheAlreadyExistsException + * @throws momento.sdk.exceptions.InternalServerException + * @throws ClientSdkException when cacheName is null + */ + public CreateCacheResponse createCache(String cacheName) { + return scsControlClient.createCache(cacheName); + } + + /** + * Deletes a cache + * + * @param cacheName The name of the cache to be deleted. + * @return The result of the cache deletion operation. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + * @throws ClientSdkException if the {@code cacheName} is null. + */ + public DeleteCacheResponse deleteCache(String cacheName) { + return scsControlClient.deleteCache(cacheName); + } + + /** Lists all caches for the provided auth token. */ + public ListCachesResponse listCaches(ListCachesRequest request) { + return scsControlClient.listCaches(request); + } + + /** + * Get the cache value stored for the given key. + * + * @param cacheName Name of the cache to get value from + * @param key The key to get + * @return {@link CacheGetResponse} containing the status of the get operation and the associated + * value data. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key is null + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CacheGetResponse get(String cacheName, String key) { + return scsDataClient.get(cacheName, key); + } + + /** + * Get the cache value stored for the given key. + * + * @param cacheName Name of the cache to get value from + * @param key The key to get + * @return {@link CacheGetResponse} containing the status of the get operation and the associated + * value data. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key is null + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CacheGetResponse get(String cacheName, byte[] key) { + return scsDataClient.get(cacheName, key); + } + + /** + * Sets the value in cache with a given Time To Live (TTL) seconds. + * + *

If a value for this key is already present it will be replaced by the new value. + * + * @param cacheName Name of the cache to store the item in + * @param key The key under which the value is to be added. + * @param value The value to be stored. + * @param ttlSeconds Time to Live for the item in Cache. This ttl takes precedence over the TTL + * used when building a cache client {@link SimpleCacheClient#builder(String, int)} + * @return Result of the set operation. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key, value is null or if ttlSeconds is less than or equal to zero + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CacheSetResponse set(String cacheName, String key, ByteBuffer value, int ttlSeconds) { + return scsDataClient.set(cacheName, key, value, ttlSeconds); + } + + /** + * Sets the value in the cache. If a value for this key is already present it will be replaced by + * the new value. + * + *

The Time to Live (TTL) seconds defaults to the parameter used when building this Cache + * client - {@link SimpleCacheClient#builder(String, int)} + * + * @param cacheName Name of the cache to store the item in + * @param key The key under which the value is to be added. + * @param value The value to be stored. + * @return Result of the set operation. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key or value is null + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CacheSetResponse set(String cacheName, String key, ByteBuffer value) { + return scsDataClient.set(cacheName, key, value); + } + + /** + * Sets the value in cache with a given Time To Live (TTL) seconds. + * + *

If a value for this key is already present it will be replaced by the new value. + * + * @param key The key under which the value is to be added. + * @param value The value to be stored. + * @param ttlSeconds Time to Live for the item in Cache. This ttl takes precedence over the TTL + * used when building a cache client {@link SimpleCacheClient#builder(String, int)} + * @return Result of the set operation. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key or value is null or ttlSeconds is less than or equal to zero + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CacheSetResponse set(String cacheName, String key, String value, int ttlSeconds) { + return scsDataClient.set(cacheName, key, value, ttlSeconds); + } + + /** + * Sets the value in the cache. If a value for this key is already present it will be replaced by + * the new value. + * + *

The Time to Live (TTL) seconds defaults to the parameter used when building this Cache + * client - {@link SimpleCacheClient#builder(String, int)} + * + * @param cacheName Name of the cache to store the item in + * @param key The key under which the value is to be added. + * @param value The value to be stored. + * @return Result of the set operation. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key or value is null + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CacheSetResponse set(String cacheName, String key, String value) { + return scsDataClient.set(cacheName, key, value); + } + + /** + * Sets the value in cache with a given Time To Live (TTL) seconds. + * + *

If a value for this key is already present it will be replaced by the new value. + * + * @param cacheName Name of the cache to store the item in + * @param key The key under which the value is to be added. + * @param value The value to be stored. + * @param ttlSeconds Time to Live for the item in Cache. This ttl takes precedence over the TTL + * used when building a cache client {@link SimpleCacheClient#builder(String, int)} + * @return Result of the set operation. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key or value is null or ttlSeconds is less than or equal to zero + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CacheSetResponse set(String cacheName, byte[] key, byte[] value, int ttlSeconds) { + return scsDataClient.set(cacheName, key, value, ttlSeconds); + } + + /** + * Sets the value in the cache. If a value for this key is already present it will be replaced by + * the new value. + * + *

The Time to Live (TTL) seconds defaults to the parameter used when building this Cache + * client - {@link SimpleCacheClient#builder(String, int)} + * + * @param cacheName Name of the cache to store the item in + * @param key The key under which the value is to be added. + * @param value The value to be stored. + * @return Result of the set operation. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key or value is null + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CacheSetResponse set(String cacheName, byte[] key, byte[] value) { + return scsDataClient.set(cacheName, key, value); + } + + /** + * Get the cache value stored for the given key. + * + * @param cacheName Name of the cache to get the item from + * @param key The key to get + * @return Future with {@link CacheGetResponse} containing the status of the get operation and the + * associated value data. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key is null + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CompletableFuture getAsync(String cacheName, byte[] key) { + return scsDataClient.getAsync(cacheName, key); + } + + /** + * Get the cache value stored for the given key. + * + * @param cacheName Name of the cache to get the item from + * @param key The key to get + * @return Future with {@link CacheGetResponse} containing the status of the get operation and the + * associated value data. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key is null + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CompletableFuture getAsync(String cacheName, String key) { + return scsDataClient.getAsync(cacheName, key); + } + + /** + * Sets the value in cache with a given Time To Live (TTL) seconds. + * + *

If a value for this key is already present it will be replaced by the new value. + * + * @param cacheName Name of the cache to store the item in + * @param key The key under which the value is to be added. + * @param value The value to be stored. + * @param ttlSeconds Time to Live for the item in Cache. This ttl takes precedence over the TTL + * used when building a cache client {@link SimpleCacheClient#builder(String, int)} + * @return Future containing the result of the set operation. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key or value is null or ttlSeconds is less than or equal to zero + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CompletableFuture setAsync( + String cacheName, String key, ByteBuffer value, int ttlSeconds) { + return scsDataClient.setAsync(cacheName, key, value, ttlSeconds); + } + + /** + * Sets the value in the cache. If a value for this key is already present it will be replaced by + * the new value. + * + *

The Time to Live (TTL) seconds defaults to the parameter used when building this Cache + * client - {@link SimpleCacheClient#builder(String, int)} + * + * @param cacheName Name of the cache to store the item in + * @param key The key under which the value is to be added. + * @param value The value to be stored. + * @return Future containing the result of the set operation. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key or value is null + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CompletableFuture setAsync( + String cacheName, String key, ByteBuffer value) { + return scsDataClient.setAsync(cacheName, key, value); + } + + /** + * Sets the value in cache with a given Time To Live (TTL) seconds. + * + *

If a value for this key is already present it will be replaced by the new value. + * + * @param cacheName Name of the cache to store the item in + * @param key The key under which the value is to be added. + * @param value The value to be stored. + * @param ttlSeconds Time to Live for the item in Cache. This ttl takes precedence over the TTL + * used when building a cache client {@link SimpleCacheClient#builder(String, int)} + * @return Future containing the result of the set operation. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key or value is null or ttlSeconds is less than or equal to zero + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CompletableFuture setAsync( + String cacheName, byte[] key, byte[] value, int ttlSeconds) { + return scsDataClient.setAsync(cacheName, key, value, ttlSeconds); + } + + /** + * Sets the value in the cache. If a value for this key is already present it will be replaced by + * the new value. + * + *

The Time to Live (TTL) seconds defaults to the parameter used when building this Cache + * client - {@link SimpleCacheClient#builder(String, int)} + * + * @param cacheName Name of the cache to store the item in + * @param key The key under which the value is to be added. + * @param value The value to be stored. + * @return Future containing the result of the set operation. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key or value is null + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CompletableFuture setAsync(String cacheName, byte[] key, byte[] value) { + return scsDataClient.setAsync(cacheName, key, value); + } + + /** + * Sets the value in cache with a given Time To Live (TTL) seconds. + * + *

If a value for this key is already present it will be replaced by the new value. + * + * @param cacheName Name of the cache to store the item in + * @param key The key under which the value is to be added. + * @param value The value to be stored. + * @param ttlSeconds Time to Live for the item in Cache. This ttl takes precedence over the TTL + * used when building a cache client {@link SimpleCacheClient#builder(String, int)} + * @return Future containing the result of the set operation. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key or value is null or ttlSeconds is less than or equal to zero + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CompletableFuture setAsync( + String cacheName, String key, String value, int ttlSeconds) { + return scsDataClient.setAsync(cacheName, key, value, ttlSeconds); + } + + /** + * Sets the value in the cache. If a value for this key is already present it will be replaced by + * the new value. + * + *

The Time to Live (TTL) seconds defaults to the parameter used when building this Cache + * client - {@link SimpleCacheClient#builder(String, int)} + * + * @param cacheName Name of the cache to store the item in + * @param key The key under which the value is to be added. + * @param value The value to be stored. + * @return Future containing the result of the set operation. + * @throws momento.sdk.exceptions.PermissionDeniedException + * @throws ClientSdkException if key or value is null + * @throws momento.sdk.exceptions.CacheNotFoundException + * @throws momento.sdk.exceptions.InternalServerException + */ + public CompletableFuture setAsync(String cacheName, String key, String value) { + return scsDataClient.setAsync(cacheName, key, value); + } + + @Override + public void close() { + scsControlClient.close(); + } +} diff --git a/momento-sdk/src/main/java/momento/sdk/SimpleCacheClientBuilder.java b/momento-sdk/src/main/java/momento/sdk/SimpleCacheClientBuilder.java new file mode 100644 index 00000000..c0dca8cf --- /dev/null +++ b/momento-sdk/src/main/java/momento/sdk/SimpleCacheClientBuilder.java @@ -0,0 +1,23 @@ +package momento.sdk; + +import java.util.Optional; +import momento.sdk.exceptions.ValidationException; + +/** Builder for {@link momento.sdk.SimpleCacheClient} */ +public final class SimpleCacheClientBuilder { + + private final String authToken; + private final int itemDefaultTtlSeconds; + + SimpleCacheClientBuilder(String authToken, int itemTtlDefaultSeconds) { + this.authToken = authToken; + this.itemDefaultTtlSeconds = itemTtlDefaultSeconds; + } + + public SimpleCacheClient build() { + if (itemDefaultTtlSeconds < 0) { + throw new ValidationException("Item's time to live in Cache cannot be negative."); + } + return new SimpleCacheClient(authToken, itemDefaultTtlSeconds, Optional.empty()); + } +} diff --git a/momento-sdk/src/main/java/momento/sdk/exceptions/ValidationException.java b/momento-sdk/src/main/java/momento/sdk/exceptions/ValidationException.java new file mode 100644 index 00000000..1c39b26d --- /dev/null +++ b/momento-sdk/src/main/java/momento/sdk/exceptions/ValidationException.java @@ -0,0 +1,12 @@ +package momento.sdk.exceptions; + +/** Exception when SDK client side validation fails. */ +public class ValidationException extends ClientSdkException { + public ValidationException(String message, Throwable cause) { + super(message, cause); + } + + public ValidationException(String message) { + super(message); + } +}