Rails唯一性验证器在奇怪的双xhr请求上失败

时间:2016-06-26 09:00:36

标签: ruby-on-rails ruby validation xmlhttprequest production-environment

我有一个使用{remote:true}发送的表单和一个在电子邮件字段中具有唯一性验证器的Adopter模型。现在我在生产环境中有一个奇怪的错误(我无法在开发中重现它):

有时候如果人们正在创建一个采用者,那么发布请求将被发送两次(大部分时间一切正常)。当发生这种情况时,我的Rails应用程序会使用相同的电子邮件创建两个采用者。以下是引发请求的问题摘录(我删除了无聊和私人内容):

Started GET "/" for 123.456.789.012 at 2016-06-25 15:03:37 +0000
Processing by LandingController#index as HTML
request http://ipinfo.io/123.456.789.012
{
  "ip": "123.456.789.012",
  "hostname": "12345678.dip0.t-ipconnect.de",
  "city": "",
  "region": "",
  "country": "DE",
  "loc": "1.0000,1.0000",
  "org": "Organistion name"
}
  Rendered landing/index.html.slim within layouts/application (1.6ms)
  Rendered layouts/application/_head.html.slim (2.0ms)
  Rendered layouts/application/_ga.html.slim (0.1ms)
Completed 200 OK in 39ms (Views: 6.8ms | ActiveRecord: 0.0ms)
Started POST "/adopters" for 123.456.789.012 at 2016-06-25 15:05:42 +0000
Processing by AdoptersController#create as JS
  Parameters: {"utf8"=>"✓", "name"=>"Mika", "answer"=>{"for"=>"friend", "for_precise"=>"male_friend", "nature"=>"humorous", "interests"=>["technology", "music"]}, "email"=>"nobody@shouldknow.com"}
request http://ipinfo.io/123.456.789.012
{
  "ip": "123.456.789.012",
  "hostname": "12345678.dip0.t-ipconnect.de",
  "city": "",
  "region": "",
  "country": "DE",
  "loc": "1.0000,1.0000",
  "org": "Organistion name"
}
  Adopter Load (0.7ms)  SELECT  "adopters".* FROM "adopters" WHERE "adopters"."referral_code" = $1 LIMIT 1  [["referral_code", "97f32b"]]
  CACHE (0.0ms)  SELECT  "adopters".* FROM "adopters" WHERE "adopters"."referral_code" = $1 LIMIT 1  [["referral_code", "97f32b"]]
   (0.3ms)  BEGIN
   (0.4ms)  SELECT COUNT(*) FROM "adopters" WHERE "adopters"."sign_up_ip" = $1  [["sign_up_ip", "123.456.789.012/32"]]
  Adopter Exists (0.9ms)  SELECT  1 AS one FROM "adopters" WHERE "adopters"."email" = 'nobody@shouldknow.com' LIMIT 1
  Adopter Exists (0.4ms)  SELECT  1 AS one FROM "adopters" WHERE "adopters"."referral_code" IS NULL LIMIT 1
  Adopter Load (0.4ms)  SELECT  "adopters".* FROM "adopters" WHERE "adopters"."referral_code" = $1 LIMIT 1  [["referral_code", "58b7fd"]]
   (0.2ms)  SELECT COUNT(*) FROM "adopters" WHERE "adopters"."sign_up_ip" = $1  [["sign_up_ip", "89.15.237.123/32"]]
  Adopter Exists (0.4ms)  SELECT  1 AS one FROM "adopters" WHERE ("adopters"."email" = 'lukas.thorr@web.de' AND "adopters"."id" != 52) LIMIT 1
  Adopter Exists (0.3ms)  SELECT  1 AS one FROM "adopters" WHERE ("adopters"."referral_code" = '97f32b' AND "adopters"."id" != 52) LIMIT 1
  SQL (2.0ms)  UPDATE "adopters" SET "referred_count" = $1, "updated_at" = $2 WHERE "adopters"."id" = $3  [["referred_count", 1], ["updated_at", "2016-06-25 15:05:42.492589"], ["id", 52]]
  SQL (1.2ms)  INSERT INTO "adopters" ("name", "email", "gift_for_person", "gift_for_person_nature", "gift_for_person_interests", "sign_up_ip", "referred_by", "locale", "created_at", "updated_at", "referral_code", "referred_count") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING "id"  [["name", "Mika"], ["email", "nobody@shouldknow.com"], ["gift_for_person", "male_friend"], ["gift_for_person_nature", "humorous"], ["gift_for_person_interests", "[\"technology\",\"music\"]"], ["sign_up_ip", "123.456.789.012/32"], ["referred_by", 52], ["locale", "de"], ["created_at", "2016-06-25 15:05:42.484886"], ["updated_at", "2016-06-25 15:05:42.484886"], ["referral_code", "58b7fd"], ["referred_count", 0]]
