diff --git a/lib/cacheman.ex b/lib/cacheman.ex index 1f4848e..3495e8c 100644 --- a/lib/cacheman.ex +++ b/lib/cacheman.ex @@ -81,6 +81,8 @@ defmodule Cacheman do use GenServer require Logger + @default_put_options [ttl: :infinity] + # # Cacheman API # @@ -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. @@ -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: @@ -226,6 +259,25 @@ 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?, [ @@ -233,7 +285,12 @@ defmodule Cacheman do 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 @@ -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, [ diff --git a/lib/cacheman/backend/redis.ex b/lib/cacheman/backend/redis.ex index 591d1ff..794e00b 100644 --- a/lib/cacheman/backend/redis.ex +++ b/lib/cacheman/backend/redis.ex @@ -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 @@ -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) diff --git a/test/cacheman_test.exs b/test/cacheman_test.exs index b3c4e9d..4016bca 100644 --- a/test/cacheman_test.exs +++ b/test/cacheman_test.exs @@ -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) @@ -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) @@ -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) @@ -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)