Odaeus

Odaeus

Gen_tcp accept socket across processes gives `:einval`

Hi everyone,

I’m implementing something that needs to read a TCP stream for the first time and trying to get my head around gen_tcp. I start a GenServer to handle the connection and want to be able to make other calls to it so I thought spawning the blocking :gen_tcp.accept call would be efficient but it seems the socket doesn’t survive being transferred between processes. Does anyone know why? Or if I’m making an elementary mistake here?

Here’s a minimal script that demonstrates the problem:

# testtcp.exs
{:ok, socket} = :gen_tcp.listen(0, [])
{:ok, port} = :inet.port(socket) |> dbg()
current = self()
# Spawn a process just for accepting connections and send the new connection back to use when it
# happens.
spawn(fn ->
  case :gen_tcp.accept(socket) do
    {:ok, conn} ->
      dbg(:inet.sockname(conn))
      send(current, {:new_conn, conn} |> dbg())
  end
end)

# Spawn to connect to the socket.
spawn(fn ->
  dbg(:gen_tcp.connect('localhost', port, [], :infinity))
end)

receive do
  {:new_conn, conn} ->
    dbg({:received_conn, conn})
    dbg(:inet.sockname(conn))
end

Which outputs:

[tcptest.exs:3: (file)]
:inet.port(socket) #=> {:ok, 40015}

[tcptest.exs:8: (file)]
:inet.sockname(conn) #=> {:ok, {{127, 0, 0, 1}, 40015}}

[tcptest.exs:14: (file)]
:gen_tcp.connect('localhost', port, [], :infinity) #=> {:ok, #Port<0.7>}

[tcptest.exs:9: (file)]
{:new_conn, conn} #=> {:new_conn, #Port<0.8>}

[tcptest.exs:19: (file)]
{:received_conn, conn} #=> {:received_conn, #Port<0.8>}

[tcptest.exs:20: (file)]
:inet.sockname(conn) #=> {:error, :einval}

(there’s no way to add line numbers to code blocks here right?)

You’ll notice the dbg output is slightly out of order, but it shows the change between calling :inet.sockname in the same process that accepted the socket (valid) and in the receive loop ({:error, :einval}).

Thanks for any help!
Andrew

Marked As Solved

al2o3cr

al2o3cr

There’s the concept of a “controlling process” in gen_tcp that makes sending socket references between processes a little tricky.

BUT

The issue you’re encountering is simpler: a common cause of sockname returning EINVAL is if the socket has already shut down.

Here’s my read of how things happen in your code:

  • main process calls :gen_tcp.listen
  • first spawned process calls :gen_tcp.accept and blocks
  • second spawned process calls :gen_tcp.connect and blocks
  • first spawned process wakes up, prints out sockname, and sends conn to the main process
  • Then there’s a race between the three processes:
    • the second spawned process is now connected, gen_tcp.connect unblocks and the SECOND PROCESS EXITS. One end of the TCP connection starts shutting down
    • the FIRST PROCESS EXITS, taking the other end of the TCP connection down
    • the main process tries to call sockname on conn, which is shut down or shutting down

A quick way to check for conditions like this is to add Process.sleep(10000) (or your favorite large number) at the end of each function passed to spawn, which forces the processes to stay alive longer. Doing that in your example results in the code working.

Also Liked

bortzmeyer

bortzmeyer

Good question :slight_smile: I “know” it mostly by analogy with Unix processes, where sockets are just indexes to a local-to-the-process table.

Last Post!

Odaeus

Odaeus

For completeness, it looks like one way to get this to work on the accepter side is to use :gen_tcp.controlling_process/2 before send/2 with the value of the destination process.

Where Next?

Popular in Questions Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
Lily
In templates/appointment/index.html.eex: &lt;%= for appointment &lt;- @appointments do %&gt; &lt;tr&gt; &lt;td&gt;&lt;%= appoi...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New

Other popular topics Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54921 245
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 42533 114
New

We're in Beta

About us Mission Statement