環境
- 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 でできると楽なんだけれど何か方法無いかな
0 件のコメント:
コメントを投稿