processelixirerlang-otpsymmetry

link asymmetry in process ring


I'm currently working through the ubiquitous process ring in elixir. The ring is linked, but in the following fashion:

iex(1)> Ring.Worker.create_ring_of_linked_processes(3)
Ring.Worker.create_ring_of_linked_processes(3)
[%{"links" => [#PID<0.121.0>, #PID<0.120.0>], "pid" => #PID<0.122.0>},
 %{"links" => [#PID<0.120.0>, #PID<0.122.0>], "pid" => #PID<0.121.0>},
 %{"links" => [#PID<0.121.0>], "pid" => #PID<0.120.0>}]

I've noticed an asymmetry in the links here - should #PID<0.120.0> have the mapping "links" => [#PID<0.121.0>,#PID<0.122.0>] rather than just "links" => [#PID<0.121.0>] ?

The code is as follows:

  def loop() do
    receive do
      {:link, pid} when is_pid(pid) ->
        Process.link(pid)
        loop()
    end
  end

  def create_ring_of_linked_processes(num_of_processes) do
    num_of_processes
    |> create_processes
    |> link_processes([])
  end


  def link_processes([pid1, pid2 | rest], linked_processes) do
    send(pid1, {:link, pid2})
    :timer.sleep(1)
    {:links, links} = Process.info(pid1, :links)
    link_processes(
      [pid2 | rest], [%{"pid" => pid1, "links" => links} | linked_processes]
    )
  end

  def link_processes([pid | []], linked_processes) do
    %{"pid" => first_pid, "links" => _} = List.last(linked_processes)
    send(pid, {:link, first_pid})
    :timer.sleep(1)
    {:links, links} = Process.info(pid, :links)
    [%{"pid" => pid, "links" => links} | linked_processes]
  end

  @spec create_processes(integer) :: [pid]
  def create_processes(num_of_processes) do
    for _ <- 1..num_of_processes, do: spawn(__MODULE__, :loop, [])
  end

Solution

  • This is because you're linking the processes at the same time as collecting its :links, but some links for that process are being created after you collect its links.

    For example, if you spawn a process a, and then collect its links, it'll be an empty list.

    iex(1)> a = spawn(fn -> :timer.sleep(:infinity) end)
    #PID<0.82.0>
    iex(2)> Process.info(a, :links)
    {:links, []}
    

    If you spawn b now and link it to a, b will have [a] in its links and a will have [b].

    iex(3)> b = spawn(fn -> Process.link(a); :timer.sleep(:infinity) end)
    #PID<0.85.0>
    iex(4)> Process.info(b, :links)
    {:links, [#PID<0.82.0>]}
    iex(5)> Process.info(a, :links)
    {:links, [#PID<0.85.0>]}
    

    So, you need to collect the links for each process after all the linking is complete if you want the final links for each process.