Current section
Files
Jump to
Current section
Files
lib/cache/cache.ex
defmodule Spoxy.Cache do
@moduledoc """
"""
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