Search code examples
erlang

Erlang accepting connection in socket fails only when there are multiple layers of processes


I ran into a strange problem. I'm testing the code found in the docs that creates several processes to accept incoming connections from a listen socket:

-module(servertest).

%% API
-export([start/2, server/1]).

start(Num,LPort) ->
  case gen_tcp:listen(LPort,[{active, false},{packet,0}]) of
    {ok, ListenSock} ->
      start_servers(Num,ListenSock),
      {ok, Port} = inet:port(ListenSock),
      Port;
    {error,Reason} ->
      {error,Reason}
  end.

start_servers(0,_) ->
  ok;
start_servers(Num,LS) ->
  spawn(?MODULE,server,[LS]),
  start_servers(Num-1,LS).

server(LS) ->
  io:format("server started~n", []),
  case gen_tcp:accept(LS) of
    {ok,S} ->
      loop(S),
      server(LS);
    Other ->
      io:format("accept returned ~w - goodbye!~n",[Other]),
      ok
  end.

loop(S) ->
  inet:setopts(S,[{active,once}]),
  receive
    {tcp,S,Data} ->
      Answer = Data, % Not implemented in this example
      gen_tcp:send(S,Answer),
      loop(S);
    {tcp_closed,S} ->
      io:format("Socket ~w closed [~w]~n",[S,self()]),
      ok
  end.

This works perfectly fine:

servertest:start(1, 1241).
server started
1241

The problem starts when I add another layer of spawn and the code that creates the listen socket starts the accept servers has already been spawned:

-module(servertest2).

%% API
-export([start/2, server/1, start_listening/2]).

start(Num,LPort) ->
  erlang:spawn_link(?MODULE, start_listening, [Num,LPort]).

start_listening(Num,LPort) ->
  case gen_tcp:listen(LPort,[{active, false},{packet,0}]) of
    {ok, ListenSock} ->
      start_servers(Num,ListenSock),
      {ok, Port} = inet:port(ListenSock),
      Port;
    {error,Reason} ->
      {error,Reason}
  end.

start_servers(0,_) ->
  ok;
start_servers(Num,LS) ->
  spawn(?MODULE,server,[LS]),
  start_servers(Num-1,LS).

server(LS) ->
  io:format("server started~n", []),
  case gen_tcp:accept(LS) of
    {ok,S} ->
      loop(S),
      server(LS);
    Other ->
      io:format("accept returned ~w - goodbye!~n",[Other]),
      ok
  end.

loop(S) ->
  inet:setopts(S,[{active,once}]),
  receive
    {tcp,S,Data} ->
      Answer = Data, % Not implemented in this example
      gen_tcp:send(S,Answer),
      loop(S);
    {tcp_closed,S} ->
      io:format("Socket ~w closed [~w]~n",[S,self()]),
      ok
  end.

When it's started like this, the accept immediately (without any incoming connection) returns {error,closed}:

servertest2:start(1, 1242).
server started
<0.13452.0>
accept returned {error,closed} - goodbye!

For clarify here's a diff between the two versions:

--- servertest.erl      2021-11-25 00:04:32.000000000 +0100
+++ servertest2.erl     2021-11-25 00:04:01.000000000 +0100
@@ -1,9 +1,12 @@
--module(servertest).
+-module(servertest2).
 
 %% API
--export([start/2, server/1]).
+-export([start/2, server/1, start_listening/2]).
 
 start(Num,LPort) ->
+  erlang:spawn_link(?MODULE, start_listening, [Num,LPort]).
+
+start_listening(Num,LPort) ->
   case gen_tcp:listen(LPort,[{active, false},{packet,0}]) of
     {ok, ListenSock} ->
       start_servers(Num,ListenSock),

Thank you!


Solution

  • Without testing it, I'd say that in the second case the process that's the owner of the listen socket dies naturally after spawning the acceptors, thus closing the listen socket.

    Add some timer:sleep or receive after calling start_servers to verify it.