0%

Hoofdstuk 10 · eindproject

Eindproject: een live scorebord

Eindproject: Schrijf het contract van het scorebord

Elk onderdeel van het scorebord bestaat: snapshots bij binnenkomst, samenvoegende outboxen, en een tokencontrole. Maar het zijn allemaal die pl_ws.py aanroept, een programma dat voor deze cursus is geschreven en elke client speelt. Niemand zou het vanuit een kunnen starten, niets op papier vertelt een client wat hij kan verwachten, en niets zegt hoe een quiz eindigt. Dit hoofdstuk maakt er twee programma’s van die iemand anders zou kunnen draaien: een server en een client.

Het project bevat al het leidingwerk dat de cursus je heeft geleerd: een controle op ruimte en twee nieuwe keywords op open_server, twee parsers voor de , drie nieuwe velden op het bord (last_version, open en finished), een half geschreven scoreboard_client.py, tests/test_client.py, een README.md, en een opzet in SCOREBOARD_PROTOCOL.md. Het contract komt eerst: je schrijft op wat beide programma’s beloven, en daarna laat je de server zich houden aan de ene regel die het toevoegt.

Wat het contract moet zeggen

SCOREBOARD_PROTOCOL.md is voor iemand die een client gaat schrijven en je code nooit heeft gezien. Elk van de acht kopjes beantwoordt één vraag die die persoon zou stellen:

  • Paths: waar maak ik verbinding, wat moet een scorebijhouder sturen om binnengelaten te worden, en welke antwoorden kunnen me vóór de upgrade wegsturen?

  • Messages: wat bevat elk van de drie berichttypes, en hoeveel punten mag een score dragen?

  • Versions: wie nummert de borden, en wat doe ik met een bord dat niet nieuwer is dan het laatste dat ik toonde?

  • Joining: wat komt er eerst?

  • Closing: welke beëindigt mijn verbinding, en wanneer?

  • Bounds: hoeveel verbindingen mogen er open zijn, hoeveel scores vormen een quiz, en hoe lang draait een server?

  • Exit codes: wat betekenen 0, 1 en 2 voor elk programma?

  • Not in this version: wat moet ik niet verwachten?

Alles daarin is iets wat een client kan waarnemen. De locks, outboxen en threads van de server blijven erbuiten, omdat geen client het verschil zou kunnen zien.

Welke hiervan hoort in SCOREBOARD_PROTOCOL.md?

Hoe een quiz eindigt

make_board(teams, max_updates=10) slaat de lengte van de quiz op als board["last_version"]. De quiz is voorbij zodra board["version"] die bereikt, en dan moet elke kijker de laatste snapshot ontvangen en een close met 1000.

De voor de hand liggende manier is de hele server afsluiten met code 1000 zodra apply_score de laatste versie maakt. Maar shutdown() sluit elke open verbinding vanuit threads die het zelf start, niet vanuit de eigen thread van elke kijker, dus een kijker wiens thread de laatste snapshot nog niet heeft verstuurd, kan gesloten worden voordat hij hem krijgt. Een versie die zo was geschreven, draaide de quizavond die je later in les 5 draait, op een lokale computer, niet op de site, met drie updates en een met opzet vertraagde kijker S. In alle drie de runs ontving kijker S alleen versie 0, zoals in deze regels uit de eerste:

run 1: viewer B received versions [0, 1, 2, 3], then the server closed it with 1000 'the quiz is over'
run 1: viewer S received versions [0], then the server closed it with 1000 'the quiz is over'

Kijker S zag nooit een score.

De regel uit hoofdstuk 8 lost dit op: de eigen thread van elke kijker is de enige thread die naar hem stuurt, en een close is ook een send. Dus de handler van elke kijker beëindigt de quiz voor zijn eigen kijker: hij stuurt een snapshot uit zijn outbox; als de versie van die snapshot board["last_version"] heeft bereikt, roept hij connection.close(1000, "the quiz is over") aan en keert hij terug; zijn finally verwijdert hem uit het register.

Test de versie van de snapshot die je net hebt verstuurd, niet board["version"]: het bord kan de laatste versie bereiken terwijl er nog een oudere snapshot onderweg is, en dan sluiten zou de laatste overslaan. Coalescing maakt de regel veilig, omdat niets de laatste snapshot kan vervangen. Een kijker die binnenkomt nadat de quiz voorbij is, krijgt de laatste snapshot als snapshot bij binnenkomst en wordt meteen gesloten.

apply_score houdt de andere helft: zodra de versie de laatste heeft bereikt, werpt hij ValueError("the quiz is over") op voordat er iets verandert, en de scorekeeper_handler uit hoofdstuk 9 maakt daar een error-envelop van.

Wie stuurt in de afbeelding elke close?

De scorebijhouder biedt alleen de laatste versie aan; de eigen thread van elke kijker stuurt die, sluit met 1000 en vertrekt, de laatste die vertrekt zet finished, en pas dan stopt de hoofdthread de server, in plaats van een shutdown die de trage kijker zou kunnen sluiten vóór zijn laatste send.

In woorden: de thread van de scorebijhouder biedt alleen de laatste snapshot aan; de thread van elke kijker, die van de vertraagde B inbegrepen, stuurt hem, sluit met 1000 en vertrekt; de laatste die vertrekt zet finished, en pas dan stopt de server.

Waarom sluit de eigen handler van elke kijker zijn verbinding aan het eind van de quiz, in plaats van dat de server elke verbinding tegelijk sluit?

Het event finished

