Skip to content

Commit

Permalink
feat: add sorted set put and fetch by rank functions (momentohq#36)
Browse files Browse the repository at this point in the history
* feat: add sorted set put and fetch by rank functions

Add sorted set put element and put elements methods.

Add sorted set fetch by rank.

Flesh out input validation.

Create unit test module for cache-client that handles input validation
tests to avoid cluttering the integration tests.

Make the Momento.Error cause an exception instead of a string to better
capture info from unexpected exceptions.
  • Loading branch information
nand4011 authored Jun 20, 2023
1 parent b6d3416 commit 9f27b46
Show file tree
Hide file tree
Showing 11 changed files with 1,164 additions and 57 deletions.
178 changes: 176 additions & 2 deletions src/integration-test/momento/cache_client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ defmodule CacheClientTest do
value = "test_value"

{:error, error} = CacheClient.set(cache_client, cache_name, key, value, "sixty")
assert String.contains?(error.message, "The TTL must be a positive float")
assert String.contains?(error.message, "The TTL must be a float")

{:error, error} = CacheClient.set(cache_client, cache_name, key, value, -20.0)
assert String.contains?(error.message, "The TTL must be a positive float")
assert String.contains?(error.message, "The TTL must be positive")
end

test "get/3 returns miss when no value is found for a key", %{
Expand Down Expand Up @@ -136,4 +136,178 @@ defmodule CacheClientTest do
{:error, error} = CacheClient.delete(cache_client, cache_name, 12345)
assert String.contains?(error.message, "The key must be a binary")
end

describe "sorted_set_put_elements/5" do
test "should be able to put elements in a sorted set, overwriting existing elements", %{
cache_client: cache_client,
cache_name: cache_name
} do
sorted_set_name = random_string(16)
first_elements = %{"key1" => 1.0, "key2" => 2.0}
second_elements = %{"key2" => 5.0, "key3" => 3.0}

:miss = CacheClient.sorted_set_fetch_by_rank(cache_client, cache_name, sorted_set_name)

{:ok, _} =
CacheClient.sorted_set_put_elements(
cache_client,
cache_name,
sorted_set_name,
first_elements
)

{:ok, hit} = CacheClient.sorted_set_fetch_by_rank(cache_client, cache_name, sorted_set_name)
assert hit.value == [{"key1", 1.0}, {"key2", 2.0}]

{:ok, _} =
CacheClient.sorted_set_put_elements(
cache_client,
cache_name,
sorted_set_name,
second_elements
)

{:ok, hit} = CacheClient.sorted_set_fetch_by_rank(cache_client, cache_name, sorted_set_name)
assert hit.value == [{"key1", 1.0}, {"key3", 3.0}, {"key2", 5.0}]
end
end

describe "sorted_set_put_element/6" do
test "should be able to put individual elements in a sorted set, overwriting existing elements",
%{
cache_client: cache_client,
cache_name: cache_name
} do
sorted_set_name = random_string(16)

:miss = CacheClient.sorted_set_fetch_by_rank(cache_client, cache_name, sorted_set_name)

{:ok, _} =
CacheClient.sorted_set_put_element(cache_client, cache_name, sorted_set_name, "key1", 1.0)

{:ok, hit} = CacheClient.sorted_set_fetch_by_rank(cache_client, cache_name, sorted_set_name)
assert hit.value == [{"key1", 1.0}]

{:ok, _} =
CacheClient.sorted_set_put_element(cache_client, cache_name, sorted_set_name, "key1", 5.0)

{:ok, hit} = CacheClient.sorted_set_fetch_by_rank(cache_client, cache_name, sorted_set_name)
assert hit.value == [{"key1", 5.0}]

{:ok, _} =
CacheClient.sorted_set_put_element(cache_client, cache_name, sorted_set_name, "key2", 2.0)

{:ok, hit} = CacheClient.sorted_set_fetch_by_rank(cache_client, cache_name, sorted_set_name)
assert hit.value == [{"key2", 2.0}, {"key1", 5.0}]
end
end

describe "sorted_set_fetch_by_rank/6" do
test "should be able to fetch in ascending and descending order", %{
cache_client: cache_client,
cache_name: cache_name
} do
sorted_set_name = random_string(16)
elements = %{"key1" => 1.0, "key2" => 2.0, "key3" => 3.0}

:miss = CacheClient.sorted_set_fetch_by_rank(cache_client, cache_name, sorted_set_name)

{:ok, _} =
CacheClient.sorted_set_put_elements(cache_client, cache_name, sorted_set_name, elements)

{:ok, hit} =
CacheClient.sorted_set_fetch_by_rank(
cache_client,
cache_name,
sorted_set_name,
sort_order: :asc
)

assert hit.value == [{"key1", 1.0}, {"key2", 2.0}, {"key3", 3.0}]

{:ok, hit} =
CacheClient.sorted_set_fetch_by_rank(
cache_client,
cache_name,
sorted_set_name,
sort_order: :desc
)

assert hit.value == [{"key3", 3.0}, {"key2", 2.0}, {"key1", 1.0}]
end

test "should be able to fetch a subset of a sorted set", %{
cache_client: cache_client,
cache_name: cache_name
} do
sorted_set_name = random_string(16)
elements = [{"key1", 1.0}, {"key2", 2.0}, {"key3", 3.0}, {"key4", 4.0}, {"key5", 5.0}]

:miss = CacheClient.sorted_set_fetch_by_rank(cache_client, cache_name, sorted_set_name)

{:ok, _} =
CacheClient.sorted_set_put_elements(cache_client, cache_name, sorted_set_name, elements)

{:ok, hit} =
CacheClient.sorted_set_fetch_by_rank(cache_client, cache_name, sorted_set_name,
start_rank: 0
)

assert hit.value == [
{"key1", 1.0},
{"key2", 2.0},
{"key3", 3.0},
{"key4", 4.0},
{"key5", 5.0}
]

{:ok, hit} =
CacheClient.sorted_set_fetch_by_rank(cache_client, cache_name, sorted_set_name,
start_rank: 0,
end_rank: 5
)

assert hit.value == [
{"key1", 1.0},
{"key2", 2.0},
{"key3", 3.0},
{"key4", 4.0},
{"key5", 5.0}
]

{:ok, hit} =
CacheClient.sorted_set_fetch_by_rank(cache_client, cache_name, sorted_set_name,
start_rank: 0,
end_rank: 100
)

assert hit.value == [
{"key1", 1.0},
{"key2", 2.0},
{"key3", 3.0},
{"key4", 4.0},
{"key5", 5.0}
]

{:ok, hit} =
CacheClient.sorted_set_fetch_by_rank(cache_client, cache_name, sorted_set_name,
start_rank: 1,
end_rank: 3
)

assert hit.value == [{"key2", 2.0}, {"key3", 3.0}]

{:ok, hit} =
CacheClient.sorted_set_fetch_by_rank(
cache_client,
cache_name,
sorted_set_name,
start_rank: 1,
end_rank: 3,
sort_order: :desc
)

assert hit.value == [{"key4", 4.0}, {"key3", 3.0}]
end
end
end
2 changes: 1 addition & 1 deletion src/integration-test/momento/integration_test_utils.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ defmodule Momento.IntegrationTestUtils do
}
}

