Current section
Files
Jump to
Current section
Files
lib/cache/cache.ex
defmodule Spoxy.Cache do
@moduledoc """
responsible on orchestration of the request lifecycle.
in case the request is cached returns the response,
if the cached data is in the cache but with stale data,
the cached data will be returned when executed within `non-blocking` mode.
when the request isn't in the cache, trigger a calculation (called prerender)
and stores the result for later usage.
"""
alias Spoxy.Cache.Janitor
def async_get_or_fetch(mods, req, req_key, opts \\ []) do
Task.async(fn ->
started = System.system_time(:milliseconds)
{:ok, resp} = get_or_fetch(mods, req, req_key, opts)
ended = System.system_time(:milliseconds)
{:ok, resp, ended - started}
end)
end
def get_or_fetch(mods, req, req_key, opts \\ []) do
{prerender_module, store_module, tasks_executor_mod} = mods
hit_or_miss = get(store_module, req_key, opts)
case hit_or_miss do
{:hit, {resp, metadata}} ->
if should_invalidate?(req, resp, metadata) do
# the cache holds stale data,
# now we need to decide if we will force refreshing the cache
# before returning a response (a _blocking_ call)
# or whether we'll return a stale reponse and enqueue
# a background task that will refresh the cache
blocking = Keyword.get(opts, :blocking, false)
if blocking do
# we don't want the stale data, so force recalculation
refresh_req!({prerender_module, store_module}, req, req_key, opts)
else
# we'll spawn a background task in a fire-and-forget manner
# that will make sure the stale data is refreshed
# so that future requests, will benefit from a fresh data
Task.start(fn ->
enqueue_req(tasks_executor_mod, req_key, req, opts)
end)
# returning the stale data
{:ok, resp}
end
else
# we have a fresh data in the cache
{:ok, resp}
end
{:miss, _} ->
# we have nothing in the cache, we need to calculate the request's value
refresh_req!({prerender_module, store_module}, req, req_key, opts)
end
end
def get(store_module, req_key, opts \\ []) do
{:ok, table_name} = Keyword.fetch(opts, :table_name)
lookup = lookup_req(store_module, table_name, req_key)
case lookup do
nil -> {:miss, "couldn't locate in cache"}
_ -> {:hit, lookup}
end
end
def refresh_req!({prerender_module, store_module}, req, req_key, opts) do
case do_req(prerender_module, req) do
{{:ok, resp}, :active} ->
{:ok, table_name} = Keyword.fetch(opts, :table_name)
{:ok, ttl_ms} = Keyword.fetch(opts, :ttl_ms)
version = UUID.uuid1()
metadata = %{version: version}
store_opts = [table_name, {req, req_key, resp, metadata}, opts]
store_req!(store_module, store_opts)
do_janitor_work = Keyword.get(opts, :do_janitor_work, true)
if do_janitor_work do
Janitor.schedule_janitor_work(
store_module,
table_name,
req_key,
version,
ttl_ms * 2
)
end
{:ok, resp}
{{:ok, resp}, :passive} ->
{:ok, resp}
{{:error, reason}, _} ->
{:error, reason}
end
end
def do_req(prerender_module, req) do
apply(prerender_module, :perform, [req])
end
def store_req!(store_module, opts) do
apply(store_module, :store_req!, opts)
end
def lookup_req(store_module, table_name, req_key) do
apply(store_module, :lookup_req, [table_name, req_key])
end
def should_invalidate?(_req, _resp, metadata) do
%{expires_at: expires_at} = metadata
System.system_time(:milliseconds) > expires_at
end
def enqueue_req(tasks_executor_mod, req_key, req, opts) do
tasks_executor_mod.enqueue_task(req_key, [req, opts])
end
end