0%

Caching en voorwaardelijke verzoeken · oefening

Een client die onthoudt

Je server zet nu een tag op elke leesactie, zegt hoe lang die mag worden hergebruikt, en beantwoordt een overeenkomende If-None-Match met een 304 zonder body. Deze les geeft wire.py een kleine cache: cached_get(url, cache, *, now, make_connection, timeout), een GET die onthoudt.

Beslissen of een opgeslagen antwoord nog vers is, vereist de huidige tijd, een probleem dat je al eerder hebt opgelost. In het eindproject van Python in Practice, hoofdstuk 13, les 2, nam cache_is_stale now_epoch als in plaats van time.time() aan te roepen, zodat een test er elk gewenst moment aan kon geven. cached_get doet hetzelfde: de aanroeper geeft now mee als een aantal seconden, en niets in wire.py leest een klok.

Vers, verlopen, weg

cache is een waarvan de aanroeper de eigenaar is en die hij bij elke aanroep meegeeft, eerst leeg. cached_get houdt per één entry bij, onder de URL:

cache[url] = {"etag": tag, "expires": now + max_age, "record": record}

record is het record dat request_once voor de 200 teruggaf (hoofdstuk 6, les 4): "status", "reason", "headers", "body" en "url". etag is de tag van het antwoord, bewaard om later mee te vragen. expires is het moment waarop de kopie verloopt: het tijdstip waarop hij werd opgeslagen plus de max-age van het antwoord.

Elke aanroep valt in een van drie situaties, en het record dat cached_get teruggeeft, noemt die in een nieuwe sleutel, "source":

  • Niets opgeslagen: stuur een gewone GET, sla het antwoord op als het kan worden opgeslagen, en geef het terug met "source": "network".

  • Vers: stuur niets. Geef een kopie van het opgeslagen record terug met "source": "cache".

  • Verlopen: vraag het de server met If-None-Match en de opgeslagen tag. Een 304 betekent dat de kopie nog klopt: geef hem terug met "source": "revalidated". Een 200 betekent dat hij veranderd is: sla de nieuwe op en geef die terug, met "source": "network".

Tegen je server, waarvan de antwoorden max-age=60 dragen, gaan drie aanroepen zo:

Op 0 wordt het antwoord opgehaald en opgeslagen, op 30 komt het uit de cache, op 61 vernieuwt een 304 het.

Op 0 wordt de 200 opgeslagen, met tag T en max-age=60, verlopen vanaf 60. Op 30 wordt er niets verstuurd. Op 61 krijgt If-None-Match: T een 304 met max-age=60, vers tot 121. Drie aanroepen, twee verzoeken, één body op de verbinding.

De grens is die van HTTP

Wanneer precies verloopt een kopie? RFC 9111 (sectie 4.2) geeft een formule: een antwoord is vers als zijn versheidsduur groter is dan zijn huidige leeftijd. Hier is de duur max-age en de leeftijd now min het tijdstip waarop hij werd opgeslagen. Op het moment dat de leeftijd max-age bereikt, is “groter dan” onwaar, dus de kopie is verlopen. Met expires als opslagtijd plus max-age is de test now < entry["expires"]: vers vóór dat moment, verlopen vanaf dat moment.

Dat is niet de grens die Python in Practice trok. De bestandscache daar was een eigen contract, en dat zei dat een leeftijd gelijk aan de limiet nog als vers telde, dus vergeleek hij age > max_age. Beide zijn redelijke keuzes voor een cache die je zelf ontwerpt. Hier zijn de regels die van , omdat de server die max-age=60 stuurt, die in de zin van HTTP bedoelt, dus een kopie die op 0 is opgeslagen, is op precies 60 verlopen.

Een antwoord dat op 100 is opgeslagen, draagt max-age=60. Wat drukt dit af?

stored_at = 100
max_age = 60
for now in [130, 160]:
    print(now < stored_at + max_age)

Een antwoord dat op 100 is opgeslagen, heeft max-age=60. cached_get wordt aangeroepen met now=160. Wat doet hij?

Opnieuw valideren

Een verlopen entry wordt niet weggegooid, omdat de tag ervan de download nog kan besparen. cached_get stuurt GET met één header, {"If-None-Match": entry["etag"]}, via request_once, en geeft de make_connection en timeout die hij kreeg door, zoals follow deed. Daarna leest hij de cache-control van het antwoord met cache_rules.

Bij een 304 is het opgeslagen record het antwoord, en de 304 bepaalt hoe lang het vers blijft: de expires van de entry wordt now plus de eigen max-age van de 304. Daarom herhaalt je server cache-control op een 304. Een 304 zonder cache-control zou max_age 0 geven, dus deze client vraagt het de volgende keer gewoon opnieuw. De body van een 304 is leeg, dus het eigen record van de 304 teruggeven zou de aanroeper niets geven; geef in plaats daarvan een kopie van het opgeslagen record terug.

Bij een 200 vervangt de nieuwe versie de entry, zoals elk antwoord van het netwerk.

