Send/2 VS GenServer.cast, which one to use and why?

Background

I am making a Phoenix LiveView app. In this app, one of possible actions a user can perform takes a very long time. So, as per community advice I now have an asynchronous system, where processes send messages to each other.

To be fair, this approach works like a charm. I am surprised by how smooth everything is. I have a client that sends a GenServer.cast to a some ugly worker, and when this ugly worker is done with all the fascinating work of murdering hamsters processing the data, it sends a message to the client.

Question

Now a question arises in my mind. Why use a send and not a GenServer.cast? Honestly, the way I see it, they both do the same thing:

They send a message to the void. If someone receives it, great. If not, they don’t care.

So this begs the following questions:

  • What are the main differences between send and GenServer.cast?
  • When should I use one over the other?

Do let me know your opinions!

2 Likes

You should (almost) never call send/2 against PID that you know that is GenServer. send/2 is primitive used for situations when you do not know who you talk to. You can use send/2 to self() though.

So if you write API for your server then you should never use send/2 and always use GenServer.cast/2 instead.

1 Like

My worker doesn’t know if the Client is a GenServer or not. Nor do I think it should for that matter.
Does this mean that in this specific case, you would agree that I should use send in communication from Worker → Client ?

It depends a bit on the API.

If your GenServer module owns how data is sent to it, than it can use cast without issue. This is the “you know the receiver is a GenServer” case, because the code doing the cast knowns the receiver is a GenServer, even if anyone calling the public API doesn’t.

def send_something(something) do
  GenServer.cast(__MODULE__, {:somthing, something})
end

Where send is needed is when the sender only knows a pid or name of a process, but there’s no function interface to use. This includes for example anything pubsub.

4 Likes

Yes. In that case it should be send/2.

I am confused.

def send_something(something) do
  GenServer.cast(__MODULE__, {:somthing, something})
end

In this scenario, If my client calls send_something, we only know that the Worker is a GenServer (because of the cast).

A client calling send_something does not mean the client itself is a GenServer.
The only option I see for my understanding to be incorrect, is if only a GenServer can invoke send_something (thus forcing the client to be one) because if a non-GenServer client tries it, the function blows up or something.

What am I missing?

For this function the caller of send_something doesn’t supply the pid to send the data to. The function hardcodes __MODULE__ as the name of the process to cast to. Usually if you do that you can expect the process registered for that name to be a GenServer. Most often the GenServer is even defined in the same module as send_something.

Sometimes such an API even allows you do supply a process or name of being the receiver, though then the expectation is a different one compared to send. You’d expect the process supplied happens to be a GenServer, more often even the GenServer defined in the same module as the function. Everything else is not meant to happen.

On the other hand you use send if you don’t know how the receiving process is implemented, but you also don’t care how it’s implemented.

As an example:

You might have a pool of workers, where Pool.send(:worker_1, :something) internally does a GenServer.cast. There’s a fixed contract between the pool and it’s workers, so it can be fine to depend on the workers being GenServers.

You also have phoenix pubsub, which sends messages to whichever process subscribed. The process sending to subscribers doesn’t (want to) know anything about subscribers, so it uses send.

1 Like

What you’re missing is that traditionally with genservers the genserver “value” is opaque to the client. So you do:

{:ok, server} = SomeModule.start_link(opts)

server could be a pid, but it could also be a :via tuple, or any number of other values. Then you interact with the genserver via SomeModule.do_something_cool(server, args). The client should generally not be calling send or GenServer.cast explicitly.

9 Likes

If you have a job that will run for a while, like more than a few dozen milliseconds, I’d recommend to spawn or use a Task. If you use a genserver, then it could get severely backed up in some corner case and your call will timeout.

If you have to serialize in one genserver, you can use GenServer.call with a small timeout. Within the handle_call, you postpone the real work into a handle_continue and return :ok as soon as possible to the caller. This way, the caller can timeout and retry as it sees fit.

If the whole thing can be busy for minutes in a not very pathological case, you may want to use Oban.

@LostKobrakai , @benwilson512

Maybe I am not explaining myself correctly. Allow me to introduce a specific example:

