:gen_statem.call vs GenServer.Call

So something I noticed, :gen_statem and GenServer (or, really, :gen_server) both issue {pid, ref} as the from parameter when you issue a call. However, the pid for :gen_statem is NOT the PID of the calling process (somehow it does ultimately get forwarded back to its caller). Does anyone happen know why that is the case? Obviously the DT is “supposed to be opaque” so we’re “not supposed to ask these kind of questions” but I was kind of bugged out by this, and intensely curious.

gen_statem will keep a reference to the calls internally. This allows you to change states and have other handle_events processed, once you are ready to reply you use the reply action to do so. So I believe the main difference is that you are actually getting gen_statem’s internal reference. Although I’m slightly surprised the to isn’t the originating pid, http://erlang.org/doc/man/gen_statem.html#type-from

1 Like

strange, I just did this:

iex(1)> me = self()               
#PID<0.105.0>
iex(2)> spawn(fn -> :gen_statem.call(me, :what) end)
#PID<0.108.0>
iex(3)> flush
{:"$gen_call", {#PID<0.108.0>, #Reference<0.3084291029.2281963523.125427>},
 :what}

I’ll have to double check that I’m not doing something bizzare in my unit tests that are failing.

In your example above, the caller is the spawned task, not your shell, so the results makes sense. Are you saying you’re not seeing those same results in your application tests?

yeah, I figured out the strangeness, at least. :gen_statem's call does something unexpected when you specify a timeout:

iex(1)> me = self()
#PID<0.105.0>
iex(2)> spawn(fn -> :gen_statem.call(me, :what, 5000) end)
#PID<0.108.0>
iex(3)> flush()
{:"$gen_call", {#PID<0.109.0>, #Reference<0.1224518546.947126274.256689>},
 :what}
:ok
iex(4)> 

whereas GenServer does the expected thing:

iex(1)> me = self()
#PID<0.105.0>
iex(2)> spawn(fn -> GenServer.call(me, :what, 5000) end)
#PID<0.108.0>
iex(3)> flush()
{:"$gen_call", {#PID<0.108.0>, #Reference<0.1644207679.2021654531.37493>},
 :what}
:ok

oh damn, it’s in the erlang docs.

http://erlang.org/doc/man/gen_statem.html#call-2

For Timeout < infinity, to avoid getting a late reply in the caller’s inbox if the caller should catch exceptions, this function spawns a proxy process that does the call. A late reply gets delivered to the dead proxy process, hence gets discarded. This is less efficient than using Timeout == infinity.

Also, yeah, the behavior is not the same as :gen_server. Which is nice and intuitive, huh?

1 Like

gen_statem is quite a different beast than gen_server, in the end though gen_statem is extremely powerful and very flexible, but the power and flexibility definitely comes with a more complicated API and some nuances. I’ve really enjoyed using it, but even the whole state v data thing is super confusing at first.

well I ran across this “feature” while debugging my :gen_statem shim library. You may be interested in some of the design decisions I made:

https://hexdocs.pm/state_server/StateServer.html

and in particular, organizing your code by State:

https://hexdocs.pm/state_server/StateServer.State.html#content

(which I think is the proximal reason for having all of your handler be converted to a single handle_event - that way your .erl file can be vertically organized by state, IMHO but we have better ways ™ of doing it in elixir).

I’ve actually been working on a library that takes StatesLanguage JSON files and creates gen_statem processes. It’s working out pretty awesome so far. The Parallel and Map stuff is really cool, state machines within state machines. It relies heavily on pattern matching, or “vertical organization”

AWS produced the spec and it’s used with their Step Function system for serverless/lambda function coordination.

I’ll hopefully be open sourcing it soon-ish.

3 Likes

I’ve published the StatesLanguage library, you can see a post about it here, StatesLanguage - Declaratively design state machines that compile to Elixir based :gen_statem processes with the States Language JSON specification