Building Beacon #2 - Site Supervisor

Wooden blocks on top of engineering and construction materials.
Leandro Pereira

Senior Software Engineer

Leandro Pereira

Build your mobile app more efficiently with LiveView Native. Book a free consult today to learn how we can put it to work for you.

Continuing our Building Beacon series, let’s see how each site is started with its own config and how it behaves when one of them starts to fail.

In the previous article, we’ve seen that each site starts a supervisor with some children that are responsible for the site’s operation, which includes storing the site config, loading resources, and more. We’ll discuss those inner parts in future articles, but for now, we want to share how sites are started, how they can be restarted to update the configuration at runtime, and how the entire application behaves when a site fails.

Isolated Sites

Since Beacon supports running multiple sites in the same application, one site site must not affect others when it fails, poisoning the whole system. They need to be isolated from each other, and our approach is to start separated supervision trees for each one:

A supervision tree

Let’s suppose we have a single project with the sites :site_a and :site_b running, each serving a home page at http://localhost:4000/site_a and http://localhost:4000/site_b respectively.

Let’s use ab to prove they are able to serve requests:

ab -n 1000 http://localhost:4000/site_a

Complete requests:      1000
Failed requests:        0
ab -n 1000 http://localhost:4000/site_b

Complete requests:      1000
Failed requests:        0

So far, so good. They served 1000 out of the 1000 requested. Now let’s intentionally kill the :site_b supervisor and see what happens.

We’ll start both ab commands simultaneously, and then kill the :site_b before the test ends:

Supervisor.terminate_child(Beacon, :site_b)

The function Supervisor.terminate_child/2 kills the :site_b supervisor instantly, which is the worst case scenario for a site failure, simulating a complete outage of that site. Let’s see the results:

ab -n 1000 http://localhost:4000/site_a

Complete requests:      1000
Failed requests:        0
ab -n 1000 http://localhost:4000/site_b

Complete requests:      1000
Failed requests:        458

As expected, :site_a is still up and running with no requests failed, and :site_b failed to serve half of the requests (it could be more or less depending on the timing Supervisor.terminate_child/2 was called).

That’s exactly what we wanted: isolation.

Start and Stop Sites

Now that we know how to kill a site, we should also know how to start it again, even with an update config.

Beacon provides the Beacon.boot/1 function that essentially does that for us:

  1. Terminates and clean up the site supervisor from the main Beacon.Supervisor
  2. Build a spec child based on the new config
  3. Start the site again with the new config

All sites are started automatically, you don’t need to call that function manually but let’s suppose we want to update the :mode config of a running site from :manual to :live, which requires starting some processes and sending some messages, so to better achieve that we need to reboot the site:

current_config = Beacon.Config.fetch!(:site_a)
new_config = %{current_config | mode: :live}
Beacon.boot(new_config)

You should see in the server logs:

[info] Beacon.Boot booting site site_a
[info] Beacon.Boot finished booting site site_a
{:ok, #PID<0.982.0>}

This operation requires minimal downtime because all resources (layouts, pages, components) are lazy loaded. We’ll learn more about the Loader in an upcoming article.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box