要約

  • HTTPのAgeは、オリジンサーバーがレスポンスを生成または正常に検証してからの秒数をキャッシュが見積もった値であり、URL、文書、表現のバイト列そのものの作成時刻ではない。
  • キャッシュは上流のAgeDate、要求と応答の時刻、転送遅延、ローカル滞在時間を組み合わせ、保存レスポンスを検証せず再利用するときに一つのcurrent_ageで旧値を置き換える。
  • 新鮮かどうかは、現在年齢と、キャッシュ指示子、Expiresまたは許可されたヒューリスティックから得た寿命との比較で決まる。Ageだけでは検証も再利用許可も証明できない。

二分のレスポンスに古い記事が入る

三年前に公開され、その後一字も変わっていない記事を考える。オリジンが二分前に保存済み表現の有効性を確認し、キャッシュがそれを配信したなら、Age: 120は矛盾ではない。数値は記事の成立からの時間ではなく、今回再利用されるレスポンスがオリジンで生成または検証されてからの推定時間を示す。

日常語では「年齢」は対象物の属性に見える。しかしHTTPで年齢を持つのはレスポンスである。URLが指す資源はサーバーより古いかもしれず、同じバイト列は何度も検証されうる。キャッシュエントリーも更新、置換、削除される。Ageはそのどの履歴台帳でもない。

必要だったのは、独立したキャッシュをまたいで再利用判断を続けるための小さな時間状態だった。下流が毎回ゼロから数えれば、レスポンスは中継のたびに若返る。オリジンのDateだけに頼れば、時計のずれが年齢を負数にも過大値にもする。

HTTP/1.0には共通の応答年齢がなかった

1996年のHTTP/1.0仕様はキャッシュ、日付、期限を扱ったが、定義されたレスポンスヘッダーにAgeはなく、上流滞在時間を運ぶ標準計算もなかった。実装はDateExpires、自分の受信時刻を使えたものの、別のキャッシュですでに費やされた時間を一つの共通値として受け取れなかった。

1997年のHTTP/1.1はAgeを導入した。送信するキャッシュが見積もりを載せ、受信側がその値を補正して延長する。初期仕様は、保存レスポンスが正常に再検証された場合、年齢の基準を元の生成時ではなく検証時に置くと明記した。最初からこのフィールドは資料の年代測定ではなく、再利用の時間管理だった。

中央の時計局は作られなかった。すべてのキャッシュログを一か所に集めることも要求されない。プロトコルは継承可能な値と計算規則だけを共有し、各実装にローカルな計測と判断の責任を残した。

二つの不完全な見方を合わせる

レスポンスを受け取ったキャッシュは、初期年齢を二方向から求められる。一つは受信時刻とオリジンのDateとの差である。

apparent_age = max(0, response_time - date_value)

オリジン側の時計が進んでいると差は負になるため、ゼロを下限にする。この表見年齢は分かりやすいが、二台の時計が十分に合っていることに依存する。

もう一つは、上流から届いたAgeに今回の要求応答遅延を加える方法だ。

response_delay = response_time - request_time

corrected_age_value = age_value + response_delay

こちらは同じローカル時計の経過時間を使える。しかし上流の全キャッシュがAgeを正しく挿入しなければ過小評価になる。古い実装や欠陥のある実装が残る経路では、その可能性を無視できない。

保守的な互換計算は大きい方を採る。

corrected_initial_age = max(apparent_age, corrected_age_value)

現行仕様は、Ageを入れない古いキャッシュを心配しなくてよい場合には、修正年齢値をそのまま使うことも認める。重要なのは、どちらかの遠隔時計に絶対的な権威を与えず、見える証拠からローカルコードが再現可能な結果を出すことだった。

滞在時間は滞在した場所でしか測れない

保存後に何秒待つかは、上流には予測できない。次の要求が来た時点で、そのキャッシュが自分の滞在時間を加える。

