Empowering Real-Time Interactive Applications with Elixir and Phoenix

Phoenix, a framework written in Elixir that represents the modern approach to web application development, especially when it comes to real-time interactive apps. Phoenix is designed to play along well with Elixir. In this blog, we look at the reasons why Elixir and Phoenix make a perfect fit for real-time interactive platform applications that involve a several users at the same time

Empowering Real-Time Interactive Applications with Elixir and Phoenix

Elixir is a relatively new programming language that is inspired by Erlang. Phoenix, a framework written in Elixir that represents the modern approach to web application development, especially when it comes to real-time interactive apps. Phoenix is designed to play along well with Elixir. In this blog, we look at the reasons why Elixir and Phoenix make a perfect fit for real-time interactive platform applications that involve a several users at the same time.

New Phoenix project
# phx.new command with the --live flag will create a new project with 
# LiveView installed and configured 

mix phx.new --live chat_room

Creating a phoenix application using the above command provides everything to get started with the real-time features. The key features to look at are the Endpoints, Sockets, Channels, Plugs, ETS, Presence, and LiveView.

Endpoints

An endpoint is simply a boundary where all requests to your web application begin, and it serves few purposes:

  • It provides a wrapper for starting and stopping processes that are part of the supervision tree.

  • It defines the initial plug pipeline for the requests to pass through.

  • It manages configurations for your application.

The endpoint.ex handles all the typical web server setup: sockets, static content, sessions, and routing.

Declaring a socket
# Declaring a socket at "/socket" URI and instruct our app to process 
# all these connection requests via UserSocket module

socket "/socket", 
      CurveFeverWeb.UserSocket, 
      websocket: true, 
      longpoll: false

socket "/live", 
      Phoenix.LiveView.Socket, 
      websocket: [connect_info : [session: @session_options]]

The application endpoint serves as the beginning point where our socket endpoint is defined. It is the place where a socket is defined on the /socket URI and notifies our app that all connection requests will be handled by the UserSocket module. Phoenix is designed to use WebSockets by default rather than long-polling, but it comes with support for both.

Real-time web applications often use the WebSocket protocol to relay information from the server to the client, either directly or indirectly via a framework (such as Socket.io). The Phoenix framework has in-built WebSocket functionality, which may be used with the Socket and Channel modules.

It is the responsibility of the sockets to tie transports and channels together. Once connected to a socket, incoming and outgoing events are routed to channels. The incoming client data is routed to channels via transports.

Channels

The Phoenix library's cardinal component is channels. It allows millions of connected clients to communicate in soft real-time.

Chris McCord, Creator of the Phoenix framework was able to achieve2 million websocket connections2 million websocket connectionson a single computer with Phoenix Channels, but by splitting traffic across numerous nodes, it may perform even better.

The Channel module is a GenServer implementation with join callback functions that enable sockets to connect to channel topics. A client connecting to a channel topic will trigger the join callback function through which a new Phoenix.Channel.Server Process is launched. The Channel server process subscribes to the joined channel topic on the local PubSub.

Data Pipelines Ref Solution

This Phoenix.Channel.Server Process then relays the message to any clients on the same server that are connected to the same channel topic. The message is then forwarded by the PubSub server to any remote PubSub servers operating on other nodes in the cluster, which then delivers it to their own subscribing clients.

Data Pipelines Ref Solution

Plugs

Plug is a standard for constructing modules that can be shared between web applications. Phoenix's HTTP layer is driven by Plug, which allows you to construct simple functions that take a data structure, usually a connection, and modify it little before returning it. Every request to your Phoenix server goes through a number of steps until all of the data is ready to be sent as a response.

Additionally, Phoenix's key components, such as the endpoints, routers, and controllers, have proven to be nothing more than plugs.

Usage of Plugs
# Example usage of plugs by looking at the router.ex of a phoenix application

