0%

Caching und bedingte Anfragen · Abschlussprojekt

Abschlussprojekt: Projekt: Eine Leseliste, die Caching versteht

Beide Hälften des -Cachings sind vorhanden. Dein Server versieht jeden Lesezugriff mit einem Tag, sagt max-age=60 und beantwortet ein Tag, das er erkennt, mit einer bodylosen 304. Dein Client verwendet eine frische Kopie ohne Nachfrage wieder, revalidiert eine veraltete mit If-None-Match und startet seinen Timer mit der 304 neu. Dieses Projekt schließt die eine verbliebene Lücke, bei der es um Privatsphäre geht statt um Geschwindigkeit, und beweist dann, dass die beiden Hälften übereinstimmen.

Anforderungen

SERVICE_CONTRACT.md sagt bereits alles, in seinem Abschnitt „Caching“ und in „The client in wire.py“:

  • Jede 200 auf GET oder HEAD auf /books (mit oder ohne ?limit=) und /books/{id} trägt etag und cache-control: max-age=60, bei HEAD dasselbe wie bei GET.

  • Ein passendes if-none-match bekommt 304 mit demselben etag und cache-control, ohne Body und ohne content-length.

  • Jede Antwort für /login, /logout und /me trägt cache-control: no-store.

  • cached_get verwendet eine gespeicherte Kopie wieder, solange now kleiner als ihr Ablaufzeitpunkt ist, revalidiert eine veraltete, startet den Timer mit einer 304 neu und ersetzt den Eintrag bei einer 200.

  • Eine mit no-store gekennzeichnete Antwort wird nie gespeichert. Eine mit no-cache gekennzeichnete Antwort wird gespeichert und jedes Mal revalidiert.

Neu sind no-store auf drei Routen und die letzte Zeile; der Rest sind die Lektionen 3 bis 5, noch einmal geprüft.

Nicht alles sollte aufbewahrt werden

GET /me antwortet {"user": "ana"} oder {"user": null}: wer auf diesem Client gerade angemeldet ist. Das ändert sich bei jeder An- und Abmeldung und gehört einer Person, also sollte nirgends eine Kopie davon liegen. Heute bewahrt dein Client von /me nur deshalb nichts auf, weil die Antwort kein Tag hat: ein Nebeneffekt, keine Entscheidung. Eine spätere Änderung, die jede 200 mit einem Tag versieht, würde deinen Client stillschweigend eine Anmeldung behalten lassen, nachdem sich der Benutzer abgemeldet hat. Bei einem gemeinsamen Cache wäre es schlimmer: Dem nächsten Benutzer könnte der Name des vorherigen angezeigt werden.

Also sagt der Server es offen, mit der Direktive aus Lektion 1, die „nicht aufbewahren“ bedeutet: cache-control: no-store, auf jeder Antwort für /login, /logout und /me, die Ablehnungen eingeschlossen. Und der Client gehorcht ihr, egal was die Antwort sonst trägt.

Gleichzeitig lernt der Client no-cache. Ein Server sendet es für eine Antwort, die aufbewahrt werden darf, aber vor jeder Verwendung geprüft werden muss, zum Beispiel eine Seite, die sich oft ändert und wenig kostet, sie zu revalidieren. In cache_rules ist das max_age 0: Der Ablaufzeitpunkt des Eintrags ist der Moment, in dem er gespeichert wurde, also findet ihn der nächste Aufruf veraltet und fragt mit seinem Tag. no-cache schlägt jedes max-age in derselben Antwort.

Meilensteine

Zuerst cache_rules, Stufe 2. Eine Direktive no-store setzt "store" auf False. Eine Direktive no-cache setzt "max_age" auf 0, egal wo sie unter den Direktiven steht, vor oder nach einem max-age. Direktivennamen ignorieren weiterhin Groß- und Kleinschreibung. Sorg dann dafür, dass cached_get eine Antwort nur speichert, wenn cache_rules "store": True sagt und sie außerdem eine 200 mit einem etag ist.

Dann der Server. Setz in handle, nachdem die Route ihre Antwort erzeugt hat, cache-control auf der Antwort auf no-store, wenn der Pfad /login, /logout oder /me ist. Das Szenario who aus Kapitel 7 zeigt die neue Zeile: Führ python pl_http.py who aus, und unter jeder Antwort für /login, /me und /logout gibt das Hilfsprogramm cache-control: no-store aus.

Dann sieh dir den Wendepunkt an. Führ aus:

python pl_http.py cache

Das ist das Kapitel in drei Aufrufen: Bei 0 bringt ein GET den Body, bei 30 wird überhaupt nichts gesendet, und bei 61 bringt ein GET mit dem Tag eine 304 ohne Body zurück, also zeigt requests 1, 0 und 1. Die letzten beiden Aufrufe sind /me, und jeder sendet eine Anfrage:

