Search code examples
ruby-on-railsrubydatabasepostgresqlvalidates-uniqueness-of

Issue of User Duplication in Rails Application


I'm using 'rails', '~> 5.2.4', '>= 5.2.4.1' ruby '2.7.1' and gem 'pg', '>= 0.18', '< 2.0'

I'm encountering an issue in my Rails application where some users are being duplicated in the database. This problem occurs intermittently and seems to be related to a duplicated request for the user creation action, resulting in the creation of two user records simultaneously, despite validations implemented to ensure the uniqueness of fields such as email and CPF.

Analyzing the logs in Elastic Search, I've identified that there are two duplicate entries for user creation action logs, indicating that the duplication is occurring at the request level.

In my Customer model, I've implemented uniqueness validations for fields like email and CPF, using the scope of the white_label_origin field to ensure that uniqueness is checked only within the correct context.

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

Validations Implemented in the Customer Model:

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?

I've already searched about it and found this article which explains I need to use index so I found on migrations this:

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

I'd like to understand what's causing this duplication of users in my application and how I can fix this issue to prevent it from occurring in the future. What could be the possible causes of this behavior and how can I implement an effective solution to ensure the unique creation of users?


Solution

  • This is a common race conditions that happens when people double-clicking to submit a form.

    The double click leads the browser to send two requests to the web server. And both requests are processed by two different server processes or threads at the same time. Both processes check if the user already exists at the same time, both get the answer that such a user does not exist yet. And then both processes will create the user.

    That means that a unique validation in your model can only protect you from duplicate records to some extent, and fail when there are duplicate requests almost at the same time.

    To ensure that there are no duplicate records at any time, you need to add a unique index to your database for those attributes too. That means, the validation protects you for common use cases and allows returning a proper error message to the user. And the unique index in the database protects your data in those cases where the application was not able to catch the issue.

    Add an index to the database:

    add_index :customers, :email, unique: true
    

    Handle the possible error (validation and unique constraint violation) in your controller:

    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