Trouble Understanding Erlang Gen_Server Architecture
Asked Answered
V

2

6

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!

Venlo answered 15/5, 2017 at 15:3 Comment(0)
K
9

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.

Kareem answered 15/5, 2017 at 20:42 Comment(1)
Thank you! It makes more sense when I did some io:format calls on each function to see the flow.Venlo
S
5

I am in early stages of learning Erlang and I need some further assistance

  1. Look at some simple, non gen_server client-server examples. Try to come up with a simple idea for your own client-server and write the code.
  2. Learn about parameterizing a simple server with a module name.
  3. Learn about gen_server and behaviors.
  4. Practice converting a simple server to a gen_server. Using a text editor with a split window is really handy.
  5. Learn about gen_tcp and sockets.
  6. Look at examples that combine gen_tcp and sockets with gen_server.

See here:

http://erlang.org/doc/design_principles/des_princ.html

I would not start at step 6, which you seem to be doing.

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?

No callback. TCP data bypasses the whole gen_server architecture. TCP data comes in through the back door, so to speak, along with other intruders. So gen_server:handle_info() is there to deal with them. handle_info() checks the servers mailbox for any messages that match the pattern specified as the argument to handle_info().

Anything that needs to be done to the TCP data is done inside handle_info(). Of course, if you need to do some complicated data processing in handle_info(), you can always call helper functions to calculate some result:

handle_info({tcp, Socket, RawData}, State) ->
    Result1 = computerInGermanyProcess(RawData),
    Result2 = computerInFranceProcess(RawData),
    Result3 = computerInRussiaProcess(RawData),    
    Result4 = State#state.stored_val,

    gen_tcp:send(Socket, [Result1, Result2, Result3, Result4]),
    {noreply, State};  %%Because a TCP message landed in the mailbox with no From value, 
                       %%do not reply to From, and do not mess with the value in State.

 computerInGermanyProcess(RawData) ->
          %% Possibly use gen_tcp and sockets again to send a message
          %% to another computer to get some data in order to
          %% calculate Result1:
          Result1.
 computerInFranceProcess(RawData) ->
          ...
          Result2.
 computerInRussiaProcess(RawData) ->
          ...
          Result3.         

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.

Client:                                                                    
+------------------------------------------------------+------------------------------------------------------+
| my_request() ->                                      |   handle_call({dostuff, Val}, ClientPid, State) ->   |
|     Request = {dostuff, 10},                         |       %%Do something with Val, State                 |
|     Server = ?MODULE,                                |       Response = {result, 45},                       |
|     Response = gen_server:call(Server, Request).     |       NewState = ....,                               |
|                            |                         |       {Response, NewState}.                          |
|                            |       from gen_server:  |                                                      |
|                            |            start_link() |                                     ^                |
|                            |                 |       |                                     |                |
+----------------------------+-----------------+-------+-------------------------------------+----------------+
                             |                 |                                             |
                             |                 |                                             |
+----------------------------+-----------------+-------+                                     |
|-module(gen_server).        |                 |       |                                     |
|-export([call/2,....]).     V                 |       |                                     |
|                                              |       |                                     |
|call(Server, Request) ->                      V       |                                     |
|  Server ! {request, Request, call, self(), Module} --+-->+                                 |                     
|  receive                                             |   |                                 ^                
|      {reply, Response, Server} ->                    |   |                                 |
|          Response      ^                             |   V                                 |
|  end.                  |                             |   |                                 |
+------------------------+-----------------------------+   |                                 |
|   Mailbox              |                             |   |                                 |
|                        |                             |   |                                 |
|       {reply, Response, Server}  <----------<--------+---+--------------<--------------+   |
|                                                      |   V                             ^   ^      
+------------------------------------------------------+   |                             |   |
                                                           |                             |   |
                                                           |                             |   |
