# # the following model class, with example migration, with demonstrate how # rails validation (*all* validation not only validates_uniqueness_of) is # fatally flawed. get your db setup, run a migration to bootstrap the table, # and then run this to trigger the problem: # # echo ' Fubar.hork_the_db ' | ./script/runner /dev/stdin # # a couple of things to to notice: # # 1) all transactions are happening in a separate process # 2) 16 processes at a time try to insert the same 'uniq' data # # how does this happen? # # process A runs the query to check uniqueness, gets 'ok' # process B runs the query to check uniqueness, gets 'ok' # process A commits # process B commits # # a classic race condition # # wrapping in a transaction will not help unless the isolation level is # serializable # (http://www.postgresql.org/docs/7.4/interactive/transaction-iso.html) *and* # you are using transactions for all db activity. even so called 'atomic' # actions like create! are victim to this # # i've found a few links to this online like # http://kpumuk.info/ruby-on-rails/validates_uniqueness_of-vs-mysql-unique-index/ # which have notice the same thing, but it worries me that this fact does not # seem to be generally known or dealt with. # # the answer is simply putting all contraints in the db and giving up on nice # model.errors, but that removes alot of the niceness ar brings to the table. # # class Fubar < ActiveRecord::Base class Migration < ActiveRecord::Migration def self.up create_table :fubars do |t| t.column :must_be_uniq, :text t.timestamps end end def self.down drop_table :fubars end end validates_uniqueness_of :must_be_uniq, :if => :you_want_fubar_data def you_want_fubar_data true end def self.hork_the_db Dir.chdir RAILS_ROOT do delete_all create_a_script run_a_bunch_of_parallel_requests_in_separate_processes watch_the_db_get_horked end end def self.create_a_script open("a.rb", "w"){|fd| fd.puts "Fubar.create! :must_be_uniq => ARGV.shift"} end def self.run_a_bunch_of_parallel_requests_in_separate_processes Thread.new do loop do threads = [] must_be_uniq = rand 16.times do threads << Thread.new{ system "./script/runner a.rb #{ must_be_uniq } >/dev/null 2>&1" } end threads.map{|t| t.join} end end end def self.watch_the_db_get_horked loop do must_be_uniq = find(:all).map{|fubar| fubar.must_be_uniq} total = must_be_uniq.size unique = must_be_uniq.uniq.size if total == unique y "total" => total, "unique" => unique else y "total!" => total, "unique!" => unique end sleep 1 end end end __END__ # some sample output of run ... --- total: 0 unique: 0 --- total: 1 unique: 1 --- total!: 5 unique!: 2 --- total!: 5 unique!: 2 --- total!: 5 unique!: 2 ...