losing messages from ports within GenServer

382 views
Skip to first unread message

Sitaram Chamarty

unread,
Aug 20, 2015, 6:21:40 AM8/20/15
to elixir-l...@googlegroups.com
Hi,

I'm puzzled by some odd behaviour when listening for messages from a
"Port", within a GenServer module.

This module is supposed to take in an arbitrary number of shell
commands, and keep "N" of them running at any given time. As soon as
one command finishes, the next waiting command starts running.

That might look like `xargs -P` but xargs prints all the output jumbled
up; this code collects each shell command's output and prints it in one
chunk when the command is done.

I use the messages that `Port.open(spawn_executable ...` puts out to do
this. Each line becomes a ":data" message, and at the end you get a
":exit_status" message. I wait for those exit messages, then -- based
on the port in that message, grab all the data messages for that port
and print them.

Unforunately, some messages (not always the same ones either) are always
lost. (And if an ":exit_status" message is lost, that command is never
recognised to have completed!)

Curiously, if I replace all of this with an Agent instead, it all works
fine!

https://gist.github.com/sitaramc/7b0c11d94a90d1e5273a has 3 files:
server.ex (the one that fails), agent.ex (works great!) and helper-1.sh
(a shell helper command; 3 lines of code and that's includig a "sleep"
command!)

José Valim

unread,
Aug 20, 2015, 6:38:28 AM8/20/15
to elixir-l...@googlegroups.com
I haven't investigated the code in deep but you should never call "receive" from inside a GenServer or an Agent. That's because they already have their own receive loop that handles a lot of different messages, including system ones, code change and what not. Once you call your own "receive", your loop does not know how to handle any of those messages, so you end-up crippling your GenServer / Agent implementations.

What you need to do is, if you are full, you put the messages in a list (or in a queue) and start taking them out. You can look at the :queue module in Erlang.

Some pseudo-code:

def handle_call({:cmd, cmd}, _from, state) do
  if count > @max_count do
    state = add_to_queue(state)
    ...
  else
    start_new(cmd, state)
    ...
  end
end

Then implement handle_info to receive the port messages:

def handle_info({p, {:exit_status, _}}, state) do
  case take_from_queue(state) do
    {:ok, cmd} -> start_new(cmd, state)
    :error -> state # nothing in the queue
  end
  ...
end

Alternatively, you are really building a pool. So you can use one of the many existing solutions, like poolboy.




José Valim
Skype: jv.ptec
Founder and Director of R&D


--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/55D5AA2D.2070600%40gmail.com.
For more options, visit https://groups.google.com/d/optout.

Sitaram Chamarty

unread,
Aug 20, 2015, 8:12:50 AM8/20/15
to elixir-l...@googlegroups.com
On 08/20/2015 04:08 PM, José Valim wrote:
> I haven't investigated the code in deep but *you should never call
> "receive" from inside a GenServer or an Agent*. That's because they

That was the key insight I was missing. That, and the handle_info tip
-- to help handle port messages -- is what I needed.

As for poolboy, TBH this pool is stateless and quite simple, being just
a counter. What I have works fine for this use case; I'll look at
poolboy for more ambitious tasks.

Thanks again for your quick response!

regards
sitaram
> *José Valim*
> www.plataformatec.com.br <http://www.plataformatec.com.br/>
> Skype: jv.ptec
> Founder and Director of R&D
>
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-talk%2Bunsu...@googlegroups.com>.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/55D5AA2D.2070600%40gmail.com.
> For more options, visit https://groups.google.com/d/optout.
>
>
> --
> You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-ta...@googlegroups.com>.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/CAGnRm4JdUWCyAZPCfig3X33Wpop6-RW6cyWAr%3Dp6SL5nCM72rA%40mail.gmail.com <https://groups.google.com/d/msgid/elixir-lang-talk/CAGnRm4JdUWCyAZPCfig3X33Wpop6-RW6cyWAr%3Dp6SL5nCM72rA%40mail.gmail.com?utm_medium=email&utm_source=footer>.

