Slow Clients and Lost Connections · capstone
Capstone project: Project: Scoreboard v2
Scoreboard v2 is written. Every viewer has an outbox that keeps only the newest snapshot, every send happens on the viewer’s own thread, a viewer that closes is noticed within half a second, and tests/test_scoreboard.py and then runs the server with a viewer slowed on purpose and with a viewer that goes silent.
Two tests
Open tests/test_scoreboard.py. The message tests and the make_connection helper from Chapter 7 are still there; Chapter 7’s three server tests are gone, because they asserted that a score is sent straight to each viewer, which Lesson 3 changed on purpose. Two placeholders wait for you.
test_an_outbox_keeps_only_the_newest checks Lesson 2’s policy: make an outbox with scoreboard_server.make_outbox(), offer it three snapshots with different versions, and assert that take returns the last one, that a second take returns None, and that outbox["event"].is_set() is False afterwards. Snapshots as small as {"type": "snapshot", "version": 4, "scores": {}} are enough; offer and take never look inside them.
test_a_viewer_leaves_the_registry_whatever_ends_it checks the finally block. Make a board with scoreboard_server.make_board(["Owls"]) and a stand-in with make_connection("v1"), then give the stand-in a send of your own that raises RuntimeError, the kind of mistake except ConnectionClosed: does not catch. Call scoreboard_server.viewer_handler(viewer, board) inside try, with except RuntimeError: pass, because the handler lets that board["viewers"] is empty.
That second test ends the viewer’s conversation through its stand-in: the first send raises, so the handler is over at once. That matters here. A test that lets the viewer CHECK_EVERY) of real time each time the loop finds nothing waiting, because these tests use real locks and events. Each test should end its viewer through the stand-in’s send or recv, never by waiting for it.
These are the two promises v2 adds to v1, and they are the ones a later change could break quietly. An offer that keeps the oldest snapshot would still send viewers whole boards, only stale ones, and a finally block moved into an except clause would still pass every run in which viewers close politely. Neither mistake shows up in a transcript; a test catches both in milliseconds.
Keep import scoreboard_server at the top and call its
Run the tests in the
python -m pytest -q
A finished version, run on the site, reported 7 passed: the five message cases and your two tests.
The slowed viewer again
Run Lesson 1’s scenario against v2. pl_ws.py, a program written for this course that plays every client, connects viewer A and viewer S, a viewer this program slows down on purpose, 0.3 s before each send to it, and the scorekeeper scores three times:
python pl_ws.py slow-viewer
The same run printed:
scenario slow-viewer: viewer S is slowed down on purpose; every send from the server to S waits 0.3 s first
server options from your open_server: ping_interval=1.0, ping_timeout=1.0, close_timeout=1.0
viewer A joined; its first message was a snapshot with version 0
viewer S joined; its first message was a snapshot with version 0
the scorekeeper sent 1 point for Owls; its reply was a snapshot with version 1
the scorekeeper sent 2 points for Foxes; its reply was a snapshot with version 2
the scorekeeper sent 3 points for Herons; its reply was a snapshot with version 3
score 1: the reply came before the server finished sending version 1 to S
score 2: the reply came, and the server never sent version 2 to S (a newer snapshot replaced it)
score 3: the reply came before the server finished sending version 3 to S
the server shut down
viewer A received versions [0, 1, 2, 3], then the server closed it with 1001
viewer S received versions [0, 1, 3], then the server closed it with 1001
Compare it with Lesson 1, where every reply came after the slow send to S. Now the scorekeeper’s replies come first. The second line shows that the program built its server with your open_server’s values. A, which is fast, saw every version; S, which is slow, skipped one.
Scoreboard v2 serves viewer S, which is slowed down so every send to it waits 0.3 s, while the scorekeeper scores three times. S receives versions 0, 1 and 3. Is that a fault?
The dead viewer
The last scenario connects viewer A and viewer D, which completes its
python pl_ws.py dead-viewer
The same run printed:
scenario dead-viewer: viewer D completes its handshake, then never reads and never answers a ping (a dead path, simulated)
server options from your open_server: ping_interval=1.0, ping_timeout=1.0, close_timeout=1.0
viewer A joined; its first message was a snapshot with version 0
viewer D joined and went silent; the registry holds 2 viewers
the scorekeeper sent 1 point for Owls; its reply was a snapshot with version 1
library log: keepalive ping failed
the registry dropped D before the second score
the scorekeeper sent 2 points for Foxes; its reply was a snapshot with version 2
the server shut down
viewer A received versions [0, 1, 2], then the server closed it with 1001
The library log: line is the keepalive ping failed block from Lesson 4, which pl_ws.py prints as one line. A run of the same scenario on Chapter 8’s starting files, before Lesson 4, still held D 5 seconds after the first score. With your three values, the whole run took a few seconds on that computer.
Press Check my work once your tests pass. It checks your server as Lessons 2 to 4 did, checks that the scorekeeper is answered with no viewer send at all, and runs your tests against its own server and three broken ones: an offer that keeps the oldest snapshot, a viewer_handler that removes the viewer only when its connection closes, and a take that leaves the snapshot in place. Your tests must pass the first, and at least one of them must fail against each of the others. It does not read your terminal or the transcripts.
What is still open
Scoreboard v2 keeps the scorekeeper fast and the registry clean. It still lets anyone who reaches /score change the scores: the server never asks who is connecting. Chapter 9 decides who may score, before the handshake is answered.
Task
Replace the two placeholders in tests/test_scoreboard.py with tests, and keep the message tests, closed_error and make_connection:
test_an_outbox_keeps_only_the_newest: make an outbox withscoreboard_server.make_outbox(), offer it three snapshots with different versions, and assert thattakereturns the last one offered, that a secondtakereturnsNone, and that the outbox’s event is clear.test_a_viewer_leaves_the_registry_whatever_ends_it: make a board and a stand-in withmake_connection, give the stand-in asendthat raisesRuntimeError, callscoreboard_server.viewer_handlerinsidetrywithexcept RuntimeError: pass, and assert thatboard["viewers"]is empty.
End each viewer’s conversation through its stand-in’s send or recv; never wait for an idle check. Keep import scoreboard_server and call its from scoreboard_server import.
Run python -m pytest -q until every test passes, and run python pl_ws.py slow-viewer and python pl_ws.py dead-viewer to see v2 at work. Then press Check my work. It first checks your server as Lessons 2 to 4 did, with stand-ins and no thread, and checks that scorekeeper_handler answers each score with no viewer send. Then it reads your test file and runs your tests in this process: once with a working server of its own, where at least three tests must be collected and all must pass, and once each with three servers that each contain one mistake (an offer that keeps the oldest snapshot, a viewer_handler that removes the viewer only when its connection closes, and a take that leaves the snapshot in place), where at least one test must fail. A run that takes longer than 2 seconds is stopped; against a broken server that counts as noticing the mistake, against the working one it is reported. The check does not read your