Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

net.poll

import net

Everything here is re-exported by net, so import net is enough and the names are called as net.*. Importing net.poll on its own works too and reaches the same definitions.

Readiness polling: given a set of sockets, which of them can be read or written right now, without blocking.

Every other call in net blocks. TcpStream.read() waits until its own socket has something to say and the thread does nothing else in the meantime, which is right for a client and wrong for a server: a thread that commits to one connection cannot serve another until that connection lets it go. A server built only from blocking calls can hold exactly as many concurrent connections as it has threads.

A Poller is the way out. It watches many sockets at once and tells you which ones are ready, so a single thread can own hundreds of connections and spend its time only on the ones with work:

import net

var server = net.TcpStream()
server.bind('127.0.0.1:8080')

var poller = net.Poller()
var clients = {}
var next_token = 1

# Token 0 watches the listener itself; an incoming connection makes
# it readable exactly the way data does on a connected socket.
poller.add(server, 0, net.READABLE)

while true {
  for event in poller.wait(1000) {
    var token = event[0]

    if token == 0 {
      var client = server.accept()
      clients[next_token] = client
      poller.add(client, next_token, net.READABLE)
      next_token++
      continue
    }

    var client = clients[token]

    # A peer that has gone away is reported rather than left to be
    # discovered by a read that returns nothing, forever.
    if event[1] & (net.ERROR | net.HANGUP) != 0 {
      poller.remove(token)
      clients.remove(token)
      client.close()
      continue
    }

    echo client.read(1024).to_string()
  }
}

Isolates

A Poller watches the sockets of the isolate that owns it, and nothing about it crosses an isolate boundary. Several worker isolates each keeping their own poller over their own connections is the intended shape, and is exactly the shared-nothing model.

A socket that is closed, upgraded into a TlsStream, or moved to another isolate stops being able to supply a descriptor. Rather than this being a rule you have to remember, wait() reports such a socket as ERROR against its own token, because the descriptor is resolved fresh on every call and never remembered between them. It is not possible for a poller to end up watching a descriptor that the operating system has since handed to something else.

What polling does not do

Readiness is not a guarantee. A socket reported READABLE has data now, but a read can still come up short, and a large write can still be partial on a socket reported WRITABLE. Poll to decide what to work on; keep handling short reads and writes the way you would anyway.

Constants

READABLE

net.READABLE: int = 1

Interest: tell me when this socket has data to read, a connection to accept, or an end-of-stream to observe.

WRITABLE

net.WRITABLE: int = 2

Interest: tell me when this socket can be written to without blocking.

ERROR

net.ERROR: int = 4

Readiness only: the socket has failed, or its handle can no longer supply a descriptor because it was closed, upgraded, or moved to another isolate. Deregister it.

HANGUP

net.HANGUP: int = 8

Readiness only: the peer has closed its end. Any data already sent is still readable, so drain the socket before closing it.

Classes

Poller

class net.Poller

A set of sockets to watch, each under a token of your choosing.

The token is whatever number is convenient for finding the socket again

  • an index into your own table, a connection id. wait() hands back the tokens that are ready, never the sockets, so the mapping stays yours.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.Poller()

Returns a new, empty Poller.

Poller.add()

net.Poller.add(socket, token: number, interest: ?number)

Starts watching socket under token.

Registering a token that is already present replaces it, so a caller that loses track cannot end up with one token naming two sockets.

Parameters

  • socket (TcpStream|TlsStream) — connected or bound; a bound listener becomes readable when a connection is waiting
  • token (number) — your own identifier for this socket
  • interest (?number) — READABLE, WRITABLE, or both; defaults to READABLE

Returns — Poller: this same instance, for chaining

Raises ValueError if interest names neither readability nor writability

Poller.modify()

net.Poller.modify(token: number, interest: number)

Changes what token is watched for, leaving the socket alone.

This is how a connection with a reply still to write switches from waiting for a request to waiting for room to send, and back again.

Parameters

  • token (number)
  • interest (number)

Returns — bool: whether the token was registered

Raises ValueError if interest names neither readability nor writability

Poller.remove()

net.Poller.remove(token: number)

Stops watching token, and releases this poller’s reference to its socket. Removing a token that is not registered does nothing rather than failing.