Saša Jurić

unread,
Aug 20, 2015, 8:57:10 AM8/20/15
to elixir-lang-talk
Shameless plug: perhaps you might find useful my recent post on the topic of ports (http://www.theerlangelist.com/2015/08/outside-elixir.html)
Near the end I discuss a GenServer that wraps the port handling logic. The code is available here: https://gist.github.com/sasa1977/3bf1753675a77f18805a

Sitaram Chamarty

unread,
Aug 21, 2015, 12:15:17 AM8/21/15
to elixir-l...@googlegroups.com
On 08/20/2015 06:27 PM, Saša Jurić wrote:
> Shameless plug: perhaps you might find useful my recent post on the
> topic of ports
> (http://www.theerlangelist.com/2015/08/outside-elixir.html)

I already read that; your blog is on my RSS feed list :)

I am *totally* new to both Erlang and Elixir, and didn't even know ports
existed until I read your post. And that is when I switched my code
from using Task.async/await(System.cmd(...)) to "Port.open".

But my needs were much simpler -- a pure shell command with only
stdout/stderr, no stdin, no "interaction" of any kind, no need to
pack/unpack, etc., so I glossed over most of it.

> Near the end I discuss a GenServer that wraps the port handling logic.
> The code is available here:
> https://gist.github.com/sasa1977/3bf1753675a77f18805a

Speaking of "shameless", I am ashamed to say I even briefly glanced
through the code, but failed to notice that all the receive statements
shown in the previous code snippets in the main article had now
disappeared, to be replaced by handle_info. Even if I had noticed, I
may not have realised the significance of that change :-(

I'll take this opportunity to thank you for your excellent blog; I'm
learning a lot from it, though perhaps a bit slower than most :)

regards
sitaram

> On Thursday, August 20, 2015 at 12:21:40 PM UTC+2, Sitaram Chamarty wrote:
>
> Hi,
>
> I'm puzzled by some odd behaviour when listening for messages from a
> "Port", within a GenServer module.
>
> This module is supposed to take in an arbitrary number of shell
> commands, and keep "N" of them running at any given time. As soon as
> one command finishes, the next waiting command starts running.
>
> That might look like `xargs -P` but xargs prints all the output jumbled
> up; this code collects each shell command's output and prints it in one
> chunk when the command is done.
>
> I use the messages that `Port.open(spawn_executable ...` puts out to do
> this. Each line becomes a ":data" message, and at the end you get a
> ":exit_status" message. I wait for those exit messages, then -- based
> on the port in that message, grab all the data messages for that port
> and print them.
>
> Unforunately, some messages (not always the same ones either) are always
> lost. (And if an ":exit_status" message is lost, that command is never
> recognised to have completed!)
>
> Curiously, if I replace all of this with an Agent instead, it all works
> fine!
>
> https://gist.github.com/sitaramc/7b0c11d94a90d1e5273a <https://gist.github.com/sitaramc/7b0c11d94a90d1e5273a> has 3 files:
> server.ex (the one that fails), agent.ex (works great!) and helper-1.sh
> (a shell helper command; 3 lines of code and that's includig a "sleep"
> command!)
>
> --
> You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-ta...@googlegroups.com>.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/6137a239-fc27-469e-84d5-2678895ae789%40googlegroups.com <https://groups.google.com/d/msgid/elixir-lang-talk/6137a239-fc27-469e-84d5-2678895ae789%40googlegroups.com?utm_medium=email&utm_source=footer>.

Sasa Juric

unread,
Aug 21, 2015, 4:33:54 AM8/21/15
to elixir-l...@googlegroups.com
I thought it was a strange coincidence you’re asking about that just a few days after I blogged on the topic :-) Glad you find the article useful.
I’ll consider adding an explicit note that in GenServer version receives are replaced with handle_info.


You received this message because you are subscribed to a topic in the Google Groups "elixir-lang-talk" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elixir-lang-talk/SbwY23ORz3A/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elixir-lang-ta...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/55D6A5CF.7020800%40gmail.com.