defmodule CurveFeverWeb.Router do
  use CurveFeverWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, {CurveFeverWeb.LayoutView, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", CurveFeverWeb do
    pipe_through :browser

    live "/", SigninLive, :index
    get("/join_game", GameController, :join)
    get("/game", GameController, :index)
    get("/lobby", LobbyController, :join)
    get("/join_lobby", LobbyController, :index)
  end
end

Imagine it as a middleware stack where requests travel through these plugs. They can be used, for example, to determine whether the request contains HTML, get the session, or secure the request. This all takes place before reaching the controller.

ETS- Erlang Term Storage

ETS is an in-memory key/value store pretty much similar to Redis integrated right into OTP, which is usable in elixir via Erlang interoperability. It can store large volumes of data and provides real-time data access.

Each ETS table, like almost everything else in Elixir and Erlang, is created and owned by a process. When an owner process terminates, the tables it owns are deleted. It offers a significant advantage that may assist developers in easing their life when caching is required.

Phoenix Presence

The Phoenix Presence feature allows to store and expose topic-specific information to all of a channel's subscribing clients across cluster. The Presence module makes it simple to manage client-side reception of presence-related events with very little code.

To store and broadcast topic-specific information to all channels with a given topic, Presence employs a Conflict-Free Replicated Data Type (CRDT). Phoenix Presence, unlike a centralised data store like Redis, has no single point of failure or single source of truth. This makes the Presence data store self-healing and repeatable across all of your application cluster's nodes.

Phoenix Presence can be easily employed for real-time use-cases like the list of users present/left in a given chat or a game room.

LiveView

The LiveView feature. Despite being an optional part of Phoenix's web framework, it is becoming increasingly important. Phoenix LiveView is a library built on top of Phoenix that provides exceptional real-time interactivity without the need to write Javascript for most situations.

LiveView is a process that listens to both internal and external events and, as it receives those events, updates its state. The state itself comprises nothing more than immutable Elixir data structures. The events are either messages from other processes or replies from the client/browser.

LifeCycle of a LiveView

Data Pipelines Ref Solution

When a LiveView connects to a client, it starts as a regular HTTP request and HTML response. A persistent WebSocket connection to the server is opened when the initial page is loaded via assets/js/app.js and JavaScript. The persistent WebSocket connection established between client and server allows LiveView applications to respond faster to user events as there is less data that needs to be transmitted compared to stateless requests.

Websocket connection
# The below script opens a websocket connection, passing the csrf-token, 
# rendered in the <meta> tag in the header

let csrfToken = document.querySelector("meta[name='csrf-token']")
                        .getAttribute("content")

let liveSocket = new LiveSocket("/live", 
                      Socket, 
                      { hooks: Hooks, params: {_csrf_token: csrfToken} })
Callback to user events
# handle_info callback reacting to user events

def handle_info({:enter_lobby, attrs}, socket) do

  %{player_name: player_name} = attrs
  Logger.info(player_name: player_name)
  url = Routes.lobby_path(
      socket,
      :join,
      player_name: player_name)

  socket = socket
  |> put_temporary_flash(:info, "Entered the lobby")
  |> push_redirect(to: url)

  {:noreply, socket}
end
        

How Phoenix achieves fault-tolerance and scalability?

One of the most important aspects of developing real-time applications is to ensure that they are highly available. When it comes to making a system highly available, two main considerations are fault-tolerance and scalability.

Phoenix inherits its characteristics from Elixir, which in turn derived them from Erlang, thanks to the primitives of the BEAM virtual machine and OTP's architectural patterns. BEAM employs processes as the primary unit of concurrency. The process provides the basis for building scalable, fault-tolerant, distributed systems.

Scalability

By creating a dedicated process for each task, all available CPU cores can be taken into advantage thereby achieving maximum parallelization. By running the processes in parallel allows achieving scalability- the ability to address a load increase by adding more hardware power that the system automatically takes advantage of.

As part of the Phoenix application, every HTTP request (basically every browser tab open) joining a Phoenix Channel utilizes this capability. Upon establishing connection to the application, a new Phoenix.Channel.Server is created on the Erlang Virtual Machine so that each request is completely independent of the others. Erlang was designed for this exact purpose. Everything in Erlang is a lightweight process that communicates by sending and receiving messages.

Fault-Tolerance

Processes ensure isolation, which is crucial to fault-tolerance - limiting the impact of runtime errors that ultimately occur. By creating isolated processes, the idea is not to let them modify each other's state but can communicate with one another using messages.

Elixir applications do not shut down the whole system when one of their components fails. Instead, they restart the affected parts while the rest of the system keeps operating.

Supervisors are essentially GenServers that have the capability to start, monitor, and restart processes. By letting the processes crash instead of attempting to handle all possible exceptions within them, the “Fail Fast"- approach transfers responsibility to the process supervisor. Supervisors ensure that those processes are restarted if needed, restoring their initial states.

When the application first launches, supervisors usually start with a list of children to work with. When our app first starts up, however, the supervised children may not be known (for instance, a web app that starts a new process to handle a user connecting to our site). In these instances, we'll need a supervisor who can start the children on demand. This scenario is handled by the Dynamic Supervisor.

Dynamic supervisors also help in isolating when one of the component real-time interactive applications crashes. Dynamic supervisor supports a :one_for_one strategy to ensure that if a child process terminates, it won't terminate any of the other child processes being managed by the same supervisor.

Summary

As discussed above, several key components that we use to build a performant and robust real-time system exist in Phoenix and Elixir. In the process, one will get a better understanding of Erlang and OTP, which may prevent them from relying on external dependencies.


Phoenix
Elixir
LiveView
Real-time Interactive applications
Phoenix Presence
BEAM
Erlang/OTP
Sockets

By Suresh Thiruppathi, Rithik KK
February 20, 2023

Talk to us for more insights

What more? Your business success story is right next here. We're just a ping away. Let's get connected.