diff --git a/src/androidInstrumentedTest/kotlin/software/momento/kotlin/sdk/LimitExceededExceptionTest.kt b/src/androidInstrumentedTest/kotlin/software/momento/kotlin/sdk/LimitExceededExceptionTest.kt new file mode 100644 index 0000000..92da6f4 --- /dev/null +++ b/src/androidInstrumentedTest/kotlin/software/momento/kotlin/sdk/LimitExceededExceptionTest.kt @@ -0,0 +1,23 @@ +package software.momento.kotlin.sdk + + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@LargeTest +class LimitExceededExceptionTest: BaseAndroidTestClass() { + + @Test + fun shouldFailWithResourceExhaustedMessage() = runTest { + val key = "cache"; + val value = 'x'.toString().repeat(5_300_000) // 5.3MB + + val setResponse = cacheClient.set(cacheName, key, value) + val stringifiedResponse = setResponse.toString() + assert(stringifiedResponse.contains("Request size limit exceeded for this account")) + } +} diff --git a/src/commonMain/kotlin/software/momento/kotlin/sdk/exceptions/CacheServiceExceptionMapper.kt b/src/commonMain/kotlin/software/momento/kotlin/sdk/exceptions/CacheServiceExceptionMapper.kt index 613fbd5..7451576 100644 --- a/src/commonMain/kotlin/software/momento/kotlin/sdk/exceptions/CacheServiceExceptionMapper.kt +++ b/src/commonMain/kotlin/software/momento/kotlin/sdk/exceptions/CacheServiceExceptionMapper.kt @@ -34,13 +34,14 @@ public object CacheServiceExceptionMapper { val errorDetails = MomentoTransportErrorDetails( MomentoGrpcErrorDetails(statusCode, e.message!!, metadata) ) - convertStatusException(e, errorDetails, statusCode) + val errorCause = metadata?.get(Metadata.Key.of("err", Metadata.ASCII_STRING_MARSHALLER)) + convertStatusException(e, errorDetails, statusCode, errorCause) } else -> UnknownException(SDK_FAILED_TO_PROCESS_THE_REQUEST, e) } } - private fun convertStatusException(e: Throwable, errorDetails: MomentoTransportErrorDetails, statusCode: Status.Code): SdkException { + private fun convertStatusException(e: Throwable, errorDetails: MomentoTransportErrorDetails, statusCode: Status.Code, errorCause: String?): SdkException { return when (statusCode) { Status.Code.INVALID_ARGUMENT, Status.Code.UNIMPLEMENTED, Status.Code.OUT_OF_RANGE, Status.Code.FAILED_PRECONDITION -> BadRequestException( @@ -52,9 +53,9 @@ public object CacheServiceExceptionMapper { e, errorDetails ) Status.Code.UNAUTHENTICATED -> AuthenticationException(e, errorDetails) - Status.Code.RESOURCE_EXHAUSTED -> LimitExceededException( - e, errorDetails - ) + Status.Code.RESOURCE_EXHAUSTED -> { + LimitExceededException(errorCause, errorDetails) + } Status.Code.NOT_FOUND -> NotFoundException(e, errorDetails) Status.Code.ALREADY_EXISTS -> AlreadyExistsException(e, errorDetails) Status.Code.UNKNOWN -> UnknownServiceException(e, errorDetails) diff --git a/src/commonMain/kotlin/software/momento/kotlin/sdk/exceptions/LimitExceededException.kt b/src/commonMain/kotlin/software/momento/kotlin/sdk/exceptions/LimitExceededException.kt index 7a58a6d..4bd8046 100644 --- a/src/commonMain/kotlin/software/momento/kotlin/sdk/exceptions/LimitExceededException.kt +++ b/src/commonMain/kotlin/software/momento/kotlin/sdk/exceptions/LimitExceededException.kt @@ -2,19 +2,67 @@ package software.momento.kotlin.sdk.exceptions import software.momento.kotlin.sdk.internal.MomentoTransportErrorDetails -/** - * Requested operation couldn't be completed because system limits were hit. - */ +private enum class LimitExceededMessageWrapper(val message: String) { + TOPIC_SUBSCRIPTIONS_LIMIT_EXCEEDED("Topic subscriptions limit exceeded for this account"), + OPERATIONS_RATE_LIMIT_EXCEEDED("Request rate limit exceeded for this account"), + THROUGHPUT_RATE_LIMIT_EXCEEDED("Bandwidth limit exceeded for this account"), + REQUEST_SIZE_LIMIT_EXCEEDED("Request size limit exceeded for this account"), + ITEM_SIZE_LIMIT_EXCEEDED("Item size limit exceeded for this account"), + ELEMENT_SIZE_LIMIT_EXCEEDED("Element size limit exceeded for this account"), + UNKNOWN_LIMIT_EXCEEDED("Limit exceeded for this account") +} + public class LimitExceededException( - cause: Throwable, - transportErrorDetails: MomentoTransportErrorDetails + errCause: String? = null, + transportErrorDetails: MomentoTransportErrorDetails, + cause: Throwable? = null ) : MomentoServiceException( MomentoErrorCode.LIMIT_EXCEEDED_ERROR, - MESSAGE, + determineMessageWrapper(errCause, transportErrorDetails), cause, transportErrorDetails ) { private companion object { - const val MESSAGE = "Request rate, bandwidth, or object size exceeded the limits for this account. To resolve this error, reduce your usage as appropriate or contact Momento to request a limit increase." + private fun determineMessageWrapper( + errCause: String?, + transportErrorDetails: MomentoTransportErrorDetails + ): String { + // First, determine the message based on `errCause` + errCause?.let { + val lowerCaseErrCause = it.lowercase() + + when (lowerCaseErrCause) { + "topic_subscriptions_limit_exceeded" -> + return LimitExceededMessageWrapper.TOPIC_SUBSCRIPTIONS_LIMIT_EXCEEDED.message + "operations_rate_limit_exceeded" -> + return LimitExceededMessageWrapper.OPERATIONS_RATE_LIMIT_EXCEEDED.message + "throughput_rate_limit_exceeded" -> + return LimitExceededMessageWrapper.THROUGHPUT_RATE_LIMIT_EXCEEDED.message + "request_size_limit_exceeded" -> + return LimitExceededMessageWrapper.REQUEST_SIZE_LIMIT_EXCEEDED.message + "item_size_limit_exceeded" -> + return LimitExceededMessageWrapper.ITEM_SIZE_LIMIT_EXCEEDED.message + "element_size_limit_exceeded" -> + return LimitExceededMessageWrapper.ELEMENT_SIZE_LIMIT_EXCEEDED.message + else -> { /* Do nothing */ } + } + } + + // If `errCause` is null, fall back to checking transport error details + transportErrorDetails.grpc.details.lowercase().let { details -> + when { + "subscribers" in details -> return LimitExceededMessageWrapper.TOPIC_SUBSCRIPTIONS_LIMIT_EXCEEDED.message + "operations" in details -> return LimitExceededMessageWrapper.OPERATIONS_RATE_LIMIT_EXCEEDED.message + "throughput" in details -> return LimitExceededMessageWrapper.THROUGHPUT_RATE_LIMIT_EXCEEDED.message + "request limit" in details -> return LimitExceededMessageWrapper.REQUEST_SIZE_LIMIT_EXCEEDED.message + "item size" in details -> return LimitExceededMessageWrapper.ITEM_SIZE_LIMIT_EXCEEDED.message + "element size" in details -> return LimitExceededMessageWrapper.ELEMENT_SIZE_LIMIT_EXCEEDED.message + else -> { /* Do nothing */} + } + } + + // Default message if no conditions match + return LimitExceededMessageWrapper.UNKNOWN_LIMIT_EXCEEDED.message + } } } diff --git a/src/jvmTest/kotlin/software/momento/kotlin/sdk/LimitExceededExceptionTest.kt b/src/jvmTest/kotlin/software/momento/kotlin/sdk/LimitExceededExceptionTest.kt new file mode 100644 index 0000000..ed7f1b2 --- /dev/null +++ b/src/jvmTest/kotlin/software/momento/kotlin/sdk/LimitExceededExceptionTest.kt @@ -0,0 +1,17 @@ +package software.momento.kotlin.sdk + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class LimitExceededExceptionTest: BaseJvmTestClass() { + + @Test + fun shouldFailWithResourceExhaustedMessage() = runTest { + val key = "cache"; + val value = 'x'.toString().repeat(5_300_000) // 5.3MB + + val setResponse = cacheClient.set(cacheName, key, value) + val stringifiedResponse = setResponse.toString() + assert(stringifiedResponse.contains("Request size limit exceeded for this account")) + } +}