Handing TCP Ports

There are couple of reasons you want lithos to open tcp port on behalf of your application:

  1. Running multiple instances of the application, each sharing the same port
  2. Smooth upgrade of you app, where some of processes are running old version of software and some run new one
  3. Grow and shrink number of processes without any application code to support that
  4. Using port < 1024 and not starting process as root
  5. Each process is in separate cgroup, so monitoring tools can have fine-grained metrics over them

Note

While you could use SO_REUSE_PORT socket option for solving #1 it’s not universally available option.

Forking inside the application doesn’t work as well as running each process by lithos because in the former case your memory limits apply to all the processes rather than being fine-grained.

Following sections describe how to configure various software stacks and frameworks to use tcp-ports opened by lithos.

It’s possible to run any software that supports systemd socket activation with tcp-ports of lithos. With the config similar to this:

environ:
  LISTEN_FDS: 1   # application receives single file descriptor
  # ... more env vars ...
tcp-ports:
  8080: # port number
    fd: 3   # SD_LISTEN_FDS_START, first fd number systemd passes
    host: 0.0.0.0
    listen-backlog: 128   # application may change this on its own
    reuse-addr: true
# ... other process settings ...

Python3 + Asyncio

For development purposes you probably have the code like this:

async def init(app):
    ...
    handler = app.make_handler()
    srv = await loop.create_server(handler, host, port)

To use tcp-ports you should check environment variable and pass socket if that exists:

import os
import socket

async def init(app):
    ...
    handler = app.make_handler()
    if os.environ.get("LISTEN_FDS") == "1":
        srv = await loop.create_server(handler,
            sock=socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM))
    else:
        srv = await loop.create_server(handler, host, port)

This assumes you are configured environ and tcp-ports as described above.

Python + Werkzeug (Flask)

Werkzeug supports the functionality out of the box, just put configure the environment:

environ:
  WERKZEUG_SERVER_FD: 3
  # ... more env vars ...
tcp-ports:
  8080: # port number
    fd: 3  # this corresponds to WERKZEUG_SERVER_FD
    host: 0.0.0.0
    listen-backlog: 128   # default in werkzeug
    reuse-addr: true
# ... other process settings ...

Or you can pass fd=3 to werkzeug.serving.BaseWSGIServer.

Another hint: do not use processes != 1. Better use lithos’s instances to control the number of processes.

Python + Twisted

Old code that looks like:

reactor.listenTCP(PORT, factory)

You need to change into something like this:

if os.environ.get("LISTEN_FD") == "1":
    import socket
    sock = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM)
    sock.set_blocking(False)
    reactor.adoptStreamPort(sock.fileno(), AF_INET, factory)
    sock.close()
    os.close(3)
else:
    reactor.listenTCP(PORT, factory)

Golang + net/http

Previous code like this:

import "net/http"

srv := &http.Server{ .. }
if err := srv.ListenAndServe(); err != nil {
    log.Fatalf("Error listening")
}

You should wrap into something like this:

import "os"
import "net"
import "net/http"

srv := &http.Server{ .. }
if os.Getenv("LISTEN_FDS") == "1" {
    listener, err := net.FileListener(os.NewFile(3, "fd 3"))
    if err != nil {
        log.Fatalf("Can't open fd 3")
    }
    if err := srv.Serve(listener); err != nil {
        log.Fatalf("Error listening on fd 3")
    }
} else {
    if err := srv.ListenAndServe(); err != nil {
        log.Fatalf("Error listening")
    }
}

Node.js with Express Framework

Normal way to run express:

let port = 3000
app.listen(port, function() {
    console.log('server is listening on', this.address().port);
})

Turns into the following code:

let port = 3000;
if (process.env.LISTEN_FDS && parseInt(process.env.LISTEN_FDS, 10) === 1) {
    port = {fd:3};
}
app.listen(port, function() {
    console.log('server is listening on', this.address().port);
})