Packages
finitomata
0.29.8
0.41.0
0.40.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.30.3
0.30.2
0.30.1
0.30.0
0.29.10
0.29.9
0.29.8
0.29.7
0.29.6
0.29.5
0.29.4
0.29.3
0.29.2
0.29.1
0.29.0
0.28.1
0.28.0
0.27.1
0.27.0
0.26.4
0.26.3
0.26.2
0.26.1
0.26.0
0.25.0
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.7
0.23.6
0.23.5
0.23.4
0.23.3
0.23.2
0.23.1
0.23.0
0.22.1
0.22.0
0.21.4
0.21.3
0.21.2
0.21.1
0.21.0
0.20.2
0.20.1
0.20.0
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.1
0.17.0
0.16.0
0.15.1
0.15.0
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.0
0.12.1
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.1
0.1.0
The FSM implementation generated from PlantUML textual representation.
Current section
Files
Jump to
Current section
Files
lib/finitomata/cache.ex
defmodule Finitomata.Cache do
@moduledoc since: "0.26.0"
@moduledoc """
The self-curing cache based on `Finitomata` implementation.
This implementation should not be chosen for typical caching scenarios,
use [`cachex`](https://hexdocs.pm/cachex) and/or [`con_cache`](https://hexdocs.pm/con_cache)
instead.
The use-case for this implementation would be somewhat like a self-updated local replica
of the remote data. Unlike typical cache implementations, this one might keep the cached
values up-to-date, configured by `ttl:` argument. Bsaed on processes (backed by `Finitomata`,)
this implementation updates itself periodically, making the value retrieval almost instant.
Consider a remote service supplying currency exchange rates by polling. One might instruct
`Finitomata.Cache` to retrieve values periodically (say, once per a minute,) and then
the consumers of this cache would be able to retrieve the up-to-date values locally without
a penalty of getting a value after a long period (cache miss.)
First of all, the `Finitomata.Cache` implementation should be added to a supervision tree
```elixir
{Finitomata.Cache, [
[id: MyCache, ttl: 60_000, live?: true, type: Infinitomata, getter: &MyMod.getter/1]]}
```
Once the supervisor is started, the values might be retrieven as
```elixir
Finitomata.Cache.get(MyCache, :my_key_1, live?: false) # use default getter
Finitomata.Cache.get(MyCache, :my_key, getter: fn _ -> ExtService.get(:my_key) end)
```
"""
defmodule Config do
@moduledoc false
@spec init(id :: Finitomata.id(), opts :: keyword()) :: :ok
def init(id, opts \\ []) do
:persistent_term.put({__MODULE__, id}, opts)
end
@spec get(id :: Finitomata.id()) :: keyword()
def get(id), do: :persistent_term.get({__MODULE__, id}, [])
end
defmodule Value do
@moduledoc false
@fsm """
idle --> |init!| ready
ready --> |set| set
set --> |ready!| ready
ready --> |stop| done
"""
use Finitomata,
fsm: @fsm,
auto_terminate: true,
timer: 1,
impl_for: [:on_transition],
listener: :mox
defstruct key: nil,
value: :error,
since: nil,
getter: nil,
live?: false,
ttl: Application.compile_env(:finitomata, :cache_ttl, 5_000)
@impl Finitomata
def on_transition(:idle, :init!, _nil, %Value{} = state) do
{:ok, :ready, state}
end
def on_transition(:idle, :init!, _nil, state) do
{:ok, :ready, struct!(__MODULE__, state)}
end
@impl Finitomata
def on_transition(:ready, :set, {getter, live?, value}, %__MODULE__{} = state)
when is_function(getter, 1) do
{:ok, :set,
%__MODULE__{
state
| since: DateTime.utc_now(),
value: {:ok, value},
getter: getter,
live?: live?
}}
end
def on_transition(:ready, :set, getter, %__MODULE__{key: key} = state)
when is_function(getter, 1) do
on_transition(:ready, :set, {getter, state.live?, getter.(key)}, state)
end
def on_transition(:ready, :set, _, %__MODULE__{} = state) do
{:ok, :set, %__MODULE__{state | since: DateTime.utc_now(), value: :error}}
end
@impl Finitomata
def on_timer(:ready, %{timer: {_, 1}, payload: %__MODULE__{ttl: ttl}}) do
{:reschedule, ttl}
end
def on_timer(:ready, %{payload: %__MODULE__{getter: getter, live?: true} = payload} = state) do
Logger.debug(
"Cache value for " <>
inspect(Finitomata.State.human_readable_name(state, false)) <> " is to be renewed"
)
{:transition, {:set, getter}, payload}
end
def on_timer(:ready, %{payload: %__MODULE__{live?: false} = payload} = state) do
Logger.debug(
"Cache value for " <>
inspect(Finitomata.State.human_readable_name(state, false)) <> " is to be unset"
)
{:transition, {:set, :error}, payload}
end
def on_timer(_, %{payload: payload}), do: {:ok, payload}
end
require Logger
schema = [
id: [
required: true,
type: :any,
doc:
"The unique `ID` of this _Finitomata_ “branch,” when `nil` the `#{inspect(__MODULE__)}` value would be used"
],
type: [
required: false,
default: Infinitomata,
type: {:custom, Finitomata, :behaviour, [Finitomata.Supervisor]},
doc:
"The actual `Finitomata.Supervisor` implementation (typically, `Finitomata` or `Infinitomata`)"
],
ttl: [
required: true,
type: :pos_integer,
doc:
"The default time-to-live value in seconds, after which the value would be either revalidated or discarded"
],
live?: [
required: false,
default: false,
type: :boolean,
doc:
"When `true`, the value will be automatically renewed upon expiration (and discarded otherwise)"
],
getter: [
required: false,
default: nil,
type: {:or, [{:fun, 1}, {:in, [nil]}]},
doc:
"The shared for all instances getter returning a value based on the name of the instance, used as a key"
]
]
@schema NimbleOptions.new!(schema)
@doc """
Supervision tree embedder.
## Options to `Finitomata.Cache.start_link/1`
#{NimbleOptions.docs(@schema)}
"""
@spec start_link([unquote(NimbleOptions.option_typespec(@schema))]) :: Supervisor.on_start()
def start_link(opts \\ []) do
opts = NimbleOptions.validate!(opts, @schema)
id = Keyword.fetch!(opts, :id)
type = Keyword.fetch!(opts, :type)
Config.init(id, opts)
type.start_link(id)
end
@spec child_spec([unquote(NimbleOptions.option_typespec(@schema))]) :: Supervisor.child_spec()
def child_spec(opts \\ []) do
opts = NimbleOptions.validate!(opts, @schema)
id = Keyword.fetch!(opts, :id)
%{
id: {__MODULE__, id},
start: {__MODULE__, :start_link, [opts]}
}
end
@spec opts(id :: Finitomata.id()) :: [unquote(NimbleOptions.option_typespec(@schema))]
@doc false
defp opts(id), do: Config.get(id)
@doc """
Retrieves the value either cached or via `getter/1` anonymous function and caches it.
Spawns the respective _Finitomata_ instance if needed.
"""
@spec get(
id :: Finitomata.id(),
key :: key,
opts :: [
{:getter, (key -> value)}
| {:live?, boolean()}
| {:reset, boolean()}
| {:ttl, pos_integer()}
]
) :: {DateTime.t(), value} | {:instant, value} | :error
when key: any(), value: any()
def get(id, key, opts \\ []) do
opts = id |> opts() |> Keyword.merge(opts)
{reset, opts} = Keyword.pop(opts, :reset, false)
{getter, opts} = Keyword.pop(opts, :getter, nil)
ttl = Keyword.fetch!(opts, :ttl)
live? = Keyword.fetch!(opts, :live?)
type = Keyword.fetch!(opts, :type)
maybe_start =
if not type.alive?(id, key) do
type.start_fsm(
id,
key,
Value,
struct!(Value, key: key, getter: getter, live?: live?, ttl: ttl)
)
end
case maybe_start do
{:ok, _pid} ->
if is_function(getter, 1) do
{:created, tap(getter.(key), &type.transition(id, key, {:set, {getter, live?, &1}}))}
else
Logger.error("Initial call to `Finitomata.Cache.get/3` must contain a getter")
type.transition(id, key, :stop)
:error
end
{:error, error} when not is_tuple(error) ->
Logger.warning("Could not start FSM. Error: " <> inspect(error))
:error
# nil | {:error, {:already_started, _pid}}
_ ->
case {reset, getter, type.state(id, key, :payload)} do
{false, nil, %Value{since: since, value: {:ok, value}}} ->
{since, value}
{false, getter, %Value{since: since, value: {:ok, value}}} ->
Logger.warning(
"Setting a `getter` without `reset` does not make any sense, got: " <>
inspect(getter)
)
{since, value}
{_, getter, %Value{}} when is_function(getter, 1) ->
{:instant, tap(getter.(key), &type.transition(id, key, {:set, {getter, live?, &1}}))}
{_, nil, %Value{getter: getter}} when is_function(getter, 1) ->
{:instant, tap(getter.(key), &type.transition(id, key, {:set, {getter, live?, &1}}))}
_ ->
Logger.warning("`getter` must be either a function of arity `0` or `nil`")
:error
end
end
end
@doc false
@spec get_naive(
id :: Finitomata.id(),
key :: key,
opts :: [
{:getter, (key -> value)}
| {:live?, boolean()}
| {:reset, boolean()}
| {:ttl, pos_integer()}
]
) :: {DateTime.t(), value} | {:instant, value} | :error
when key: any(), value: any()
def get_naive(id, key, opts \\ []) do
opts = id |> opts() |> Keyword.merge(opts)
{reset, opts} = Keyword.pop(opts, :reset, false)
{getter, opts} = Keyword.pop(opts, :getter, nil)
ttl = Keyword.fetch!(opts, :ttl)
live? = Keyword.fetch!(opts, :live?)
type = Keyword.fetch!(opts, :type)
id
|> type.start_fsm(
key,
Value,
struct!(Value, key: key, getter: getter, live?: live?, ttl: ttl)
)
|> case do
{:ok, _pid} ->
if is_function(getter, 1) do
{:created, tap(getter.(key), &type.transition(id, key, {:set, {getter, live?, &1}}))}
else
Logger.warning("Initial call to `Finitomata.Cache.get/3` must contain a getter")
type.transition(id, key, :stop)
:error
end
{:error, {:already_started, _pid}} ->
case {reset, getter, type.state(id, key, :payload)} do
{false, nil, %Value{since: since, value: {:ok, value}}} ->
{since, value}
{false, getter, %Value{since: since, value: {:ok, value}}} ->
Logger.warning(
"Setting a `getter` without `reset` does not make any sense, got: " <>
inspect(getter)
)
{since, value}
{_, getter, %Value{}} when is_function(getter, 1) ->
{:instant, tap(getter.(key), &type.transition(id, key, {:set, {getter, live?, &1}}))}
{_, nil, %Value{getter: getter}} when is_function(getter, 1) ->
{:instant, tap(getter.(key), &type.transition(id, key, {:set, {getter, live?, &1}}))}
_ ->
Logger.warning("`getter` must be either a function of arity `0` or `nil`")
:error
end
{:error, error} ->
Logger.warning("Could not start FSM. Error: " <> inspect(error))
:error
end
end
@doc """
Erases the cache assotiated with `key`.
"""
@spec erase(id :: Finitomata.id(), key :: any()) :: :ok
def erase(id, key) do
type = id |> opts() |> Keyword.fetch!(:type)
type.transition(id, key, :stop)
end
end