board["finished"] is een threading.Event, net als het event van elke outbox sinds hoofdstuk 8. Daar wekte een event de thread van één kijker; dit event vertelt het programma dat de quiz voorbij is en dat geen kijker meer op zijn laatste snapshot wacht. De serve_scoreboard van les 2 wacht erop voordat hij de server stopt.

Wie het register na de laatste versie leeg achterlaat, zet het: meestal remove_viewer, als hij de laatste kijker verwijdert; of apply_score, na zijn broadcast, als er helemaal geen kijker geregistreerd is, want dan roept niemand remove_viewer aan. Beide lezen de versie en het register terwijl ze board["lock"] vasthouden, zodat de twee feiten op hetzelfde moment waar zijn. finished mag nooit gezet worden terwijl er een kijker geregistreerd is: dat is weer de race van hierboven.

Een kijker wiens netwerkpad wegviel, vertrekt pas als keepalive het merkt, ongeveer drie seconden later (hoofdstuk 8), dus het einde kan zoveel later komen; de deadline van les 2 begrenst het wachten.

De laatste score wordt verwerkt terwijl er twee kijkers geregistreerd zijn. Wanneer wordt finished gezet?

Je opdracht

Vul SCOREBOARD_PROTOCOL.md in met je eigen woorden, en verander daarna apply_score, remove_viewer en viewer_handler zodat de server zich aan de regel hierboven houdt. Elke verandering bestaat uit een paar regels rond één : een versie vergeleken met board["last_version"].

Draai de tests in de terminal:

python -m pytest -q
echo $?

Zes tests met assert False falen tot les 4 en 5, dus echo $?, de exitstatus van pytest, drukt 1 af. Speel daarna een korte quiz:

python pl_ws.py last-update

Dit bouwt je server voor een quiz van twee updates, laat kijkers A en B binnenkomen (B met opzet vertraagd) en stuurt drie scores. Een afgemaakte versie, op de site uitgevoerd, drukte af:

scenario last-update: a quiz of two updates; viewers A and B join, B slowed down on purpose; the scorekeeper scores three times
server options from your open_server: ping_interval=1.0, ping_timeout=1.0, close_timeout=1.0
your open_server installs a request check, so the scorekeeper sends the token from token.txt
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
the scorekeeper sent 5 points for Herons; its reply was an error with version 2: 'the quiz is over'
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'
finished was set 1 time(s); the registry held 0 viewers the first time
finished was set only after both viewers had left: True
the server shut down

Vóór je verandering maakte de derde score versie 3 en werd finished nooit gezet. Nu krijgt hij een error-envelop op versie 2, krijgen beide kijkers de laatste snapshot en daarna 1000, en gaat finished één keer omhoog, nadat beide vertrokken zijn.

De server weet nu hoe een quiz eindigt, maar hij draait nog alleen binnen pl_ws.py. Les 2 geeft hem een opdrachtregel en een deadline.

Opdracht

Houd in SCOREBOARD_PROTOCOL.md elk kopje en vervang elke (fill in) door eigen zinnen. Noem onder Paths /watch en /score en de statussen 401 en 503; onder Messages de snapshot, score en error en het puntenbereik, 1 tot en met 10; onder Closing de codes 1000, 1001, 1008 en 1011; onder Bounds de opties --max-clients, --max-updates en --deadline met hun grootste , 5, 50 en 60 seconden; onder Exit codes wat 0, 1 en 2 betekenen.

In scoreboard_server.py:

  1. apply_score: werp binnen de lock, voordat er iets verandert, ValueError("the quiz is over") op zodra board["version"] board["last_version"] heeft bereikt. Roep na de broadcast board["finished"].set() aan als de versie nu de laatste heeft bereikt en board["viewers"] leeg is.

  2. remove_viewer: zet na het verwijderen, terwijl je het bord nog onder de lock leest, board["finished"] onder dezelfde .

  3. viewer_handler: roep na het sturen van een snapshot waarvan de eigen versie board["last_version"] heeft bereikt connection.close(1000, "the quiz is over") aan en keer terug. Houd de finally die remove_viewer aanroept.

Laat elke andere zoals hij is.

Draai python -m pytest -q (zes tests met assert False falen tot les 4 en 5) en python pl_ws.py last-update. Klik daarna op Check my work.

Check my work draait je server met stand-in-verbindingen en zonder thread, en vervangt scoreboard_server.threading door eigen locks en events, dus houd import threading en maak locks alleen binnen functies. Ze draait een kijker één keer voor elk moment waarop een andere thread zou kunnen draaien, met één en met drie scores die daar aankomen: in een quiz die nog lang niet voorbij is, moet de kijker de snapshot bij binnenkomst krijgen en daarna alleen nieuwere borden, nooit verstuurd onder een lock; in een quiz die bij die scores eindigt, moet de kijker de laatste snapshot toegestuurd krijgen, daarna één keer gesloten worden met 1000, zonder dat er na de close nog iets wordt verstuurd, en moet finished gezet worden precies als de quiz voorbij is en er geen kijker geregistreerd is. Ze controleert een kijker die na het einde binnenkomt, een mislukte, een dode en een crashende kijker, de antwoorden aan de scorebijhouder (een score na de laatste versie krijgt een error-envelop en verandert niets), en de tokencontrole uit hoofdstuk 9 met een token dat je nog niet hebt gezien. Uit SCOREBOARD_PROTOCOL.md leest ze alleen de kopjes, de afwezigheid van (fill in), en de feiten die hierboven genoemd zijn; de rest van de formulering is van jou. Elke sluitreden is prima.