axelson
Design problem: I "want" to do a GenServer.call from within the GenServer
I’m currently working on a GenServer that uses erlexec to talk to an external binary over stdin/stdout. So I already have a handle_call that associates a “command” with the output of the program (which involves a timeout to know when the “command” is done running). The problem is that within that handle_call I want to execute another “command”.
The “easiest” way to accomplish that would be by using handle_call. But of course that will not work because a GenServer cannot call itself since that would result in a deadlock. Another possibility that might work is to spawn a Task, and that task can run the GenServer.call, that way the deadlock can be avoided.
But my question is this: By even posing this problem is it indicating a fault in my design? Should there be an easier way from within the GenServer process to share the code path that sends the “command” to the erlexec process along with the debounced timeout behavior? Should I introduce another GenServer and have that GenServer be responsible for the “higher-level” protocol (which is why I currently want to execute a “command” during a handle_call). Curious to know any thoughts.
Most Liked
sasajuric
Personally I’d have GenServer message protocol map 1:1 to the message protocol of the external program. In other words, if external program supports commands foo and bar, I’d only have handle_call for these two operations. If I wanted to do both, I’d call foo and then bar from the client process. This composition could still be wrapped in the interface function of the GenServer.
If the operation needs to be performed atomically, i.e. we don’t want any other client to invoke something untill both foo and bar have finished, my first option would be to support the composite foo_bar command in the external program. If that’s not possible (e.g. if I don’t have the control of the program code), then I’d solve this in the GenServer code by keeping track of the remaining commands which need to be invoked before returning the response with GenServer.reply.
al2o3cr
You could have the sequencing of the command and the subcommand happen in the calling process:
def SequencedOperations do
use GenServer
def run_simple_command(...etc...) do
GenServer.call(...)
end
def run_complicated_command(...) do
result1 = run_simple_command(...)
run_other_simple_command(..., result1)
end
# handle_call etc
end
The client still sees a simple blocking API - SequencedOperations.run_complicated_command() and the GenServer is solely responsible for holding the state of the connection to the external binary.
Something to think about regarding doing this asynchronously: what happens if another call comes in when a command is currently running? Does it go into a queue somehow? What’s the observable state of the GenServer “mid-command”?
At work we tried a similar asynchronous pattern in a complex state machine, and it was a mess - suddenly the async reply could arrive after the machine changed state, and the possible combinations got really unwieldy. In your case, consider simplifying the GenServer to selectively receive just the {:stdout, _, _} message:
def handle_call({:do_thing, arg1}, from, state) do
# send command
# ...
receive do
{:stdout, _, reply_value} ->
# do something with reply_value and reply to from
{:DOWN, _, _, _, reason} ->
# uh-oh the process went away
# could also let this sit in the mailbox and have a top-level handler for it, with different timeout behavior
after
10000 ->
# timed out!
end
To answer the previous questions, this approach:
- relies on the process mailbox to store calls that arrive while one is already in-flight
- the state is only observable when a command is not in-flight
akoutmos
I have always stayed away from doing things like that personally as it may have unintended consequences..that and it feel dirty GenServer — Elixir v1.20.2 ![]()
I’m not sure I follow 100% what you are trying to accomplish…so my suggestion may be way off the mark. But here goes.
Based off of your “ideal flow” chart, would it work if the caller makes a cast call to your GenServer along with the pid of itself and then immediately afterwards has a receive block waiting on a message back. In the mean time, the GenServer can perform a number of non-blocking operations in the background and once enough information has been aggregated, it can send/2 back to the original caller’s pid (which needs to be stored in the GenServer state from the initial cast call).
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









