0%

Langsame Clients und verlorene Verbindungen · Abschlussprojekt

Abschlussprojekt: Projekt: Anzeigetafel v2

Anzeigetafel v2 ist geschrieben. Jeder Zuschauer hat eine Outbox, die nur den neuesten Snapshot behält, jedes Senden passiert im eigenen Thread des Zuschauers, ein Zuschauer, der schließt, wird innerhalb einer halben Sekunde bemerkt, und trennt einen Zuschauer, der weg ist. Was fehlt, ist ein eigener Beweis. Dieses Projekt fügt tests/test_scoreboard.py zwei Tests hinzu und führt den Server dann mit einem absichtlich gebremsten Zuschauer und mit einem Zuschauer aus, der verstummt.

Zwei Tests

Öffne tests/test_scoreboard.py. Die Nachrichtentests und der Helfer make_connection aus Kapitel 7 sind noch da; die drei Servertests aus Kapitel 7 sind weg, weil sie zugesichert haben, dass eine Wertung direkt an jeden Zuschauer gesendet wird, was Lektion 3 absichtlich geändert hat. Zwei Platzhalter warten auf dich.

test_an_outbox_keeps_only_the_newest prüft die Strategie aus Lektion 2: Erstelle eine Outbox mit scoreboard_server.make_outbox(), biete ihr drei Snapshots mit verschiedenen Versionen an und sichere zu, dass take den letzten zurückgibt, dass ein zweites take None zurückgibt und dass outbox["event"].is_set() danach False ist. Snapshots so klein wie {"type": "snapshot", "version": 4, "scores": {}} genügen; offer und take schauen nie hinein.

test_a_viewer_leaves_the_registry_whatever_ends_it prüft den finally-Block. Erstelle eine Tafel mit scoreboard_server.make_board(["Owls"]) und einen Stellvertreter mit make_connection("v1"), und gib dem Stellvertreter dann ein eigenes send, das RuntimeError auslöst, die Art von Fehler, die except ConnectionClosed: nicht abfängt. Rufe scoreboard_server.viewer_handler(viewer, board) innerhalb von try auf, mit except RuntimeError: pass, weil der Handler diese hinauslässt, wie er soll. Sichere dann zu, dass board["viewers"] leer ist.

Dieser zweite Test beendet das Gespräch des Zuschauers über seinen Stellvertreter: Das erste Senden löst eine Ausnahme aus, also ist der Handler sofort vorbei. Das ist hier wichtig. Ein Test, der die Zuschauer- bis zu einer Leerlaufprüfung laufen lässt, würde jedes Mal, wenn die Schleife nichts Wartendes findet, eine halbe Sekunde (CHECK_EVERY) echte Zeit warten, weil diese Tests echte Locks und Events verwenden. Jeder Test soll seinen Zuschauer über send oder recv des Stellvertreters beenden, niemals, indem er auf ihn wartet.

Das sind die beiden Versprechen, die v2 zu v1 hinzufügt, und genau sie könnte eine spätere Änderung still kaputt machen. Ein offer, das den ältesten Snapshot behält, würde Zuschauern immer noch ganze Tafeln senden, nur veraltete, und ein finally-Block, der in eine except-Klausel verschoben wurde, würde trotzdem jeden Lauf bestehen, in dem Zuschauer höflich schließen. Keiner der beiden Fehler zeigt sich in einem Protokoll; ein Test erwischt beide in Millisekunden.

Behalte import scoreboard_server oben und rufe seine in jedem Test über das auf, wie es die Datei schon tut. Check my work führt deine Tests gegen einen eigenen funktionierenden Server aus und dann gegen Server, die je einen Fehler enthalten, und es kann diese nur einsetzen, wenn deine Tests sie beim Laufen über das Modul nachschlagen.

Führe die Tests im aus:

python -m pytest -q

Eine fertige Version hat auf der Seite 7 passed gemeldet: die fünf Nachrichtenfälle und deine zwei Tests.

Noch einmal der gebremste Zuschauer

Führe das Szenario aus Lektion 1 gegen v2 aus. pl_ws.py, ein für diesen Kurs geschriebenes Programm, das jeden Client spielt, verbindet Zuschauer A und Zuschauer S, einen Zuschauer, den dieses Programm absichtlich bremst, 0,3 s vor jedem Senden an ihn, und der Punktezähler wertet dreimal:

python pl_ws.py slow-viewer

Derselbe Lauf hat ausgegeben:

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

