ndrean

ndrean

I am circling around with the following problem and no post helped me so far.
I have a GenServer module named MyApp.Repo here that runs GenServer.cast but GenServer.call returns an error and kills the process.

GenServer MyApp.Repo terminating
** (ArgumentError) unknown registry: MyApp.Repo

When I start the app, I check:

iex> Enum.member?(Process.registered(), MyApp.Repo) 
=> true

The GenServer module MyApp.Repo is supervised as a child by the Application supervisor.

children = [
      MyAppWeb.Telemetry,
      {Phoenix.PubSub, name: MyApp.PubSub, adapter: Phoenix.PubSub.PG2},
      MyApp.Repo,
     MyAppWeb.Endpoint
  ...

MyApp.Repo module is a basic GenServer that registers a :name:

def start_link(),
  do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
 
def save_with_cast(user),
  do: GenServer.cast(__MODULE__, {:new, user})

def save_with_call(user) do
  IO.inspect(user, label: "CALL______")  
  GenServer.call(__MODULE__, {:new, user})
end
 
def handle_call({:new, user}, _from,_state) do
    IO.puts("CALL______")
    Phoenix.PubSub.broadcast_from(MyApp.PubSub, self(), @topic, {:perform_new, user})
  {:reply, user, [])
end

def handle_cast({:new, user}, _state) do
   Phoenix.PubSub.broadcast_from(MyApp.PubSub, self(), @topic, {:perform_new, user})
  {:noreply, []}
end

def handle_info({:perform_new, message}, _state) do
  ...

MyApp.save_with_cast(user) runs :ok but not MyApp.save_with_call(user).

Showing Posts 11 to 20

al2o3cr

al2o3cr

A general observation: if you’re ignoring the GenServer’s state in every handler, you probably don’t need a GenServer. For instance, save_with_call could be simplified to:

def save_with_call(user) do
  Phoenix.PubSub.broadcast_from(MyApp.PubSub, self(), @topic, {:perform_new, user})
end

As a bonus, this approach doesn’t have a single process (the GenServer) forcing everything to happen one-at-a-time.

ndrean

ndrean OP

OK, good point, in fact, I really wondered, indeed, but how do I respond to :perform_new if not from a Genserver? As you say, I only use the messaging part.

msimonborg

msimonborg

You can have a GenServer (or any interested process) subscribe to and handle the messages, or a pool of them with a pool manager dispatching the messages, but you don’t need to send the messages from the GenServer if that’s all that save_with_* is doing. You can pubsub those messages from any process and your MyApp.Repo servers will subscribe to that topic. Then you’ll avoid routing new messages through the same process that wants to receive them, which significantly reduces the work load on each one and increases their throughput receiving the messages

ndrean

ndrean OP

Firstly apologize, just a beginner with Elixir. Then yes I believe I started with this, not using GenServer.call. This relieves a bit of pressure but I may still need the GenServer behaviour. Nodes subscribe to this pubusub topic. Nodes also listen to :net_kernel.monitor_nodes events, and broadcast on a :nodeup event. I need to capture the message the nodes broadcasts and do something, so I don’t know how to do this is not using a handle_info matcher offered by the server

ndrean

ndrean OP

Yes, I don’t need this handle_call indeed.

msimonborg

msimonborg

No need to apologize! OTP takes time to learn but it’s a rewarding process.

Yes exactly, you can have a process or pool of processes on each node subscribing to the pubsub messages, even though they can be broadcasted by any caller in the cluster.

You will also need a long running process to listen for these :nodeup events and rebroadcast them, and any interested process can subscribe to the topic. You should probably have a NodeListener with the sole responsibility to handle and rebroadcast these events. All of the messages we’ve discussed so far can be handled by the handle_info/2 callback of the GenServer behaviour.

You only need to call a GenServer if it holds some state (data, connection, etc.) or performs some computation and you need the results returned. It’s always synchronous. If the calling process can do the work itself then it probably should unless you need to offload it for async behavior or error isolation, in which case you might consider spawning a Task instead of sending it to a single GenServer.

msimonborg

msimonborg

A basic implementation of what I think you’re looking for

defmodule MyApp.NodeListener do
  use GenServer
  require Logger

  def start_link(arg), do: GenServer.start_link(__MODULE__, arg, name: __MODULE__)

  def init(_arg) do
    :net_kernel.monitor_nodes(true)
    {:ok, []}
  end

  def handle_info({:nodeup, _} = payload, state) do
    Phoenix.PubSub.broadcast!(MyApp.PubSub, "cluster", payload)
    {:noreply, state}
  end

  def handle_info(_, state), do: {:noreply, state}
end

defmodule MyApp.Repo do
  use GenServer
  require Logger

  def start_link(arg), do: GenServer.start_link(__MODULE__, arg, name: __MODULE__)

  def init(_arg) do
    Phoenix.PubSub.subscribe(MyApp.PubSub, "cluster")
    Phoenix.PubSub.subscribe(MyApp.PubSub, "users")
    {:ok, []}
  end

  def handle_info({:new, user}, state) do
    Logger.info("Repo (#{inspect(node())}) received user: #{inspect(user)}")
    {:noreply, state}
  end

  def handle_info({:nodeup, _} = msg, state) do
    Logger.info("Repo (#{inspect(node())}) received #{inspect(msg)}")
    {:noreply, state}
  end
end

defmodule MyApp.Users do
  def save(user) do
    Phoenix.PubSub.broadcast!(MyApp.PubSub, "users", {:new, user})
  end
end

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      {Phoenix.PubSub, name: MyApp.PubSub},
      MyApp.NodeListener,
      MyApp.Repo
    ]
    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

