During my recent query with regard to “Functional Web Development with Elixir, OTP, and Phoenix”, Lance Halvorson kindly directed my attention toward the GenServer Documentation:
handle_call/3
must be used for synchronous requests. This should be the default choice as waiting for the server reply is a useful backpressure mechanism.handle_cast/2
must be used for asynchronous requests, when you don’t care about a reply. A cast does not even guarantee the server has received the message and, for this reason, should be used sparingly.
Can somebody please direct me towards some material that may explain how we arrived at these particular recommendations?
My current puzzlement is based on the observation that many modern architectures rely less and less on “synchronous” operations and are moving more into a pipelined model where {event,current_state}
enters the processing pipeline and new_state
pops out of the other end - and perhaps more importantly that the provider of event
isn’t necessarily designed to be the consumer of new_state
. As simple examples I would point to React’s Flux, The Elm Architecture, and perhaps to some extent ReactiveX (when done correctly).
In essence I would have thought that waiting an for an “unnecessary response” adds an unnecessary interaction dependency that unnecessarily reduces concurrency - realizing this would ultimately lead to to a design style where one would strive to make responses unnecessary wherever possible - at which point cast/2
/ handle_cast/2
would become the defacto default interaction (rather than call/3
/ handle_call/3
).
“Designing for Scalability with Erlang/OTP; Chapter 4: Generic Servers - Message Passing - Asynchronous Message Passing” p.84
In some applications, client functions return a hardcoded value, often the atom ok, relying on side effects executed in the callback module. Such functions could be implemented as asynchronous calls.
On the topic of backpressure DSEO talks about backpressure/load regulation frameworks like jobs and Safetyvalve. There really isn’t an indication that trying to manage backpressure at the granularity of a single message exchange is a good idea.
“Designing for Scalability with Erlang/OTP; Chapter 15: Scaling Out - Load Regulation and Backpressure” p.421
Start controlling load only if you have to. When deploying a website for your local flower shop, what is the risk of everyone in town flocking to buy flowers simultaneously? If, however, you are deploying a game back end that has to scale to millions of users, load regulation and backpressure are a must.
That being said forcing call/3
based requests can be a legitimate tactic to prevent individual client processes from overwhelming a server with requests as described in Building Non Blocking Erlang Apps. Essentially a GenServer
isn’t actually obliged to immediately return a reply in handle_call/3
but can choose to answer the request later with reply/2
- i.e. the server can keep the client blocked while not being blocked itself.