Hi everyone! Super excited to join the community here
I’m reading the book “Programming Elixir 1.6” from PragProg and hit the following exercise:
The ticker process in this chapter is a central server that sends events to
registered clients. Reimplement this as a ring of clients. A client sends a
tick to the next client in the ring. After 2 seconds, that client sends a tick
to its next client.
When thinking about how to add clients to the ring, remember to deal
with the case where a client’s receive loop times out just as you’re adding
a new process. What does this say about who has to be responsible for
updating the links?
My initial naive idea is as follows:
- Each process knows it’s ‘next’ process
- When a registration message with a new client PID arrives, point itself to the new client, and the new client to the ‘next’ client. This way, a new client can be inserted into the ring at any point.
However, I’m seeing nasty race conditions, as each addition to the ring has 2 messages, that tend to arrive in whichever order - by design.
Is such implementation possible in principle, or should I stick to a central process that maintains the state in a single location instead?
Very new to Elixir, so I’m probably mis-applying the programming paradigms here
defmodule Ch16.Exercise4.TickerRing do
@interval 2000
# @name :ticker_ring
def generate(client_id, next_pid, is_active) do
receive do
{:tick, sender_id} ->
IO.puts("client ##{client_id} received tick from ##{sender_id}")
generate(client_id, next_pid, true)
{:repoint, new_client_pid} ->
IO.puts(
"client ##{client_id} received repoint to PID ##{inspect(new_client_pid)}, current next in line: #{inspect(next_pid)}"
)
generate(client_id, new_client_pid, is_active)
{:register, new_client_pid} ->
IO.puts(
"client ##{client_id} received registration with PID ##{inspect(new_client_pid)}, current next in line: #{inspect(next_pid)}"
)
if(is_nil(next_pid)) do
# new client with nil next pid
send(new_client_pid, {:repoint, self()})
generate(client_id, new_client_pid, is_active)
else
# old client receiving a registration
send(new_client_pid, {:repoint, next_pid})
generate(client_id, new_client_pid, is_active)
end
after
@interval ->
if(is_active) do
IO.puts("tick at ##{client_id}, sending to #{inspect(next_pid)}")
if(next_pid != nil) do
send(next_pid, {:tick, client_id})
generate(client_id, next_pid, false)
else
generate(client_id, next_pid, is_active)
end
else
# IO.puts("client ##{client_id} not active, doing nothing...")
generate(client_id, next_pid, is_active)
end
end
end
end
pid1 = spawn(Ch16.Exercise4.TickerRing, :generate, ["1", nil, true])
IO.puts("pid1: #{inspect(pid1)}")
pid2 = spawn(Ch16.Exercise4.TickerRing, :generate, ["2", nil, false])
IO.puts("pid2: #{inspect(pid2)}")
send(pid1, {:register, pid2})
pid3 = spawn(Ch16.Exercise4.TickerRing, :generate, ["3", nil, false])
IO.puts("pid3: #{inspect(pid3)}")
send(pid2, {:register, pid3})