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.
# 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 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 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.
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.
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.
# 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
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.
# 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} })
# 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.
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.