Skip to content

Commit

Permalink
Merge pull request #2 from renderedtext/vm/batch_operations
Browse files Browse the repository at this point in the history
Batch operations
  • Loading branch information
VeljkoMaksimovic authored Aug 5, 2022
2 parents 5d59dd2 + 4668def commit 7bd0d36
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 9 deletions.
87 changes: 83 additions & 4 deletions lib/cacheman.ex
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ defmodule Cacheman do
use GenServer
require Logger

@default_put_options [ttl: :infinity]

#
# Cacheman API
#
Expand Down Expand Up @@ -109,14 +111,25 @@ defmodule Cacheman do
The response can be one of the following:
- {:ok, value} - if the entry is found
- {:ok, nil} - if the entry is not-found
- {:error, description} - if there was an error while communicating with cache backends
- {:ok, nil} - if the entry is not-found, or redis error occurres
"""
def get(name, key) do
GenServer.call(full_process_name(name), {:get, key})
end

@default_put_options [ttl: :infinity]
@doc """
Gets a list of values from the cache.
{:ok, ["value1", "value2"]} = Cacheman.get(:app, ["key1", "key2"])
The response can be one of the following:
- {:ok, list_of_values} - if the entry is found. In place of every key that was not found will be nil value.
All values will be nil if redis cant be reached.
"""
def get_batch(name, keys) when is_list(keys) do
GenServer.call(full_process_name(name), {:get_batch, keys})
end

@doc """
Puts a value into the cache.
Expand Down Expand Up @@ -144,6 +157,26 @@ defmodule Cacheman do
end
end

@type key_value_pair :: {String.t(), String.t()}
@doc """
Puts a list of key-value pairs into the cache as a part of single request to the redis server
{:ok, no_of_successful_inserts} = Cacheman.put_batch(:app, [{"key1", "value1"}, {"key2", "value2"}])
The response can be one of the following:
- {:ok, no_of_successful_inserts} - if no errors were raised by Redix
- {:error, redix_error} - if there was an error while communicating with cache backends
Same as with Put function, TTL can optionally provided
"""
@spec put_batch(String.t(), list(key_value_pair), list()) ::
{:ok, integer}
| {:error, Redix.Protocol.ParseError | Redix.Error | Redix.ConnectionError}
def put_batch(name, key_value_pairs, put_opts \\ @default_put_options) do
GenServer.call(full_process_name(name), {:put_batch, key_value_pairs, put_opts})
end