Vergleiche es mit Lektion 1, wo jede Antwort nach dem langsamen Senden an S kam. Jetzt kommen die Antworten an den Punktezähler zuerst. Die zweite Zeile zeigt, dass das Programm seinen Server mit den deines open_server gebaut hat. A, der schnell ist, hat jede Version gesehen; S, der langsam ist, hat eine übersprungen.

Im Lauf mit v2 hat S die Versionen 0, 1 und 3 empfangen. Ist das ein Fehler?

Der tote Zuschauer

Das letzte Szenario verbindet Zuschauer A und Zuschauer D, der seinen abschließt und danach nie liest und nie auf einen antwortet: ein toter Netzwerkpfad, simuliert. Der Punktezähler wertet einmal, das Programm wartet, bis der Server D trennt, und der Punktezähler wertet noch einmal.

python pl_ws.py dead-viewer

Derselbe Lauf hat ausgegeben:

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

Die Zeile library log: ist der Block keepalive ping failed aus Lektion 4, den pl_ws.py als eine Zeile ausgibt. Ein Lauf desselben Szenarios mit den Startdateien von Kapitel 8, vor Lektion 4, hielt D 5 Sekunden nach der ersten Wertung noch fest. Mit deinen drei Werten hat der ganze Lauf auf diesem Computer ein paar Sekunden gedauert.

Klicke auf Check my work, sobald deine Tests bestehen. Die Prüfung prüft deinen Server so wie die Lektionen 2 bis 4, prüft, dass der Punktezähler ganz ohne Senden an einen Zuschauer beantwortet wird, und führt deine Tests gegen einen eigenen Server und drei kaputte aus: ein offer, das den ältesten Snapshot behält, ein viewer_handler, das den Zuschauer nur entfernt, wenn seine Verbindung schließt, und ein take, das den Snapshot an seinem Platz lässt. Deine Tests müssen gegen den ersten bestehen, und mindestens einer von ihnen muss gegen jeden der anderen scheitern. Sie liest weder dein Terminal noch die Protokolle.

Was noch offen ist

Anzeigetafel v2 hält den Punktezähler schnell und das Register sauber. Sie lässt aber noch jeden, der /score erreicht, die Punktestände ändern: Der Server fragt nie, wer sich verbindet. Kapitel 9 entscheidet, wer werten darf, bevor der Handshake beantwortet wird.

Aufgabe

Ersetze die beiden Platzhalter in tests/test_scoreboard.py durch Tests, und behalte die Nachrichtentests, closed_error und make_connection:

  • test_an_outbox_keeps_only_the_newest: Erstelle eine Outbox mit scoreboard_server.make_outbox(), biete ihr drei Snapshots mit verschiedenen Versionen an und sichere zu, dass take den zuletzt angebotenen zurückgibt, dass ein zweites take None zurückgibt und dass das Event der Outbox gelöscht ist.

  • test_a_viewer_leaves_the_registry_whatever_ends_it: Erstelle eine Tafel und einen Stellvertreter mit make_connection, gib dem Stellvertreter ein send, das RuntimeError auslöst, rufe scoreboard_server.viewer_handler innerhalb von try mit except RuntimeError: pass auf und sichere zu, dass board["viewers"] leer ist.

Beende das Gespräch jedes Zuschauers über send oder recv seines Stellvertreters; warte nie auf eine Leerlaufprüfung. Behalte import scoreboard_server und rufe seine in den Tests über das auf, niemals mit from scoreboard_server import.

Führe python -m pytest -q aus, bis jeder Test besteht, und führe python pl_ws.py slow-viewer und python pl_ws.py dead-viewer aus, um v2 bei der Arbeit zu sehen. Klicke dann auf Check my work. Die Prüfung prüft zuerst deinen Server so wie die Lektionen 2 bis 4, mit Stellvertretern und ohne Thread, und prüft, dass scorekeeper_handler jede Wertung ohne Senden an einen Zuschauer beantwortet. Dann liest sie deine Testdatei und führt deine Tests in diesem Prozess aus: einmal mit einem eigenen funktionierenden Server, bei dem mindestens drei Tests gesammelt werden und alle bestehen müssen, und je einmal mit drei Servern, die je einen Fehler enthalten (ein offer, das den ältesten Snapshot behält, ein viewer_handler, das den Zuschauer nur entfernt, wenn seine Verbindung schließt, und ein take, das den Snapshot an seinem Platz lässt), bei denen mindestens ein Test scheitern muss. Ein Lauf, der länger als 2 Sekunden dauert, wird gestoppt; gegen einen kaputten Server zählt das als Bemerken des Fehlers, gegen den funktionierenden wird es gemeldet. Die Prüfung liest dein nicht.