Design pattern GenServer tree

So I’ve been exploring Elixir/Erlang/OTP and totally love the syntaxes, concepts and theory models involving the actor model (thank you community, Carl Hewitt, Joe, Jose, et cetera). At this point I’m messing with GenServers and am seeking some guidance on a design pattern (this is pure hobby fun).

Say I have a ‘main’ GenServer which receives a call to find some word in multiple files, it distributes this silly task to a few workers (e.g. one per cpu, and they are also GenServers) that on their turn invoke a cast on a GenServer that does the actual searching.

Now imagine this crazy algorithm is in a competition with its fellow workers and as soon as one worker has found the word the other workers should stop(restart?) and start looking for the next word. How would I implement this as the mailbox of each process wont be read until the running task is finished. So the worker won’t cancel its, now, redundant search (poor worker :frowning:).

I thought of calling Process.exit on the actual worker once the parent receives a new instruction from the main GenServer, is this the way to go? Does ‘killing’ and starting a new GenServer include a lot of overhead or is this still considered ‘lightweight’ ?

Thanks for your insights!

Perhaps the snippet I describe is above considered an anti pattern and I should delve into supervisors more… anyway this already has been an interesting learning experience :slight_smile:

If I’m getting your idea correctly, you can start your workers via a supervisor with a one_for_all strategy and terminate as soon as the required task is finished, thus killing off the rest.

5 Likes

you know, I thought this would work, but it doesn’t:

test_pid = self()

children = Enum.map(1..10, &(%{
  id: "#{&1}",
  start: {Task, :start_link, [fn ->
    idx = &1

    # pick a random amount of time.
    duration = Enum.random(1000..10000) + 1000
    
    # report existence.
    IO.puts("starting #{idx} with #{duration} ms")

    # cache the pid of this task in the process mailbox of the test
    send(test_pid, {:pid_of, idx, self()})

    # sleep

    Process.sleep(duration)

    # return the result of our task.

    send(test_pid, {:finished, idx})

    IO.puts("#{idx} finished.")

    #Process.exit(self(), :kill)
  end]},
  restart: :temporary,
  #shutdown: :brutal_kill
}))

{:ok, sup} = Supervisor.start_link(children, strategy: :one_for_all)

#wait for the first to finish.
receive do {:finished, _} -> :ok end

# wait a hot moment for the task supervisor to do its thing
Process.sleep(100)

Enum.each(1..10, fn idx ->
  #check on the cached messages
  receive do {:pid_of, ^idx, pid} ->
    IO.puts("is #{idx} alive? #{Process.alive?(pid)}")
  end
end)

I tried (uncommenting the Process.kill), or (uncommenting the shutdown: :brutal_kill)
If you remove restart: :temporary it does correctly do the :one_for_all thing, so I either I’m setting something wrong in my code or there’s an undocumented interaction (or bug?) in the handling code:

http://erlang.org/doc/design_principles/sup_princ.html

seems to imply that you can do this, as it says “A temporary child process is never restarted (not even when the supervisor restart strategy is rest_for_one or one_for_all and a sibling death causes the temporary process to be terminated).” which doesn’t make sense if one_for_all interacts with temporary…