Search code examples
erlangerlang-otpgen-server

Trouble Understanding Erlang Gen_Server Architecture


I am in early stages of learning Erlang and I need some further assistance. Not sure if this will get any sunlight but here it goes ... I am looking for a flow diagram on how the example works.

Example Code: https://github.com/erlware/Erlang-and-OTP-in-Action-Source/blob/master/chapter_03/tr_server.erl

Let me explain my problem ...

1> tr_server:start_link().

I understand this, it calls start_link(?DEFAULT_PORT) which calls the gen_server:start_link -- and this actually gets a call back to the tr_server(?MODULE) init([Port]).

init([Port]) ->
    {ok, LSock} = gen_tcp:listen(Port, [{active, true}]),
    {ok, #state{port = Port, lsock = LSock}, 0}.

This is also understood. You send data to the server, gen_server:handle_info/2 gets processed, and therefore calls, ?MODULE:handle_info/2 -- its a case, and since we returned a timeout in ?MODULE:init, it will case match the handle_info(timeout, #state{lsock = LSock} = State).

Okay, this makes sense.

This is where I start to get confused on the flow of Erlang

For a couple of days I have been reading online resources on this (including the Erlang-and-OTP-in-action) -- where this example comes from -- also: http://learnyousomeerlang.com/clients-and-servers

I am unsure how the flow of Erlang servers work. It is my understanding, that any messages sent to the server get processed by gen_server:handle_info/2 if they are out of bound -- meaning if they are not configured or match any other gen_server:handle_call/3? This means, any TCP data is automatically handled by the gen_server:handle_info/2 -- which gets a call back to the ?MODULE:handle_info?

What I dont understand is how and where handle_call, handle_cast play into the server architecture -- NOR do I understand the flow of the server from client->server architecture (up until where I get confused). I think this is very important to illustrate diagrams of flow much like circuitry diagrams.

Here is the main question: What is the flow of the server when the client sends the following:

lists:reverse([1,2,3]).

In plain text, it would be nice to get a flow diagram to understand how it works. From the text, and from the examples, its not very clear how it works. It is not really clear why we need:

get_count() ->
    gen_server:call(?SERVER, get_count).

stop() ->
    gen_server:cast(?SERVER, stop).

I appreciate any answers, I know it can be exhausting to explain! Sorry for any grammar mistakes!


Solution

  • It looks like you have a good idea of the flow in the case of data coming from the tcp port and the server handling this through the handle_info callback. That is one kind of client/server interaction, between the Erlang code and some external client connected to the port. But within an Erlang system, you also have client/server relationships between Erlang processes, where both sides are running Erlang code. (Even if it's just the gen_server process and the Erlang command shell process.)

    When you use the gen_server:call/cast client functions, they wrap your message in a way that you never see, but the receiving gen_server process will recognize this and use it to classify the message, then pass the unwrapped message to the corresponding handle_call/handle_cast. Apart from that, the flow is the same as for incoming data on the tcp port: in both cases it's just asynchronous messages to the server, being received and dispatched to the correct function. Meanwhile on the client side, the gen_server:call() function will wait for a reply (the sender's Pid is included in the wrapper), while gen_server:cast() proceeds immediately.

    These are really just convenience functions. In principle, the gen_server could have had just a single callback for handling all kinds of messages, leaving it up to you to encode whether it's a call or cast and how to react. But by providing these library functions and classifying the messages for you, it reduces the risk of treating a call like a cast or vice versa, or confusing an out of band message with a proper call/cast. The flow is the same in all cases: Client -> Server -> Callback [ -> Server Reply -> Client ].

    Thus, you could implement the get_count() function using ?SERVER ! {get_count, self()}, handling that message in your handle_info() callback instead of in handle_call(). (Just don't forget to send the reply back to the Pid included in the message or the client will be stuck forever.)

    Or you could skip implementing user API functions like get_count() completely and tell your users to just send {get_count, self()} to the server process and wait for the reply (the shape of which must also be documented). But then you can't change the details of how those messages look under the hood later. The gen_server:call/cast functions help you hide such messy implementation details and make it less likely that you screw up the client/server communication.

    Hope this helps.