0%

Slow Clients and Lost Connections · practice

Notice a Dead Peer

Viewers now send from their own threads, and a viewer that closes leaves the registry within half a second. One kind of viewer still stays forever: one that is gone without saying so. A phone walks out of Wi-Fi , a laptop lid closes, a cable is pulled. No close arrives, and the server’s recv never returns b"" to say the other end closed, because the machine that would close it is no longer reachable. A send to that viewer still succeeds, because, as Lesson 1 showed, it only puts bytes into buffers. An idle recv(timeout=0) finds nothing waiting, which is also what it finds on a healthy, quiet viewer.

The only way to tell a dead peer from a quiet one is to ask a question that needs an answer. has one: a , which Chapter 4 taught you to answer with a pong carrying the same payload. A live peer answers within moments. A dead one never does.

Three numbers

The library sends those pings for you, on the connection’s own schedule, and three of its settings decide how long a dead viewer can stay:

  • ping_interval: how often it sends a ping;

  • ping_timeout: how long it waits for the pong;

  • close_timeout: how long it waits, after sending a close frame, for the other side to finish closing.

When ping_timeout passes with no pong, the library fails the connection: it sends a close frame with code 1011 and the reason keepalive ping timeout, then closes its own end of the connection, so the peer’s recv would return b"" once it read that far, and then waits up to close_timeout for the peer’s side to close. A dead peer never closes, so the library waits out the whole close timeout. Only then do the handler’s recv and send raise. The picture shows it as a time line, running down, for a viewer whose last pong came at the start.

One ping interval after the last pong a ping goes out, one ping timeout later the library sends close 1011 and closes its end, and one close timeout after that the handler's next look raises ConnectionClosedError and the viewer leaves the registry.

In words: one ping interval after the last pong a ping goes out; one ping timeout later the library sends close 1011 and closes its end; one close timeout later the handler’s next look raises ConnectionClosedError, and the viewer leaves the registry.

The defaults are 20, 20 and 10 seconds. They suit the internet, where a slow mobile network can take seconds to answer. They make a demonstration on one machine very slow, so this chapter passes 1.0 second for each: PING_INTERVAL, PING_TIMEOUT and CLOSE_TIMEOUT at the top of scoreboard_server.py. In local tests, a silent client was dropped about 1.5 s after its last pong with 0.5 s for each , and about 3 s after with 1.0 s for each. With the 0.5 s ping values and the default close timeout, the handler heard only when the silent client finally closed its own , 5 s in. A real dead client never closes its socket, so the full 10 s would pass.

With ping_interval=1.0, ping_timeout=1.0 and the default close_timeout=10, about how long can a dead viewer stay registered after its last pong?

What the handler sees

When the library gives up, the handler’s next recv or send raises ConnectionClosedError. Read it as you learned in Chapter 6: exc.sent is the close frame this side sent, with exc.sent.code 1011 and exc.sent.reason keepalive ping timeout, and exc.rcvd is None, because no close frame came back. Your describe_close from Chapter 6 reads rcvd, so on the server it would report 1006: no close frame arrived.

There is nothing new to write in viewer_handler. Its next idle check calls recv(timeout=0), which raises the ConnectionClosedError; except ConnectionClosed: pass ends the quietly; the finally block removes the viewer. Lesson 3’s idle check is how a dead viewer is noticed.

Your server's keepalive gives up on a viewer. Which code does the exception's sent hold?

A line in the log

When the close timeout runs out, the library also writes a block to standard error: a first line, keepalive ping failed, and a . Shortened to its first line and its lines, a local run printed:

keepalive ping failed
TimeoutError: timed out while closing connection
websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received

That is the library reporting a dead peer, not a fault in your code. This lesson’s Run uses a stand-in viewer, so the block does not appear here. In Lesson 6, pl_ws.py meets a real silent client and prints the block as one line.

Configure the server in one place

open_server(board, *, serve=serve) makes the library server for a board, with these values, and returns it without starting it. Its body is one call to serve, with the handler from make_handler(board), the host 127.0.0.1, 0, and the three values as keyword named after them. Port 0 lets the system choose a free port, and 127.0.0.1 keeps the server on this machine. The serve is a keyword-only argument whose default is the library’s serve, the same kind of seam run_echo had in Chapter 6: a caller can pass another and see what open_server would have asked for. pl_ws.py does exactly that, to learn your three values before it builds its server, and the Lesson 4 section of Run does it too. Whoever calls open_server runs the server with serve_forever and stops it with shutdown.

Your task

In scoreboard_server.py, write open_server(board, *, serve=serve): call serve with make_handler(board), "127.0.0.1", 0 and the three keyword arguments, and return what it returns.

Run

Press Run. The Lesson 4 section calls your open_server with a serve of its own that records the keyword arguments, prints them, and then gives viewer_handler a stand-in viewer D whose idle check raises the exception the library raises after a missed pong, as if had given up on it. A finished version, run on the site, printed:

Lesson 4: a viewer that stopped answering pings
    ping_interval = 1.0
    ping_timeout = 1.0
    close_timeout = 1.0
    after the library gave up on D, the registry holds: []

Then press Submit.

Lesson 5 crosses to the other side of the connection: what a viewer’s program should do when its server drops it.

Task

In scoreboard_server.py, write open_server(board, *, serve=serve). Call the serve once, with make_handler(board) as the handler, "127.0.0.1" as the host and 0 as the , and the keyword ping_interval=PING_INTERVAL, ping_timeout=PING_TIMEOUT and close_timeout=CLOSE_TIMEOUT. Return what serve returns.

Do not call serve_forever or shutdown, and do not use a with block: whoever runs the server does that.

Press Run: the Lesson 4 section prints ping_interval = 1.0, ping_timeout = 1.0, close_timeout = 1.0 and after the library gave up on D, the registry holds: []. Standard error stays empty: the stand-in viewer D raises the library’s without a real connection, so no keepalive ping failed line appears in this Run.

Then press Submit. The check calls open_server with a stand-in serve that records its arguments and refuses to be started; it replaces the ’s own serve first, so nothing can open a port. It expects the handler from make_handler(board) (it calls that handler with a stand-in /watch viewer and checks that the viewer joins that board), "127.0.0.1" and port 0, and ping_interval, ping_timeout and close_timeout each given as a number above 0 and at most 2.0. Then it gives viewer_handler a viewer whose second idle check raises the ConnectionClosedError the library raises after a missed (sent 1011, nothing received), and expects the handler to return quietly with the registry empty. The keyword arguments may come in any order, and values below 1.0 are fine.