[ActiveJob]   Adopter Load (0.6ms)  SELECT  "adopters".* FROM "adopters" WHERE "adopters"."id" = $1 LIMIT 1  [["id", 54]]
[ActiveJob] [ActionMailer::DeliveryJob] [3f82f6c1-3f4e-4411-a377-2a667d32559f] Performing ActionMailer::DeliveryJob from Inline(mailers) with arguments: "AdopterMailer", "signup_email", "deliver_now", gid://Mycoolsite-prelauncher/Adopter/54
[ActiveJob] [ActionMailer::DeliveryJob] [3f82f6c1-3f4e-4411-a377-2a667d32559f]   Rendered adopter_mailer/signup_email.html.slim (3.0ms)
[ActiveJob] [ActionMailer::DeliveryJob] [3f82f6c1-3f4e-4411-a377-2a667d32559f] 
AdopterMailer#signup_email: processed outbound mail in 8.1ms
Started POST "/adopters" for 123.456.789.012 at 2016-06-25 15:05:44 +0000
Processing by AdoptersController#create as JS
  Parameters: {"utf8"=>"✓", "name"=>"Mika", "answer"=>{"for"=>"friend", "for_precise"=>"male_friend", "nature"=>"humorous", "interests"=>["technology", "music"]}, "email"=>"nobody@shouldknow.com"}
request http://ipinfo.io/123.456.789.012
{
  "ip": "123.456.789.012",
  "hostname": "12345678.dip0.t-ipconnect.de",
  "city": "",
  "region": "",
  "country": "DE",
  "loc": "1.0000,1.0000",
  "org": "Organistion name"
}
  Adopter Load (1.2ms)  SELECT  "adopters".* FROM "adopters" WHERE "adopters"."referral_code" = $1 LIMIT 1  [["referral_code", "97f32b"]]
  CACHE (0.0ms)  SELECT  "adopters".* FROM "adopters" WHERE "adopters"."referral_code" = $1 LIMIT 1  [["referral_code", "97f32b"]]
   (0.2ms)  BEGIN
   (0.3ms)  SELECT COUNT(*) FROM "adopters" WHERE "adopters"."sign_up_ip" = $1  [["sign_up_ip", "123.456.789.012/32"]]
  Adopter Exists (0.5ms)  SELECT  1 AS one FROM "adopters" WHERE "adopters"."email" = 'nobody@shouldknow.com' LIMIT 1
  Adopter Exists (0.3ms)  SELECT  1 AS one FROM "adopters" WHERE "adopters"."referral_code" IS NULL LIMIT 1
  Adopter Load (0.2ms)  SELECT  "adopters".* FROM "adopters" WHERE "adopters"."referral_code" = $1 LIMIT 1  [["referral_code", "860fc4"]]
   (0.2ms)  SELECT COUNT(*) FROM "adopters" WHERE "adopters"."sign_up_ip" = $1  [["sign_up_ip", "89.15.237.123/32"]]
  Adopter Exists (0.4ms)  SELECT  1 AS one FROM "adopters" WHERE ("adopters"."email" = 'lukas.thorr@web.de' AND "adopters"."id" != 52) LIMIT 1
  Adopter Exists (0.3ms)  SELECT  1 AS one FROM "adopters" WHERE ("adopters"."referral_code" = '97f32b' AND "adopters"."id" != 52) LIMIT 1
[ActiveJob] [ActionMailer::DeliveryJob] [3f82f6c1-3f4e-4411-a377-2a667d32559f] 
Sent mail to nobody@shouldknow.com (1613.9ms)
[ActiveJob] [ActionMailer::DeliveryJob] [3f82f6c1-3f4e-4411-a377-2a667d32559f] Date: Sat, 25 Jun 2016 15:05:42 +0000
From: Mycoolsite <subscribed@mysite.io>
To: nobody@shouldknow.com
Message-ID: <576e9dc67f160_83f98bcce081c5454@1c50fd29020a.mail>
Subject: Mycoolsite - Deine Anmeldung
Mime-Version: 1.0
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: quoted-printable
some email content

