Have You Tried Just Using a Function?

in #programming8 years ago

Have You Tried Just Using a Function?

Last month I read Saša Jurić’s To Spawn, or Not to Spawn article and its been lurking in my subconscious ever since.

I was recently working on the command handler and event sourcing system that drives my new project, Inject Detect, and this exact topic reared its head. I realized that I had been overcomplicating the project with exessive usage of Elixir processes.

Refactoring my command handler from a process into simple functions hugely simplified the application, and opened the doors for a new set of functionality I wanted to implement.

The Command Handler Process

For a high level overview, a “command” in Inject Detect represents something you want to do in the system, like requesting a new sign-in token for a user (RequestSignInToken), or ingesting a batch of queries from a user’s application (IngestQueries).

Commands are “handled” by passing them to the command handler:

%IngestQueries{application_id: application.id, queries: queries}
|> CommandHandler.handle(command)

The job of the command handler is to determine if the command is allowable based on the state of the system and the current user’s authorizations. If valid, the command being handled (IngestQueries in this case) will produce a list of events (such as IngestedQuery and IngestedUnexpectedQuery). These events are saved to the database, and a handful of “event listeners” are notified.

Command Handler as a Process

Originally, the CommandHandler was implemented as a GenServer-based Elixir process. The call to CommandHandler.handle triggered a GenServer.call to the CommandHandler process:

def handle(command, context \\ %{}) do
  GenServer.call(__MODULE__, {:handle, command, context})
end

The corresponding handle_call callback would handle the command, store the resulting events, and synchronously notify any interested listeners:

def handle_call({:handle, command, context}, _, []) do
  with {:ok, events, context} <- handle_command(command, context),
       {:ok, _}               <- store_events(events),
  do
    notify_listeners(events, context)
    {:reply, {:ok, context}, []}
  else
    error -> {:reply, error, []}
  end
end

Triggering Commands from Listeners

For several weeks, this solution worked just fine. It wasn’t until I started adding more complex event listeners that I ran into real issues.

I mentioned earlier that event listeners are notified whenever an event is produced by a command. In some cases, these listeners may want to fire off a new command. For instance, when an IngestedUnexpectedQuery event is fired, a listener may want to execute a SendUnexpectedEmail command.

Implementing this feature blew up in my face.

Because listeners are called synchronously from my CommandHandler.handle function, another call to CommandHandler.handle from within a listener would result in a GenServer timeout.

The first call to CommandHandler.handle won’t reply until the second CommandHandler.handle call is finished, but the second CommandHandler.handle call won’t be processed until the first call finishes. The second call will wait until it hits its timeout threshold and eventually fail.

We’ve hit a deadlock.

The only way to handle this situation would be to execute either the second call to CommandHandler.handle, or the entire listener function within an unsupervised, asynchronous process:

Task.start(fn -> 
  %SendUnexpectedEmail{...}
  |> CommandHandler.handle
end)

I wasn’t willing to go down this road due to testing difficulties and a general distrust of unsupervised children.

Command Handler as a Function

After mulling over my deadlock problem, the solution slapped me in the face.

The functionality of the command handler could be entirely implemented as a module of simple functions. No process or GenServer required.

A quick refactor led me to this solution:

def handle(command, context, listeners) do
  with {:ok, events, context} <- handle_command(command, context),
       {:ok, _}               <- store_events(events)
  do
    notify_listeners(events, context, listeners)
    {:ok, context}
  end
end

After the refactor, a synchronously called event listener can recursively call CommandHandler.handle to handle any follow-up commands it wants to execute.

Perfect.

Have You Tried Just Using a Function?

In hindsight, I had no particular reason for implementing the CommandHandler module as a GenServer. It managed no state and had no specific concurrency concerns that demanded the use of a process.

When given a hammer, everything starts to look like a nail.

Remember to use the right tool for the job. In many cases, the right tool is the simplest tool. Often, the simplest tool for the job is to just use a function.

Sort:  

ZachAMcCoy Zach McCoy tweeted @ 09 Mar 2017 - 23:26 UTC

Have you tried just using a function?

Disclaimer: I am just a bot trying to be helpful.