Een antwoord wordt alleen opgeslagen als het een 200 is met precies één etag, dus een opgeslagen entry heeft altijd een tag om mee te vragen. Een 404, of een 200 zonder tag, wordt teruggegeven en niet opgeslagen. Dat is een vereenvoudiging die deze client maakt: HTTP zou een cache ook een 200 met max-age en zonder tag laten bewaren en hergebruiken zolang die vers is.

Geef kopieën terug: "source", of een andere sleutel op het hoogste niveau, zetten op entry["record"].copy() laat het opgeslagen record zoals het was. De kopie is wel ondiep: de geneste dict headers wordt nog met de cache gedeeld.

Op 160 komt er een 304 binnen met cache-control: max-age=30. Wanneer stuurt cached_get zijn volgende verzoek voor die URL?

Echte caches houden meer bij: Age, hoe lang een antwoord al in een andere cache heeft gestaan; Vary, voor antwoorden die per verschillen; en een geschatte versheid als er geen max-age is. Deze cache leeft in een dict voor één run; het eindproject bewaart er een in een bestand.

Jouw opdracht

Schrijf in wire.py twee .

cache_rules(values) neemt de met cache-control- van een antwoord en geeft {"store": True, "max_age": seconds} terug. Splits elke waarde bij komma’s, strip elk directief en zet het in kleine letters, en als er een met max-age= begint, lees je het getal erna. Alleen ASCII-cijfers tellen, zoals in read_body: controleer isascii() en isdigit() vóór int(). Zonder bruikbare max-age is max_age 0. store is voorlopig altijd True; les 6 voegt de directieven toe die dat veranderen.

cached_get(url, cache, *, now, make_connection=http.client.HTTPConnection, timeout=TIMEOUT) volgt de drie situaties hierboven, zonder klok.

Voer daarna uit:

python pl_http.py cache

Het hulpprogramma roept je cached_get tegen je eigen server aan op 0, 30 en 61 voor /books, en daarna twee keer voor /me, en drukt de bron van elk record af en hoeveel verzoeken de server bereikten. Op 30 zou je dit moeten zien:

--- your cached_get() at now 30 for /books ---
source: 'cache'
status: 200

en verderop requests: 0 voor die aanroep. Op 61 gaat er één verzoek met de tag de deur uit:

request 1  GET /books  (If-None-Match: "ba811c01801a2c40d5e507c3c439e7e966380095a4001999178055355cebbec1")  ->  304 Not Modified
--- your cached_get() at now 61 for /books ---
source: 'revalidated'

/me wordt twee keer gevraagd, omdat het antwoord geen tag heeft. Klik daarna op Check my work.

Je client zou ook een antwoord bewaren dat noemt wie er is ingelogd, als dat met een tag kwam. Les 6 zorgt ervoor dat dat nooit gebeurt.

Opdracht

Schrijf cache_rules(values) en cached_get(url, cache, *, now, make_connection, timeout) in wire.py. Importeer nergens time en lees nergens een klok: elke tijd komt uit now.

  • cache_rules(values) geeft {"store": True, "max_age": seconds} terug. Splits elke bij komma’s, strip elk directief en zet het in kleine letters; bij max-age=N met N bestaande uit ASCII-cijfers is max_age N. Anders is het 0.

  • cached_get houdt per één entry bij in cache: {"etag": tag, "expires": now + max_age, "record": record}.

    • Vers (now is kleiner dan de expires van de entry): stuur niets; geef een kopie van het opgeslagen record terug met "source": "cache".

    • Verlopen: stuur GET met If-None-Match op de opgeslagen tag, via request_once. Bij een 304 zet je de expires van de entry op now plus de max-age van de 304, en geef je een kopie van het opgeslagen record terug met "source": "revalidated".

    • Anders, en als er niets is opgeslagen, krijgt het record van het antwoord "source": "network". Sla het op als het een 200 is met precies één etag; sla anders niets op.

    • Geef make_connection en timeout door aan request_once.

Voer python pl_http.py cache uit in de om je cache tegen je eigen server aan het werk te zien.

Klik daarna op Check my work. Die roept cache_rules aan met waarden die je nog niet hebt gezien, waaronder Max-Age, twee regels, max-age=abc en max-age=². Hij roept cached_get aan met een vervanger voor make_connection waarvan de verbindingen met vooraf vastgelegde antwoorden reageren, op tijdstippen 1000 en 86400 met max-age 45 en 7, op hosts en die je nog niet hebt gezien. Hij controleert het verzoek dat elke aanroep stuurt (geen als het vers is, een gewone GET als er niets is opgeslagen, If-None-Match met de opgeslagen tag als het verlopen is), het record en zijn "source", dat de entry verlopen is op precies het tijdstip van opslaan plus max-age, dat een 304 de timer opnieuw start met zijn eigen max-age en de opgeslagen body teruggeeft, dat een 200 de entry vervangt, dat een 404, een 200 zonder tag en een 200 met twee tags niet worden opgeslagen, dat het veranderen van een teruggegeven record de cache niet verandert, dat elke verbinding wordt gesloten en de time-out wordt doorgegeven, en dat wire.py geen time importeert. Niets uit je terminal wordt beoordeeld.