Ruby
api
RSS

ETag等の比較で更新をチェックしてリソースを取得したい

More than 3 years have passed since last update.

追記:injectメソッドで書き直した

レスポンスヘッダでETagやLast-Modifiedを提供しているリソースへアクセスするとき、最終更新時と比較しつつ、定期的に更新をチェックするようなプログラムを書きたいときがあるだろう。ETagであれば、どこかに直前のETagを保持しておいて、次のリクエストヘッダに含めてGETするようなループを書けば良さそうだ、というところまでは思い当たる。しかし、「キモーイ」「変数に状態を持たせておいて、それをループの中で何度も書き換えたりするのが許されるのは小学生までだよね」「キャハハハハハ」と言われてしまったら……一体どのようにこれを実現すればいいのだろうか。

最終的にやりたいことをRubyで書くと、以下のようになる。

c = Crawler.new

c.start do |response|
# 更新があるごとに何かする
end

これなら、startメソッドの中にイイカンジでyield的なものを書けば行けるだろうと思ったのだけど、ETagを次の処理に渡すのがなかなかうまくできなかった。FIFOっぽいなと思って、結局Queueを使って書いた。

require "logger"

require "open-uri"
require "thread"

$logger = Logger.new($stderr)
$logger.level = Logger::DEBUG

class Crawler
def initialize(interval)
@queue = Queue.new
@interval = interval
end

def start(&block)
t = Thread.start(block) do |block|
while connection = @queue.shift
response = connection.start
block.yield(response)
sleep @interval
@queue << Connection.new(@interval, response.meta["etag"])
end
end
@queue << Connection.new(@interval)
t.join
end
end

class Connection
ENDPOINT = URI("https://example.com/index.rss")
OPTION = {
read_timeout: 5,
}

def initialize(interval, etag = "")
@interval = interval
@etag = etag
end

def start
open(ENDPOINT, OPTION.merge("If-None-Match" => @etag))
rescue OpenURI::HTTPError
sleep @interval
retry
rescue => e
$logger.warn("%s: %s" % [e.class, e.message])
sleep @interval
retry
end
end

うーん。スレッドの処理をもう少しコンパクトに書きたいところ。ロガーの書き方も、もうちょっと考えましょう。

以下のようなコードを書けば、3秒おきにリソースにアクセスして、更新があったときに本体を取得できる。このコード自体は、起動すると、更新があるごとに新しいETagを延々とログに吐く。

c = Crawler.new(3)

c.start do |response|
$logger.info(response.meta["etag"])
end