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

Rails 7.1 changes established database connection after calling rails db:test:prepare for multi-database apps #51830

Open
GUI opened this issue May 14, 2024 · 4 comments · May be fixed by #51862

Comments

@GUI
Copy link
Contributor

GUI commented May 14, 2024

Steps to reproduce

  1. rails new example

  2. In the new app, setup a multiple-database configuration config/database.yml file:

    default: &default
      adapter: sqlite3
      pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
      timeout: 5000
    
    development:
      primary:
        <<: *default
        database: db/development.sqlite3
      animals:
        <<: *default
        database: db/animals_development.sqlite3
    
    test:
      primary:
        <<: *default
        database: db/test.sqlite3
      animals:
        <<: *default
        database: db/animals_test.sqlite3
    
  3. Add the following to the beginning of db/seeds.rb:

    puts "Seed DB Config: #{ApplicationRecord.connection.pool.db_config.inspect}"
    
  4. Run rails db:migrate to create the schema files.

  5. Run RAILS_ENV=test rails db:drop db:test:prepare db:seed

Expected behavior

In Rails 7.0 and prior, the seed task would run against the default, primary database connection, which is what I would expect. This can be seen by the output of this example app on Rails 7.0:

RAILS_ENV=test rails db:drop db:test:prepare db:seed
Dropped database 'db/test.sqlite3'
Dropped database 'db/animals_test.sqlite3'
Seed DB Config: #<ActiveRecord::DatabaseConfigurations::HashConfig:0x0000000121fd1368 @env_name="test", @name="primary", @configuration_hash={:adapter=>"sqlite3", :pool=>5, :timeout=>5000, :database=>"db/test.sqlite3"}>

Actual behavior

In Rails 7.1+, the seed task runs against the last database connection defined, which may cause seeding to break. Here is the same output from this type of multi-database example app in Rails 7.1:

RAILS_ENV=test rails db:drop db:test:prepare db:seed
Dropped database 'db/test.sqlite3'
Dropped database 'db/animals_test.sqlite3'
Seed DB Config: #<ActiveRecord::DatabaseConfigurations::HashConfig:0x000000012d0d76f8 @env_name="test", @name="animals", @configuration_hash={:adapter=>"sqlite3", :pool=>5, :timeout=>5000, :database=>"db/animals_test.sqlite3"}>

Note that now the seeding process is trying to run while connected to the secondary animals database. In a more real-life scenario, this may cause seeding to fail, since seeds may not work if running against a non-primary database where your model tables may not exist.

You can also see how it's the db:test:prepare task that is affecting the active connection, since if you call db:seed by itself in Rails 7.1, it does still connect to the expected primary database. So this issue is specific to chaining multiple tasks in a single invocation after db:test:prepare.

RAILS_ENV=test rails db:seed
Seed DB Config: #<ActiveRecord::DatabaseConfigurations::HashConfig:0x0000000125c5a140 @env_name="test", @name="primary", @configuration_hash={:adapter=>"sqlite3", :pool=>5, :timeout=>5000, :database=>"db/test.sqlite3"}>

System configuration

Rails version: 7.1.3.2

Ruby version: 3.3.1

Additional notes

  • I believe this happens since the db:test:purge task (which is called as a dependency of db:test:prepare) loops over the database connections and leaves the app connected to whatever the last connection to be looped over was: https://github.com/rails/rails/blob/v7.1.3.2/activerecord/lib/active_record/railties/databases.rake#L559-L563
  • As noted above, you can side-step this issue if you call the tasks with separate CLI invocations. So, for example, if you call RAILS_ENV=test rails db:drop db:test:prepare && RAILS_ENV=test rails db:seed instead, then the seed task does execute against the expected primary database, but this means tasks are working different when called in isolation versus being chained.
  • In this example, it impacts the seed task, but this could impact any other rake task chained after db:test:prepare in the same CLI invocation, since in all cases, it leaves the active database connection as the last one defined for multi-database apps.
  • I don't think this happened in Rails 7.0 because db:test:prepare would specifically reset the connection back to the default connection: https://github.com/rails/rails/blob/v7.0.8.1/activerecord/lib/active_record/railties/databases.rake#L559-L563
  • So I believe it's related to these changes in 7.1: Move calls on Base connection to methods for rake tasks #46270 However, db:test:purge doesn't use the new with_temporary_connection_for_each approach (which does help avoid these type of problems). But I'm note entirely certain it can use the new approach, since on a quick test, I ran into errors, since the database doesn't exist yet in my example case (due to the db:drop). But there might be other better ways to leverage the new setup for this specific task.
  • Alternatively, another possible solution/workaround would be to call ActiveRecord::Base.establish_connection(ActiveRecord::Tasks::DatabaseTasks.env.to_sym) at the end of the db:test:purge task to reset any connections the loop may have established. That worked in a quick test, but again, I'm unsure if that's necessarily the best solution.
@justinko
Copy link
Contributor

This fixes it but I don't think it's the direction we want to go in: justinko@2fc0ded

I think there's some confusion about responsibility between database_tasks.rb and databases.rake. I'm looking into it.

@zzak
Copy link
Member

zzak commented May 18, 2024

@justinko I think a problem with that approach is that those tasks already call establish_connection, e.g.:

establish_connection(public_schema_config)

@justinko
Copy link
Contributor

Which is essentially idempotent/no-op as connection on the next line is the same connection from with_temporary_connection (in my branch). At least it is with the mysql tasks, as that's all I've tested so far.

Not saying this is the correct approach but we do need to find a clean way to restore to the original db config.

justinko added a commit to justinko/rails that referenced this issue May 19, 2024
@justinko justinko linked a pull request May 19, 2024 that will close this issue
4 tasks
justinko added a commit to justinko/rails that referenced this issue May 20, 2024
@justinko
Copy link
Contributor

@zzak ready for review #51862

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