Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New find_or_create_by behaviour raises RecordNotFound in edge rails #48035

Closed
jdelStrother opened this issue Apr 23, 2023 · 4 comments · Fixed by #48053
Closed

New find_or_create_by behaviour raises RecordNotFound in edge rails #48035

jdelStrother opened this issue Apr 23, 2023 · 4 comments · Fixed by #48053

Comments

@jdelStrother
Copy link
Contributor

jdelStrother commented Apr 23, 2023

Steps to reproduce

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem "rails", github: "rails/rails", branch: "main"
  gem "mysql2"
end

require "active_record"
require "minitest/autorun"
require "logger"

opts = {
  database: "unique_keys_test",
  adapter: "mysql2",
  username: "root"
}
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.establish_connection(opts.except(:database))
ActiveRecord::Base.connection.drop_database(opts[:database])
ActiveRecord::Base.connection.create_database(opts[:database])
ActiveRecord::Base.establish_connection(opts)

ActiveRecord::Schema.define do
  create_table :tags, force: true do |t|
    t.string :name
    t.index :name, unique: true
  end
end

class Tag < ActiveRecord::Base
end

class BugTest < Minitest::Test
  def test_find_or_create_by
    concurrently do
      Tag.find_or_create_by(name: "foo")
    rescue ActiveRecord::RecordNotUnique
      # some app logic that handles RecordNotUnique
    end

    assert_equal 1, Tag.count
  end

  def concurrently(&block)
    2.times.map {
      Thread.new {
        Tag.transaction {
          block.call
        }
      }
    }.map(&:join)
  end
end

Expected behavior

In Rails 7.0, one of the find_or_create_by calls raises RecordNotUnique (which we rescue and handle at the application-level).

Actual behavior

In 7.1 alpha, it raises RecordNotFound:

Backtrace
/Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation/finder_methods.rb:380:in `raise_record_not_found_exception!': Couldn't find Tag with [WHERE `tags`.`name` = ?] (ActiveRecord::RecordNotFound)
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation/finder_methods.rb:104:in `take!'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation/finder_methods.rb:87:in `find_by!'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:218:in `rescue in create_or_find_by'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:215:in `create_or_find_by'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:176:in `find_or_create_by'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/querying.rb:23:in `find_or_create_by'
	from uniquekeys.rb:43:in `block in test_association_stuff'
	from uniquekeys.rb:54:in `block (3 levels) in concurrently'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb:490:in `block in within_new_transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb:488:in `within_new_transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb:327:in `transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/transactions.rb:212:in `transaction'
	from uniquekeys.rb:53:in `block (2 levels) in concurrently'
