For read-only systems, like Heroku, you won't be able to use the built-in Mnesia backend cache for distribution and to persist cache data between restarts. Instead let's use Redix to store our cache data in Redis.
First add Redix to your list of dependencies in mix.exs
:
def deps do
[
# ...
{:redix, "~> 0.9.2"}
]
end
Now set up your WEB_PATH/pow_redis_cache.ex
like so:
defmodule MyAppWeb.PowRedisCache do
@behaviour Pow.Store.Base
alias Pow.Config
@redix_instance_name :redix
def put(config, key, value) do
key = redis_key(config, key)
ttl = Config.get(config, :ttl)
value = :erlang.term_to_binary(value)
command = put_command(key, value, ttl)
Redix.noreply_command(@redix_instance_name, command)
end
defp put_command(key, value, ttl) when is_integer(ttl) and ttl > 0, do: ["SET", key, value, "PX", ttl]
defp put_command(key, value, _ttl), do: ["SET", key, value]
def delete(config, key) do
key = redis_key(config, key)
Redix.noreply_command(@redix_instance_name, ["DEL", key])
end
def get(config, key) do
key = redis_key(config, key)
case Redix.command(@redix_instance_name, ["GET", key]) do
{:ok, nil} -> :not_found
{:ok, value} -> :erlang.binary_to_term(value)
end
end
def keys(config) do
namespace = redis_key(config, "")
length = String.length(namespace)
{:ok, values} = Redix.command(@redix_instance_name, ["KEYS", "#{namespace}*"])
Enum.map(values, &String.slice(&1, length..-1))
end
defp redis_key(config, key) do
namespace = Config.get(config, :namespace, "cache")
"#{namespace}:#{key}"
end
end
We'll need to start the Redix application on our app startup, so in application.ex
add {Redix, name: :redix}
to your supervision tree:
def start(_type, _args) do
import Supervisor.Spec
children = [
# ...
{Redix, name: :redix}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
By default localhost Redis is used, but you can update this by using a Redis URI: {Redix, {"redis://:secret@redix.example.com:6380/1", [name: :redix]}}
Finally update the config with your new Redis cache backend:
config :my_app, :pow,
user: MyApp.Users.User,
repo: MyApp.Repo,
cache_store_backend: MyAppWeb.PowRedisCache
And now you've a running Redis cache store backend!
defmodule MyAppWeb.PowRedisCacheTest do
use ExUnit.Case
doctest MyAppWeb.PowRedisCache
alias MyAppWeb.PowRedisCache
@default_config [namespace: "test", ttl: :timer.hours(1)]
test "can put, get and delete records" do
assert PowRedisCache.get(@default_config, "key") == :not_found
PowRedisCache.put(@default_config, "key", "value")
:timer.sleep(100)
assert PowRedisCache.get(@default_config, "key") == "value"
PowRedisCache.delete(@default_config, "key")
:timer.sleep(100)
assert PowRedisCache.get(@default_config, "key") == :not_found
end
test "fetch keys" do
PowRedisCache.put(@default_config, "key1", "value")
PowRedisCache.put(@default_config, "key2", "value")
:timer.sleep(100)
assert Enum.sort(PowRedisCache.keys(@default_config)) == ["key1", "key2"]
end
test "records auto purge" do
config = Keyword.put(@default_config, :ttl, 100)
PowRedisCache.put(config, "key", "value")
:timer.sleep(50)
assert PowRedisCache.get(config, "key") == "value"
:timer.sleep(100)
assert PowRedisCache.get(config, "key") == :not_found
end
end