Mechanize の hook 処理について

RubyでScrapeする時はmechanizeを使って書いているのですが、ふと毎回入れていた sleep 処理を hook で対応できないかと調べてみたところ、以下の記事を見つけました。

Regulating / rate limiting ruby mechanize - Stack Overflow  
なるほど、 history_added 若干名前から想像つきにくいpropertyが存在するようです。

ソース眺めてみた

ググると他にもありそうなのでソースを眺めてみたところ、 gems/mechanize-2.7.5/lib/mechanize.rb にhook処理はまとめられていました。

content_encoding_hooks

  # A list of hooks to call before reading response header 'content-encoding'.
  #
  # The hook is called with the agent making the request, the URI of the
  # request, the response an IO containing the response body.

content-encoding を読み込む前に呼ばれるhook処理。
content-encoding の書き換えに利用できる。
Problems with text/csv Content-Encoding = UTF-8 in Ruby Mechanize - Stack Overflow

history_added

mechanizeは内部的にhistoryの管理を行っており、historyに追加する際に呼ばれるhook。

Callback which is invoked with the page that was added to history.

attr_accessor :history_added

post_connect_hooks

response取得後に呼ばれるhook処理。

  # A list of hooks to call after retrieving a response. Hooks are called with
  # the agent, the URI, the response, and the response body.

pre_connect_hooks

response取得前に呼ばれるhook処理。

  # A list of hooks to call before retrieving a response. Hooks are called
  # with the agent, the URI, the response, and the response body.

どれ使おうか

一見、history_added より post_connect_hooks の方が名前的にも良さそうなのですが、
redirect時の挙動が変わってきます。
1回redirectされるurlへのrequestだと、history_added は1度呼ばれますが、post_connect_hooks は2度呼ばれてしまいます。
redirect時もhookしたい場合は、post_connect_hooks、それ以外は history_added を使うのが良さそうです。

Simple or trump(CodeEval)

簡易版大富豪の実装。
Scoreが80%ぐらいで、何かの考慮漏れがありそうなんだけど、よくわからないので一旦諦め。

CHALLENGE DESCRIPTION:

First playing cards were invented in Eastern Asia and then spread all over the world taking different forms and appearance. In India, playing cards were round, and they were called Ganjifa. In medieval Japan, there was a popular Uta-garuta game, in which shell mussels were used instead of playing cards.
In our game, we use playing cards that are more familiar nowadays. The rules are also simple: an ace beats a deuce (2) unless it is a trump deuce.

INPUT SAMPLE:

The first argument is a path to a file. Each line includes a test case which contains two playing cards and a trump suit. Cards and a trump suite are separated by a pipeline (|). The card deck includes thirteen ranks (from a deuce to an ace) of each of the four suits: clubs (♣), diamonds (♦), hearts (♥), and spades (♠). There are no Jokers.

AD 2H | H
KD KH | C
JH 10S | C

OUTPUT SAMPLE:

Your task is to print a card that wins. If there is no trump card, then the higher card wins. If the cards are equal, then print both cards.

2H
KD KH
JH

CONSTRAINTS:

  1. The card deck includes ranks from a deuce (2) to an ace, no Jokers.
  2. If the cards are equal, then print both cards.
  3. The number of test cases is 40.

My Code

#!/usr/bin/env ruby -w
class Card
  TRUMP_DUCE_RANK = 15
  SIGN_CARD_RANK = {
    "J" => 11,
    "Q" => 12,
    "K" => 13,
    "A" => 14
  }
  attr_reader :rank

  def initialize(card, trump)
    @card = card
    @trump = trump
    @rank = card_to_rank(card, trump)
  end

  def to_s
    @card
  end

  private
    def card_to_rank(card, trump)
      number = card[0...-1]
      rank = SIGN_CARD_RANK[number] || number.to_i
      rank = TRUMP_DUCE_RANK if rank == 2 && card[-1] == trump
      rank
    end
end

def game(cards, trump)
  x, y = cards.map { |card| Card.new(card, trump) }
  if x.rank > y.rank
    x.to_s
  elsif x.rank == y.rank
    "#{x} #{y}"
  else
    y.to_s
  end
end

ARGF.each_line do |line|
  cards_str, trump = line.chomp.split(" | ")
  cards = cards_str.split
  puts game(cards, trump)
end

ActiveRecord::StatementInvalid: SQLite3::BusyException: database is locked

RailsでSQLite3 の lock timeout が発生したので少し調べて見ました。
(原因は複数transactionが発生してロックかかってただけです)

まずは

みんな大好きstackoverflow。
ruby on rails - SQLite3::BusyException - Stack Overflow

一時的な対策ではあるが、 database.yml の timeout 値を変更することでBusyExceptionsを減らすことができるかもねという記載があります。

ソースを見て見ます

activerecord

# activerecord-5.0.2/lib/active_record/connection_adapters/sqlite3_adapter.rb:30
      db = SQLite3::Database.new(
        config[:database].to_s,
        results_as_hash: true
      )

      db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout]

SQLite3のbusy_timeout を呼んでいるだけのようです。

sqlite3

# sqlite3-1.3.13/lib/sqlite3/pragmas.rb
    def busy_timeout=( milliseconds )
      set_int_pragma "busy_timeout", milliseconds
    end

set_int_pragma に値を渡しているだけ。

    def set_int_pragma( name, value )
      execute( "PRAGMA #{name}=#{value.to_i}" )
    end

PRAGMA〜 という文字列で実行しているだけでした。

PRAGMAステートメント

全く知らなかったのですが、SQLite3固有の拡張コマンドでした。

 
busy_timeout は以下。
Pragma statements supported by SQLite

他にもcache sizeを変更したり同期タイミング(default_synchronous)を変更することで50倍以上高速になるそうです。
色々チューニングできそうです。