defmodule Worker do
  use GenServer

  ##############
  # Public API #
  ##############

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

  # Client Process will run this call. With `self()`  I get the client's pid so I can reply later.
  @spec do_something_cool(String.t()) :: :ok
  def do_something_cool(greeting), do: GenServer.cast(__MODULE__, {:do_something_cool, greeting, self()})

  ##############
  # Callbacks  #
  ##############
   
  @impl GenServer
  @spec init(state) :: {:ok, state}
  def init(state), do: {:ok, state}

  # Worker Process runs this code. Worker does not know what Client is (is it a GenServer or something else?)
  # It also does not know if it is alive. It shouldn't know either. 
  @impl GenServer
  def handle_cast({:do_something_cool, greeting, from_pid}, state) do
    
    Process.sleep(10_000) # this simulates a very long operation
    send(from_pid, "#{greeting} World!")

    {:noreply, state}
  end
end

Then in the client, I would have a handle_info or something else that receives this message from send and does something with it, like displaying it on console for example.

# In some client
:ok = Worker.do_something_cool("Hello")

.... 

Would you still advise I use GenServer.cast here, instead of send?
Why so?

@derek-zhou I like backpressure generally speaking. However in this specific case, I opted for using cast instead of call, so I won’t have the blocking issues you mentioned.

Yes

I treat call and cast as the “active” interface into a genservers functionality. Think going to a vending machine an pressing a button expecting it to do something. The documentation calls cast “asynchronous requests”.

Plain messages are often more “passive”. Their message contents are usually less of a request by itself, but more information of the environment, where the genserver itself decides on how to react to. Think cruise control in a car. A message from a sensor with a measured distance is not a request to slow down, but the system using the measurement might decide to slow down the car based on said information.

Those are not hard rules though.

So you defend that in this code:

# Worker Process runs this code. Worker does not know what Client is (is it a GenServer or something else?)
  # It also does not know if it is alive. It shouldn't know either. 
  @impl GenServer
  def handle_cast({:do_something_cool, greeting, from_pid}, state) do
    
    Process.sleep(10_000) # this simulates a very long operation
    send(from_pid, "#{greeting} World!")

    {:noreply, state}
  end

I replace send with a GenServer.cast.

  • What if the other process is not a GenServer at all?
  • How can I know that in advance?
  • Won’t the whole thing crash if I expect it to be a GenServer but the client is not?

That’s not what I was proposing. The answer to a request is not a request as well. Also as you noted you don’t know who the caller is. But I support the idea of :do_something_cool being a request and therefore cast being used to send it to the GenServer.

I apologize for not being clear. I absolutely 100% agree with you on this point.
The post in general and question were referring to:

# Worker Process runs this code. Worker does not know what Client is (is it a GenServer or something else?)
  # It also does not know if it is alive. It shouldn't know either. 
  @impl GenServer
  def handle_cast({:do_something_cool, greeting, from_pid}, state) do
    
    Process.sleep(10_000) # this simulates a very long operation
    send(from_pid, "#{greeting} World!")

    {:noreply, state}
  end

As in:

  • Should I replace send(from_pid, "#{greeting} World!") with a GenServer.cast?
  • Would it be a good practice to force my client to be a GenServer?

I truly thank you for all your patience, and again I apologize for not being clearer before.

No sir.

3 Likes

If you have only cast and no call whatsoever for this genserver, and you have no need for resource cleanup in an orderly shutdown, then you are fine.

1 Like

I think you should be using a call here. The GenServer can launch a Task that ultimately uses GenServer.reply to send the response back to the original caller.

4 Likes

I support that idea but if I remember correctly that is to offload long work from a liveview process in order to not block the view, so just send would be the better choice.

The other way would be to just spawn a task from the liveview process and have no genserver at all.

But that’s off topic I guess.

I think if you can send a call (which is almost always) you should send a call, even if it only sends back :ok. If the GenServer is overloaded with things to do, it’s annoying clients that are making too many requests that get shutdown. This ‘fixes’ the root cause automatically. Versus if you have casts and (from the clients perspective) silently lose your target server because it’s message queue has blown up beyond it’s capacity. This causes a ‘greater service outage’ which imo is harder to debug and doesn’t ‘potentially fix’ your root cause. When the server comes back in the supervision tree, you could be in a situation where will just get overwhelmed by the same annoying clients that are still alive and have simply repointed themselves to the new incarnation of the server.

Nothing is guaranteed, of course if you have some very effective restart logic and a resource bottleneck that limits you more than you need for your service as a whole, you will still get screwed.

Default to call. If you know what you’re doing, use cast. (almost) never use send.