Skip to content

Task and gen_tcp ​

Source: Elixir guide, Task and gen_tcp.

This chapter serves requests over TCP with Erlang's :gen_tcp and explores Task. First an echo server.

A TCP server, in broad strokes, does three things:

  1. Listen on a port and get hold of a socket
  2. Accept a client connection
  3. Read a request and write a response
elixir
defmodule KV.Server do
  require Logger

  def accept(port) do
    {:ok, socket} =
      :gen_tcp.listen(port, [:binary, packet: :line, active: false, reuseaddr: true])
    Logger.info("Accepting connections on port #{port}")
    loop_acceptor(socket)
  end

  defp loop_acceptor(socket) do
    {:ok, client} = :gen_tcp.accept(socket)
    serve(client)
    loop_acceptor(socket)
  end

  defp serve(socket) do
    socket |> read_line() |> write_line(socket)
    serve(socket)
  end

  defp read_line(socket) do
    {:ok, data} = :gen_tcp.recv(socket, 0)
    data
  end

  defp write_line(line, socket), do: :gen_tcp.send(socket, line)
end

The options: :binary (binaries, not lists), packet: :line (one line at a time), active: false (block on recv until data arrives) and reuseaddr: true (reuse the address if the listener crashes). Test with telnet 127.0.0.1 4040. When the client quits, recv returns {:error, :closed}, which the {:ok, data} match doesn't expect. We'll fix that later. More urgent: nothing supervises this.

Tasks ​

Task.start_link/1 runs an existing function in a new process that can be part of a supervision tree. As a child spec, {Task, fn -> ... end}:

elixir
children = [
  {Registry, name: KV, keys: :unique},
  {DynamicSupervisor, name: KV.BucketSupervisor, strategy: :one_for_one},
  {Task, fn -> KV.Server.accept(4040) end}
]

It works, but only for one client at a time: serve/1 runs inside the acceptor and never returns, so a second client connects and gets no echo.

serve loops forever, so the acceptor can't accept again.

Flawed concurrency ​

Serve each client in its own task:

elixir
{:ok, pid} = Task.start_link(fn -> serve(client) end)
:ok = :gen_tcp.controlling_process(client, pid)

Two clients now work at once, until one quits. Two causes: the unhandled {:error, :closed}, and the fact that each task is linked to the acceptor, so one crash takes the acceptor and every other client down.

Links without a supervisor let one failure cascade.

The rule of the guide: start processes as children of supervisors.

A task supervisor ​

Task.Supervisor supervises tasks and is better suited than DynamicSupervisor here. Add it before the acceptor, because order matters: the acceptor must not accept requests before its supervisor and buckets exist. Shutdown runs in reverse.

elixir
{Task.Supervisor, name: KV.ServerSupervisor},
{Task, fn -> KV.Server.accept(4040) end}
elixir
{:ok, pid} = Task.Supervisor.start_child(KV.ServerSupervisor, fn -> serve(client) end)
:ok = :gen_tcp.controlling_process(client, pid)

controlling_process makes the serving task the socket's owner. Otherwise the acceptor would own every socket, and its crash would close them all.

Each connection gets its own supervised task. A crash stays contained.

Restart strategies ​

What happens on a crash is part of the child spec's :restart:

:restartRestarts the process
:permanentalways, whatever the exit reason (the default)
:transientonly if it exits abnormally
:temporarynever

Task.child_spec/1 says :temporary, and KV.Bucket says nothing, so it is :permanent. Are those right?

  • Buckets: permanent. Users shouldn't have to recreate them.
  • Acceptor: it is critical, since nobody can connect without it. It must be :permanent.
  • Connection tasks: temporary. The cause may be the connection itself, and restarting over it would fail again.

So override the acceptor's spec:

elixir
Supervisor.child_spec({Task, fn -> KV.Server.accept(4040) end}, restart: :permanent)
Numbers are start order. Shutdown goes in reverse.

Production ​

A real TCP server runs a pool of acceptors, each with its own supervisor (PartitionSupervisor could do it). In practice use Ranch (Erlang) or Thousand Island (Elixir).

Site code: MIT. Pages and diagrams are derived from the Elixir documentation (Apache-2.0). Not affiliated with the Elixir Team.