cache_client = CacheClient.create!(config, credential_provider)
cache_client = CacheClient.create!(config, credential_provider, 120.0)
[cache_name: cache_name, cache_client: cache_client]
end

Expand Down
115 changes: 102 additions & 13 deletions src/lib/momento/cache_client.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
defmodule Momento.CacheClient do
alias Momento.Auth.CredentialProvider
alias Momento.Responses.{CreateCache, DeleteCache, ListCaches, Set, Get, Delete}
alias Momento.Responses.SortedSet
alias Momento.Requests.CollectionTtl
alias Momento.Internal.ScsControlClient
alias Momento.Internal.ScsDataClient
alias Momento.Config.Configuration, as: Configuration
Expand All @@ -13,19 +16,22 @@ defmodule Momento.CacheClient do
@enforce_keys [
:config,
:credential_provider,
:default_ttl_seconds,
:control_client,
:data_client
]
defstruct [
:config,
:credential_provider,
:default_ttl_seconds,
:control_client,
:data_client
]

@opaque t() :: %__MODULE__{
config: Configuration.t(),
credential_provider: CredentialProvider.t(),
default_ttl_seconds: float(),
control_client: ScsControlClient.t(),
data_client: ScsDataClient.t()
}
Expand All @@ -44,14 +50,16 @@ defmodule Momento.CacheClient do
"""
@spec create!(
config :: Configuration.t(),
credential_provider :: CredentialProvider.t()
credential_provider :: CredentialProvider.t(),
default_ttl_seconds :: float()
) :: t()
def create!(config, credential_provider) do
def create!(config, credential_provider, default_ttl_seconds) do
with control_client <- ScsControlClient.create!(credential_provider),
data_client <- ScsDataClient.create!(credential_provider) do
%__MODULE__{
config: config,
credential_provider: credential_provider,
default_ttl_seconds: default_ttl_seconds,
control_client: control_client,
data_client: data_client
}
Expand All @@ -70,7 +78,7 @@ defmodule Momento.CacheClient do
- `{:ok, %Momento.Responses.ListCaches.Ok{caches: caches}}` on a successful listing.
- `{:error, error}` tuple if an error occurs.
"""
@spec list_caches(client :: t()) :: Momento.Responses.ListCaches.t()
@spec list_caches(client :: t()) :: ListCaches.t()
def list_caches(client) do
ScsControlClient.list_caches(client.control_client)
end
Expand All @@ -92,7 +100,7 @@ defmodule Momento.CacheClient do
@spec create_cache(
client :: t(),
cache_name :: String.t()
) :: Momento.Responses.DeleteCache.t()
) :: CreateCache.t()
def create_cache(client, cache_name) do
ScsControlClient.create_cache(client.control_client, cache_name)
end
Expand All @@ -113,7 +121,7 @@ defmodule Momento.CacheClient do
@spec delete_cache(
client :: t(),
cache_name :: String.t()
) :: Momento.Responses.DeleteCache.t()
) :: DeleteCache.t()
def delete_cache(client, cache_name) do
ScsControlClient.delete_cache(client.control_client, cache_name)
end
Expand All @@ -139,11 +147,11 @@ defmodule Momento.CacheClient do
cache_name :: String.t(),
key :: binary(),
value :: binary(),
ttl_seconds :: float()
) ::
Momento.Responses.Set.t()
ttl_seconds :: float() | nil
) :: Set.t()
def set(client, cache_name, key, value, ttl_seconds) do
ScsDataClient.set(client.data_client, cache_name, key, value, ttl_seconds)
ttl = ttl_seconds || client.default_ttl_seconds
ScsDataClient.set(client.data_client, cache_name, key, value, ttl)
end

