Skip to content

Commit 1be424d

Browse files
authored
Expand on why we supervise, not how (#14764)
Closes #14763.
1 parent 3ae49eb commit 1be424d

File tree

1 file changed

+23
-52
lines changed

1 file changed

+23
-52
lines changed

lib/elixir/pages/anti-patterns/process-anti-patterns.md

Lines changed: 23 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -251,71 +251,55 @@ GenServer.cast(pid, {:report_ip_address, conn.remote_ip})
251251

252252
#### Problem
253253

254-
In Elixir, creating a process outside a supervision tree is not an anti-pattern in itself. However, when you spawn many long-running processes outside of supervision trees, this can make visibility and monitoring of these processes difficult, preventing developers from fully controlling their applications.
254+
In Elixir, creating a process outside a supervision tree is not an anti-pattern in itself. However, when you spawn many long-running processes outside of supervision trees, this can make visibility and monitoring of these processes difficult, preventing developers from fully controlling their lifecycle.
255255

256256
#### Example
257257

258-
The following code example seeks to illustrate a library responsible for maintaining a numerical `Counter` through a `GenServer` process *outside a supervision tree*. Multiple counters can be created simultaneously by a client (one process for each counter), making these *unsupervised* processes difficult to manage. This can cause problems with the initialization, restart, and shutdown of a system.
258+
The following code example seeks to illustrate a library responsible for maintaining a numerical `Counter` through a `Agent` process *outside a supervision tree*.
259259

260260
```elixir
261261
defmodule Counter do
262262
@moduledoc """
263-
Global counter implemented through a GenServer process.
263+
Global counter implemented as an Agent.
264264
"""
265265

266-
use GenServer
266+
use Agent
267267

268268
@doc "Starts a counter process."
269269
def start_link(opts \\ []) do
270-
initial_value = Keyword.get(opts, :initial_value, 0)
270+
initial_state = Keyword.get(opts, :initial_value, 0)
271271
name = Keyword.get(opts, :name, __MODULE__)
272-
GenServer.start(__MODULE__, initial_value, name: name)
272+
Agent.start_link(fn -> initial_state end, name: name)
273273
end
274274

275275
@doc "Gets the current value of the given counter."
276-
def get(pid_name \\ __MODULE__) do
277-
GenServer.call(pid_name, :get)
276+
def get(name \\ __MODULE__) do
277+
Agent.get(name, fn state -> state end)
278278
end
279279

280280
@doc "Bumps the value of the given counter."
281-
def bump(pid_name \\ __MODULE__, value) do
282-
GenServer.call(pid_name, {:bump, value})
283-
end
284-
285-
@impl true
286-
def init(counter) do
287-
{:ok, counter}
288-
end
289-
290-
@impl true
291-
def handle_call(:get, _from, counter) do
292-
{:reply, counter, counter}
293-
end
294-
295-
def handle_call({:bump, value}, _from, counter) do
296-
{:reply, counter, counter + value}
281+
def bump(name \\ __MODULE__, value) do
282+
Agent.get_and_update(fn state -> {state, value + state} end)
297283
end
298284
end
299285
```
300286

287+
While it is possible to start the process outside of a supervision tree:
288+
301289
```elixir
302290
iex> Counter.start_link()
303291
{:ok, #PID<0.115.0>}
304-
iex> Counter.get()
292+
iex> Counter.bump(13)
305293
0
306-
iex> Counter.start_link(initial_value: 15, name: :other_counter)
307-
{:ok, #PID<0.120.0>}
308-
iex> Counter.get(:other_counter)
309-
15
310-
iex> Counter.bump(:other_counter, -3)
311-
12
312-
iex> Counter.bump(Counter, 7)
313-
7
294+
iex> Counter.get()
295+
13
314296
```
315297

298+
Such processes are harder to observe and control their lifecycle. For example, if you have other processes that depend on the `Counter` above, you will need ad-hoc mechanisms to make sure they are initialized in order. Furthermore, when your application is shutting down, there is no guarantee when they are terminated.
299+
316300
#### Refactoring
317301

318-
To ensure that clients of a library have full control over their systems, regardless of the number of processes used and the lifetime of each one, all processes must be started inside a supervision tree. As shown below, this code uses a `Supervisor` as a supervision tree. When this Elixir application is started, two different counters (`Counter` and `:other_counter`) are also started as child processes of the `Supervisor` named `App.Supervisor`. One is initialized with `0`, the other with `15`. By means of this supervision tree, it is possible to manage the life cycle of all child processes (stopping or restarting each one), improving the visibility of the entire app.
302+
To ensure that clients of a library have full control over their systems, regardless of the number of processes used and the lifetime of each one, all processes must be started inside a supervision tree. As shown below, this code uses a `Supervisor` as a supervision tree.
319303

320304
```elixir
321305
defmodule SupervisedProcess.Application do
@@ -338,21 +322,8 @@ defmodule SupervisedProcess.Application do
338322
end
339323
```
340324

341-
```elixir
342-
iex> Supervisor.count_children(App.Supervisor)
343-
%{active: 2, specs: 2, supervisors: 0, workers: 2}
344-
iex> Counter.get(Counter)
345-
0
346-
iex> Counter.get(:other_counter)
347-
15
348-
iex> Counter.bump(Counter, 7)
349-
7
350-
iex> Supervisor.terminate_child(App.Supervisor, Counter)
351-
iex> Supervisor.count_children(App.Supervisor) # Only one active child
352-
%{active: 1, specs: 2, supervisors: 0, workers: 2}
353-
iex> Counter.get(Counter) # The process was terminated
354-
** (EXIT) no process: the process is not alive...
355-
iex> Supervisor.restart_child(App.Supervisor, Counter)
356-
iex> Counter.get(Counter) # After the restart, this process can be used again
357-
0
358-
```
325+
Besides having a deterministic order in which processes are started, supervision trees also guarantee they are terminated in reverse order, allowing you to perform any necessary clean up during shut down. Furthermore, supervision strategies allows us to configure exactly how process should act in case of unexpected failures.
326+
327+
Finally, applications and supervision trees can be introspected through applications like the [Phoenix.LiveDashboard](http://github.com/phoenixframework/phoenix_live_dashboard) and [Erlang's built-in observer](https://www.erlang.org/doc/apps/observer/observer_ug):
328+
329+
<img src="assets/kv-observer.png" alt="Observer GUI screenshot" />

0 commit comments

Comments
 (0)