--- your cached_get() at now 0 for /me ---
source: 'network'

Optional: Teste den Cache selbst. fakes.py hat scripted_response, und scripted_connections aus dem Projekt von Kapitel 6. Füge scripted_connections der Zeile from fakes import in tests/test_wire.py hinzu, und einen Test wie diesen:

def test_a_fresh_copy_needs_no_request():
    calls = []
    reply = scripted_response(200, "OK", [["etag", '"v1"'], ["cache-control", "max-age=60"]], b"[]")
    connections = scripted_connections([reply], calls)
    cache = {}
    wire.cached_get("http://books.test/books", cache, now=0, make_connection=connections)
    record = wire.cached_get("http://books.test/books", cache, now=59, make_connection=connections)
    assert record["source"] == "cache"
    assert len(calls) == 1

Der zweite Aufruf stellt keine Verbindung her, also reicht eine vorbereitete Antwort. Ein Test für no-store folgt demselben Muster: zwei Aufrufe, zwei Verbindungen. Führ dann im Projektordner alle Tests aus:

python -m pytest -q

Check my work führt deine Tests einmal aus, also müssen sie alle bestehen, und keiner darf einen brauchen: Verwende die Ersatzobjekte in fakes.py.

Was bewiesen wird, und wie

Check my work prüft jede Hälfte für sich mit , die du noch nicht gesehen hast, und verbindet dann dein cached_get in einem Prozess mit deinem eigenen reading_server.handle, wie es das Projekt von Kapitel 6 getan hat. Jede Verbindung, die dein Client öffnet, baut das Anfrage-, das http_adapter bauen würde, führt dein read_body und handle aus und verwandelt dein Antwort-Dictionary zurück in eine Antwort. Es behält außerdem jedes Antwort-Dictionary, damit eine 304 mit Body auffällt. Bei 0, 30 und 61 muss dein Server zwei Anfragen sehen, die zweite mit deinem Tag und mit einer bodylosen 304 beantwortet, und alle drei Ergebnisse müssen denselben Body enthalten. Ein PUT durch dein handle zwischen zwei Aufrufen muss dafür sorgen, dass das nächste veraltete GET den neuen Body bringt, und /me muss jedes Mal gefragt und nie gespeichert werden. Der Lauf cache zeigt dieselbe Übereinstimmung über eine echte Verbindung; er ist zum Zuschauen da und wird nicht bewertet.

Drück dann Check my work.

Jeder Austausch hat bisher eine Verbindung geöffnet, eine Anfrage gesendet, eine Antwort gelesen und sie geschlossen. Das ist die Art von HTTP/1.0, und dein Server spricht sie immer noch. Kapitel 9 stellt den Server auf HTTP/1.1 um, wo eine Verbindung viele Anfragen trägt, und bringt deinem Client bei, zu erkennen, wo jede Antwort endet.

Aufgabe

Stell die Leseliste fertig, die Caching versteht: no-store und no-cache in wire.py und no-store auf drei Routen in reading_server.py.

  • cache_rules(values): Eine Direktive no-store setzt "store" auf False; eine Direktive no-cache setzt "max_age" auf 0, egal welches max-age die Antwort außerdem trägt. Direktivennamen ignorieren Groß- und Kleinschreibung, und jede Zeile zählt.

  • cached_get speichert eine Antwort nur, wenn sie eine 200 mit genau einem etag ist und cache_rules "store": True sagt. Alles aus Lektion 5 bleibt.

  • In handle trägt jede Antwort für /login, /logout und /me cache-control: no-store.

  • Deine Tests in tests/ müssen mit python -m pytest -q bestehen, und keiner darf socket importieren.

Führ python pl_http.py cache und python pl_http.py who im aus, um beide Hälften auf einer echten Verbindung zu beobachten.

Drück dann Check my work. Es prüft den Server mit einem Zustand, den du noch nicht gesehen hast: Tags auf GET und HEAD, ein Tag, das sich nach einem PUT ändert, eine bodylose 304, die etag und cache-control wiederholt, und no-store auf POST /login, GET und HEAD /me und POST /logout. Es ruft cache_rules mit no-store, no-cache, max-age=60, max-age=60, no-store, private, max-age=5 und anderen auf, und cached_get mit vorbereiteten Antworten für Frische, die Grenze bei genau max-age, den Timer, den eine 304 neu startet, no-store und no-cache. Dann verbindet es dein cached_get in einem Prozess mit deinem eigenen handle: Bei 0, 30 und 61 erreichen zwei Anfragen deinen Server, und die zweite wird mit 304 ohne Body beantwortet; ein PUT zwischen den Aufrufen bringt den neuen Body; /me wird jedes Mal gefragt und nie gespeichert. Zuletzt führt es deine Tests einmal aus und bricht einen Lauf ab, der länger als 3 Sekunden dauert. Nichts aus deinem Terminal wird bewertet.