$ iex --name a@127.0.0.1 -S mix

# in another terminal
$ iex --name b@127.0.0.1 -S mix

iex(a@127.0.0.1)1> Node.connect(:"b@127.0.0.1")
true

19:43:49.050 [info]  Repo (:"a@127.0.0.1") received {:nodeup, :"b@127.0.0.1"}

19:43:49.050 [info]  Repo (:"b@127.0.0.1") received {:nodeup, :"a@127.0.0.1"}

iex(b@127.0.0.1)1> MyApp.Users.save(1)
:ok

19:44:10.905 [info]  Repo (:"b@127.0.0.1") received user: 1

19:44:10.905 [info]  Repo (:"a@127.0.0.1") received user: 1
ndrean

ndrean OP

Thanks, I refactored with your comments and challenged myself. Quite a few takeaways there. First about backpressure. Not sure to understand this term? Is this related to filling up the server’s mailbox? So you mentioned a pool of servers to push it a bit further. I will probably keep this for later as I imagine you may need libraries like Poolboy for this.
Then besides code readability or “testability”, does segregation bring better performance in terms of robustness on load? For example:

  • two PubSub topic is better than one? (I could not extract a PID to check).
  • designing two servers instead of one relieves partly the load on the mailbox I imagine.
  • also about error handling. Should there be a rescue?
    I ask these questions because concurrency between processes is a delicate topic, and reading about the usage guidelines of GenStage, again they speak about “backpressure”. Maybe some book you may recommend?

Finally, as a side comment, I noticed the catch-all clause you introduced: handle_info(_, state), do: {:noreply, state}.

LostKobrakai

LostKobrakai

Backpressure describes the act of not accepting more work before having completed previous/existing work. This allows you to prevent parts of your system from being overloaded, because at best you cannot sent them more work than they can handle. This will push work to queue up in known places where you can then deal with it.

msimonborg

msimonborg

I think you would also be able to implement something simple with the PartitionSupervisor coming in 1.14. You would have to do some hashing to make sure the messages are not duped on multiple server partitions.

To me, node up and new user are separate topics, so by splitting them you can have other processes subscribe to one or the other in the future without coupling them together

I put the NodeListener as its own process precisely because it has one responsibility. It reduces the load on Repo because Repo doesn’t care about nodedown (at least that’s what I took away from what you said). And if other processes may be interested in cluster membership, the NodeListener can be the hub for those events for the whole node, without needing multiple processes to be subscribing to net_kernel.monitor_nodes.

If there is an error that you expect might happen and you have a way to recover from it, then I think you handle it explicitly with pattern matching. If there is a real exception with no recovery then “let it crash”, and the supervisor will restart your process at a known good state.

I added this because you said

So I assumed you were only interested in :nodeup, not :nodedown. If you monitor nodes you will receive both, so I discarded the :nodedown messages. Maybe that was a wrong assumption :smile:

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews