How do you send/receive messages with non-GenServer OTP modules?

So, I’m extremely new to Elixir. In order to communicate with processes, you can of course use send(target_pid, message).

But, in general for this to work, you need:

  1. The target process to persist its existence for as long as you need it to receive the messages (if it doesn’t exist, it can’t handle/reply to messages sent to it, etc).

  2. It needs to have functions that can scan the message box and handle the messages.

GenServer of course injects your custom modules with code to ensure the above two requirements are satisfied.

Otherwise you’d have to code for the above yourself.

But, what about (custom modules that use) non-GenServer OTP modules?

How do you communicate with things like Supervisor processes, etc? I’ve tried to make my custom modules that use Supervisor (and separately, Registry) able to send/receive/handle/reply to messages, but can’t seem to pull it off.

I’ve tried to look up how to achieve it, but haven’t found much.

Again, if I create a custom module that uses Supervisor, I could potentially custom-code the persistence and message-handling features for its processes of a GenServer, but that seems superfluous.

Is there a way to perhaps combine GenServer & other OTP modules to combine their functionality, e.g. ‘use GenServer, use Supervisor’; or perhaps does this automatically happen (I.e. do most other OTP modules come with GenServer functionality); do other OTP modules have their own way of allowing you to communicate with their processes? I’ve tried to research for answers, but have found little.

Again though, being so new, I’m sure I’ve probably missed what will turn out to be the obvious answer to this.

What are your thoughts?

Many thanks indeed, really appreciate it!

Hi :wave:

The basics of processes can be found here, as the answer to the title is in the question itself, you use send and receive (more on that on the link). But still you need to keep the process alive right? So you make a function with a body containing a receive block, then at the end of it you call that function again, so it will keep repeating the receive block. Since code executed inside processes are synchronous it will block on the receive block until a message arrives.

Here is an example:

def loop do
  receive do
   {:greet, who} -> 
        "hello #{who}"
        loop()
   :bye ->
        "Bye!"
   _ -> 
        "Yea, sure"
        loop()
  end
end

pid = spawn(fn -> loop() end)
Process.alive?(pid)
send(pid, {:greet, "LionelHutz"})

To keep a state inside the process just pass it as argument on the loop function, and when you call it again inside the receive you pass the updated state. If you need the reference to another process, you pass as argument too.

2 Likes

Also…

  • You generally don’t put logic in supervisor, they are meant to restart other processes and have limited functionality
  • You always need to know how to reach your processes, by name, by registry, by global, by swarn etc.

Supervisor are often reached by their name…

start_link(__MODULE__, nil, name: __MODULE__)

Then… (the module is called Distrib.DynSup)

iex> Process.whereis Distrib.DynSup
#PID<0.1099.0>

I am curious about what You want to achieve by merging a supervisor with a gen_server

1 Like

FWIW, the way GenServer ensures this is with a monitor: otp/lib/stdlib/src/gen.erl at e986547370ab6d46f19d2c8d0ff0d05147de698e · erlang/otp · GitHub

That ensures that either the reply arrives, or the DOWN message from the monitor.

2 Likes

Thanks so much for your suggestions!

@joaoevangelista
In terms of building the loop and so forth for a process to keep it alive, and then using send and receive, this much I know.

GenServer of course is a module that abstracts all that logic, so you don’t have to recode it for your own custom modules. Consequently with GenServer based processes, it’s really easy to communicate - we only have to code implementation for start, init, handle_call, handle_cast, and the two interface functions for making a call and a cast.

So, I’m wondering if other OTP modules/custom modules based on them have such inbuilt functionality? Or, if we want to communicate with them, do we have to code in the entire loop-state etc logic (or some part of it) in ourselves, each time we set up a module based on something like a Supervisor, etc?

@kokolegorille
Yeah, I see that Supervisor is supposed to be used with minimum logic, but I thought it prudent to ask my Q for:

  1. In case I do need to contact with a Supervisor process.
  2. To figure out how to contact not just supervisors, but any non-GenServer based process, without having to manually code all the loop-state persistence stuff in it.

