Yes and I guess that you know that a synchronous client call the is gen_statem:call or gen_server:call never has required the server side to be synchronous. You can use gen_statem:reply or gen_server:reply to answer later possible from a different state or an other stage.
Are those Kafka functions? Is there a surveillance possibility to, something like monitoring the request, so you can know if it will never be answered?
It sounds like the root problem is, as said, that the call is Kafka synchronous. Unfortunately it tends to be a bit harder to cover all cases when doing it asynchronouslyâŠ
Iâd say we are looking at two different but related possible features here:
- The possibility to generate events that arrive now, but not immediately; a general time-out cannot be used because it is either immediately (
0
), or in 1 ms or more. - The possibility to âpollâ for external events like with
receive ... after 0
.
Case 1) can already be done with self() ! Msg
and the info
event type. The same can be said about general timers. There is a timer module that you can use. The {timeout, Name}
event type is a convenience feature to simplify handling timer references. We could add something similar for general events to arrive ânowâ.
Case 2) cannot be done with gen_statem
, but with gen_server
.
The question is how useful/essential that feature really is⊠And depending on exactly how case 1) is solved, it might also solve case 2).
For case 1) we need to decide how to queue the events. The most straightforward way would be to send them as messages so they are handled and prioritized just lika any other message. If we have an internal queue we need to implement a priority order, and I guess that however we implement that it will be surprising, at least when you mix in sending messages to yourself.
If gen_statem
would receive after 0
until there is no external message the external messages may starve out our appended events.
If gen_statem
would alternate between the appended messages and the process mailbox we might get event ordering surprises.
I think the least surprising possibility would be to handle appended events roughly like timers, i.e let the gen_statem
engine handle references and send reference tagged messages with self() ! {Ref, Event, Content}
. Message ordering becomes no surprise, and it would feel like next_event
, but prioritized like external events.
This doesnât solve how to âpollâ for one external event, thoughâŠ
Iâll drill more into my specific use case, addressing your questions and one of Ingelaâs points:
Thereâs an interaction between two processes: a âconnectionâ and a âconsumerâ.
The actual network call to Kafka is asynchronous â send a request, use {active, true}
to wait for a response. This is done by a âconnectionâ process that wraps the socket. Itâs implemented as a gen_statem
and it exposes a synchronous interface: the caller does a gen_statem:call
and the connection process uses (as Ingela suggests) gen_statem:reply
when the response comes back from the Kafka broker.
From the point of view of the caller, itâs synchronous.
This makes it extremely useful (and readable, in simple scripts) for dealing with low-level Kafka API messages.
Naively, you can (and I did) simply loop, doing something like FetchResponse = kafka_conn:call(C, FetchRequest)
repeatedly. This is the âconsumerâ process â itâs polling for messages on a given Kafka topic. Important here is that Fetch requests have a timeout â the broker wonât respond until either some messages are ready or the timeout expires (or thereâs an error).
I originally did this with a gen_statem
and the continuous {next_event, internal, fetch}
mechanism, discussed at the top of the thread.
This had the problems discussed in (#1):
- Infinite loop: because the
next_event
, er, events are higher-priority than normal messages, it didnât respond togen_statem:stop()
orgen_statem:call()
or'EXIT'
signals (from the linked connection process). - Because it blocked in the
kafka_conn:call
, it would have been laggy when responding anyway.
So, knowing that kafka_conn
is a gen_statem
, I made the synchronous gen_statem:call
asynchronous by using gen_statem:send_request/4
and gen_statem:check_response/3
, allowing the âconsumerâ process to handle stop, call, EXIT, etc. (#3)
But Iâm having problems with that: when I get the response, I do the next_event
thing, which means I miss when the connection process dies, and the consumer process crashes with noproc
when it makes the next call to the connection.
However, thinking about it, thatâs a separate problem â if I were to immediately issue the next call, Iâd probably see the same â I mean: the process died asynchronously, so Iâve gotta deal with it anyway, right?
The reason for the long thread is that there just doesnât seem to me to be a comfortable way to implement a continuous loop with a gen_statem
which allows for external messages to be processed â without one of the following: introducing some async calls; sending messages to oneself; or (non-zero) timeouts.
And maybe thatâs fine. Maybe this is a scenario where I shouldnât be using a gen_statem
.
Maybe.
On the other hand, had you used a gen_server
and itâs timeout mechanism, I suppose you wouldnât have noticed any problem since its timeout 0 does a receive ... after 0
.
So there is something that cannot be done with gen_statem
, but with gen_server
(and gen_fsm
, presumably), which bugs meâŠ, and that is polling external events.
This is a corner case, since the basic idea of a state machine, and for a gen_*
behaviour, maybe even more than for a generic process, is about reacting to external events when they happen, and sit waiting in a receive statement in between⊠It is bad style to block for 500 ms not responding to system messages. So I donât know if this problem deserves a solution.
The fact that my example blocks for 500ms is not good, agreed. But itâs a red herring: it could have blocked for only 50ms. The problem is that looping over something that doesnât use messages is hard/ugly (delete as appropriate) to do correctly in a gen_statem
, and I initially reached for the wrong âsolutionâ â using next_event
.
But: weâve looked at a bunch of different options in the thread, so thereâs something useful for everyone here.