Caching und bedingte Anfragen · Abschlussprojekt
Abschlussprojekt: Projekt: Eine Leseliste, die Caching versteht
Beide Hälften des 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
GEToderHEADauf/books(mit oder ohne?limit=) und/books/{id}trägtetagundcache-control: max-age=60, beiHEADdasselbe wie beiGET.Ein passendes
if-none-matchbekommt 304 mit demselbenetagundcache-control, ohne Body und ohnecontent-length.Jede Antwort für
/login,/logoutund/meträgtcache-control: no-store.cached_getverwendet eine gespeicherte Kopie wieder, solangenowkleiner als ihr Ablaufzeitpunkt ist, revalidiert eine veraltete, startet den Timer mit einer 304 neu und ersetzt den Eintrag bei einer 200.Eine mit
no-storegekennzeichnete Antwort wird nie gespeichert. Eine mitno-cachegekennzeichnete 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 fakes.py.
Was bewiesen wird, und wie
Check my work prüft jede Hälfte für sich mit 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-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 Direktiveno-storesetzt"store"aufFalse; eine Direktiveno-cachesetzt"max_age"auf 0, egal welchesmax-agedie Antwort außerdem trägt. Direktivennamen ignorieren Groß- und Kleinschreibung, und jede Zeile zählt.cached_getspeichert eine Antwort nur, wenn sie eine 200 mit genau einemetagist undcache_rules"store": Truesagt. Alles aus Lektion 5 bleibt.In
handleträgt jede Antwort für/login,/logoutund/mecache-control: no-store.Deine Tests in
tests/müssen mitpython -m pytest -qbestehen, und keiner darfsocketimportieren.
Führ python pl_http.py cache und python pl_http.py who im
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 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.