# Copyright(c) 2015-2019 ACCESS CO., LTD. All rights reserved. use Croma defmodule Antikythera.Memcache do @moduledoc """ Easy-to-use in-memory cache for each executor pool. #{inspect(__MODULE__)} behaves as a key-value storage. Cached key-value pairs are internally stored in ETS. It accepts arbitrary (but not too large) terms as both keys and values. ## Usage iex> Antikythera.Memcache.write("foo", "bar", epool_id, 3_600) :ok iex> Antikythera.Memcache.read("foo", epool_id) {:ok, "bar"} ## Limits The number of records and the size of keys and values are limited. - The maximum number of records for each executor pool is #{AntikytheraCore.ExecutorPool.MemcacheWriter.max_records()}. - If exceeds the limit, a record nearest to expiration is evicted so that a new record can be inserted. - The maximum size of keys and values is defined in `Antikythera.Memcache.Key` and `Antikythera.Memcache.Value`. - To know how the size of keys and values is calculated, see `Antikythera.TermUtil`. - If exceeds the limit, `write/5` returns an error `:too_large_key` or `:too_large_value`. ## Lifetime of records There are 2 cases where records in #{inspect(__MODULE__)} are evicted: 1. Records are expired (see [Mechanism of Expiration](#module-mechanism-of-expiration) below for more details) 2. Reach the maximum number of records for each executor pool (as mentioned in [Limits](#module-limits)) Please note that records in #{inspect(__MODULE__)} could be evicted anytime. ## Mechanism of Expiration The lifetime of records must be set as `lifetime_in_sec` in `write/5`. This lifetime does not guarantee that records remain in the entire specified lifetime. To avoid the [thundering herd](https://en.wikipedia.org/wiki/Thundering_herd_problem), whether records are expired is decided probabilistically. The probability of expiration is shown in the following. ![Mechanism of Expiration](assets/MemcacheExpiration.png) If the thundering herd becomes a big problem, adjust `prob_lifetime_ratio` in `write/5`. """ @default_ratio 0.9 alias Croma.Result, as: R alias AntikytheraCore.ExecutorPool.MemcacheWriter alias Antikythera.ExecutorPool.Id, as: EPoolId alias AntikytheraCore.Ets.Memcache alias Antikythera.Memcache.{Key, Value, NormalizedFloat} @doc """ Read the value associated with the `key` from #{inspect(__MODULE__)}. Please note that records in #{inspect(__MODULE__)} could be evicted anytime so the error handling must be needed. """ defun read(key :: Key.t, epool_id :: v[EPoolId.t]) :: R.t(Value.t, :not_found) do Memcache.read(key, epool_id) |> R.bind(fn {_, expire_at, prob_expire_at, value} -> if expired?(expire_at, prob_expire_at) do {:error, :not_found} else {:ok, value} end end) end @doc """ Try to read a value associated with the `key` from #{inspect(__MODULE__)} and if that fails, write a value returned by `fun` to #{inspect(__MODULE__)}. `fun` is evaluated only if a value is not found, and the new value returned by `fun` is stored in #{inspect(__MODULE__)}. If a value is found in #{inspect(__MODULE__)} or writing the new value to #{inspect(__MODULE__)} succeeds, the value is returned as `{:ok, value}`, but if writing the new value fails, an error is returned in the same manner as `write/5`. Parameters `lifetime_in_sec` and `prob_lifetime_ratio` are used to call `write/5` and the details are described above. """ defun read_or_else_write(key :: Key.t, epool_id :: v[EPoolId.t], lifetime_in_sec :: v[non_neg_integer], prob_lifetime_ratio :: v[NormalizedFloat.t] \\ @default_ratio, fun :: (-> Value.t)) :: R.t(Value.t, :too_large_key | :too_large_value) do case read(key, epool_id) do {:ok, value} -> {:ok, value} {:error, _not_found} -> value = fun.() case write(key, value, epool_id, lifetime_in_sec, prob_lifetime_ratio) do :ok -> {:ok, value} err -> err end end end defp expired?(expire_at, prob_expire_at) do case System.monotonic_time(:millisecond) do now when now < prob_expire_at -> false now when expire_at < now -> true now -> rnd = :rand.uniform() t0 = expire_at - prob_expire_at t1 = now - prob_expire_at rnd < (t1 / t0) end end @doc """ Write a key-value pair to #{inspect(__MODULE__)}. See above descriptions for more details. """ defun write(key :: Key.t, value :: Value.t, epool_id :: v[EPoolId.t], lifetime_in_sec :: v[non_neg_integer], prob_lifetime_ratio :: v[NormalizedFloat.t] \\ @default_ratio) :: :ok | {:error, :too_large_key | :too_large_value} do cond do not Key.valid?(key) -> {:error, :too_large_key} not Value.valid?(value) -> {:error, :too_large_value} true -> MemcacheWriter.write(key, value, epool_id, lifetime_in_sec, prob_lifetime_ratio) end end defmodule Key do @max_size 128 @moduledoc """ A type module of keys for `Antikythera.Memcache`. The maximum size of keys is #{@max_size} bytes. To know how the size is calculated, see `Antikythera.TermUtil`. """ @type t :: any defun valid?(key :: term) :: boolean do Antikythera.TermUtil.size_smaller_or_equal?(key, @max_size) end defun max_size() :: non_neg_integer, do: @max_size end defmodule Value do @max_size 65_536 @moduledoc """ A type module of values for `Antikythera.Memcache`. The maximum size of values is #{@max_size} bytes. To know how the size is calculated, see `Antikythera.TermUtil`. """ @type t :: any defun valid?(value :: term) :: boolean do Antikythera.TermUtil.size_smaller_or_equal?(value, @max_size) end defun max_size() :: non_neg_integer, do: @max_size end defmodule NormalizedFloat do use Croma.SubtypeOfFloat, min: 0.0, max: 1.0 end end