0

Ich habe ein Formular, das mit {remote: true} sendet, und ein Adopter-Modell mit einem Eindeutigkeits-Validierer im E-Mail-Feld. Jetzt habe ich einen seltsamen Fehler in der Produktionsumgebung (und ich konnte es nicht in der Entwicklung reproduzieren):Rails-Eindeutigkeits-Validierer schlägt auf seltsame doppelte XHR-Anforderung fehl

Manchmal, wenn Leute einen Adopter erstellen, wird die Post-Anfrage zweimal gesendet (die meiste Zeit alles funktioniert gut) . Wenn dies geschieht, erstellt meine Rails-App zwei Adopters mit derselben E-Mail-Adresse. Es folgt ein Auszug aus dem Problem verursacht Anfragen (I entfernt langweilig und private Sachen):

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"=>"[email protected]"} 
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" = '[email protected]' 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" = '[email protected]' 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", "[email protected]"], ["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"=>"[email protected]"} 
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" = '[email protected]' 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" = '[email protected]' 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 [email protected] (1613.9ms) 
[ActiveJob] [ActionMailer::DeliveryJob] [3f82f6c1-3f4e-4411-a377-2a667d32559f] Date: Sat, 25 Jun 2016 15:05:42 +0000 
From: Mycoolsite <[email protected]> 
To: [email protected] 
Message-ID: <[email protected]> 
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", "[email protected]"], ["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 [email protected] (1356.5ms) 
[ActiveJob] [ActionMailer::DeliveryJob] [d91eebb4-14ce-43e3-845c-6655c293de60] Date: Sat, 25 Jun 2016 15:05:44 +0000 
From: Mycoolsite <[email protected]> 
To: [email protected] 
Message-ID: <[email protected]> 
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) 

Dies ist der Adopter # Aktion erstellen (die zweimal aufgerufen wird):

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 

Dies ist mein Adopter Modell:

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 

Also im Grunde gibt es zwei Probleme. Erstens, dass manchmal die Post-Anfrage zweimal gesendet und verarbeitet wird. Und zweitens, wenn der erste passiert, scheitert der Eindeutigkeitsprüfer.

+1

In Bezug auf die Anfrage zweimal gemacht: wenn Sie sagen "in Produktion" meinen Sie nur in der Produktionsumgebung, oder ist es eine Live-App mit Benutzern? Im letzteren Fall möchten Sie vielleicht nach dem Klicken auf die Schaltfläche "Senden" für das Formular deaktivieren. Eine erstaunliche Anzahl von Personen tendiert immer noch dazu, auf alles doppelt zu klicken. – omnikron

+0

Es ist das letztere. Ich werde das tun, danke! –

Antwort

2

Erstens: Ich habe bemerkt, dass Benutzer immer noch denken, dass sie Likes doppelklicken müssen.

Sekunden eins: Nein, es scheitert nicht, es ist eine Race-Bedingung. Zwei Anfragen enden bei zwei verschiedenen Instanzen der App und beide Instanzen prüfen, ob ein ähnlicher Datensatz bereits existiert, beide bemerken, dass dies nicht der Fall ist und erstellen daher beide einen. Die Lösung besteht darin, dass Sie einen eindeutigen Index für Ihre Datenbank benötigen, um diese Art von Problemen zu vermeiden.

Zitat aus dem Rails Guides about ActiveRecord uniqueness validations:

Dieser Helfer bestätigt, dass der Wert des Attributs eindeutige Recht ist, bevor das Objekt gespeichert wird. Es wird keine Eindeutigkeitsbeschränkung in der Datenbank erstellt, daher kann es vorkommen, dass zwei verschiedene Datenbankverbindungen zwei Datensätze mit demselben Wert für eine Spalte erstellen, die eindeutig sein soll. Um dies zu vermeiden, müssen Sie einen eindeutigen Index für beide Spalten in Ihrer Datenbank erstellen.

+0

Ich vermute, das könnte der Grund sein. Dachte, es ist wahrscheinlich nicht, weil es zwei Sekunden zwischen den beiden Post-Anfragen gibt (zuerst bei 42s und sedond bei 44s) und das ist genug Zeit, um in die Datenbank in der ersten Anfrage zu schreiben. Ich könnte jedoch falsch liegen. –

+0

Wenn es eine 2s Lücke gibt, ist es wahrscheinlich kein Doppelklick. Wenn der Benutzer jedoch nach dem Klick noch keine Feedback-2s erhalten hat, ist die Aktion [sehr langsam im Internet] (http://www.nytimes.com/2012/03/01/technology/impatient-web-users) -flee-slow-loading-sites.html). Sie können a) unsicher sein, dass ihr erster Klick "funktioniert" hat und b) ungeduldig sind, und so zweimal klicken (oder mehr! Wie einen Aufzugsknopf eindrücken). Neben der Deaktivierung der Schaltfläche beim Klicken würde ich sicherstellen, dass es eine Rückmeldung zum Laden und zum Erfolg gibt (z. B. Spinner/Popup). Im Idealfall sollten Sie versuchen, die Aktion möglichst zu beschleunigen. – omnikron