本日は既存のバグを利用したい気分なの(httplib2 の min-fresh の扱い)

「作りかけのスクレイパは必要以上に何度もアクセスしちゃうんだよなぁ」とか、あるいは「少しくらいなら廃れててもいいからキャッシュ優先したい」な話。

あんまし WEB プログラミングしない人なんで、みたいな言い訳はもういいか。

最近色々スクレイパ的な処理を良く書くわけな。そうするとさ、スクリプトに間違いがあると頻繁にやり直すわけね。1万を超えるようなクエリを繰り返した後にようやく判明するようなミスなんか悲劇的。数時間が無駄になったりもする。

もともとそのために httplib2 (@PyPI) を使ってるわけですわ。この子はブラウザみたくローカルキャッシュを使ってくれるのでね。だけれども、「数時間前にお取り寄せ」済みのものは、期限切れになってる可能性が高い。あぁ…。これでは httplib2 でもキャッシュを使ってくれんわ、と。

どうしたもんかなぁと探ってたが httplib2、キャッシュコントロールの扱いは(完全版ではないが)かなりちゃんとしてて、headers でコントロール出来ることがわかった。まず一番簡単な「あれば必ずキャッシュを使えやゴルァ」はこれ:

1 # -*- coding: utf-8-unix -*-
2 import httplib2
3 
4 http = httplib2.Http(".dokozo_onushino_cache_dir")
5 headers = {"Cache-Control": "only-if-cached"}
6 headers, contents = http.request(url, "GET", headers=headers)

これは RFC 7234 に書かれてる仕様ね。これについてはどこみても書いてあると思う。

よしよし、と。これは「作りかけのスクレイパは必要以上に何度もアクセスしちゃうんだよなぁ」問題の解決にはなる。

けどな。ちょっと違ったニーズもあるのです、アタシには。つまりね:

  • ちょっとした代理 WEB サービスを書かねばならぬ、とする。
  • 私サービス(A)はリクエストを受けると本家サービス(B)に問い合わせる、とする。
  • つまり A サービスとしての応答性能をそこそこ担保しつつ、B からの情報は出来るだけフレッシュでありたい
  • そうはいっても本家サービスに完全に同期する必要はなく、多少の情報の遅延は許容したい、A としての応答性能の方が大事だ
  • (そもそも A サービスをホストするサーバの負荷もあまり大きくしたくはない。)

ちなみにこれは「NOAA (アメリカ海洋大気庁)」から METAR (定時飛行場実況気象通報式) をお取り寄せて解析して見せる、というサービスね、例えば。(実際に夢想してるヤツ。)

実は調べながら個人的には少々混乱したんだけれども、答えは「max-age」を弄ぶことね:

1 # -*- coding: utf-8-unix -*-
2 import httplib2
3 
4 http = httplib2.Http(".dokozo_onushino_cache_dir")
5 headers = {"Cache-Control": "max-age=5000"}
6 headers, contents = http.request(url, "GET", headers=headers)

こうしとけば、「少々なら古くてもオッケー」な制御になる。(クライアント目線で「情報が新しいかどうかはオレが決める」つぅわけである。)






話としてここまでで終われば、「あーよかったね」で済む話なんだけれど、「max-age」に気付く前に目に飛び込んでしまった「min-fresh」に関してちょっとあってさ。

無論「永遠の WEB アプリ初心者」なワタシにはお初だったんだけど、これも RFC 7234 にある仕様ね。なお、「max-stale」など、 httplib2 の設計決断であえて実装してないものがある。ともあれ min-fresh の仕様はこう:

5.2.1.3. min-fresh

Argument syntax:

delta-seconds (see Section 1.2.1)

The “min-fresh” request directive indicates that the client is
willing to accept a response whose freshness lifetime is no less than
its current age plus the specified time in seconds
. That is, the
client wants a response that will still be fresh for at least the
specified number of seconds.

This directive uses the token form of the argument syntax: e.g.,
‘min-fresh=20′ not ‘min-fresh=”20″‘. A sender SHOULD NOT generate
the quoted-string form.

この手のってたいがい頭混乱するけど、要するに「情報を新鮮とみなす期間(freshness_lifetime)を短くしたい」、という、つまり「せっかちクライアント向け」制御なわけね。あ、この説明がわかりやすい。まぁここまではいいよね。問題は Section 1.2.1。

1.2.1. Delta Seconds

The delta-seconds rule specifies a non-negative integer, representing
time in seconds.

1      delta-seconds  = 1*DIGIT

A recipient parsing a delta-seconds value and converting it to binary
form ought to use an arithmetic type of at least 31 bits of
non-negative integer range. If a cache receives a delta-seconds
value greater than the greatest integer it can represent, or if any
of its subsequent calculations overflows, the cache MUST consider the
value to be either 2147483648 (2^31) or the greatest positive integer
it can conveniently represent.

つまりこれは不正なの:

1 # -*- coding: utf-8-unix -*-
2 import httplib2
3 
4 http = httplib2.Http(".dokozo_onushino_cache_dir")
5 headers = {"Cache-Control": "min-fresh=-5000"}  # negative
6 headers, contents = http.request(url, "GET", headers=headers)

けど httplib2 はこうしてるだけ:

__init__.py から抜粋
 1 def _entry_disposition(response_headers, request_headers):
 2     # ...
 3         # ...
 4         date = calendar.timegm(email.Utils.parsedate_tz(response_headers['date']))
 5         now = time.time()
 6         current_age = max(0, now - date)
 7         # ...
 8         if cc.has_key('min-fresh'):
 9             try:
10                 min_fresh = int(cc['min-fresh'])
11             except ValueError:
12                 min_fresh = 0
13             current_age += min_fresh
14         if freshness_lifetime > current_age:
15             retval = "FRESH"

「負はダメ」というチェックはしてない。まさに「多少の古さなら “FRESH” 扱いしとくれ」なのだから、current_age を小さくしたい、てのがワタシのニーズな。なので「負にしたい」わけだ。けど RFC 7234 の仕様には反する。けど httplib2 では出来る。無論 max-age を使うのが正解だけれど。







ちなみにもっと強引な制御も可能。つまり「時計をごまかす」。now = time.time() 部分を書き換えてしまえばいい。デバッグ目的であるとか、特定のプログラムでしか使わないつもりならそういう書き換えもあってもいい。無論その場合は httplib2 は「抱え込み」ね。