Sitaram Chamarty

unread,
Oct 3, 2015, 5:32:11 AM10/3/15
to elixir-l...@googlegroups.com

Hi,

I know that you should never call "receive" from inside a GenServer or
an Agent. For the GenServer, that's what "handle_info" is for (not sure
what the eqvt is for Agent but that's not my question today).

What I would like to know is, is it safe to "receive" on the *client*
side?

If not, what is the equivalent of handle_info that can be used client-side?

regards
sitaram

Jason M Barnes

unread,
Oct 3, 2015, 12:34:35 PM10/3/15
to elixir-l...@googlegroups.com
It depends on how you are communicating with the server.  If you require a response from the GenServer, you should probably be calling GenServer.call/2 on the client side with handle_call/3 on the server side.  If for some reason you need the message to be handled with handle_info/2 instead, then the normal send/receive pattern on the client side is fine.

Jason

sitaram

--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.

Sitaram Chamarty

unread,
Oct 4, 2015, 11:45:03 PM10/4/15
to elixir-l...@googlegroups.com
On 03/10/15 22:04, Jason M Barnes wrote:
> It depends on how you are communicating with the server. If you
> require a response from the GenServer, you should probably be calling
> GenServer.call/2 on the client side with handle_call/3 on the server
> side. If for some reason you need the message to be handled with
> handle_info/2 instead, then the normal send/receive pattern on the
> client side is fine.

The use case I have is something like this (this is on "client"):

- GenServer.call
- get back a PID from GenServer
- expect (eventually) responses from that PID

The PID is spawned by the GenServer "server side"'s but it is not under
its control or linked or monitored (and I don't need it to be). It is
given the caller PID (the second argument in handle_call) so it knows
whom to respond to.

On the client side, I handle that with a receive-with-timeout (while
doing other things like handling more of its "input").

It's that receive I was worried about, but it seems from what you say
that it should be fine.

Thanks again
sitaram

>
> Jason
>
> On Sat, Oct 3, 2015 at 5:32 AM, Sitaram Chamarty <sita...@gmail.com <mailto:sita...@gmail.com>> wrote:
>
>
> Hi,
>
> I know that you should never call "receive" from inside a GenServer or
> an Agent. For the GenServer, that's what "handle_info" is for (not sure
> what the eqvt is for Agent but that's not my question today).
>
> What I would like to know is, is it safe to "receive" on the *client*
> side?
>
> If not, what is the equivalent of handle_info that can be used client-side?
>
> regards
> sitaram
>
> --
> You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-talk%2Bunsu...@googlegroups.com>.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/560FA091.3090306%40gmail.com.
> For more options, visit https://groups.google.com/d/optout.
>
>
> --
> You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-ta...@googlegroups.com>.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/CAEx7qBMZrdaLGDQ%3DV-pYorea4f%3DBUtvm4_FeEy-rcw8AUSjxwA%40mail.gmail.com <https://groups.google.com/d/msgid/elixir-lang-talk/CAEx7qBMZrdaLGDQ%3DV-pYorea4f%3DBUtvm4_FeEy-rcw8AUSjxwA%40mail.gmail.com?utm_medium=email&utm_source=footer>.

Peter Hamilton

unread,
Oct 4, 2015, 11:48:54 PM10/4/15
to elixir-l...@googlegroups.com
In the event of the timeout you will want to make sure you clean up that message from the clients inbox. The spawned pid will still send it and if your receive times out then you won't ever clear it out.

To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/5611F236.5020007%40gmail.com.

Sitaram Chamarty

unread,
Oct 5, 2015, 12:05:11 AM10/5/15
to elixir-l...@googlegroups.com
On 05/10/15 09:18, Peter Hamilton wrote:
> In the event of the timeout you will want to make sure you clean up that message from the clients inbox. The spawned pid will still send it and if your receive times out then you won't ever clear it out.

Thanks. That's a subtle point that I am sure I would have missed (or
understood only after some pain).

