GenServer + monotonic_time TTLs

GenServer is Agent + arbitrary message handling. We need handle_info for active expiry sweeps in the exercises. State grows to %{store: %{}, expires: %{}}.

04-expiry/server.exs
elixir
defp check_expired(state, key) do
  case Map.get(state.expires, key) do
    nil -> {state, false}
    ts ->
      if now_ms() >= ts do
        {%{state | store: Map.delete(state.store, key), expires: Map.delete(state.expires, key)}, true}
      else
        {state, false}
      end
  end
end

defp now_ms, do: System.monotonic_time(:millisecond)

check_expired runs inside every read. Returns a new state.

System.monotonic_time is immune to NTP jumps. The number is meaningless on its own (a count of native units since BEAM startup) but the difference between two readings is meaningful. For TTL deadlines, that is exactly what we want.

04-expiry/server.exs
elixir
def handle_call({:ttl, key}, _from, state) do
  {state, expired} = check_expired(state, key)
  reply =
    cond do
      expired -> -2
      not Map.has_key?(state.store, key) -> -2
      not Map.has_key?(state.expires, key) -> -1
      true ->
        remaining = max(0, Map.fetch!(state.expires, key) - now_ms())
        div(remaining, 1000)
    end
  {:reply, reply, state}
end

TTL returns remaining seconds; -1 means no TTL; -2 means missing.

Quiz: Quiz

Loading practice…