Last month I posted an article about using Elixir’s DynamicSupervisor
behavior to recursively connect our Elixir-based node to peers throughout Bitcoin’s peer-to-peer network.
The last part of that article talked about how we could limit the exponential growth of our set of connected peers by setting a hard cap on the number of processes supervised by our dynamic Node.Supervisor
process.
We went through the rigmarole of building this child process cap ourselves, but it was pointed out to me that we could have used DynamicSupervisor
’s built in :max_children
option to accomplish the same thing!
Our Hand-Rolled Solution
When we implemented our own restriction on the number of peers we allow our node to connect to, we did it within the BitcoinNetwork.connect_to_node/2
function:
def connect_to_node(ip, port) do
if count_peers() < Application.get_env(:bitcoin_network, :max_peers) do
DynamicSupervisor.start_child(BitcoinNetwork.Node.Supervisor, %{
id: BitcoinNetwork.Node,
start: {BitcoinNetwork.Node, :start_link, [{ip, port}]},
restart: :transient
})
else
{:error, :max_peers}
end
end
The count_peers/0
helper function simply calls out to DynamicSupervisor.count_children/1
to count the number of processes being supervised by our dynamic Node.Supervisor
:
BitcoinNetwork.Node.Supervisor
|> DynamicSupervisor.count_children()
|> Map.get(:active)
If the number of active peers is less than our specified number of :max_peers
, we allow the connection. Otherwise, we return an :error
tuple.
Elixir’s Solution
If we read through the DynamicSupervisor
documentation, we’ll find that we can pass a :max_children
option to DynamicSupervisor.start_link/2
. Digging through Elixir’s source, we can see that, when present, the :max_children
option does literally exactly what we did in our hand-rolled solution:
if dynamic < max_children do
handle_start_child(child, %{state | dynamic: dynamic + 1})
else
{:reply, {:error, :max_children}, state}
end
If dynamic
, the number of processes currently being supervised by the supervisor, is less than the specified max_children
, add the child. Otherwise, return an :error
tuple.
Refactoring
Refactoring our original solution to make use of the :max_children
option largely consists of removing our original solution. We’ll start by gutting the guard in our BitcoinNetwork.connect_to_node/2
function:
def connect_to_node(ip, port) do
DynamicSupervisor.start_child(BitcoinNetwork.Node.Supervisor, %{
id: BitcoinNetwork.Node,
start: {BitcoinNetwork.Node, :start_link, [{ip, port}]},
restart: :transient
})
end
This means we can also remove our count_peers/0
helper function.
Now we simply need to add the :max_children
option to our dynamic supervisor when it starts up:
{:ok, pid} =
Supervisor.start_link(
[
{DynamicSupervisor,
name: BitcoinNetwork.Node.Supervisor,
strategy: :one_for_one,
max_children: Application.get_env(:bitcoin_network, :max_peers)}
],
strategy: :one_for_one
)
That’s all there is to it!
Our limited set of peers.
Spinning up our Bitcoin node with a low value for :max_peers
shows that our Node.Supervisor
is honoring our limit.
Final Thoughts
My final thoughts are that I should really spend more time reading through the Elixir and Erlang documentation. There’s quite a few gems hidden in plain sight that would do me quite a bit of good to know about.
I’d also like to thank the Redditor who pointed the :max_children
option out to me. Thanks, ParticularHabit!