The idea of combining GenServer and Supervisor/other OTP module, is just to see if we can inject both the functionality of GenServer and whatever other OTP module you want, into the same custom module, to make messaging easier.

@al2o3cr

Ah yes, so I’ve seen with monitors, thanks!


Though still, the overall question stands: when you want to communicate with processes of non-GenServer based modules (i.e. custom modules you write that make use of other OTP modules, but also processes based on pure OTP modules, like just a registry, or just a Supervisor) - do you have to yourself code in the loop-state and send/receive functionality, that GenServer usually provides? Or, do other OTP modules (and thus custom modules that implement them) have their own built in, handy functionality for messaging?

Thanks!

Implementing the behaviours of all the gen_* modules in erlang comes with a loop (and quite a few more otp things) included. If you’re not using those you’ll need to implement the loop and the other features by yourself. There’s no gen_process or something akin to that. gen_server is the most generic of the gen_* behaviours.

1 Like

The overwhelming majority of the time, you should use functions provided by the module to communicate with its underlying process; for instance, you would write:

how_many = Supervisor.count_children(MySupervisorModule)

and not

how_many = GenServer.call(MySupervisorModule, {:count_children}, :infinity)

The former ultimately uses GenServer.call just like this, but that implementation detail shouldn’t leak out of Supervisor.

Regarding your deeper question, there’s an existential issue: starting a Supervisor will ultimately result in gen_server:start_link being called. A supervisor IS a GenServer. A Registry IS a Supervisor. This pattern is common because Erlang’s gen_server handles a bunch of stuff (system messages, code changes, etc) that complicate the basic receive loop.

You’ll only see that loop reimplemented when additional semantics are needed - for instance, some features of gen_statem cause the receive loop to check for internal messages before checking the process mailbox.

2 Likes

Ok, so what you guys are saying is that other OTP modules (and thus the custom modules you build with them) do indeed have some manner of auto-built in send/receive and state-continuation loop functionality (indeed, they in fact make use of that of the GenServer modules, because they are GenServers), for you to communicate with processes built on those modules:

It’s just that those OTP modules are intended for very specific uses, and rather than having an e.g. general ‘Supervisor.call(…)’ method, these modules have methods like Supervisor.count_children, whose functionality is built on top of the internal GenServer.call(…).
And that you thus shouldn’t try to communicate with these modules outside of these methods, to get it to implement logic that’s grander than what a Supervisor/Registry/etc is specifically intended for.

If you do need to do such communication to implement more general logic, then you indeed need to make use of a GenServer in your custom module, and just delegate the Supervisor/Registry/etc bits to a separate, more specialized custom module?

Is that about right?

Many thanks indeed, team.

Yup! In fact the loop itself isn’t even all that hard to follow, you can find it here: otp/lib/stdlib/src/gen_server.erl at master · erlang/otp · GitHub

1 Like

Ok, great. And the second half of what I wrote just there - that you do use their built in commutation functions to communicate with them, but they only allow you to message over very specific tasks, and if you need to communicate with them about/get them to execute extra logic, on top of their specific OTP function, then what you actually need is your own, general custom GenServer module that handles all of that, and then simply delegates the OTP parts to the required specific OTP units, in separate niche modules?

Is all that correct, too?

Thanks so much!

GenServer separates repetitive logic of the loop and receive block, from your custom logic. It is already highly customizable, and Supervisor is just a specialized GenServer.

I recommend this link to understand how they are made (and why)

Non OTP modules are generally used for the functional core, They don’t involve processes, and they should be “pure”. You don’t send them a message, but You use their internal functions.

1 Like

If you need an OTP compliant process but for some reason can’t use an existing behaviour it is actually not hard to do. You need to follow 5 rules and most of the support you find in the :proc_lib and :sys modules. Check this description http://erlang.org/doc/design_principles/spec_proc.html, though they call them “special processes”.

5 Likes