環境
- OSX Yosemite 10.10.4
 - rbenv 0.4.0
 - ruby-build 20150719
 - Ruby 2.2.2
 - Rails 4.2.2
 - Rspec 3.3.1
 - Capybara 2.4.4
 - Postgresql 9.2.13
 
状況と解説と解決策
想定としては
- ユーザが resource を new する
 - resource に対して Rails 側が unique な値を追加する
 - unique な値は DB 内では重複してはいけない
 
といった感じです。
とりあえずアバウトなサンプルとして
- 本の名前と価格をユーザが入れる
 - ISBNをRails側が順番に振っていく
 - もし本の登録が無くなったらそのISBNは再利用
 
みたいなサンプルを書いてみました。
まず、リクエストを並列に投げるには spec 内で fork しちゃうと良いみたいです。
validates_uniqueness_of をすり抜ける程度には近いタイミングで実行されるようなので、同時に受け取ったテストと考えて良いかな、と思っています。
実際、 SQLite3 + validates_uniqueness_of のみを書いてある commit では、2つのリクエストを同時に投げると unique に振らないといけない ISBN に対して同じ値でレコードが2つ追加されてしまいます。
DB内で重複を排除するには unique な index を貼ることで回避してます。
そうすることで SQLite3 でも重複された値は格納されないようになるのですが
- /Users/atton/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/sqlite3-1.3.10/lib/sqlite3/statement.rb:108:in `step':
 - SQLite3::BusyException: database is locked: INSERT INTO "books" ("name", "price", "created_at", "updated_at") VALUES (?, ?, ?, ?)
 - (ActiveRecord::StatementInvalid)
 
ActiveRecord::StatementInvalid を rescue で拾ってから再格納しようとしても BusyException が出るのでうまくいかず。
ということで DB を PostgreSQL に変えてみます。
そうすると、 validates_uniqueness_of をすり抜けて重複した値が入ってきた時には ActiveRecord::RecordNotUnique が返ってきます。
こいつを rescue してきちんとした値にしてやればどうにか目的は達成できました。
ちなみにちゃんと rescue するロジックを書いた場合だとテストで同時に80リクエストくらい捌けました。fork しまくったせいでメモリを数GBとか使うので80くらいが限界。
コマンドとか
Github に上げたサンプルをとりあえず動かすには
- 
git clone https://github.com/atton-/sample_of_rspec_to_concurrent_requests_using_capybara.git
 - cd sample_of_rspec_to_concurrent_requests_using_capybara
 - git checkout sqlite3_and_validates_uniqueness_of
 - RAILS_ENV=test bundle exec rake db:drop db:create db:migrate spec
 - sqlite3 と validates_uniqueness_of のみ。
 - unique であって欲しい column に同じ値が入ってしまう
 - git checkout use_postgresql
 - RAILS_ENV=test bundle exec rake db:drop db:create db:migrate spec
 - PostgreSQL + Unique Index
 - unique であって欲しい column に同じ値は入らない
 - 同じ値を入れようとすると ActiveRecord::RecordNotUnique
 - git checkout handle_record_not_unique
 - RAILS_ENV=test bundle exec rake db:drop db:create db:migrate spec
 - PostgreSQL + Unique Index + ActiveRecord::RecordNotUnique を rescue
 - ActiveRecord::RecordNotUnique をきちんと処理してやれば unique にしてあげられる
 
PostgreSQL の設定は環境変数に設定するか database.yml を変更してください。
まとめ
- 特定の column の値を Unique にしたかったら unique index を貼っておくと validates_uniqueness_of をすり抜けられてもDBに入るのは止められる
 - SQLite3 だと Unique index がある table に同時に insert とかすると BusyException とか出ちゃう
 - PostgreSQL だとほぼ同時なリクエストでも捌けた
 - PostgreSQL だと返ってくる exception は ActiveRecord::RecordNotUnique に変わる
 
気になるところ
- Unique Index を貼る方法で Uniqueness を確保する方法って一般的なのかな
 - 特定の column の値が [1,2,3,4, 6,7,8,9] みたいな状態で 5 を取ってくる SQL ってあったっけ(今は 1 から順に ActiveRecord#exists? で回してるので O(n) なんだよなー)
 - 並列にリクエストを投げるのを SQLite3 でできると楽なんだけれど何か方法無いかな