Server:                                                    |                             |   |
+------------------------------------------------------+   |                             |   |
|    Mailbox                                           |   |                             |   |
|                                                      |   V                             ^   ^
|        {request, Request, call, ClientPid, Module} <-+---+                             |   |
|                            |                         |                                 |   |
+----------------------------+-------------------------+-----------------------------+   |   |                 
|                            |                                                       |   |   |
|loop(State) ->              |                                                       |   |   |
|    receive                 V                                                       |   ^   ^
|        {request, Request, call, ClientPid, Module}  ->                             |   |   |           ^
|            {Response, NewState} = Module:handle_call(Request, ClientPid, State} ---+---|-->+           |
|            ClientPid ! {reply, Response, self()}, ----------->---------------------+-->+            To Client
|            loop(NewState);                                                         |                   ^
|        {request, Request, cast, ClientPid, Module} ->                              |                   |
|            NewState = Module:handle_cast(Request, State), ------->---------->------|----->------------>+
|            loop(NewState);                                                         |
|        ...                                                                         |
|        ...                                                                         |                                      
|    end.                                                                            |
+------------------------------------------------------------------------------------+

The flow when a client calls gen_server:call():

  1. Client calls gen_server:start_link() which at a minimum specifies the module in which the handle_call/handle_cast functions are defined.

  2. Client calls gen_server:call(ServerName, Request), which is usually wrapped in an interface function.

  3. gen_server:call(ServerName, Request) is defined to send a message to the server, something like this:

     ServerName ! {request, Request, call, self(), ModName}.
    

    ModName was previously bound to the atom that was specified in gen_server:start_link(): the second argument is where you specify the module name that contains the definitions of the functions handle_call(), handle_cast(), etc.

  4. When the server receives that message, the server calls ModName:handle_call(), and your ModName:handle_call() code does something with the Request:

    handle_call(Request, ClientPid, ServerLoopValue) ->
        %%Compute some result using information in Request/ServerLoopValue
    
  5. The last line of your ModName:handle_call() function tells the server what to send back to the client as a response:

      {Response, NewServerLoopValue}.
    

    Then the server does something like this:

      From ! {reply, Response, ServerPid}.
      loop(NewServerLoopValue).
    

    and NewServerLoopValue becomes the new global variable for the server's loop(). Every server has a loop() function that looks something like this:

    loop(ServerLoopValue) ->
        receive
            {request, dothis, From} ->
                Result1 = ...SomeValue + 5....,
                From ! {Result1, self()},
                loop(NewServerLoopValue);
            {request, {dothat, 10}, From} ->
                Result2 = ... SomeValue - 10...,
                From ! {Result2, self()},
                loop(NewServerLoopValue);
            {request, stop, From}
                %%do not call loop()
        end.
    

    ServerLoopValue is like a global variable that all the different requests can see. The various gen_server request handlers can use information stored in ServerLoopValue to calculate a response, or they can add information to ServerLoopValue that other request handlers can use in the future.

The flow going in the back door of a gen_server using a TCP socket with {active, true}, {packet, 4}:

  1. Client calls gen_tcp:send().

  2. At the server's end of the socket, Erlang reads the data from the socket, constructs a message tuple, and puts the message tuple in the server's mailbox.

  3. The server retrieves the {tcp, ...} message from the mailbox and calls handle_info().

  4. handle_info() calls gen_tcp:send(Socket, Response) to send a response back to the client.

  5. The last line of handle_info() tells the server what value to use when calling the server's loop() function:

    {noreply, SomeValue}   =>  loop(SomeValue)
    

The flow going in the back door of a gen_server using a TCP socket with {active, false}, {packet, 0}:

Erlang gen_tcp not receiving anything

Suckle answered 16/5, 2017 at 8:48 Comment(2)
Wow, did you draw this diagram yourself?Paramount
@IlyaVassilevsky, Yes! Does it make any sense?Suckle

© 2022 - 2024 — McMap. All rights reserved.