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