(Although in this specific example the timeout is only so the client can
go do other things before it checks again later (i.e., it's basically
polling), and the received message *will* be dealt with soon enough.)

regards
sitaram
> > To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-talk%2Bunsu...@googlegroups.com> <mailto:elixir-lang-talk%2Bunsu...@googlegroups.com <mailto:elixir-lang-talk%252Buns...@googlegroups.com>>.
> > To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/560FA091.3090306%40gmail.com.
> > For more options, visit https://groups.google.com/d/optout.
> >
> >
> > --
> > You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
> > To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-talk%2Bunsu...@googlegroups.com> <mailto:elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-talk%2Bunsu...@googlegroups.com>>.
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-talk%2Bunsu...@googlegroups.com>.
> --
> You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-ta...@googlegroups.com>.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/CAOMhEnwCRmmD44reS3FhPXAr4qMh18epu-ZL7FH6eJ2-PE0sOg%40mail.gmail.com <https://groups.google.com/d/msgid/elixir-lang-talk/CAOMhEnwCRmmD44reS3FhPXAr4qMh18epu-ZL7FH6eJ2-PE0sOg%40mail.gmail.com?utm_medium=email&utm_source=footer>.

Alexei Sholik

unread,
Oct 5, 2015, 2:35:51 AM10/5/15
to elixir-l...@googlegroups.com
If understand you correctly, you have a situation like this:

pid = GenServer.call(server, {:something, ...})
receive do
  {^pid, <payload>} -> ...
  after <timeout> -> ...
end

A more idiomatic way to handle this would be as follows. Return :noreply from your handle_call, it will make the client block until it gets back a reply some time later or until the timeout of a GenServer.call runs out. You can adjust that timeout by passing a 3rd argument to GenServer.call.

## server
handle_call({:something, ...}, from, state) do
  _pid = spawn(fn ->
    # do stuff
    # ...
    # Finally, send a response back to the client process
    GenServer.reply(from, <stuff>)
  end)
  {:noreply, state}
end

## client
stuff = try do
  GenServer.call(server, {:something, ...}, <timeout>)
catch
  :exit, :timeout -> ...
end

Probably the biggest downside of this approach is that you need to catch an exit if you want to handle the timeout case. On the other hand, it's using the tools from OTP designed for this kind of need.

Cheers,
Alex

To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/5611F236.5020007%40gmail.com.

Sasa Juric

unread,
Oct 5, 2015, 4:09:07 AM10/5/15
to elixir-l...@googlegroups.com

