This programming language crashes on purpose

This programming language crashes on purpose

How Elixir "Fails Fast"

As a student developer, my experience with programming has mostly been with object-oriented languages. However, I've recently started learning Elixir, a functional programming language known for its concurrency model and "fail fast" philosophy.

Elixir's concurrency model is based on the actor model, in which each process operates independently and shares no mutable state with other processes. This means that each process can crash independently, without affecting the overall system.

However, this also means that it's important for the system to be able to recover from these crashes quickly and gracefully. This is where Elixir's "fail fast" philosophy comes in. In Elixir, processes are designed to crash quickly and propagate errors up the process tree until they are handled by a supervisor.

Thread Pools

Here's how you start a thread pool in elixir:

# define a worker function
defmodule MyWorker do
  def perform(task) do
    # do some work here
    result = task * 2
    IO.puts("Task result: #{result}")
  end
end

# start a pool of workers (5 in this example)
:poolboy.start_link(MyWorker, [], name: :poolex, size: 5)

# queue up some work for the pool
:poolboy.transaction(:poolex, fn pid ->
  :poolboy.checkout(:poolex)
  |> MyWorker.perform()
  |> :poolboy.checkin()
end)

But you never know when one of the processes in the pool fails, the task would be left incomplete and it would be a pain to figure out the failed task and execute it again, assimilating the result. Here's how the Supervisor fits into the picture.

The Supervisor

A supervisor is a process that monitors the child processes running beneath it. If a child process crashes, the supervisor can restart it or take other appropriate actions to recover from the crash.

This approach to handling concurrency and failures has several benefits. First, it simplifies development by allowing developers to focus on writing small, independent processes rather than worrying about managing shared state. Second, it increases the scalability of the application by allowing processes to run on multiple cores and distribute work across the system.

To illustrate how this works in practice, let's take a look at an example of using Elixir's thread pool to perform some computationally intensive work.

# define a worker module
defmodule MyWorker do
  def perform(task) do
    # do some work here
    result = task * 2
    IO.puts("Task result: #{result}")
  end
end

# define a supervisor module
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

# start the supervisor
MySupervisor.start_link()

# queue up some work for the worker
MyWorker.perform(10)

In this example, we define a MySupervisor module that acts as a supervisor for a set of MyWorker worker processes. The MySupervisor module initializes ten MyThread processes and returns them as children.

The supervisor module, MySupervisor, uses the Supervisor behavior and defines a start_link function that starts the supervisor process. The init function of the supervisor module initializes the supervisor by defining its children, which in this case is a single worker process defined by the MyWorker module. The supervise function is called with the children list and a strategy: :one_for_one, which means that if a child process crashes, it will be restarted by the supervisor.

The MyThreadPool module also defines an execute function that delegates to the MyThread processes. When the execute function is called, it sends a message to one of the MyThread processes, which executes the given function and returns the result.

This allows us to perform work in parallel, utilizing all available CPU cores. If any of the worker processes crash or encounter an error, the supervisor can restart them automatically.

The Elegance

In conclusion, Elixir's concurrency model and "fail fast" philosophy make it a powerful tool for building highly scalable, fault-tolerant systems. The combination of processes, supervisors, and message passing provides a simple and elegant way to handle concurrency and errors.