axelson

axelson

Scenic Core Team

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

sasajuric

Author of Elixir In Action

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

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

akoutmos

Author of Build a Weather Station with Elixir and Nerves

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 :stuck_out_tongue:

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).

Where Next?

Popular in Questions Top

lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
Kurisu
For example for a current url like http://localhost:4000/cosmetic/products?_utf8=✓&query=perfume&page=2, I would like to get: ...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
earth10
Hi, I’m just starting to build a side-project with Elixir and Phoenix and doing some basic test with Elixir alone. What strikes me is th...
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36352 110
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement