Skip to content

Instantly share code, notes, and snippets.

@samjonester
Last active January 23, 2019 20:08
Show Gist options
  • Save samjonester/72ab24a8a9ecfeab5a065200ce2d4159 to your computer and use it in GitHub Desktop.
Save samjonester/72ab24a8a9ecfeab5a065200ce2d4159 to your computer and use it in GitHub Desktop.
Elixir Info

Libraries for some things

Concurrency in Phoenix

Erlang processes are a way to separate / isolate the runtime concerns of components of an application. Some examples of runtime concerns are parallel execution, fault tolerance, concurrent state management, independent scaling. Spawned processes can be linked to a parent, meaning that if the parent crashes, the child crashes and vice-versa.

Elixir has a few nice abstractions on top of processes for distinct purposes. These are inspired by Erlang behaviors and have been ported to Elixir as modules.

Execute a single action, often with little or no communication with other processes. Designed for parallel processing.

  • Tasks are generally linked to parent processes.
  • Tasks expose async/await to execute a function. Example use pattern:
def (things_to_process)
  things_to_process
  |> Enum.map(&(Task.async(MyModule, :func, [&1]))
  |> Enum.map(&Task.await/1)
end

An abstraction around managing state in a way that is safe for concurrency. Erlang applications (and therefore our Phoenix web apps) will often use ETS as a way to store in-memory state. In a “rails-like” environment you would deploy a cluster of servers and share state with a service like Redis (i.e. cache). Erlang exposes the ability for processes to communicate across a distributed runtime (clustered nodes). This enables state to be stored hot in an Agent of a single node and be accessible by any process within the cluster. The main difference between caching in Redis and via Agents is in recovery. The reason to use a Redis store becomes the need for a persistence (and recovery) model for the store.

  • Spawned with a function for initializing the internal state. Think of this as the ability to define a “cache warming” function.
  • The agent exposes the ability to get or update values within state in a concurrent execution environment.

A server-like runtime that accepts messages from clients. It encapsulates 1+ aspect of an isolated process runtime and allows communication through message passing.

  • Spawned with the initial state of the server
  • Allows definition of callbacks to handle messages being passed to the server
    • call (handle_call): client is blocked waiting on a response
    • cast (handle_cast): client broadcasts a message to the server without receiving a response
    • send (handle_info): handle_call/handle_cast are for the public api of your server and handle_info is for other messages (i.e. monitoring). You should define a “catch-all” clause to avoid unexpected messages crashing the server.

An abstraction that defines a separation between components of a system. Applications have the ability to define a lifecycle that includes loading, starting, stopping (i.e. web app).

  • Releases are packaged and deployed with distillery. Releases can expose applications with different ports, or combine applications into a single port.
  • Can be supervised as a long running process (i.e. Phoenix app)
  • “Messages” can be sent between applications by calling module functions, enabling “library-like” separation without the runtime lifecycle.

Gotchas

  • Processes (like GenServer) can become a concurrency bottleneck. If multiple parallel client processes are calling a single GenServer, then the GenServer becomes a blocking component in the system.
  • Applications cannot have circular references within a single umbrella
  • Race conditions can be created within an Agent by the sequence P1 access, P2 modification, P1 modification based on previous access.
  • Agent crashes will drop the current state

What does this mean for Phoenix Applications?

  • Ranch (tcp socket pool) -> Cowboy (http server) -> Phoenix:
    Provides concurrent requests. Each request is a single process. This means that multiple parallel requests calling a shared resource (i.e. a single GenServer or Application process) can create a performance bottleneck

  • Applications within an umbrella should separate runtime concerns or “peelable” bits of functionality A common approach:

    • web app
    • admin app
    • business logic + ecto stuffs
      Additionally:
    • things you’d put in a dir under lib in rails
    • shared state that needs to be accessed/manipulated in a concurrent environment
    • things you may want to pull out into a separate clustered node but aren’t sure can be completely decoupled
  • "naive caching" should be accomplished by using an Agent (simple communications) or a GenServer (complex communications). What makes this a naive cache is the lack of a built in eviction strategy or expiration strategy.

    Additional Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment