Elixir Functions

 

Building Distributed Systems with Elixir and OTP

In the world of modern software development, building distributed systems has become increasingly crucial. Distributed systems allow us to create highly scalable, fault-tolerant applications that can handle large workloads and provide high availability. Elixir, a functional and concurrent programming language, paired with OTP (Open Telecom Platform), provides an ideal foundation for constructing robust distributed systems.

Building Distributed Systems with Elixir and OTP

In this blog, we will explore the core concepts and advantages of using Elixir and OTP for distributed programming. We will delve into OTP’s building blocks, such as supervisors, GenServers, and event handling, to create fault-tolerant and distributed applications. Along the way, we will also provide code examples to demonstrate the power and simplicity of Elixir and OTP for building distributed systems.

1. Understanding Distributed Systems

A distributed system is a collection of independent components that work together as a cohesive unit to achieve a common goal. These components communicate and coordinate with each other through message passing, enabling them to share information and collaborate.

Some key characteristics of distributed systems include scalability, fault tolerance, and high availability. Scalability allows the system to handle increased workloads by adding more nodes or resources. Fault tolerance ensures that the system can recover gracefully from failures without affecting overall functionality. High availability guarantees that the system remains operational even when some of its components are experiencing issues.

2. The Elixir Language

Before diving into building distributed systems, let’s briefly introduce Elixir. Elixir is a dynamic, functional programming language built on top of the Erlang Virtual Machine (BEAM). It is designed to be concurrent, fault-tolerant, and scalable, making it an excellent choice for distributed systems.

One of Elixir’s core strengths lies in its lightweight processes, also known as Erlang processes. These processes are not operating system threads but rather independent units of execution managed by the BEAM runtime. Due to their lightweight nature, Elixir processes can be created in large numbers, making them suitable for building highly concurrent systems.

Elixir also emphasizes immutability, making it easier to reason about the state of a system. It discourages shared mutable state, which is a common source of bugs and complexity in distributed systems.

3. OTP: The Building Blocks for Distributed Systems

OTP (Open Telecom Platform) is a set of libraries and design principles built on top of the Erlang runtime. It provides a powerful framework for building distributed and fault-tolerant applications. Elixir fully embraces OTP, making it easier for developers to leverage its capabilities.

3.1. Supervisors

Supervisors are at the heart of OTP’s fault-tolerance strategy. They are responsible for starting, monitoring, and restarting processes when failures occur. When a process crashes, the supervisor restarts it to maintain system integrity.

Let’s take a look at a simple example of a supervisor in Elixir:

elixir
defmodule MySupervisor do
  use Supervisor

  def start_link do
    Supervisor.start_link(__MODULE__, [])
  end

  def init(_) do
    children = [
      worker(MyWorker, [])
    ]
    supervise(children, strategy: :one_for_one)
  end
End

In this example, we define a supervisor module MySupervisor using the Supervisor module. The init/1 function specifies the children processes to supervise. In this case, we have a single worker process called MyWorker. If the MyWorker process crashes, the supervisor will automatically restart it.

3.2. GenServers

GenServers (Generic Servers) are the workhorses of Elixir and OTP. They encapsulate the state and behavior of a process, allowing other processes to interact with them through messages. GenServers provide a clean and standardized way to handle state and concurrency.

Here’s a basic GenServer implementation:

elixir
defmodule MyGenServer do
  use GenServer

  # GenServer callbacks
  def init(state) do
    {:ok, state}
  end

  def handle_call(:get_state, _from, state) do
    {:reply, state, state}
  end

  def handle_cast({:update_state, new_state}, state) do
    {:noreply, new_state}
  end
end

In this example, we define a GenServer module MyGenServer using the GenServer module. The init/1 callback initializes the state of the GenServer. The handle_call/3 callback handles synchronous requests, while the handle_cast/2 callback handles asynchronous requests. This clear separation allows us to reason about the behavior of the GenServer more easily.

3.3. Event Handling

In distributed systems, events play a crucial role in communication between components. Elixir provides a built-in event handling mechanism through its GenEvent module. GenEvents allow processes to subscribe to events and receive notifications whenever events are triggered.

Let’s create a simple event handler using GenEvent:

elixir
defmodule MyEventHandler do
  use GenEvent

  def handle_event(event, state) do
    # Custom event handling logic here
    {:ok, state}
  end
end

In this example, we define an event handler module MyEventHandler using the GenEvent module. The handle_event/2 callback specifies how the event should be handled when it is triggered. You can implement your custom logic inside this callback to react to events appropriately.

4. Building a Distributed System with Elixir and OTP

Now that we have an understanding of Elixir, OTP, and its building blocks, let’s put this knowledge into practice by building a simple distributed system: a distributed key-value store.

4.1. Setting Up Nodes

In a distributed system, nodes represent individual instances of the application that communicate with each other. Let’s create three Elixir nodes, each running on a separate terminal:

Terminal 1:

elixir
iex --sname node1@localhost --cookie mycookie

Terminal 2:

elixir
iex --sname node2@localhost --cookie mycookie

Terminal 3:

elixir
iex --sname node3@localhost --cookie mycookie

The –sname flag sets a short name for the node, and the –cookie flag specifies the cookie for authentication between nodes. Make sure to use the same cookie for all nodes in the distributed system.

4.2. Implementing the Distributed Key-Value Store

In our distributed key-value store, we will use a GenServer to store and manage key-value pairs, and we will use a GenEvent to handle events related to data changes. Let’s create the modules for the key-value store:

KeyValueStore.ex

elixir
defmodule KeyValueStore do
  use GenServer
  use GenEvent

  # GenServer callbacks
  def init(state) do
    {:ok, state}
  end

  def handle_call({:get, key}, _from, state) do
    {:reply, Map.get(state, key), state}
  end

  def handle_cast({:put, key, value}, state) do
    new_state = Map.put(state, key, value)
    {:noreply, new_state}
  end

  # GenEvent callbacks
  def handle_event({:put, key, value}, state) do
    IO.puts("Value #{value} for key #{key} added.")
    {:ok, state}
  end
end

In this example, we define the KeyValueStore module, which uses both the GenServer and GenEvent modules. The handle_call/3 callback handles requests to retrieve values based on keys, while the handle_cast/2 callback handles requests to add new key-value pairs. Additionally, the handle_event/2 callback handles events when new key-value pairs are added to the store.

4.3. Starting Nodes and Establishing Communication

Let’s start the previously created nodes and establish communication between them:

Terminal 1:

elixir
iex(node1@localhost)1> Node.connect(:'node2@localhost')
iex(node1@localhost)2> Node.connect(:'node3@localhost')

Terminal 2:

elixir
iex(node2@localhost)1> Node.connect(:'node1@localhost')
iex(node2@localhost)2> Node.connect(:'node3@localhost')

Terminal 3:

elixir
iex(node3@localhost)1> Node.connect(:'node1@localhost')
iex(node3@localhost)2> Node.connect(:'node2@localhost')

The Node.connect/1 function establishes communication between the nodes by connecting them to each other. Once the nodes are connected, they can send messages to each other.

4.4. Creating the Distributed Key-Value Store

Let’s create the KeyValueStore process on all three nodes and use it to store and retrieve key-value pairs:

Node 1

elixir
iex(node1@localhost)1> {:ok, pid} = GenServer.start_link(KeyValueStore, %{}, name: :key_value_store)
iex(node1@localhost)2> GenServer.call(pid, {:put, :name, "Alice"})
iex(node1@localhost)3> GenServer.call(pid, {:get, :name})

Node 2

elixir
iex(node2@localhost)1> {:ok, pid} = GenServer.start_link(KeyValueStore, %{}, name: :key_value_store)
iex(node2@localhost)2> GenServer.call({:key_value_store, :'node1@localhost'}, {:put, :age, 30})
iex(node2@localhost)3> GenServer.call({:key_value_store, :'node1@localhost'}, {:get, :age})

Node 3

elixir
iex(node3@localhost)1> {:ok, pid} = GenServer.start_link(KeyValueStore, %{}, name: :key_value_store)
iex(node3@localhost)2> GenServer.call({:key_value_store, :'node1@localhost'}, {:put, :country, "USA"})
iex(node3@localhost)3> GenServer.call({:key_value_store, :'node1@localhost'}, {:get, :country})

In this example, we create a KeyValueStore process on each node and interact with it by sending messages. The processes on different nodes can communicate with each other seamlessly, enabling a distributed key-value store.

Conclusion

Building distributed systems with Elixir and OTP offers a powerful and elegant approach to handling concurrency, scalability, and fault tolerance. The lightweight processes, combined with OTP’s building blocks such as supervisors, GenServers, and event handling, make Elixir an ideal choice for distributed programming.

In this blog, we explored the fundamentals of distributed systems, introduced the Elixir language, and delved into OTP’s key components. We also created a simple distributed key-value store to demonstrate how Elixir and OTP can be used to build distributed applications.

By mastering Elixir and OTP, you can unlock the full potential of distributed systems and develop resilient and scalable applications capable of handling the challenges of modern software development. Happy coding!

Previously at
Flag Argentina
Brazil
time icon
GMT-3
Tech Lead in Elixir with 3 years' experience. Passionate about Elixir/Phoenix and React Native. Full Stack Engineer, Event Organizer, Systems Analyst, Mobile Developer.