resident_time = now - response_time

current_age = corrected_initial_age + resident_time

検証なしで保存レスポンスを使うとき、キャッシュは受信したAgeを保存したまま横流ししない。現在年齢で置き換えた一つのAgeを生成する。次のキャッシュはその総数を継承し、自分が観測した転送遅延と滞在をさらに加える。

この方法は経過時間を残す一方で、経路情報を捨てる。長く保存した二つのキャッシュと短く保存した十のキャッシュは同じ数値を出しうる。最後の値からキャッシュ数、名前、保管順、受領証を復元することはできない。

情報を狭くしたからこそ、下流は上流の運用台帳を入手せずに計算を継続できた。ただし事故調査や責任分担には別の記録が必要になる。共有フィールドが運ぶものと、組織が保存すべき証拠は同じではない。

年齢と新鮮さの寿命は別の変数だった

Age: 120は「あと120秒新鮮」でも「120秒間新鮮だった」でもない。年齢はすでに経過した推定時間であり、freshness lifetimeは検証なしの再利用を認める時間幅である。

判定は両者を比較する。

response_is_fresh = (freshness_lifetime > current_age)

寿命は共有キャッシュ向けのs-maxagemax-ageExpiresDateの差、または許可されたヒューリスティックから得られる。同じ120秒でも、寿命300秒ならfresh、60秒ならstaleになる。

staleは内容が偽、危険、古文書だという評価ではない。通常の新鮮さ比較を通らなかったという状態である。検証によって再利用可能になり、指示子や切断時の条件によってstaleな応答を使える場合もある。逆に若い応答でも、保存禁止や要求条件、誤ったバリアント選択があれば使えない。

検証後の若さは内容の若さではない

キャッシュが一日保存した表現について条件付き要求を送り、オリジンが有効性を確認したとする。同じバイト列を使い続けても、レスポンス年齢の基準は直近の成功した検証に置ける。記事の公開日や変更史を消したのではなく、再利用に対するオリジンの確認時点を更新したのである。

したがって、検証後に小さくなったAgeは内容更新の証拠ではない。大きいAgeもオリジン障害の証拠ではない。このフィールドにはETag照合、検証ステータス、更新されたメタデータが入っていない。それらは別に保存しなければならない。

現行HTTPが認める推論はさらに狭い。Ageがあれば、そのレスポンスはオリジンから直接届いたfirst-hand responseではなく、キャッシュが保存状態から生成したと分かる。ただし、そのキャッシュが今回の要求中にオリジンへ検証したのか、以前の検証結果に依拠したのかは、このフィールドだけでは分からない。Ageがないからオリジンに接触したとも言えない。古いキャッシュ、非準拠実装、観測時の欠落がありうる。

巨大値は飽和であって年代記ではない

Ageは秒単位の非負整数である。不正な値はキャッシュが無視すべき対象になる。扱える範囲を超える値や演算のオーバーフローは、歴史的な2147483648、または実装が便利に表現できる最大正整数として扱う。

この約68年超の値は実質的な無限大を安全に示し、オーバーフローで負数や小さな数に戻るのを防ぐためのものだ。レスポンスが正確にその秒数だけ保存されたという観測記録ではない。

説明可能性には式全体が要る

運用記録には最終Ageだけでなく、完全なキャッシュキーと選択バリアント、受信したDateAge、同じ時計領域の要求・応答・判断時刻、各中間計算、freshness lifetimeの根拠を残す必要がある。検証の有無、返った状態とバリデータ、fresh再利用、検証後再利用、許可されたstale再利用、失敗のどれだったかも必要だ。

最後の数値だけでは、上流滞在、転送遅延、ローカル滞在、時計差を分離できない。HTTPは中央裁定機関を設けなかった代わりに、各参加者が自分の入力と判断を説明できる構造を残した。共通規則は協調のためにあり、オブジェクトの履歴を所有するためにはなかった。

情報源