[ActiveJob] [ActionMailer::DeliveryJob] [3f82f6c1-3f4e-4411-a377-2a667d32559f] Performed ActionMailer::DeliveryJob from Inline(mailers) in 1625.69ms
[ActiveJob] Enqueued ActionMailer::DeliveryJob (Job ID: 3f82f6c1-3f4e-4411-a377-2a667d32559f) to Inline(mailers) with arguments: "AdopterMailer", "signup_email", "deliver_now", gid://Mycoolsite-prelauncher/Adopter/54
   (1.3ms)  COMMIT
  SQL (62.8ms)  UPDATE "adopters" SET "referred_count" = $1, "updated_at" = $2 WHERE "adopters"."id" = $3  [["referred_count", 1], ["updated_at", "2016-06-25 15:05:44.074420"], ["id", 52]]
  SQL (0.6ms)  INSERT INTO "adopters" ("name", "email", "gift_for_person", "gift_for_person_nature", "gift_for_person_interests", "sign_up_ip", "referred_by", "locale", "created_at", "updated_at", "referral_code", "referred_count") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING "id"  [["name", "Mika"], ["email", "nobody@shouldknow.com"], ["gift_for_person", "male_friend"], ["gift_for_person_nature", "humorous"], ["gift_for_person_interests", "[\"technology\",\"music\"]"], ["sign_up_ip", "123.456.789.012/32"], ["referred_by", 52], ["locale", "de"], ["created_at", "2016-06-25 15:05:44.068500"], ["updated_at", "2016-06-25 15:05:44.068500"], ["referral_code", "860fc4"], ["referred_count", 0]]
Completed 200 OK in 1716ms (Views: 0.3ms | ActiveRecord: 9.1ms)
[ActiveJob]   Adopter Load (4.3ms)  SELECT  "adopters".* FROM "adopters" WHERE "adopters"."id" = $1 LIMIT 1  [["id", 55]]
[ActiveJob] [ActionMailer::DeliveryJob] [d91eebb4-14ce-43e3-845c-6655c293de60] Performing ActionMailer::DeliveryJob from Inline(mailers) with arguments: "AdopterMailer", "signup_email", "deliver_now", gid://Mycoolsite-prelauncher/Adopter/55
[ActiveJob] [ActionMailer::DeliveryJob] [d91eebb4-14ce-43e3-845c-6655c293de60]   Rendered adopter_mailer/signup_email.html.slim (1.7ms)
[ActiveJob] [ActionMailer::DeliveryJob] [d91eebb4-14ce-43e3-845c-6655c293de60] 
AdopterMailer#signup_email: processed outbound mail in 4.0ms
Started GET "/rank/58b7fd" for 123.456.789.012 at 2016-06-25 15:05:44 +0000
Processing by AdoptersController#refer as HTML
  Parameters: {"code"=>"58b7fd"}
request http://ipinfo.io/123.456.789.012
{
  "ip": "123.456.789.012",
  "hostname": "12345678.dip0.t-ipconnect.de",
  "city": "",
  "region": "",
  "country": "DE",
  "loc": "1.0000,1.0000",
  "org": "Organistion name"
}
  Adopter Load (0.8ms)  SELECT  "adopters".* FROM "adopters" WHERE "adopters"."referral_code" = $1 LIMIT 1  [["referral_code", "58b7fd"]]
   (0.6ms)  SELECT COUNT(*) FROM "adopters" WHERE "adopters"."referred_by" = $1  [["referred_by", 54]]
   (0.8ms)  
      SELECT * FROM (
        SELECT adopters.id as id, adopters.referred_count as referred_count, row_number() over(order by adopters.referred_count desc) as rn FROM adopters
      ) t where id = 54

  Adopter Load (0.6ms)  SELECT  "adopters".* FROM "adopters"  ORDER BY "adopters"."referred_count" DESC, "adopters"."created_at" ASC LIMIT 15
  Rendered adopters/refer.html.slim within layouts/application (7.1ms)
  Rendered layouts/application/_head.html.slim (1.9ms)
  Rendered layouts/application/_ga.html.slim (0.1ms)
Completed 200 OK in 29ms (Views: 9.0ms | ActiveRecord: 2.8ms)
[ActiveJob] [ActionMailer::DeliveryJob] [d91eebb4-14ce-43e3-845c-6655c293de60] 
Sent mail to nobody@shouldknow.com (1356.5ms)
[ActiveJob] [ActionMailer::DeliveryJob] [d91eebb4-14ce-43e3-845c-6655c293de60] Date: Sat, 25 Jun 2016 15:05:44 +0000
From: Mycoolsite <subscribed@mysite.io>
To: nobody@shouldknow.com
Message-ID: <576e9dc8265ef_83f98bc2ae3e4546d8@1c50fd29020a.mail>
Subject: Mycoolsite - Deine Anmeldung
Mime-Version: 1.0
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: quoted-printable

some email content again

