Andrew Fontaine

Mostly code

Saving State Between API Calls

25 Nov 2018

While working on a phoenix application, I found the need to hang onto some state between API calls. It wasn’t quite enough state, and losing the state wasn’t exactly a huge loss, so I decided to use Elixir’s Agent.

To ensure my state was easy to retrieve, I needed to name my Agent after something I’d easily have. As this is a Twilio application, the phone number being called should do nicely.

defmodule HelloWorld.OngoingCall do

  def start_call(%{number: number} = state) do
    Agent.start(fn -> state end, String.to_atom(number))
  end

end

If you wanted to make sure that the Agent will recover, you could link it to a dynamic supervisor. That will not be covered here.

Twilio will provide update events as your call progresses depending on the listed events, and I want to capture failed, busy, and no-answer events. Now, we need to add a controller action.

defmodule HelloWorld.HelloWorldController do

  def update_call(conn, %{To => to, CallStatus => call_status}) do
    state =
      to
      |> OngoingCall.get_state()
      |> OngoingCall.update_state(call_status)

    render(conn, "call_updated.xml", state)
  end

end

Agent.update/3 takes a function that takes the current state and returns the new state. I can write update functions that pattern match on the current call_status to write specific update functions.

defmodule HelloWorld.OngoingCall do

  def update_state(%{number: number} = state, call_status) do
    number
    |> String.to_atom()
    |> Agent.update(update(call_status))
  end

  def update("busy") do
    fn state ->
      %{state | action: "busy"}
    end
  end

end

Now, I can keep track of an ongoing call throughout many different status events. When the call ends, we can tell the Agent to exit.

defmodule HelloWorld.HelloWorldController do

  def update_call(conn, %{To => to, CallStatus => "ended"}) do
    to
    |> OngoingCall.end_state()

    render(conn, "call_ended.xml")

  end

end

# ...
  def end_call(to, reason // :normal) do
    to
    |> String.to_atom()
    |> Agent.stop(reson)
  end
# ...

Now, if anything abnormal happens, we can pass a reason to ensure the failure is captured.

I ❤ feedback. Let me know what you think of this article on Twitter @afontaine_ca