diff --git a/lib/guardian/db/sweeper.ex b/lib/guardian/db/sweeper.ex new file mode 100644 index 0000000..b94349c --- /dev/null +++ b/lib/guardian/db/sweeper.ex @@ -0,0 +1,70 @@ +defmodule Guardian.DB.Sweeper do + @moduledoc """ + A GenServer that periodically checks for, and expires, tokens from storage. + + To leverage the automated Sweeper functionality update your project's Application + file to include the following child in your supervision tree: + + ## Example + + worker(Guardian.DB.Sweeper, [interval: 60]) + """ + use GenServer + + alias Guardian.DB.Token + + @sixty_minutes 60 * 60 * 1000 + + def start_link(opts) do + interval = Keyword.get(opts, :interval, @sixty_minutes) + GenServer.start_link(__MODULE__, [interval: interval], name: __MODULE__) + end + + @impl true + def init(state) do + {:ok, schedule(state)} + end + + @impl true + def handle_cast(:reset_timer, state) do + {:noreply, schedule(state)} + end + + @impl true + def handle_cast(:sweep, state) do + Token.purge_expired_tokens() + {:noreply, schedule(state)} + end + + @impl true + def handle_info(:sweep, state) do + Token.purge_expired_tokens() + {:noreply, schedule(state)} + end + + def handle_info(_, state), do: {:noreply, state} + + @doc """ + Manually trigger a database purge of expired tokens. Also resets the current + scheduled work. + """ + def purge do + GenServer.cast(__MODULE__, :sweep) + end + + @doc """ + Reset the purge timer. + """ + def reset_timer do + GenServer.cast(__MODULE__, :reset_timer) + end + + defp schedule(opts) do + if timer = Keyword.get(opts, :timer), do: Process.cancel_timer(timer) + + interval = Keyword.get(opts, :interval) + timer = Process.send_after(self(), :sweep, interval) + + [interval: interval, timer: timer] + end +end diff --git a/lib/guardian/db/token/sweeper.ex b/lib/guardian/db/token/sweeper.ex deleted file mode 100644 index 69219ac..0000000 --- a/lib/guardian/db/token/sweeper.ex +++ /dev/null @@ -1,44 +0,0 @@ -defmodule Guardian.DB.Token.Sweeper do - @moduledoc """ - Purges and schedule work for cleaning up expired tokens. - """ - - alias Guardian.DB.Token - - @doc """ - Purges the expired tokens and schedule the next purge. - """ - def sweep(state) do - Token.purge_expired_tokens() - schedule_work(state) - end - - @doc """ - Schedule the next purge. - """ - def schedule_work(state) do - if state[:timer] do - Process.cancel_timer(state.timer) - end - - timer = Process.send_after(self(), :sweep, state[:interval]) - Map.merge(state, %{timer: timer}) - end - - @doc false - def get_interval do - :guardian - |> Application.get_env(Guardian.DB) - |> Keyword.get(:sweep_interval, 60) - |> minute_to_ms() - end - - defp minute_to_ms(value) when is_binary(value) do - value - |> String.to_integer() - |> minute_to_ms() - end - - defp minute_to_ms(value) when value < 1, do: 1000 - defp minute_to_ms(value), do: round(value * 60 * 1000) -end diff --git a/lib/guardian/db/token/sweeper_server.ex b/lib/guardian/db/token/sweeper_server.ex deleted file mode 100644 index a006bdc..0000000 --- a/lib/guardian/db/token/sweeper_server.ex +++ /dev/null @@ -1,58 +0,0 @@ -defmodule Guardian.DB.Token.SweeperServer do - @moduledoc """ - Periocially purges expired tokens from the DB. - - ## Example - config :guardian, Guardian.DB, - sweep_interval: 60 # 1 hour - - # in your supervisor - worker(Guardian.DB.Token.SweeperServer, []) - """ - - use GenServer - alias Guardian.DB.Token.Sweeper - - def start_link(opts \\ []) do - defaults = %{ - interval: Sweeper.get_interval() - } - - state = Enum.into(opts, defaults) - - GenServer.start_link(__MODULE__, state, name: __MODULE__) - end - - @doc """ - Reset the purge timer. - """ - def reset_timer do - GenServer.call(__MODULE__, :reset_timer) - end - - @doc """ - Manually trigger a database purge of expired tokens. Also resets the current - scheduled work. - """ - def purge do - GenServer.call(__MODULE__, :sweep) - end - - def init(state) do - {:ok, Sweeper.schedule_work(state)} - end - - def handle_call(:reset_timer, _from, state) do - {:reply, :ok, Sweeper.schedule_work(state)} - end - - def handle_call(:sweep, _from, state) do - {:reply, :ok, Sweeper.sweep(state)} - end - - def handle_info(:sweep, state) do - {:noreply, Sweeper.sweep(state)} - end - - def handle_info(_, state), do: {:noreply, state} -end diff --git a/test/guardian/sweeper_test.exs b/test/guardian/sweeper_test.exs index ef7e644..2f8a728 100644 --- a/test/guardian/sweeper_test.exs +++ b/test/guardian/sweeper_test.exs @@ -1,31 +1,46 @@ -defmodule Guardian.DB.Token.SweeperTest do +defmodule Guardian.DB.SweeperTest do use Guardian.DB.TestSupport.CaseTemplate alias Guardian.DB.Token - alias Guardian.DB.Token.Sweeper + alias Guardian.DB.Sweeper - test "purge stale tokens" do - Token.create( - %{"jti" => "token1", "aud" => "token", "exp" => Guardian.timestamp() + 5000}, - "Token 1" - ) + describe "purge/0" do + test "purges expired tokens" do + Token.create( + %{"jti" => "token1", "aud" => "token", "exp" => Guardian.timestamp() + 5000}, + "Token 1" + ) - Token.create( - %{"jti" => "token2", "aud" => "token", "exp" => Guardian.timestamp() - 5000}, - "Token 2" - ) + Token.create( + %{"jti" => "token2", "aud" => "token", "exp" => Guardian.timestamp() - 5000}, + "Token 2" + ) - interval = 0 - state = %{interval: interval} - new_state = Sweeper.sweep(state) + {:ok, pid} = Sweeper.start_link(interval: 1_000_000_000) + Sweeper.purge() - token1 = get_token("token1") - token2 = get_token("token2") + GenServer.stop(pid) - assert token1 != nil - assert token2 == nil + token1 = get_token("token1") + token2 = get_token("token2") - assert new_state[:timer] != nil - assert_receive :sweep, interval + 10 + assert token1 != nil + assert token2 == nil + end + end + + describe "reset_timer" do + test "cancels and restarts the existing timer" do + interval = 1_000_000_000 + {:ok, pid} = Sweeper.start_link(interval: interval) + + [interval: ^interval, timer: timer] = :sys.get_state(pid) + + Sweeper.reset_timer() + + [interval: _interval, timer: new_timer] = :sys.get_state(pid) + + assert timer != new_timer + end end end