Secret OTP Agent

Thu, Jul 15, 2021 6 min read
Let’s learn OTP Agent by building our version using Elixir primitives

Elixir has a secret agency called OTP, with plenty of operatives in its ranks. One of the foot soldiers is the Agent.

Whenever you need to share state among different processes or by the same process at other points in time, you can use Agent. It allows you to have a separate process that keeps your state and allows you to change it and retrieve it in a concurrently safe way.

Let’s first start by testing how OTP Agent works:

iex(1)> {:ok, pid} = Agent.start_link(fn -> [] end)
{:ok, #PID<0.112.0>}
iex(2)> Agent.get(pid, fn digits -> digits end)
[]
iex(3)> Agent.update(pid, fn digits -> digits ++ ["1"] end)
:ok
iex(4)> Agent.update(pid, fn digits -> digits ++ ["2"] end)
:ok
iex(5)> Agent.get(pid, fn digits -> digits end)
["1", "2"]
iex(6)> Agent.stop(pid)
:ok

In this case, we are using Agent to keep the combination of the digits to open a safe deposit box.

The Agent functions can be called anywhere in your safe deposit secret cracking program. You only need to know the pid of the Agent’s process after starting it.

Anonymous functions are used to build, get and update state:

create the state’s initial version

{:ok, pid} = Agent.start_link(fn -> [] end)

Get the current version of the state

Agent.get(pid, fn digits -> digits end)

Update the state

Agent.update(pid, fn digits -> digits ++ ["4"] end)

Let’s build our secret agency, OoohTP, and reverse engineer Agent to build our own SecretAgent. We will use the basic functions to handle processes in Elixir, namely spawn_link, send, and receive.

First, we need to create the SecretAgent

mix new oooh_t_p

And create a file secret_agent.ex in lib/oooh_tp/secret_agent.ex.

defmodule OoohTP.SecretAgent do
  def start_link(fun) do
    pid = spawn_link(fun)
    {:ok, pid}
  end
end

It will spawn a new process and run our anonymous state function. Let’s test it.

iex(1)> {:ok, pid} = OoohTP.SecretAgent.start_link(fn -> [] end)
{:ok, #PID<0.175.0>}
iex(2)> self()
#PID<0.173.0>

We have a new process id for our SecretAgent.

But wait 😱, that process died just after it executed the anonymous function.

iex(3)> Process.alive?(pid)
false
#PID<0.173.0>

So we need to prevent the process from dying. We can do it in two possible ways, recursion or waiting for a new message. Let’s start with the first one.

defmodule OoohTP.SecretAgent do
  def start_link(fun) do
    pid = spawn_link(fn -> loop(fun.()) end)
    {:ok, pid}
  end

  defp loop(state) do
    loop(state)
  end
end

Now when we test it again:

iex(1)> {:ok, pid} = OoohTP.SecretAgent.start_link(fn -> [] end)
{:ok, #PID<0.156.0>}
iex(2)> Process.alive?(pid)
true

It’s alive and kicking. Notice that even though we have infinite recursion there, it doesn’t blow up. That is because of tail call optimization. Elixir is smart enough to notice that the last call of the function is calling itself and doesn’t add a new entry to the stack call.

Now we need to be able to get the value from the state of our Secret Agent. Let’s send a message to get the state from our server, and in the SecretAgent, let’s receive that message.

defmodule OoohTP.SecretAgent do
  def start_link(fun) do
    pid = spawn_link(fn -> loop(fun.()) end)
    {:ok, pid}
  end

  def get(pid, fun) do
    send(pid, {:get, fun})
  end

  defp loop(state) do
    receive do
      {:get, fun} ->
        IO.puts("Getting digits in #{inspect(self())}")
        fun.(state)
    end

    loop(state)
  end
end

Let’s test it:

iex(7)> {:ok, pid} = OoohTP.SecretAgent.start_link(fn -> [] end)
{:ok, #PID<0.183.0>}
iex(8)> OoohTP.SecretAgent.get(pid, fn state -> state end)
Getting digits in #PID<0.183.0>
{:get, #Function<44.40011524/1 in :erl_eval.expr/5>}

SecretAgent received our message because it printed “Getting digits…” but we didn’t get the state. πŸ€” Right, we need to send the state back to our calling function.

defmodule OoohTP.SecretAgent do
  def start_link(fun) do
    pid = spawn_link(fn -> loop(fun.()) end)
    {:ok, pid}
  end

  def get(pid, fun) do
    send(pid, {:get, fun})
  end

  defp loop(state) do
    receive do
      {:get, fun} ->
        IO.puts("Getting digits in #{inspect(self())}")
        send(??to what pid??, {:reply, fun.(state)})
    end

    loop(state)
  end
end

But wait 😱, we need the pid of the process we want to send back the state. So left pass it as an argument.

defmodule OoohTP.SecretAgent do
  def start_link(fun) do
    pid = spawn_link(fn -> loop(fun.()) end)
    {:ok, pid}
  end

  def get(pid, fun) do
    send(pid, {self(), {:get, fun}})

    receive do
      {:reply, state} -> state
    end
  end

  defp loop(state) do
    receive do
      {from, {:get, fun}} ->
        IO.puts("Getting digits in #{inspect(self())}")
        send(from, {:reply, fun.(state)})
    end

    loop(state)
  end
end

Let’s give it another try:

iex(1)> {:ok, pid} = OoohTP.SecretAgent.start_link(fn -> [] end)
{:ok, #PID<0.156.0>}
iex(2)> OoohTP.SecretAgent.get(pid, fn state -> state end)
Getting digits in #PID<0.156.0>
[]

Pretty cool, right? Now let’s update our digits; otherwise, we can’t open the safe deposit box. So it will be pretty similar to our get flow.

defmodule OoohTP.SecretAgent do
  def start_link(fun) do
    pid = spawn_link(fn -> loop(fun.()) end)
    {:ok, pid}
  end

  def get(pid, fun) do
    send(pid, {self(), {:get, fun}})

    receive do
      {:reply, state} -> state
    end
  end

  def update(pid, fun) do
    send(pid, {self(), {:update, fun}})

    receive do
      {:reply, state} -> state
    end
  end

  defp loop(state) do
    receive do
      {from, {:get, fun}} ->
        IO.puts("Getting digits in #{inspect(self())}")
        send(from, {:reply, fun.(state)})

      {from, {:update, fun}} ->
        IO.puts("Updating digits in #{inspect(self())}")
        fun.(state)
        send(from, {:reply, :ok})
    end

    loop(state)
  end
end

Let’s update some digits:

iex(1)> {:ok, pid} = OoohTP.SecretAgent.start_link(fn -> [] end)
{:ok, #PID<0.156.0>}
iex(2)> OoohTP.SecretAgent.get(pid, fn state -> state end)
Getting digits in #PID<0.156.0>
[]
iex(3)> OoohTP.SecretAgent.update(pid, fn state -> state ++ ["1"] end)
Updating digits in #PID<0.156.0>
:ok
iex(4)> OoohTP.SecretAgent.get(pid, fn state -> state end)
Getting digits in #PID<0.156.0>
[]

Oh crap, doesn’t seem to be working. πŸ€” When we loop, we need to use the new state that resulted from executing the function; otherwise, it will continue to be the same. Let’s fix it.

defmodule OoohTP.SecretAgent do
  def start_link(fun) do
    pid = spawn_link(fn -> loop(fun.()) end)
    {:ok, pid}
  end

  def get(pid, fun) do
    send(pid, {self(), {:get, fun}})

    receive do
      {:reply, state} -> state
    end
  end

  def update(pid, fun) do
    send(pid, {self(), {:update, fun}})

    receive do
      {:reply, state} -> state
    end
  end

  defp loop(state) do
    receive do
      {from, {:get, fun}} ->
        IO.puts("Getting digits in #{inspect(self())}")
        send(from, {:reply, fun.(state)})
        loop(state)

      {from, {:update, fun}} ->
        IO.puts("Updating digits in #{inspect(self())}")
        new_state = fun.(state)
        send(from, {:reply, :ok})
        loop(new_state)
    end
  end
end

Another ride in the park:

iex(1)> {:ok, pid} = OoohTP.SecretAgent.start_link(fn -> [] end)
{:ok, #PID<0.156.0>}
iex(2)> OoohTP.SecretAgent.get(pid, fn state -> state end)
Getting digits in #PID<0.156.0>
[]
iex(3)> OoohTP.SecretAgent.update(pid, fn state -> state ++ ["1"] end)
Updating digits in #PID<0.156.0>
:ok
iex(4)> OoohTP.SecretAgent.get(pid, fn state -> state end)
Getting digits in #PID<0.156.0>
["1"]

Excellent, our SecretAgent is almost similar to the OTP one. Now let’s add the secret command to allow stopping our fearless super agent.

defmodule OoohTP.SecretAgent do
  def start_link(fun) do
    pid = spawn_link(fn -> loop(fun.()) end)
    {:ok, pid}
  end

  def get(pid, fun) do
    send(pid, {self(), {:get, fun}})

    receive do
      {:reply, state} -> state
    end
  end

  def update(pid, fun) do
    send(pid, {self(), {:update, fun}})

    receive do
      {:reply, state} -> state
    end
  end

  def stop(pid) do
    send(pid, {self(), {:stop, :normal}})

    receive do
      {:reply, state} -> state
    end
  end

  defp loop(state) do
    receive do
      {from, {:get, fun}} ->
        IO.puts("Getting digits in #{inspect(self())}")
        send(from, {:reply, fun.(state)})
        loop(state)

      {from, {:update, fun}} ->
        IO.puts("Updating digits in #{inspect(self())}")
        new_state = fun.(state)
        send(from, {:reply, :ok})
        loop(new_state)

      {from, {:stop, reason}} ->
        IO.puts("Stopping the secret agent #{inspect(self())}")
        send(from, {:reply, :ok})
        exit(reason)
    end
  end
end

And now, let’s try to stop our secret agent.

iex(2)> OoohTP.SecretAgent.get(pid, fn state -> state end)
Getting digits in #PID<0.156.0>
[]
iex(3)> OoohTP.SecretAgent.update(pid, fn state -> state ++ ["1"] end)
Updating digits in #PID<0.156.0>
:ok
iex(4)> OoohTP.SecretAgent.get(pid, fn state -> state end)
Getting digits in #PID<0.156.0>
["1"]
iex(5)> OoohTP.SecretAgent.stop(pid)
Stopping the secret agent #PID<0.156.0>
:ok
iex(6)> Process.alive?(pid)
false

Awesome, we have a fully functional secret agent. But let’s just refactor it a bit because we see a lot of duplication in the client’s functions to call our agent.

defmodule OoohTP.SecretAgent do
  def start_link(fun) do
    pid = spawn_link(fn -> loop(fun.()) end)
    {:ok, pid}
  end

  def get(pid, fun), do: call(pid, {:get, fun})

  def update(pid, fun), do: call(pid, {:update, fun})

  def stop(pid), do: call(pid, {:stop, :normal})

  defp call(pid, message) do
    send(pid, {self(), message})

    receive do
      {:reply, state} -> state
    end
  end

  defp loop(state) do
    receive do
      {from, {:get, fun}} ->
        IO.puts("Getting digits in #{inspect(self())}")
        send(from, {:reply, fun.(state)})
        loop(state)

      {from, {:update, fun}} ->
        IO.puts("Updating digits in #{inspect(self())}")
        new_state = fun.(state)
        send(from, {:reply, :ok})
        loop(new_state)

      {from, {:stop, reason}} ->
        IO.puts("Stopping the secret agent #{inspect(self())}")
        send(from, {:reply, :ok})
        exit(reason)
    end
  end
end

And we now have a fully functional OoohTP SecretAgent working!