@doc """
Expand All @@ -161,8 +169,7 @@ defmodule Momento.CacheClient do
- `:miss` if the key does not exist.
- `{:error, error}` tuple if an error occurs.
"""
@spec get(client :: t(), cache_name :: String.t(), key :: binary) ::
Momento.Responses.Get.t()
@spec get(client :: t(), cache_name :: String.t(), key :: binary) :: Get.t()
def get(client, cache_name, key) do
ScsDataClient.get(client.data_client, cache_name, key)
end
Expand All @@ -181,9 +188,91 @@ defmodule Momento.CacheClient do
- `{:ok, %Momento.Responses.Delete.Ok{}}` on a successful deletion.
- `{:error, error}` tuple if an error occurs.
"""
@spec delete(client :: t(), cache_name :: String.t(), key :: binary) ::
Momento.Responses.Delete.t()
@spec delete(client :: t(), cache_name :: String.t(), key :: binary) :: Delete.t()
def delete(client, cache_name, key) do
ScsDataClient.delete(client.data_client, cache_name, key)
end

@spec sorted_set_put_element(
client :: t(),
cache_name :: String.t(),
sorted_set_name :: String.t(),
value :: binary(),
score :: float(),
opts :: [collection_ttl :: CollectionTtl.t()]
) :: SortedSet.PutElement.t()
def sorted_set_put_element(
client,
cache_name,
sorted_set_name,
value,
score,
opts \\ []
) do
collection_ttl =
Keyword.get(opts, :collection_ttl, CollectionTtl.of(client.default_ttl_seconds))

case ScsDataClient.sorted_set_put_elements(
client.data_client,
cache_name,
sorted_set_name,
[{value, score}],
collection_ttl
) do
{:ok, _} -> {:ok, %Momento.Responses.SortedSet.PutElement.Ok{}}
{:error, error} -> {:error, error}
end
end

@spec sorted_set_put_elements(
client :: t(),
cache_name :: String.t(),
sorted_set_name :: String.t(),
elements :: %{binary() => float()} | [{binary(), float()}],
opts :: [collection_ttl :: CollectionTtl.t()]
) :: SortedSet.PutElements.t()
def sorted_set_put_elements(
client,
cache_name,
sorted_set_name,
elements,
opts \\ []
) do
collection_ttl =
Keyword.get(opts, :collection_ttl, CollectionTtl.of(client.default_ttl_seconds))

ScsDataClient.sorted_set_put_elements(
client.data_client,
cache_name,
sorted_set_name,
elements,
collection_ttl
)
end

@spec sorted_set_fetch_by_rank(
client :: t(),
cache_name :: String.t(),
sorted_set_name :: String.t(),
opts :: [start_rank: integer(), end_rank: integer(), sort_order: :asc | :desc]
) :: SortedSet.Fetch.t()
def sorted_set_fetch_by_rank(
client,
cache_name,
sorted_set_name,
opts \\ []
) do
start_rank = Keyword.get(opts, :start_rank)
end_rank = Keyword.get(opts, :end_rank)
sort_order = Keyword.get(opts, :sort_order, :asc)

ScsDataClient.sorted_set_fetch_by_rank(
client.data_client,
cache_name,
sorted_set_name,
start_rank,
end_rank,
sort_order
)
end
end
Loading

0 comments on commit 9f27b46

Please sign in to comment.