Session on a GenServer

I want to use a GenServer to store my session data, it would have a unique name using UUID which is then send to the client and every time the client ask for session data it would just fetch from the GenServer

defmodule Publit.SessionService do
  use GenServer
  @expires_in_minutes 30

  defstruct [:key, :id, :name, :expires_at]

  def start_link(%{key: key} = args) do
    GenServer.start_link(__MODULE__, args, name: key)
  end

  def init(args) do
    args = Map.put(args, :expires_at, future_time())
    {:ok, struct(%Publit.SessionService{}, args)}
  end

  defp future_time(future_time \\ @expires_in_minutes) do
    DateTime.to_unix(DateTime.utc_now()) + 60 * future_time
  end

  def get(pid) do
    GenServer.call(pid, :get)
  end

  def handle_call(:get, _from, session) do
    session = Map.put(session, :expires_at, future_time())
    {:reply, session, session}
  end
end

How can I stop and kill a process that as expired expires_at < DateTime.to_unix(DateTime.utc_now()), I would like to be able to do this in the SessionService.get/1 function if possible so when it’s expired I return a nil state

One more question. If a user stoped using for a long time te service but the GenServer process for that user is still alive how can I get all those processes and kill them, should I also have another store GenServer to check what processes are long due?

Assuming you will have a GenServer per session, you can send yourself a message on init to shutdown:

def init(args) do
  Process.send_after(self(), :bye, @expires_in_minutes * 60 * 1000) # microseconds
  {:ok, struct(...)}
end

Then after a certain time, you will receive the message above, which you can handle in handle_info:

def handle_info(:bye, state) do
  {:stop, :shutdown, state}
end

But you may say: “I don’t want to shutdown if I received a get request not a long time ago”. Then you can cancel the shutdown request on every get and schedule a new one:

def init(args) do
  ref = schedule_shutdown(make_ref()) # create a fake reference to cancel the first time 
  {:ok, struct(..., ref: ref)} # store the reference in the struct too
end

def handle_call(:get, _from, session) do
  ref = schedule_shutdown(session.ref) # cancel existing reference
  {:reply, session, %{session | ref: ref}} # and set the new reference
end

defp schedule_shutdown(ref) do
  Process.cancel_timer(ref)
  Process.send_after(self(), :bye, @expires_in_minutes * 60 * 1000) # microseconds
end
7 Likes

I would use a supervisor that dynamically starts the gen servers. That way your supervisor knows about each gen sever and can map them to the user or session id. I do something similar in a project I have been working on here. Checkout gateway_supervisor.ex and cashier.ex.

Also to stop a given gen server you could setup a timeout in your init callback that sends a message to itself. This will end up in handle_info where you can return the stop tuple {:stop, reason} (docs).

I would have provided some example code but I’m on my phone… sorry :slight_smile:

Edit: and @josevalim beat me to it!

Thanks this solution solves my 2 questions.

How can I test this, I have tried to use mock but did not work, I have been looking something like timecop for Elixir seems difficult to test time with mocks

1 Like

Allow the expiry time to be passed as an argument and set it to a low value like half second in your test. Then:

Publit.SessionService.start_link(..., 500)
ref = Process.monitor(ref)
assert_receive {:DOWN, ^ref, _, _, _}, 1000 # wait at least twice the time to get the notification

That test will be slow (0.5s) but that should not be a problem if you are running your tests concurrently.

2 Likes

On a slightly different topic in this example. Be very careful with using a UUID to create a unique name. The name is an atom and if you create these dynamically at run-time then you will eventually fill the atom table and crash the system. Don’t write systems which dynamically create atoms in an unrestricted way while running. Atoms never go away.

3 Likes

Would you suggest using an ETS table to store all the pids and reference them with a UUID?

Or use the new Registry in Elixir 1.4.

1 Like

It all depends on how you are storing the UUID and whether you are continually creating new ones. If you use atoms for the UUIDs and you are continually creating new ones then it doesn’t matter whether you use ETS tables or the new registry, is it creating new ones which is the problem. If you store them in a binary then an ETS table or the Registry is fine and you can happily create new ones on the fly. But then you can’t use them directly as a label for the gen server. The name you give in the start_link must be an atom.

I would use the ETS or registry to store the process {UUID, pid} that should work and I don’t need to give a name to the GenServer process, the UUID would be a string.

That would definitely work, be safe and reasonably fast as well.