Erlang: Avoiding race condition with gen_tcp:controlling_process
You are right. In such cases you surely need {active, false}
passed among listening socket options. Consider this snippet of code:
-define(TCP_OPTIONS, [binary, {active, false}, ...]).
...
start(Port) ->
{ok, Socket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
accept(Socket).
accept(ListenSocket) ->
case gen_tcp:accept(ListenSocket) of
{ok, Socket} ->
Pid = spawn(fun() ->
io:format("Connection accepted ~n", []),
enter_loop(Socket)
end),
gen_tcp:controlling_process(Socket, Pid),
Pid ! ack,
accept(ListenSocket);
Error ->
exit(Error)
end.
enter_loop(Sock) ->
%% make sure to acknowledge owner rights transmission finished
receive ack -> ok end,
loop(Sock).
loop(Sock) ->
%% set soscket options to receive messages directly into itself
inet:setopts(Sock, [{active, once}]),
receive
{tcp, Socket, Data} ->
io:format("Got packet: ~p~n", [Data]),
...,
loop(Socket);
{tcp_closed, Socket} ->
io:format("Socket ~p closed~n", [Socket]);
{tcp_error, Socket, Reason} ->
io:format("Error on socket ~p reason: ~p~n", [Socket, Reason])
end.
Thus you will not lost anything until controlling_process
succeeds. It is known problem been discussed a lot over internets.
If you wish to use ready to go solution you surely need to take a look at Ranch project.
If the socket is active, inet:tcp_controlling_process
(called by gen_tcp:controlling_process
) sets the socket to passive, then selectively receives all messages related to that socket and sends them to the new owner, effectively moving them to the new owner's message queue. Then it restores the socket to active.
So there's no race condition: they have already thought of that and fixed it in the library.
There absolutely is a race condition. I just encountered it, today, in OTP 21.2, and that's why I'm here. A packet can arrive between the time that accept
returns and the time that inet:tcp_controlling_process
sets the socket to passive.
I just wanted to point out tiny simplification to @Keynslug's answer above. The socket can be set to active from a non-owning process, so the ack
messaging and enter_loop
are unnecessary
-define(TCP_OPTIONS, [binary, {active, false}, ...]).
...
start(Port) ->
{ok, Socket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
accept(Socket).
accept(ListenSocket) ->
case gen_tcp:accept(ListenSocket) of
{ok, Socket} ->
Pid = spawn(fun() ->
io:format("Connection accepted ~n", []),
loop(Socket)
end),
gen_tcp:controlling_process(Socket, Pid),
inet:setopts(Socket, [{active, once}]),
accept(ListenSocket);
Error ->
exit(Error)
end.
loop(Sock) ->
%% set soscket options to receive messages directly into itself
inet:setopts(Sock, [{active, once}]),
receive
{tcp, Socket, Data} ->
io:format("Got packet: ~p~n", [Data]),
...,
loop(Socket);
{tcp_closed, Socket} ->
io:format("Socket ~p closed~n", [Socket]);
{tcp_error, Socket, Reason} ->
io:format("Error on socket ~p reason: ~p~n", [Socket, Reason])
end.