Thousand Island

repository·main·Indexed 21 days ago

https://github.com/mtrudel/thousand_island

A modern, pure Elixir socket server inspired by ranch, designed for stability and performance using idiomatic OTP design principles. It features a hierarchical supervision tree to manage connections, supports custom handlers via the ThousandIsland.Handler behaviour, and utilizes telemetry for high-performance monitoring and tracing.

Tokens
7K
Snippets
20
Records
34
Agent score
76%

What's inside thousand_island

  1. Logging and Telemetry in Thousand Island

    main

    Thousand Island does not perform inline logging. Instead, it uses telemetry to emit events for servers, acceptor processes, and individual client connections.

    To aid in tracing, you can use the ThousandIsland.Logger module, which provides functions to enable or disable logging for an already running server. This logging is internally backed by telemetry events.

  2. How Thousand Island's architecture works

    main

    Thousand Island uses a hierarchical supervision tree to manage connections efficiently and reduce contention:

    1. Server: The top-level supervisor that coordinates the Listener (which binds to the port) and the AcceptorPoolSupervisor.
    2. AcceptorPoolSupervisor: A dynamic supervisor managing a pool of AcceptorSupervisor processes (defaulting to 100).
    3. AcceptorSupervisor: Manages an Acceptor (a long-lived process that accepts connections) and a DynamicSupervisor.
    4. DynamicSupervisor: Manages the individual Handler processes tied to specific client connections.

    This design ensures that Acceptor processes are long-lived and that connection acceptance is decoupled from the creation of handler processes, improving scalability and crash resiliency.

  3. Start a Thousand Island Server

    main

    A Thousand Island server is a supervision tree started using ThousandIsland.start_link/1. You must provide a handler_module (which implements ThousandIsland.Handler) and a port in the options list.

    {:ok, pid} = ThousandIsland.start_link(port: 1234, handler_module: Echo)
  4. Implement a connection handler with ThousandIsland.Handler

    main

    To handle incoming connections, you must implement the ThousandIsland.Handler behaviour in a module. This module defines how the server reacts to data, connection events, and other socket interactions. The server passes ThousandIsland.Socket instances to your handler.

    Example of a simple Echo handler:

    defmodule Echo do
      use ThousandIsland.Handler
    
      @impl ThousandIsland.Handler
      def handle_data(data, socket, state) do
        ThousandIsland.Socket.send(socket, data)
        {:continue, state}
      end
    end
    defmodule Echo do
      use ThousandIsland.Handler
    
      @impl ThousandIsland.Handler
      def handle_data(data, socket, state) do
        ThousandIsland.Socket.send(socket, data)
        {:continue, state}
      end
    end
  5. Connection Draining and Shutdown behavior

    main

    Thousand Island handles shutdown by following standard Elixir Supervisor semantics:

    1. Shutdown Initiation: The ThousandIsland.ShutdownListener shuts down the listening socket.
    2. Acceptor Shutdown: All ThousandIsland.Acceptor processes shut down immediately.
    3. Handler Grace Period: Existing Handler processes continue running. Because Handler processes trap exits, they will continue until they complete or are :brutal_killed after the shutdown_timeout expires.

    To ensure connections have enough time to finish, configure the shutdown_timeout option when starting the server.

  6. How to implement a custom (non-GenServer) Handler

    main

    If the standard GenServer implementation provided by use ThousandIsland.Handler is insufficient, you can provide any module that implements start_link/1 as the handler_module parameter to the server.

    To successfully integrate a custom module, you must follow this delicate sequence:

    1. Implement start_link/1 to return a standard GenServer.on_start() style tuple.
    2. Listen for the message {:thousand_island_ready, raw_socket, handler_config, acceptor_span, start_time}.
    3. Convert the raw_socket into a ThousandIsland.Socket using ThousandIsland.Socket.new/3.
    4. Finalize the socket by calling ThousandIsland.Socket.handshake/1.

    Important Considerations:

    • The underlying socket closes automatically when your handler process ends.
    • Use a :temporary restart strategy for your handler processes.
    • Trap exits to allow for clean shutdowns.
    • Note that custom handlers may not emit the standard :connection telemetry spans.
  7. Use ThousandIsland.Socket for connection manipulation

    main

    The ThousandIsland.Socket struct encapsulates a client connection's underlying socket. It provides a high-level interface to read, write, and manipulate connections.

    Note for Handler Implementers: If you are building a custom handler based on ThousandIsland.Handler, you typically do not need to call new/3, handshake/1, or upgrade/3 manually, as the handler manages these lifecycle steps for you.

  8. Logging and Telemetry

    main

    Thousand Island does not perform inline logging. Instead, it uses Telemetry to emit events for servers, acceptor processes, and individual client connections.

    • Tracing: Use the ThousandIsland.Logger module to aid in tracing connections at various log levels. Logging can be dynamically enabled/disabled on a running server.
    • Telemetry Events: Detailed events (including spans) are available via the ThousandIsland.Telemetry module. This allows for high-performance monitoring without the overhead of constant string formatting and logging.
  9. Configure Connection Draining and Shutdown

    main

    Thousand Island handles shutdown by shutting down the listening socket and Acceptor processes first. Existing Handler processes (client connections) are allowed to finish within the configured shutdown timeout.

    Handler processes trap exit, allowing them to continue running until they complete or are :brutal_killed after the timeout expires. Use the shutdown_timeout option to control this duration (defaults to 15000 ms).

  10. Manage timeouts in Thousand Island Handlers

    main

    Thousand Island 1.5.0+ handles network timeouts internally. You can control the timeout between messages (either network data or local mailbox messages) by returning specific values from handle_connection/2 or handle_data/3.

    Timeout Return Values

    • {:continue, state, timeout}: Sets a one-shot timeout in milliseconds. If no message is received within this window, handle_timeout/2 is called.
    • {:continue, state, {:persistent, timeout}}: Sets a persistent timeout that applies to all future messages until changed.
    • {:continue, state}: Uses the default read_timeout configured at server startup.

    Note: These timeouts apply to the interval between any messages received by the Handler. They do not affect synchronous ThousandIsland.Socket.recv calls, which have their own timeout semantics.

    def handle_data(data, socket, state) do
      # Set a persistent 5-second timeout for all future messages
      {:continue, state, {:persistent, 5000}}
    end
  11. Implement a Thousand Island Handler

    main

    To handle connections in Thousand Island, you must create a module that implements the ThousandIsland.Handler behaviour. The easiest way to do this is by using the use ThousandIsland.Handler macro, which provides a GenServer-based implementation.

    When using the macro, your handler's state must be managed in a {socket, state} tuple format for all GenServer callbacks (like handle_call, handle_cast, and handle_info).

    Lifecycle Overview

    1. handle_connection(socket, state): Called after the initial connection setup (e.g., TLS handshake).
      • Return {:close, state} to terminate the connection.
      • Return {:continue, state} to keep the connection open and wait for data asynchronously.
    2. handle_data(data, socket, state): Called when the client sends data (only if handle_connection returned {:continue, ...}).
    3. handle_close(socket, state): Called when the remote end closes the connection.
    4. handle_error(reason, socket, state): Called on socket errors or handshake failures.
    5. handle_shutdown(socket, state): Called when the server itself is shutting down.
    6. handle_timeout(socket, state): Called when no data is received within the configured read_timeout.
    defmodule ExampleHandler do
      use ThousandIsland.Handler
    
      @impl ThousandIsland.Handler
      def handle_connection(socket, state) do
        ThousandIsland.Socket.send(socket, "Hello, World!")
        {:close, state}
      end
    
      @impl ThousandIsland.Handler
      def handle_data(data, socket, state) do
        ThousandIsland.Socket.send(socket, data)
        {:continue, state}
      end
    end