defmodule WhoNeedHelp.TrackingPresenceCleanup do @moduledoc false use GenServer alias WhoNeedHelp.Tracking alias WhoNeedHelpWeb.Presence def start_link(options), do: GenServer.start_link(__MODULE__, options, name: __MODULE__) def maybe_stop(key, assignment_id, user_id, tracking_session_id) do GenServer.cast( __MODULE__, {:maybe_stop, key, assignment_id, user_id, tracking_session_id} ) end @impl GenServer def init(_options) do # Phoenix 1.8.9's locked browser client can wait up to 5 seconds between # reconnect attempts. Waiting for that same interval prevents a serving # node replacement from being mistaken for the user closing the last tab. grace_ms = Application.fetch_env!(:who_need_help, :tracking_presence_cleanup_grace_ms) if not is_integer(grace_ms) or grace_ms < 0 do raise ArgumentError, ":tracking_presence_cleanup_grace_ms must be a non-negative integer" end {:ok, %{grace_ms: grace_ms, pending: %{}}} end @impl GenServer def handle_cast( {:maybe_stop, key, assignment_id, user_id, tracking_session_id}, state ) do cleanup_key = {key, assignment_id, user_id, tracking_session_id} case Map.get(state.pending, cleanup_key) do %{timer_ref: timer_ref} -> Process.cancel_timer(timer_ref) nil -> :ok end token = make_ref() timer_ref = Process.send_after( self(), {:maybe_stop_after_grace, cleanup_key, token}, state.grace_ms ) pending = Map.put(state.pending, cleanup_key, %{timer_ref: timer_ref, token: token}) {:noreply, %{state | pending: pending}} end @impl GenServer def handle_info({:maybe_stop_after_grace, cleanup_key, token}, state) do case Map.get(state.pending, cleanup_key) do %{token: ^token} -> {key, assignment_id, user_id, tracking_session_id} = cleanup_key if Presence.get_by_key(Presence.tracking_topic(), key) == [] do Tracking.stop_browser_session(assignment_id, user_id, tracking_session_id) end {:noreply, %{state | pending: Map.delete(state.pending, cleanup_key)}} _stale_or_cancelled -> {:noreply, state} end end end