Rafael Wenzel bio photo

Rafael Wenzel

Lead DevOps Engineer

Twitter Github Stackoverflow

In this article series that I call Adventures in Erlang I am trying to teach the readers how to properly start an Erlang endeavor on their own.

Please note that this article prerequisites Erlang/OTP 18 as well as Rebar as a package manager, in order to follow on your own system. I further assume that you are running on a Linux operating system. If you are running on anything else, you need to adapt the commands respectively.

I have created a free github project for this article series, that you can find here.

If you want to learn more about the Erlang syntax, and all the stuff we use in these articles, please refer to the rather awesome free online book Learn you some Erlang.

Feedback to the last article

After posting the last article about sockets in the /r/erlang subreddit, I received a great answer from someone regarding my approach to the article’s code. There were two major points of feedback, that I thought about for some time, and studied some more for learning purposes, and also for this article series.

It is sure important to me to have proper code, to stick to best practices, and not do unnecessary things. Sure, every code anyone writes can be done better by a more experienced developer, even most likely by yourself a few months down the road. But especially with this article series, it is worth delving into this feedback, and learn something from it. Even if it’s at least just some retrospective.

By the way, if you want to read the original feedback, this article is refering to, just click this link.

Feedback #1: Socket creation and listening

The first feedback was in regard to the way we created the socket process, the listening and the spawning of the processes. It was recommended to follow the example found in the LYSE.

It was especially stated to avoid bouncing it around all over the place and just have it there, rather than how we did it.

I studied the LYSE example, as well as my example, and had to think about it for a while. Yes, the LYSE documentation is a respectable learning source, and I even link to it in the preamble to this very article series. I learn from it even today, so I will not say they did it the wrong way, because they didn’t.

Checking out the LYSE site reveals, that there are basically two examples of proper socket creation. The naive implementation which we more or less followed, and the sockserv supervisor implementation (a name I just made up, you can not find it in the documentation).

The naive implementation, despite being not even in OTP, basically spawns two processes, one for the listener, and one for the acceptor. We, on the other hand, only spawned one acceptor, and opened a new listener whenever we got an answer.

Comparing this naive approach with the better implementation, the sockserv supervisor, shows the downsides. We only listen to one connection at a time, and only when we accepted this connection, awaiting all the included handshakes and whatshamacalls, we then spawn a new listener, to wait for the next connection. And yes, although this is definitely the naive approach, probably me not even thinking about those consequences, it makes sense for a small project that we are creating.

Yes, applying the supervisor implementation would be the better outcome, by far, especially in a more productive environment, but for our current application, the purpose and the scope, our approach is perfectly fine. But, if we were to make a proper game out of it, and connection issues actually were happening, this is one thing we were to change to better that. The good thing about erlang is, that we can postpone this to a later time, as it is a non-issue now, and should be relatively easy to switch to the better approach, as shown in the rather awesome LYSE online book.

Still this is an important piece of feedback, and learning about your approaches, and better approaches out there, is always very important in our endeavors to erlang expertise.

Feedback #2: Obfuscating OTP calls

Due to lack of a better wording, I just call it that, obfuscating OTP calls. The main feedback on this was that it is not a good approach to go at our calls to processes through the gen_server:call/2 function, but rather write that out into the module, just calling those modules.

Here is a short example from our communication module. Instead of calling through gen_server like in our original code.

listener_loop(From, Server) ->
      case start_listening(?IP, ?PORT) of
        {ok, ListenSocket} -> case gen_tcp:accept(ListenSocket) of
                                {ok, Socket}    -> gen_tcp:controlling_process(Socket, From),
                                                   gen_server:call(From, {register_socket, Socket}),
                                                   gen_server:call(Server, {renew_listener}),
                                                   gen_server:call(Server, {start_game, From});
                                {error, Reason} -> {stop, Reason}
                              end;
        _                  -> gen_server:call(Server, {renew_listener})
      end.

It was recommended to rather implement functions like this in the communication module:

register_socket(Pid, Socket) ->
      gen_server:call(Pid, {register_socket, Socket}).

And those two functions in the server module:

renew_listener(Pid) ->
      gen_server:call(Pid, {renew_listener}).

    start_game(Pid, Communication) ->
      gen_server:call(Pid, {start_game, Communication}).

Therefore rewriting our initial function listener_loop as follows.

listener_loop(From, Server) ->
      case start_listening(?IP, ?PORT) of
        {ok, ListenSocket} -> case gen_tcp:accept(ListenSocket) of
                                {ok, Socket}    -> gen_tcp:controlling_process(Socket, From),
                                                   register_socket(From, Socket)                % instead of gen_server:call(From, {register_socket, Socket}),
                                                   server:renew_listener(Server),               % instead of gen_server:call(Server, {renew_listener}),
                                                   server:start_game(Server, From),             % instead of gen_server:call(Server, {start_game, From});
                                {error, Reason} -> {stop, Reason}
                              end;
        _                  -> server:renew_listener(Server),                                    % instead of gen_server:call(Server, {renew_listener})
      end.

Now this is something up for debate. And I thought about this for a long time, trying to weigh pros and cons against each other. And I assume in the end it may be mostly a personal preference, but even without further thinking about making the code clearer to myself, I am not sure whether it should be preferred to follow this recommendation.

While I agree it is probably easier to see which modules are being called, as we spell them out immediately rather than impersonally calling them through gen_server, this might also be a bad thing, especially when trying to intrinsically follow the official erlang coding conventions.

Namely as stated in rule 3.2, it is bad if modules have circular dependencies, with one module calling stuff in another module, and so on. The thing is, if the module is not available, or changed, or the underlying variable changes, or whatever, then our code will crash. It is rather likely for the module to not be available than the variable we get to completely change. So calling through gen_server might result into nothing, in the worst case, but with calling directly through a module, like in the second variant, calling might result in a crash.

Even if it is not bad if erlang crashes, due to such a circumstance as a missing or changed module, it is rather unfortunate and unnecessary.

Delving a little further into the initial feedback, it was also stated, it is easier to change calls lateron, if they change, as you have to only change the function. But, what is most likely to change is not the name of the call but rather the arity, the amount and order of passed variables, which would actually result in having to change all calls anyway. And I have yet to mention that it adds more, rather unnecessary code just to be more straightforward to the developer.

Now this makes a decission for this feedback really hard. I can see a point in changing the code to the example, but I can also see a point in not doing that. I believe in the end one should just try out both methods and see with more experience which way proves to be better. Maybe none of the ways is better than the other, they are both just different.

I will definitely keep this issue in mind when writing the code in the future. Which option I want to settle for for now? I am not completely sure, but I like the way I am doing it now, because I apparently chose it on purpose. For learning purposes it is better to do it the way we did now, as it bares more opportunity to teach about erlang internals and behavior, and points out the good parts of OTP.

Another feedback

Right as I wanted to publish this article, I received another feedback which I just couldn’t resist to comment on in here too. So the last feedback was addressing the way we created the sockets, especially with the option {active, true}, which I admit I was not quite clear about it’s behavior. Apparently it is actively listening on this connection when active is set to true, and queueing up everything that comes upon the connection. This might end up in a full queue, even before we actually start receiving information from the connection. In a worst case scenario, this might result in a crash because we filled up all memory with those information.

Thus, starting with {active, false} does NOT queue up retreived data, and should only be set to true once we are actually ready to retrieve and act on data coming through the socket connection.

Further information about this and other options we are using in creating the socket listener, can be found here.

Conclusion

I am very glad I got the feedback in the subreddit, and am thankful to the users. They made this article possible, and tought us a great lesson at a good time in the project. So stay tuned for more adventures in erlang.