[ActiveJob] [ActionMailer::DeliveryJob] [d91eebb4-14ce-43e3-845c-6655c293de60] Performed ActionMailer::DeliveryJob from Inline(mailers) in 1363.09ms
[ActiveJob] Enqueued ActionMailer::DeliveryJob (Job ID: d91eebb4-14ce-43e3-845c-6655c293de60) to Inline(mailers) with arguments: "AdopterMailer", "signup_email", "deliver_now", gid://Mycoolsite-prelauncher/Adopter/55
   (1.1ms)  COMMIT
Completed 200 OK in 1491ms (Views: 0.2ms | ActiveRecord: 72.7ms)

这是Adopter #create动作(被调用两次):

  def create
    ref_code = cookies[:h_ref]

    @adopter = Adopter.new
    @adopter.name = params[:name]
    @adopter.email = params[:email]
    @adopter.gift_for_person = params[:answer][:for_precise] if !params[:answer][:for_precise].blank?
    @adopter.gift_for_person = params[:answer][:for] if params[:answer][:for_precise].blank?
    @adopter.gift_for_person_nature = params[:answer][:nature]
    @adopter.gift_for_person_interests = params[:answer][:interests].to_json
    @adopter.sign_up_ip = request.remote_ip
    if ref_code && Adopter.find_by(referral_code: ref_code)
      @adopter.referrer = Adopter.find_by(referral_code: ref_code)
    end
    @adopter.locale = I18n.locale

    respond_to do |format|
      if @adopter.save
        cookies[:h_email] = { value: @adopter.email }
        #format.html { redirect_to rank_path(code: @adopter.referral_code) }
        format.js { 
          render :js => "window.location = '#{rank_path(code: @adopter.referral_code)}'"
        }
      else
        logger.info("Error saving user with email, #{@adopter.email}")
        # redirect_to root_path, alert: 'Something went wrong!'

        # format.js {  flash[:notice] = @adopter.errors }
        format.js { flash[:notice] = @adopter.errors }
      end
    end
  end

这是我的采用者模型:

require 'adopters_helper'

class Adopter < ActiveRecord::Base
  belongs_to :referrer, class_name: 'Adopter', foreign_key: 'referred_by'
  has_many :referrals, class_name: 'Adopter', foreign_key: 'referred_by'

  validate :not_more_than_two_adopters_per_ip
  validates :email, presence: true, uniqueness: true, format: {
    with: /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/i,
    message: 'Invalid email format.'
  }
  validates :referral_code, uniqueness: true
  validates :name, :locale, presence: true, allow_blank: false

  before_create :create_referral_code
  after_create :send_welcome_email
  before_create :compute_queue_positions

  def rank
    row_number_tupel = ActiveRecord::Base.connection.execute("
      SELECT * FROM (
        SELECT adopters.id as id, adopters.referred_count as referred_count, row_number() over(order by adopters.referred_count desc) as rn FROM adopters
      ) t where id = #{self.id}
    ")

    row_number_tupel[0]['rn']
  end

  private
    def create_referral_code
      self.referral_code = AdoptersHelper.unused_referral_code
    end

    def not_more_than_two_adopters_per_ip
      if !Rails.env.development?
        adopter_count_with_current_ip = Adopter.where(sign_up_ip: sign_up_ip).count
        if adopter_count_with_current_ip >= 3
          errors.add(:sign_up_ip, I18n.t('activerecord.errors.models.adopter.attributes.sign_up_ip.max_ips'))
        end
      end
    end

    def send_welcome_email
      AdopterMailer.signup_email(self).deliver_later
    end

    def compute_queue_positions
      self.referred_count = 0

      if !self.referrer.blank?
        self.referrer.update_attributes! referred_count: (self.referrer.referred_count + 1)
      end
    end
end

所以基本上有两个问题。第一个,有时Post请求被发送和处理两次。第二个,如果第一个发生,唯一性验证器失败。

1 个答案:

答案 0 :(得分:2)

第一个:我注意到用户仍然认为他们必须双击喜欢。

秒一:不,它没有失败,这是一种竞争条件。两个请求最终在应用程序的两个不同实例上,两个实例都检查是否已存在类似记录,两者都注意到它没有,因此两者都创建了一个。解决方案是您需要在数据库上使用唯一索引来避免这些问题。

来自Rails Guides about ActiveRecord uniqueness validations

的引用
  

此帮助程序验证属性的值在保存对象之前是唯一的。它不会在数据库中创建唯一性约束,因此可能会发生两个不同的数据库连接创建两个记录,这些记录对于您想要唯一的列具有相同的值。为避免这种情况,您必须在数据库的两列上创建唯一索引。

相关问题