Parameters

  • token (number)

Returns — bool: whether the token was registered

Poller.contains()

net.Poller.contains(token: number) -> bool

Whether token is currently registered.

Parameters

  • token (number)

Returns bool

Poller.interest()

net.Poller.interest(token: number) -> ?number

The interest currently registered for token, or nil.

Parameters

  • token (number)

Returns ?number

Poller.tokens()

net.Poller.tokens() -> list

Every registered token, in registration order.

Returns list

Poller.length()

net.Poller.length() -> number

How many sockets are being watched.

Returns number

Poller.is_empty()

net.Poller.is_empty() -> bool

Whether nothing is being watched.

Returns bool

Poller.clear()

net.Poller.clear()

Stops watching everything.

Returns — Poller: this same instance, for chaining

Poller.wait()

net.Poller.wait(timeout: ?number)

Waits until at least one registered socket is ready, and returns what happened.

Each entry is [token, flags], where flags is any combination of READABLE, WRITABLE, ERROR and HANGUP. An empty list means the timeout elapsed with nothing ready - and also that nothing is registered, since waiting on an empty set could only ever sleep for the full timeout to no purpose.

Parameters

  • timeout (?number) — milliseconds to wait; 0 polls without waiting, a negative number waits indefinitely. Defaults to waiting indefinitely.

Returns — list: [token, flags] pairs, one per ready socket

Raises Error on an underlying platform failure

Note: Test flags with &, never ==: a socket can be readable and hung up at the same time, which is what a peer that sent a final response and closed looks like.

Poller.to_string()

net.Poller.to_string()

Acceptor

class net.Acceptor

An accept loop that never parks the thread inside accept().

A blocking accept() waits inside the runtime, where nothing else on the thread gets a turn: a signal trapped with os.on_signal() is not delivered, and a flag telling the server to stop is not read, until a connection happens to arrive. On an idle server that is never, which is why Ctrl+C on one can appear to do nothing.

An Acceptor puts the listener into non-blocking mode and waits on a Poller instead. next() hands over a connection when there is one, and returns nil when the interval passes with nothing arriving, which is the loop’s chance to look at whatever else it has to look at.

import net
import os

var listener = net.TcpStream()
listener.bind('127.0.0.1:8080')

var running = [true]

os.on_signal('INT', @() {
  running[0] = false
  return true
})

var acceptor = net.Acceptor(listener)

while running[0] {
  var client = acceptor.next()

  if client == nil {
    continue
  }

  echo 'connection from ${client.peer_address()}'
  client.close()
}

Nothing is added while connections are arriving: next() tries accept() first, and only consults the poller when the queue is empty.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.Acceptor(listener, interval: ?number)

Takes over listener, which must already be bound.

The listener is put into non-blocking mode and left that way, so a caller that also accepts from it directly gets nil when nothing is waiting rather than a blocking wait.

Parameters

  • listener (TcpStream|UnixStream) — bound and listening
  • interval (?number) — Milliseconds next() waits before giving up on a round. Defaults to 100. A connection that arrives wakes the wait at once, so this is the idle interval rather than added latency: it decides how soon a stopped server and a trapped signal are noticed.

Raises ValueError if interval is negative, which would wait forever and defeat the point.

Raises Error if the listener is not bound, or cannot be put into non-blocking mode.

Acceptor.next()

net.Acceptor.next() -> ?TcpStream|?UnixStream

The next connection, or nil when the interval passed with nothing arriving.

nil means “go round again”, never “there will be no more”.

The connection comes back in blocking mode whatever mode the listener is in, since what usually follows is a request pipeline written against blocking reads bounded by timeouts.

Returns ?TcpStream|?UnixStream

Raises Error on an underlying accept failure, including the listener having been closed.

Acceptor.wait_for_one()

net.Acceptor.wait_for_one() -> TcpStream|UnixStream

Waits for a connection however long it takes, the way a blocking accept() would, while still coming up for air every interval.

Returns TcpStream|UnixStream

Raises Error on an underlying accept failure

Acceptor.listener()

net.Acceptor.listener() -> TcpStream|UnixStream

The listener this accepts from.

Returns TcpStream|UnixStream

Acceptor.to_string()

net.Acceptor.to_string()

2026, Richard Ore and Zuri contributors