summaryrefslogtreecommitdiff
path: root/lib/plugins/link/store.ex
blob: 4e2aa58dcd9be59abd362d19aaad5924ca4b32d7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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