追記: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