Railsアプリケーションにおけるユーザー重複の問題

概要

使っています ‘レール’、‘~> 5.2.4’、‘>= 5.2.4.1’ ルビー「2.7.1」 および gem ‘pg’、‘>= 0.18’、‘< 2.0’

Rails アプリケーションで、データベース内で一部のユーザーが重複するという問題が発生しました。この問題は断続的に発生し、ユーザー作成アクションの重複リクエストに関連しているようです。その結果、電子メールや CPF などのフィールドの一意性を確保するために検証が実装されているにもかかわらず、2 つのユーザー レコードが同時に作成されてしまいます。

Elastic Search のログを分析すると、ユーザー作成アクション ログに 2 つの重複エントリがあることがわかりました。これは、重複がリクエスト レベルで発生していることを示しています。

私の Customer モデルでは、white_label_origin フィールドのスコープを使用して、電子メールや CPF などのフィールドの一意性検証を実装し、正しいコンテキスト内でのみ一意性がチェックされるようにしました。

CustomersController#create

def create
  payload = {
    created_at: DateTime.now,
    name: 'Customer Controller',
    type: 'log',
    event: 'create - Customer',
    session_id: session.id.to_s,
    general_text: 'Create customer'
  }

  log_elastic(payload)
  @customer = Customer.new(customer_params.merge(require_full_signup: true)
                                          .merge(white_label_data: white_label_session))
  if @customer.save
    payload = {
      created_at: DateTime.now,
      name: 'Customer Controller',
      type: 'log',
      event: 'saved - Customer',
      customer_id: @customer.id,
      session_id: session.id.to_s,
      general_text: 'customer saved'
    }

    log_elastic(payload)
  end
end

顧客モデルに実装された検証:

validates :email, uniqueness: {
  scope: :white_label_origin,
  message: 'Email already registered'
}
validates :cpf, uniqueness: {
  scope: :white_label_origin,
  message: 'Document already registered'
}, on: :create, unless: :omniauth_customer?

すでにそれについて検索し、インデックスを使用する必要があることを説明するこの記事を見つけたので、移行で次のことを見つけました。

class AddIndexToCustomerEmailAndCpf < ActiveRecord::Migration[5.2]
  def change
    add_index :customers, :email
    add_index :customers, :cpf
  end
end

アプリケーションでこのユーザーの重複が発生する原因と、今後発生しないようにこの問題を修正する方法を理解したいと考えています。この動作の考えられる原因は何ですか?また、ユーザーを確実に一意に作成するための効果的な解決策を実装するにはどうすればよいですか?

解決策

これは、ダブルクリックしてフォームを送信するときに発生する一般的な競合状態です。

ダブルクリックすると、ブラウザは 2 つのリクエストを Web サーバーに送信します。そして、両方のリクエストは 2 つの異なるサーバー プロセスまたはスレッドによって同時に処理されます。両方のプロセスは同時にユーザーが既に存在するかどうかを確認しますが、両方ともそのようなユーザーはまだ存在しないという回答を受け取ります。そして、両方のプロセスがユーザーを作成します。

つまり、モデル内の一意の検証では重複レコードからある程度しか保護できず、ほぼ同時に重複したリクエストがある場合には失敗します。

重複レコードが常に存在しないようにするには、これらの属性に対してもデータベースに一意のインデックスを追加する必要があります。つまり、検証により一般的な使用例から保護され、ユーザーに適切なエラー メッセージを返すことができるようになります。また、アプリケーションが問題を検出できなかった場合でも、データベースの一意のインデックスによってデータが保護されます。

データベースにインデックスを追加します。

add_index :customers, :email, unique: true

コントローラーで考えられるエラー (検証および一意制約違反) を処理します。

def create
  # [...]
  @customer = Customer.new(
    customer_params.merge(
      require_full_signup: true, white_label_data: white_label_session
    )
  )
  
  if @customer.save
    # Customer was successfully created.
  else
    # validating the customer failed, see `@customer.errors` for details
  end
rescue ActiveRecord::RecordNotUnique => exception
  # Record creation failed because of the unique index in the database.
  # Depending on your usecase returning the existing customer with
  #
  #     @customer = Customer.find_by(email: @customer.email)
  #
  # might be an option to fix the issue.
end