> The use case I have is something like this (this is on "client"):
>
> - GenServer.call
> - get back a PID from GenServer
> - expect (eventually) responses from that PID
>
> The PID is spawned by the GenServer "server side"'s but it is not under
> its control or linked or monitored (and I don't need it to be). It is
> given the caller PID (the second argument in handle_call) so it knows
> whom to respond to.
>
> On the client side, I handle that with a receive-with-timeout (while
> doing other things like handling more of its "input").
>
> It's that receive I was worried about, but it seems from what you say
> that it should be fine.

This sounds to me as if the client should also be a GenServer. So you could do something like:

- Make the first GenServer call from client's init/1, store the pid in the internal state
- React to subsequent messages in client’s handle_info

Return messages could also be implemented with casts (or even calls). This could be wrapped in the client module function, so the background process simply invokes something like Client.notify(client_pid, data), where Client.notify/2 issues a cast (or a call) to the given process.

Sasa Juric

unread,
Oct 5, 2015, 6:10:56 AM10/5/15
to elixir-l...@googlegroups.com

On 05 Oct 2015, at 08:35, Alexei Sholik <alcos...@gmail.com> wrote:

A more idiomatic way to handle this would be as follows. Return :noreply from your handle_call, it will make the client block until it gets back a reply some time later or until the timeout of a GenServer.call runs out. You can adjust that timeout by passing a 3rd argument to GenServer.call.

## server
handle_call({:something, ...}, from, state) do
  _pid = spawn(fn ->
    # do stuff
    # ...
    # Finally, send a response back to the client process
    GenServer.reply(from, <stuff>)
  end)
  {:noreply, state}
end

## client
stuff = try do
  GenServer.call(server, {:something, ...}, <timeout>)
catch
  :exit, :timeout -> ...
end

This pattern can be useful in some cases where you want to keep the server process responsive so it can respond to new messages while it is processing the current one.

However, there are two issues the code above.

The first one is in the client code. Since it catches timeouts, it can happen that a server reply arrives when the client is not expecting it anymore. This will cause a message to remain in the client's queue forever. This is not necessarily problematic if the client process terminates shortly after the timeout is caught. However, if the client runs for a longer period of time and invokes such call in a loop, the message queue could grow which will hurt perf & memory. 

A solution for this is to make the client a GenServer, as I explained in my previous post. All messages will then be handled (by the handle_info) so queue buildup shouldn’t happen.


The issue with the GenServer.reply from a spawned process is that a crash of the worker will not be noticed by the client (since it monitors the GenServer and not the worker). It’s worth mentioning that we’re not using GenServer.call with :infinity timeout (which IMO we should never do), the client will ultimately fail, so perhaps that’s good enough for some cases.

If you want an immediate failure propagation, a simple solution is to link the worker process to the server which means that the crash of the worker kills the server, and this is then propagated to the client. A downside of this is that a crash of any worker spawned from the server will kill all the workers and the server.

If you want a finer grained error control you could:

1. Monitor the worker process from the GenServer
2. Make the worker send the response to the GenServer which in turn uses GenServer.reply to respond to the caller
3. Handle the :DOWN message by notifying the corresponding caller.
4. Wrap GenServer responses into {:ok, response}, {:error, reason} and pattern match in the interface function to ensure crash in the case of 3.


Sitaram Chamarty

unread,
Oct 5, 2015, 7:15:17 PM10/5/15
to elixir-l...@googlegroups.com
On 05/10/15 12:05, Alexei Sholik wrote:
> If understand you correctly, you have a situation like this:
>
> pid = GenServer.call(server, {:something, ...})
> receive do
> {^pid, <payload>} -> ...
> after <timeout> -> ...
> end
>
>
> A more idiomatic way to handle this would be as follows. Return
> :noreply from your handle_call, it will make the client block until it

The client is also doing other things; I don't want it to block at all.

(In fact, I could even use a cast -- I don't even need to know the "pid"
of the spawned process because messages will have a distinct atom at the
start).

However, I think I got my answer: it is quite safe to do a receive in a
GenServer *client* :)

regards
sitaram

> gets back a reply some time later or until the timeout of a
> GenServer.call runs out. You can adjust that timeout by passing a 3rd
> argument to GenServer.call.
>
> ## server
> handle_call({:something, ...}, from, state) do
> _pid = spawn(fn ->
>
> # do stuff
> # ...
>
> # Finally, send a response back to the client process
>
> GenServer.reply(from, <stuff>)
>
> end)
>
> {:noreply, state}
> end
>
>
> ## client
>
> stuff = try do
>
> GenServer.call(server, {:something, ...}, <timeout>)
>
> catch
>
> :exit, :timeout -> ...
>
> end
>
> Probably the biggest downside of this approach is that you need to catch an exit if you want to handle the timeout case. On the other hand, it's using the tools from OTP designed for this kind of need.
>
> Cheers,
> Alex
>
> > To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-talk%2Bunsu...@googlegroups.com> <mailto:elixir-lang-talk%2Bunsu...@googlegroups.com <mailto:elixir-lang-talk%252Buns...@googlegroups.com>>.
> > To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/560FA091.3090306%40gmail.com.
> > For more options, visit https://groups.google.com/d/optout.
> >
> >
> > --
> > You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
> > To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-talk%2Bunsu...@googlegroups.com> <mailto:elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-talk%2Bunsu...@googlegroups.com>>.
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-talk%2Bunsu...@googlegroups.com>.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/5611F236.5020007%40gmail.com.
> For more options, visit https://groups.google.com/d/optout.
>
>
> --
> You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-ta...@googlegroups.com>.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/CAAPY6eOsHSvD06iGjbFQvqHQtK65J0gcV%2Bj6rDaYuU1x2H7H7w%40mail.gmail.com <https://groups.google.com/d/msgid/elixir-lang-talk/CAAPY6eOsHSvD06iGjbFQvqHQtK65J0gcV%2Bj6rDaYuU1x2H7H7w%40mail.gmail.com?utm_medium=email&utm_source=footer>.