/Users/jon/Developer/web/vendor/bundle/nix-bundle/ruby/3.1.0/gems/mysql2-0.5.5/lib/mysql2/client.rb:151:in `_query': Mysql2::Error: Duplicate entry 'foo' for key 'tags.index_tags_on_name' (ActiveRecord::RecordNotUnique)
	from /Users/jon/Developer/web/vendor/bundle/nix-bundle/ruby/3.1.0/gems/mysql2-0.5.5/lib/mysql2/client.rb:151:in `block in query'
	from /Users/jon/Developer/web/vendor/bundle/nix-bundle/ruby/3.1.0/gems/mysql2-0.5.5/lib/mysql2/client.rb:150:in `handle_interrupt'
	from /Users/jon/Developer/web/vendor/bundle/nix-bundle/ruby/3.1.0/gems/mysql2-0.5.5/lib/mysql2/client.rb:150:in `query'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb:742:in `block (2 levels) in raw_execute'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb:1011:in `block in with_raw_connection'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb:983:in `with_raw_connection'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb:740:in `block in raw_execute'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/notifications/instrumenter.rb:58:in `instrument'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb:1121:in `log'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb:739:in `raw_execute'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb:232:in `execute'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb:239:in `execute_and_free'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb:47:in `exec_query'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb:142:in `exec_insert'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb:177:in `insert'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb:22:in `insert'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/persistence.rb:583:in `_insert_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/persistence.rb:1238:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/counter_cache.rb:177:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/locking/optimistic.rb:84:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/encryption/encryptable_record.rb:174:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/attribute_methods/dirty.rb:205:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/callbacks.rb:445:in `block in _create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/callbacks.rb:99:in `run_callbacks'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/callbacks.rb:947:in `_run_create_callbacks'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/callbacks.rb:445:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/timestamp.rb:114:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/persistence.rb:1209:in `create_or_update'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/callbacks.rb:441:in `block in create_or_update'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/callbacks.rb:99:in `run_callbacks'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/callbacks.rb:947:in `_run_save_callbacks'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/callbacks.rb:441:in `create_or_update'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/timestamp.rb:132:in `create_or_update'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/persistence.rb:709:in `save'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/validations.rb:49:in `save'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/transactions.rb:309:in `block in save'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/transactions.rb:365:in `block in with_transaction_returning_status'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb:325:in `transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/transactions.rb:361:in `with_transaction_returning_status'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/transactions.rb:309:in `save'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/suppressor.rb:52:in `save'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/persistence.rb:38:in `create'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:891:in `_create'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:103:in `block in create'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:906:in `_scoping'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:444:in `scoping'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:103:in `create'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:216:in `block in create_or_find_by'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb:490:in `block in within_new_transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb:488:in `within_new_transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb:327:in `transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/transactions.rb:212:in `transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation/delegation.rb:79:in `block in transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:906:in `_scoping'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:444:in `scoping'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation/delegation.rb:79:in `transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:216:in `create_or_find_by'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:176:in `find_or_create_by'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/querying.rb:23:in `find_or_create_by'
	from uniquekeys.rb:43:in `block in test_association_stuff'
	from uniquekeys.rb:54:in `block (3 levels) in concurrently'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb:490:in `block in within_new_transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb:488:in `within_new_transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb:327:in `transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/transactions.rb:212:in `transaction'
	from uniquekeys.rb:53:in `block (2 levels) in concurrently'
/Users/jon/Developer/web/vendor/bundle/nix-bundle/ruby/3.1.0/gems/mysql2-0.5.5/lib/mysql2/client.rb:151:in `_query': Duplicate entry 'foo' for key 'tags.index_tags_on_name' (Mysql2::Error)
	from /Users/jon/Developer/web/vendor/bundle/nix-bundle/ruby/3.1.0/gems/mysql2-0.5.5/lib/mysql2/client.rb:151:in `block in query'
	from /Users/jon/Developer/web/vendor/bundle/nix-bundle/ruby/3.1.0/gems/mysql2-0.5.5/lib/mysql2/client.rb:150:in `handle_interrupt'
	from /Users/jon/Developer/web/vendor/bundle/nix-bundle/ruby/3.1.0/gems/mysql2-0.5.5/lib/mysql2/client.rb:150:in `query'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb:742:in `block (2 levels) in raw_execute'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb:1011:in `block in with_raw_connection'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb:983:in `with_raw_connection'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb:740:in `block in raw_execute'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/notifications/instrumenter.rb:58:in `instrument'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb:1121:in `log'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb:739:in `raw_execute'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb:232:in `execute'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb:239:in `execute_and_free'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb:47:in `exec_query'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb:142:in `exec_insert'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb:177:in `insert'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb:22:in `insert'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/persistence.rb:583:in `_insert_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/persistence.rb:1238:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/counter_cache.rb:177:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/locking/optimistic.rb:84:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/encryption/encryptable_record.rb:174:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/attribute_methods/dirty.rb:205:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/callbacks.rb:445:in `block in _create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/callbacks.rb:99:in `run_callbacks'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/callbacks.rb:947:in `_run_create_callbacks'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/callbacks.rb:445:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/timestamp.rb:114:in `_create_record'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/persistence.rb:1209:in `create_or_update'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/callbacks.rb:441:in `block in create_or_update'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/callbacks.rb:99:in `run_callbacks'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/callbacks.rb:947:in `_run_save_callbacks'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/callbacks.rb:441:in `create_or_update'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/timestamp.rb:132:in `create_or_update'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/persistence.rb:709:in `save'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/validations.rb:49:in `save'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/transactions.rb:309:in `block in save'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/transactions.rb:365:in `block in with_transaction_returning_status'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb:325:in `transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/transactions.rb:361:in `with_transaction_returning_status'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/transactions.rb:309:in `save'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/suppressor.rb:52:in `save'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/persistence.rb:38:in `create'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:891:in `_create'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:103:in `block in create'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:906:in `_scoping'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:444:in `scoping'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:103:in `create'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:216:in `block in create_or_find_by'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb:490:in `block in within_new_transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb:488:in `within_new_transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb:327:in `transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/transactions.rb:212:in `transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation/delegation.rb:79:in `block in transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:906:in `_scoping'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:444:in `scoping'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation/delegation.rb:79:in `transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:216:in `create_or_find_by'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/relation.rb:176:in `find_or_create_by'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/querying.rb:23:in `find_or_create_by'
	from uniquekeys.rb:43:in `block in test_association_stuff'
	from uniquekeys.rb:54:in `block (3 levels) in concurrently'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb:490:in `block in within_new_transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activesupport/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb:488:in `within_new_transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb:327:in `transaction'
	from /Users/jon/Developer/web/vendor/cache/rails-ea7ac1594b4e/activerecord/lib/active_record/transactions.rb:212:in `transaction'
	from uniquekeys.rb:53:in `block (2 levels) in concurrently'