@doc """
Fetch is the main entrypoint for caching. The algorithm works like this:
Expand Down Expand Up @@ -226,14 +259,38 @@ defmodule Cacheman do
end
end

def handle_call({:get_batch, keys}, _from, opts) do
response =
apply(opts.backend_module, :get_batch, [
opts.backend_pid,
Enum.map(keys, fn key ->
fully_qualified_key_name(opts, key)
end)
])

case response do
{:ok, vals} ->
{:reply, {:ok, vals}, opts}

e ->
Logger.error("Cacheman - #{inspect(e)}")
{:reply, {:ok, :lists.concat(List.duplicate([nil], length(keys)))}, opts}
end
end

def handle_call({:exists?, key}, _from, opts) do
response =
apply(opts.backend_module, :exists?, [
opts.backend_pid,
fully_qualified_key_name(opts, key)
])

{:reply, response, opts}
if is_boolean(response) do
{:reply, response, opts}
else
Logger.error("Cacheman - #{inspect(response)}")
{:reply, false, opts}
end
end

def handle_call({:put, key, value, put_opts}, _from, opts) do
Expand All @@ -248,6 +305,28 @@ defmodule Cacheman do
{:reply, response, opts}
end

def handle_call({:put_batch, key_value_pairs, put_opts}, _from, opts) do
response =
apply(opts.backend_module, :put_batch, [
opts.backend_pid,
Enum.map(key_value_pairs, fn {key, value} ->
{
fully_qualified_key_name(opts, key),
value
}
end),
put_opts
])

case response do
{:ok, response_values} ->
{:reply, {:ok, response_values |> Enum.count(&(&1 == "OK"))}, opts}

_ ->
{:reply, response, opts}
end
end

def handle_call({:delete, keys}, _from, opts) do
response =
apply(opts.backend_module, :delete, [
Expand Down
24 changes: 23 additions & 1 deletion lib/cacheman/backend/redis.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,23 @@ defmodule Cacheman.Backend.Redis do
end)
end

def get_batch(conn, keys) when is_list(keys) do
list_of_commands =
Enum.map(keys, fn key ->
["GET", key]
end)

:poolboy.transaction(conn, fn c ->
Redix.pipeline(c, list_of_commands)
end)
end

def exists?(conn, key) do
:poolboy.transaction(conn, fn c ->
case Redix.command(c, ["EXISTS", key]) do
{:ok, 1} -> true
{:ok, 0} -> false
{:error, reason} -> raise reason
{:error, reason} -> reason
end
end)
end
Expand All @@ -39,6 +50,17 @@ defmodule Cacheman.Backend.Redis do
end)
end

def put_batch(conn, key_value_pairs, ttl) when is_list(key_value_pairs) do
list_of_commands =
Enum.map(key_value_pairs, fn {key, value} ->
["SET", key, value] ++ ttl_command(ttl)
end)

:poolboy.transaction(conn, fn c ->
Redix.pipeline(c, list_of_commands)
end)
end

def delete(conn, keys) do
:poolboy.transaction(conn, fn c ->
Redix.command(c, ["DEL"] ++ keys)
Expand Down
70 changes: 66 additions & 4 deletions test/cacheman_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,50 @@ defmodule CachemanTest do
assert value == content
end

test "put_batch" do
key1 = "test-#{:rand.uniform(10_000)}"
key2 = "test-#{:rand.uniform(10_000)}"

assert {:ok, nil} = Cacheman.get(:good, key1)
assert {:ok, nil} = Cacheman.get(:good, key2)

assert {:ok, 2} = Cacheman.put_batch(:good, [{key1, "value1"}, {key2, "value2"}])

assert {:ok, "value1"} = Cacheman.get(:good, key1)
assert {:ok, "value2"} = Cacheman.get(:good, key2)
end

test "put_batch with TTL" do
key1 = "test-#{:rand.uniform(10_000)}"
key2 = "test-#{:rand.uniform(10_000)}"

assert {:ok, nil} = Cacheman.get(:good, key1)
assert {:ok, nil} = Cacheman.get(:good, key2)

ttl = :timer.seconds(1)

assert {:ok, 2} = Cacheman.put_batch(:good, [{key1, "value1"}, {key2, "value2"}], ttl: ttl)

assert {:ok, "value1"} = Cacheman.get(:good, key1)
assert {:ok, "value2"} = Cacheman.get(:good, key2)

:timer.sleep(ttl)

assert {:ok, nil} = Cacheman.get(:good, key1)
assert {:ok, nil} = Cacheman.get(:good, key2)
end

test "get_batch" do
key1 = "test-#{:rand.uniform(10_000)}"
key2 = "test-#{:rand.uniform(10_000)}"

assert {:ok, 2} = Cacheman.put_batch(:good, [{key1, "value1"}, {key2, "value2"}])

assert {:ok, ["value1", "value2"]} = Cacheman.get_batch(:good, [key1, key2])
end

test "fetch and store" do
key = "test-#{System.unique_integer([:positive])}"
key = "test-#{:rand.uniform(10_000)}"

# at the start, there is no value
assert {:ok, nil} = Cacheman.get(:good, key)
Expand All @@ -56,7 +98,7 @@ defmodule CachemanTest do
end

test "TTL for keys" do
key = "test-#{System.unique_integer([:positive])}"
key = "test-#{:rand.uniform(10_000)}"
ttl = :timer.seconds(1)

assert {:ok, "hello"} = Cacheman.put(:good, key, "hello", ttl: ttl)
Expand Down Expand Up @@ -116,8 +158,28 @@ defmodule CachemanTest do
assert {:ok, nil} = Cacheman.get(:broken, "test1")
end

test "put_batch cant reach redis server" do
key1 = "test-#{:rand.uniform(10_000)}"
key2 = "test-#{:rand.uniform(10_000)}"

assert {:ok, nil} = Cacheman.get(:good, key1)
assert {:ok, nil} = Cacheman.get(:good, key2)

assert {:error, _} = Cacheman.put_batch(:broken, [{key1, "value1"}, {key2, "value2"}])

assert {:ok, nil} = Cacheman.get(:good, key1)
assert {:ok, nil} = Cacheman.get(:good, key2)
end

test "get_batch" do
key1 = "test-#{:rand.uniform(10_000)}"
key2 = "test-#{:rand.uniform(10_000)}"

assert {:ok, [nil, nil]} = Cacheman.get_batch(:broken, [key1, key2])
end

test "fetch and store" do
key = "test-#{System.unique_integer([:positive])}"
key = "test-#{:rand.uniform(10_000)}"

assert {:ok, nil} = Cacheman.get(:broken, key)
assert {:ok, "hello"} = Cacheman.fetch(:broken, key, fn -> {:ok, "hello"} end)
Expand All @@ -128,7 +190,7 @@ defmodule CachemanTest do
end

test "TTL for keys" do
key = "test-#{System.unique_integer([:positive])}"
key = "test-#{:rand.uniform(10_000)}"
ttl = :timer.seconds(1)

assert {:error, _} = Cacheman.put(:broken, key, "hello", ttl: ttl)
Expand Down

0 comments on commit 7bd0d36

Please sign in to comment.