Capstone: A Live Scoreboard · capstone
Capstone project: Finish the Bounded Server
Since Chapter 7 the server has run only inside pl_ws.py, which built it from your scoreboard_server.py, serve_scoreboard and main.
Read the supplied parts
Three supplied pieces do most of the plumbing; read them first.
server_parser() declares five options with their defaults: --port 8765, --max-clients 5, --max-updates 10, --deadline 5 seconds and --token-file token.txt. Checking their ranges is main’s job.
make_room_check(board, max_clients) returns a process_response hook. Chapter 9’s process_request hook runs before the library has checked that the request is a valid upgrade at all, so if the room were counted there, a malformed request answered with 400, or a scorekeeper refused with 401, would take a place for ever. process_response runs later and sees the answer the library is about to send. The room check counts only a 101, turns a 101 for one connection too many into 503 Service Unavailable, and lets every other answer pass. The supplied handle_connection gives the place back in its finally. On a local computer, with room for two, three viewers got 101, 101 and 503, while a 401 and a 400 took no place. One window stays open: a client that vanishes inside its own
--max-clients is how many connections may be open at once, not a total: each score command opens one connection, so a total would end a quiz after a handful of scores. A scorekeeper’s connection takes a place too, so five viewers leave no room for a score.
open_server now takes port and max_clients and passes the room check to serve as process_response.
Five connections are open and --max-clients is 5. A sixth viewer connects. What does it get?
Two ways to stop
A run ends at the first of two bounds: the last update, or the deadline. serve_scoreboard waits for both with one call, board["finished"].wait(timeout=deadline). As in Chapter 8, Event.wait returns True if the event was set and False if the timeout ran out first. So the wait is also the clock: serve_scoreboard reads no time of its own.
True means every viewer was sent the last snapshot and closed with 1000, so server.shutdown(code=1000, reason="the quiz is over") closes whatever is left, such as a scorekeeper. False means the deadline passed first: server.shutdown(code=1001, reason="the scoreboard is closing"), where 1001 means “going away”. The library’s signature is shutdown(close_connections=True, code=1001, reason=""), and shutdown() is with block’s own call on the way out does nothing more.
shutdown() also waits until every handler thread has returned, so what bounds the way out is each handler noticing its closed connection at its next idle check, within half a second, not the timeout you give join. Give join a timeout anyway: never wait without a bound.
One honest send once the buffers are full (Chapter 8, Lesson 1). It never sends the last snapshot and never leaves the registry, so finished stays clear, and the deadline closes everyone with 1001 although the quiz finished.
board["finished"].wait(timeout=8.0) returns False. What does your server do next?
Say the address, first
With --port 0 the system chooses a free serve_scoreboard reads it from server.socket.getsockname()[1] and prints listening on 127.0.0.1:PORT, with the real port, as its first line, before it waits. Print it with flush=True: when standard output is a pipe rather than a flush=True sends the line at once. The default port, 8765, is for Lesson 5’s run on your own computer.
In order, serve_scoreboard opens the server with open_server(board, token, port=port, max_clients=max_clients, serve=serve) in a with block, prints the address, starts one threading.Thread whose target is server.serve_forever, waits on finished, shuts down with 1000 or 1001, joins the thread with a timeout such as WAIT, and returns what the wait returned.
Refuse a bad command line before anything binds
main(argv=None, *, serve=serve) parses argv with server_parser() and checks each --port from 0 to 65535, --max-clients from 1 to MOST_CLIENTS, --max-updates from 1 to MOST_UPDATES, and --deadline above 0 and at most LONGEST_DEADLINE seconds. For a value outside its parser.error(message) prints the usage and your message to standard error and exits with status 2, which is what a refused command line means in every program of this course.
An unreadable or empty token file is a different failure: the command line was fine, but the program cannot do its job. Read it with read_token; on an OSError or an empty token, print one line to standard error and return 1, before anything is served. Then build the board with make_board(TEAMS, max_updates=...), call serve_scoreboard, print one closing line, and return 0. Both endings return 0: the deadline is one of the two ways a run is meant to end.
Someone runs python scoreboard_server.py --deadline 0. What happens?
Your task
Write serve_scoreboard and main. Then run the scenario that calls your main twice:
python pl_ws.py server-main
A finished version, run on the site, printed this, where PORT stands for the port number the system chose:
scenario server-main: your scoreboard_server.main() runs a quiz of two updates, then a quiz that stops at its deadline
your main() is called with --port 0 --max-updates 2 --deadline 4
viewer A joined; its first message was a snapshot with version 0
viewer B joined; its first message was a snapshot with version 0
the scorekeeper sent 3 points 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
viewer A received versions [0, 1, 2], then the server closed it with 1000 'the quiz is over'
viewer B received versions [0, 1, 2], then the server closed it with 1000 'the quiz is over'
your server | listening on 127.0.0.1:PORT
your server | the quiz is over at version 2; every viewer was closed with 1000
your main() returned 0
your main() is called with --port 0 --max-updates 2 --deadline 1
viewer A joined; its first message was a snapshot with version 0
viewer B joined; its first message was a snapshot with version 0
the scorekeeper sent 4 points for Herons; its reply was a snapshot with version 1
viewer A received versions [0, 1], then the server closed it with 1001 'the scoreboard is closing'
viewer B received versions [0, 1], then the server closed it with 1001 'the scoreboard is closing'
your server | listening on 127.0.0.1:PORT
your server | the deadline passed at version 1; every connection was closed with 1001
your main() returned 0
The lines marked your server are what your program printed, collected and shown after each run so that they do not mix with the viewers’ lines; your closing line may say it differently. Then try a refused command line, and ask the shell for the exit status:
python scoreboard_server.py --max-clients 9
echo $?
It prints the usage and your message, and echo $? prints 2. The server is a program now, but nothing on the other side is: Lesson 3 writes the client.
Task
In scoreboard_server.py, write two
serve_scoreboard(board, token, *, port=0, max_clients=MOST_CLIENTS, deadline=DEADLINE, serve=serve): in awithblock onopen_server(board, token, port=port, max_clients=max_clients, serve=serve), printlistening on 127.0.0.1:PORT(the real, from server.socket.getsockname()[1]) withflush=Trueas the first line, before anything waits; start onethreading.Threadwhose target runsserver.serve_forever; callboard["finished"].wait(timeout=deadline); if it returnsTrue, callserver.shutdown(code=1000, reason=...), otherwiseserver.shutdown(code=1001, reason=...); join the thread with a timeout such asWAIT; return what the wait returned.main(argv=None, *, serve=serve): parseargvwithserver_parser(); refuse--portoutside 0 to 65535,--max-clientsoutside 1 toMOST_CLIENTS,--max-updatesoutside 1 toMOST_UPDATES, and a--deadlineof 0 or less or aboveLONGEST_DEADLINE, each withparser.error(...)(exit status 2) before anything is served; read the token withread_token(args.token_file), and for anOSErroror an empty token print one line to standard error and return 1; build the board withmake_board(TEAMS, max_updates=args.max_updates); callserve_scoreboard(board, token, port=..., max_clients=..., deadline=..., serve=serve); print one closing line; return 0.
Run python pl_ws.py server-main and python scoreboard_server.py --max-clients 9, then echo $?. Then press Check my work. It runs Lesson 1’s checks again. It calls serve_scoreboard with a stand-in serve whose server records serve_forever, shutdown and its scoreboard_server.threading replaced by stand-ins whose thread records what it was given and runs nothing: the server must bind 127.0.0.1 and the port given, with the listening on 127.0.0.1: and the stand-in’s port, printed and flushed before the wait; exactly one thread must be started, whose target reaches serve_forever when the check calls it afterwards; the wait must get the deadline (a finished is set and 1001 when it is not, with True or False returned; and join must get a timeout. It calls main with serve_scoreboard replaced by a recorder and a token file of its own: good options must reach serve_scoreboard with the token stripped; six out-of-return 2 instead of parser.error.