defmodule Nola.Plugins.Link.Store do alias DialyxirVendored.Warnings.Apply use GenServer require Logger require Record import Ex2ms @type url() :: String.t() Record.defrecord(:link, link: nil, result: nil, at: nil) @type link :: record(:link, link: url(), result: any(), at: nil) Record.defrecord(:link_seen, key: nil, at: nil) @doc "A `link_seen` record represents a link that has been seen at a specific time in a given context." @type link_seen :: record(:link_seen, key: {url(), String.t()}, at: nil) def setup do :ets.new(:links, [:set, :public, :named_table, keypos: 2]) :ets.new(:links_witness, [:set, :public, :named_table, keypos: 2]) end @spec insert_link(url(), any()) :: true def insert_link(url, result) do :ets.insert( :links, link(link: url, result: result, at: DateTime.utc_now() |> DateTime.to_unix()) ) end @spec get_link(url()) :: any() | nil def get_link(url) do case :ets.lookup(:links, url) do [link(result: result)] -> result [] -> nil end end @spec witness_link(url(), String.t()) :: boolean() def inhibit_link?(url, key) do case :ets.lookup(:links_witness, {url, key}) do [_] -> true [] -> false end end @spec witness_link(url(), String.t()) :: :ok | :inhibit def witness_link(url, key) do if inhibit_link?(url, key) do :inhibit else :ets.insert( :links_witness, link_seen(key: {url, key}, at: DateTime.utc_now() |> DateTime.to_unix()) ) :ok end end def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__) @doc false @impl true def init(_) do setup() env = Keyword.fetch!(Application.get_env(:nola, Nola.Plugins.Link, []), :store) :erlang.send_after(env[:interval], self(), :expire) {:ok, nil} end @doc false @impl true def handle_info(:expire, state) do env = Keyword.fetch!(Application.get_env(:nola, Nola.Plugins.Link, []), :store) :erlang.send_after(env[:interval], self(), :expire) ttl = env[:ttl] / 1000 inhibit = env[:inhibit] / 1000 now = DateTime.utc_now() |> DateTime.to_unix() links_evicted = :ets.select_delete(:links, [ {{:_, :_, :_, :"$1"}, [{:<, :"$1", now - ttl}], [true]} ]) witness_evicted = :ets.select_delete(:links_witness, [ {{:_, :_, :"$1"}, [{:<, :"$1", now - inhibit}], [true]} ]) Logger.debug("evicted #{links_evicted} links and #{witness_evicted} witnesses") {:noreply, state} end end