Sitaram Chamarty

unread,
Oct 5, 2015, 7:20:07 PM10/5/15
to elixir-l...@googlegroups.com
On 05/10/15 15:40, Sasa Juric wrote:
>
>> On 05 Oct 2015, at 08:35, Alexei Sholik <alcos...@gmail.com <mailto:alcos...@gmail.com>> wrote:
>>
>> A more idiomatic way to handle this would be as follows. Return :noreply from your handle_call, it will make the client block until it gets back a reply some time later or until the timeout of a GenServer.call runs out. You can adjust that timeout by passing a 3rd argument to GenServer.call.
>>
>> ## server
>> handle_call({:something, ...}, from, state) do
>> _pid = spawn(fn ->
>>
>> # do stuff
>> # ...
>>
>> # Finally, send a response back to the client process
>>
>> GenServer.reply(from, <stuff>)
>>
>> end)
>>
>> {:noreply, state}
>> end
>>
>>
>> ## client
>>
>> stuff = try do
>>
>> GenServer.call(server, {:something, ...}, <timeout>)
>>
>> catch
>>
>> :exit, :timeout -> ...
>>
>> end
>
> This pattern can be useful in some cases where you want to keep the server process responsive so it can respond to new messages while it is processing the current one.
>
> However, there are two issues the code above.
>
> The first one is in the client code. Since it catches timeouts, it can
> happen that a server reply arrives when the client is not expecting it
> anymore. This will cause a message to remain in the client's queue

My apologies for this confusion. In my use case this won't happen; the
client is doing other things and periodically checking back on the
received messages. Or is made to check back on them by later "inputs"
it receives. Messages sitting around unread will only be a short-term
thing.

regards
sitaram

> forever. This is not necessarily problematic if the client process
> terminates shortly after the timeout is caught. However, if the client
> runs for a longer period of time and invokes such call in a loop, the
> message queue could grow which will hurt perf & memory.
>
> A solution for this is to make the client a GenServer, as I explained
> in my previous post. All messages will then be handled (by the
> handle_info) so queue buildup shouldn’t happen.
>
>
> The issue with the GenServer.reply from a spawned process is that a crash of the worker will not be noticed by the client (since it monitors the GenServer and not the worker). It’s worth mentioning that we’re not using GenServer.call with :infinity timeout (which IMO we should never do), the client will ultimately fail, so perhaps that’s good enough for some cases.
>
> If you want an immediate failure propagation, a simple solution is to link the worker process to the server which means that the crash of the worker kills the server, and this is then propagated to the client. A downside of this is that a crash of any worker spawned from the server will kill all the workers and the server.
>
> If you want a finer grained error control you could:
>
> 1. Monitor the worker process from the GenServer
> 2. Make the worker send the response to the GenServer which in turn uses GenServer.reply to respond to the caller
> 3. Handle the :DOWN message by notifying the corresponding caller.
> 4. Wrap GenServer responses into {:ok, response}, {:error, reason} and pattern match in the interface function to ensure crash in the case of 3.
>
>
> --
> You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com <mailto:elixir-lang-ta...@googlegroups.com>.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/4FB46EBD-1C9D-4544-8097-EA6C84605A2E%40gmail.com <https://groups.google.com/d/msgid/elixir-lang-talk/4FB46EBD-1C9D-4544-8097-EA6C84605A2E%40gmail.com?utm_medium=email&utm_source=footer>.