though, apparently, only when there's a transaction wrapping the find_or_create call. Without the transaction, the new find_or_create_by improvements successfully return a tag in both threads.

(I'd happily remove our rescue block and leave Rails to handle the unique keys internally, if find_or_create can be fixed to do so)

cc @casperisfine

System configuration

Rails version: 7.1.0 alpha @ 719558c

Ruby version: 3.1.3

@fatkodima
Copy link
Member

Yes, transactions are the reasons. Here is what happened.
One of the transaction interleavings I got:

TRANSACTION (0.1ms)  BEGIN
TRANSACTION (0.1ms)  BEGIN
Tag Load (1.4ms)  SELECT `tags`.* FROM `tags` WHERE `tags`.`name` = 'foo' LIMIT 1
Tag Load (1.4ms)  SELECT `tags`.* FROM `tags` WHERE `tags`.`name` = 'foo' LIMIT 1
TRANSACTION (0.6ms)  SAVEPOINT active_record_1
 TRANSACTION (0.3ms)  SAVEPOINT active_record_1
Tag Create (0.5ms)  INSERT INTO `tags` (`name`) VALUES ('foo')
TRANSACTION (0.1ms)  RELEASE SAVEPOINT active_record_1
TRANSACTION (41.0ms)  COMMIT
Tag Create (42.7ms)  INSERT INTO `tags` (`name`) VALUES ('foo')
TRANSACTION (0.2ms)  ROLLBACK TO SAVEPOINT active_record_1
TRANSACTION (0.4ms)  SAVEPOINT active_record_1
Tag Load (0.4ms)  SELECT `tags`.* FROM `tags` WHERE `tags`.`name` = 'foo' LIMIT 1
TRANSACTION (0.2ms)  ROLLBACK TO SAVEPOINT active_record_1
TRANSACTION (4.2ms)  ROLLBACK
#<Thread:0x000000011b6c71d8 bug_report.rb:53 run> terminated with exception (report_on_exception is true):
/Users/fatkodima/Desktop/oss/rails/activerecord/lib/active_record/relation/finder_methods.rb:380:in `raise_record_not_found_exception!': Couldn't find Tag with [WHERE `tags`.`name` = ?] (ActiveRecord::RecordNotFound)
	from /Users/fatkodima/Desktop/oss/rails/activerecord/lib/active_record/relation/finder_methods.rb:104:in `take!'
	from /Users/fatkodima/Desktop/oss/rails/activerecord/lib/active_record/relation/finder_methods.rb:87:in `find_by!'
	from /Users/fatkodima/Desktop/oss/rails/activerecord/lib/active_record/relation.rb:219:in `block in create_or_find_by'
	from /Users/fatkodima/Desktop/oss/rails/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb:490:in `block in within_new_transaction'
  1. The 1-st transaction checks if record exists (no).
  2. Then 2-nd transaction checks if the record exists (no).
  3. 1-st transaction creates a record and commits
  4. 2-nd transaction tries to create a record, but fails, because the record already exists.
  5. 2-nd transaction tries to find the record, but unable to do so (even though that the 1-st transaction was committed), because the isolation level of transactions in MySQL is REPEATABLE READ by default
  6. and so not found error is raised

We need to somehow read the up to date data for this row without restarting a transaction. This is achieved by SELECT .. FOR UPDATE.

So, I come up with the following:

diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 2e21c22849..f53a94efed 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -215,7 +215,7 @@ def find_or_create_by!(attributes, &block)
     def create_or_find_by(attributes, &block)
       transaction(requires_new: true) { create(attributes, &block) }
     rescue ActiveRecord::RecordNotUnique
-      find_by!(attributes)
+      where(attributes).lock.find_by!(attributes)
     end

     # Like #create_or_find_by, but calls
@@ -224,7 +224,7 @@ def create_or_find_by(attributes, &block)
     def create_or_find_by!(attributes, &block)
       transaction(requires_new: true) { create!(attributes, &block) }
     rescue ActiveRecord::RecordNotUnique
-      find_by!(attributes)
+      where(attributes).lock.find_by!(attributes)
     end

     # Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new]

If that looks ok and I am not missing something, I can open a PR.

@jdelStrother
Copy link
Contributor Author

Ahh, thanks - I couldn't figure out the REPEATABLE READ step.

The patch looks promising... though my actual code ends up doing a second lookup of the tag during a subsequent validation, which ends up raising RecordNotFound again, again due to REPEATABLE READ 😬. I'll see if I can avoid the extra lookup somehow

@jdelStrother
Copy link
Contributor Author

Yep, works well once I've removed the unnecessary lookup

@casperisfine
Copy link
Contributor

If that looks ok and I am not missing something, I can open a PR.

Hum, should we only do that if we're inside a transaction?

Feel free to open a PR we can discuss it there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants