I have an web application in elixir that looks like this
defmodule Test do
use Plug.Router
plug :match
plug :dispatch
def expensiveComputation() do
// performs an expensive computation and
// returns a list
end
get "/randomElement" do
randomElement = expensiveComputation() |> Enum.random
send_resp(conn, 200, randomElement)
end
end
Whenever I issue a GET
request to /randomElement
, expensiveComputation
gets called. The expensiveComputation
function takes a long time to run but returns the same thing every time it is called. What is the simplest way to cache the result so that it gets run only once on startup?
You could use ETS to cache expensive computations. Here's something I wrote recently, it might not be a full-fledged caching solution but it works just fine for me:
defmodule Cache do
@table __MODULE__
def start do
:ets.new @table, [:named_table, read_concurrency: true]
end
def fetch(key, expires_in_seconds, fun) do
case lookup(key) do
{:hit, value} ->
value
:miss ->
value = fun.()
put(key, expires_in_seconds, value)
value
end
end
defp lookup(key) do
case :ets.lookup(@table, key) do
[{^key, expires_at, value}] ->
case now < expires_at do
true -> {:hit, value}
false -> :miss
end
_ ->
:miss
end
end
defp put(key, expires_in_seconds, value) do
expires_at = now + expires_in_seconds
:ets.insert(@table, {key, expires_at, value})
end
defp now do
:erlang.system_time(:seconds)
end
end
First you need to call Cache.start
somewhere, so the ETS table will be created (for example in your app's start
function). Then you can use it like this:
value = Cache.fetch cache_key, expires_in_seconds, fn ->
# expensive computation
end
For example:
Enum.each 1..100000, fn _ ->
message = Cache.fetch :slow_hello_world, 1, fn ->
:timer.sleep(1000) # expensive computation
"Hello, world at #{inspect :calendar.local_time}!"
end
IO.puts message
end
In Elixir when you want state you almost always need to have a process to keep that state. The Agent
module is particularly suited for the kind of operation you want - simply wrapping some value and accessing it. Something like this should work:
defmodule Cache do
def start_link do
initial_state = expensive_computation
Agent.start_link(fn -> initial_state end, name: __MODULE__)
end
def get(f \\ &(&1)) do
Agent.get(__MODULE__, f)
end
defp expensive_computation do
# ...
end
end
Then you can plug in Cache
into your supervision tree normally, and just Cache.get
when you need the result of expensive_computation
.
Please note that this will literally run expensive_computation
on startup - in the process bringing up your supervision tree. If it is very expensive - on the order of 10 seconds or more - you might want to move the computation to the Agent
process:
def start_link do
Agent.start_link(fn -> expensive_computation end, name: __MODULE__)
end
You need to handle the case of the cache being empty in that case, while in the first example the startup is blocked until expensive_computation
is finished. You can use it by placing workers depending on the Cache
later in the startup order.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With