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!