Kevin Montuori

unread,
Oct 5, 2015, 7:22:12 PM10/5/15
to elixir-l...@googlegroups.com
>>>>> "sc" == Sitaram Chamarty <sita...@gmail.com> writes:


sc> (In fact, I could even use a cast -- I don't even need to know
sc> the "pid" of the spawned process because messages will have a
sc> distinct atom at the start).

Be careful doing that, atoms aren't GC'd. make_ref/0 is your friend
here.

k.

--
Kevin Montuori
mont...@gmail.com

Sitaram Chamarty

unread,
Oct 5, 2015, 7:52:37 PM10/5/15
to elixir-l...@googlegroups.com
On 05/10/15 13:39, Sasa Juric wrote:
>
>> The use case I have is something like this (this is on "client"):
>>
>> - GenServer.call
>> - get back a PID from GenServer
>> - expect (eventually) responses from that PID
>>
>> The PID is spawned by the GenServer "server side"'s but it is not under
>> its control or linked or monitored (and I don't need it to be). It is
>> given the caller PID (the second argument in handle_call) so it knows
>> whom to respond to.
>>
>> On the client side, I handle that with a receive-with-timeout (while
>> doing other things like handling more of its "input").
>>
>> It's that receive I was worried about, but it seems from what you say
>> that it should be fine.
>
> This sounds to me as if the client should also be a GenServer. So you could do something like:
>
> - Make the first GenServer call from client's init/1, store the pid in the internal state
> - React to subsequent messages in client’s handle_info

That is an intriguing idea. I had not thought of that. The driver for
my "client" right now is STDIN or a series of "commands" in a file, and
it maintains its state via a Stream.resource, but that could be replaced
by one more level of indirection.

> Return messages could also be implemented with casts (or even calls).

Now I'm confused. My client is A, calls GenServer B. B spawns a long
running task. Are you saying that this task should then
"GenServer.cast(A, ...)"?

Brilliant; thanks!

(If that's not what you meant, please help me understand; otherwise I
think I got it!)

regards
sitaram

Sitaram Chamarty

unread,
Oct 5, 2015, 7:57:22 PM10/5/15
to elixir-l...@googlegroups.com
On 06/10/15 04:52, Kevin Montuori wrote:
>>>>>> "sc" == Sitaram Chamarty <sita...@gmail.com> writes:
>
>
> sc> (In fact, I could even use a cast -- I don't even need to know
> sc> the "pid" of the spawned process because messages will have a
> sc> distinct atom at the start).
>
> Be careful doing that, atoms aren't GC'd. make_ref/0 is your friend
> here.

Got it. Anyway, I meant distinct in the sense that the "receive" will
know to look it (hence why the pid is not important). I did not mean
"unique for each reply".

(TBH there are probably 3 atoms; I could be getting back tuples with any
one of them as element 0, but it's still a small finite number).

regards
sitaram

Sasa Juric

unread,
Oct 6, 2015, 2:55:19 AM10/6/15
to elixir-l...@googlegroups.com

Return messages could also be implemented with casts (or even calls).

Now I'm confused.  My client is A, calls GenServer B.  B spawns a long
running task.  Are you saying that this task should then
"GenServer.cast(A, ...)”?

Yes, that’s what I was saying. You could have that cast wrapped in the module of A, so the task process just invokes something like A.notify(pid, data) which under the hood sends a cast.

This approach is more appropriate if there’s tight coupling between A and B. If you want to keep loose coupling, or maybe you have different types of processes as clients, it’s probably better to stick with plain messages.

Sitaram Chamarty

unread,
Oct 6, 2015, 11:34:52 PM10/6/15
to elixir-l...@googlegroups.com
Got it; thanks. In my case it's probably loose coupling so I'll
probably stick to plain messages from the spawned-task to the client.

Thank you (to all of you) for the discussion; I learned a lot!

regards
sitaram
Reply all
Reply to author
Forward
0 new messages