-
Notifications
You must be signed in to change notification settings - Fork 14
MySQL adapter #246
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
base: main
Are you sure you want to change the base?
MySQL adapter #246
Conversation
it from the tasks)
This ensures the adapter actually broadcasts over the WebSocket connection within the same process, which is what system tests need.
11fde4d to
d238e6b
Compare
|
I rebased onto current |
flavorjones
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh my gosh, this PR is great, thank you for the huge contribution!
I have two primary reservations:
- the devcontainer setup seems very particular for your needs (v8.0 and not v8.4 or v9.0, and mounting /var/lib/mysql) and I'd like to move that to a separate PR or discussion if that's OK.
- parallel testing doesn't work for me -- does it work for you?
On (2), in particular, I tend to run the unit test suite highly parallelized (~20 workers). But even running with NCPU=2 for two workers raises exceptions:
ActiveRecord::Tenanted::Tenant::#cache_key::scenario::mysql/primary_named_db::secondary_record::created in tenanted context#test_includes_the_tenant_name:
ActiveRecord::StatementInvalid: Mysql2::Error: Table 'schema_migrations' already exists
Just curious if this is something that doesn't happen to you, in which case I'll dig in.
I've made quite a few comments in here, but many of them are "to-dos" and don't necessarily need to be fixed in this PR.
I'm going to spend some time in the morning appending small changes to this branch in response to some of my comments. Maybe we can collaborate a bit on getting this merged?
Thank you again!!! ❤️❤️🙏
| @@ -0,0 +1,15 @@ | |||
| FROM ghcr.io/rails/devcontainer/images/ruby:3.3.5 | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is devcontainer introduced only to run the mysql server? This container is pretty big and has other utilities and libraries being installed that I don't think the test suite needs.
Why not just use the mysql docker image? I can imagine having a bin/setup script that runs something like:
docker run -d \
--restart unless-stopped \
-p "127.0.0.1:3306:3306" \
--name=mysql84 \
-e MYSQL_ROOT_PASSWORD= -e MYSQL_ALLOW_EMPTY_PASSWORD=true \
mysql:8.4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My goal was to just make setting up mysql easy and be able to run all the tests with this. To do that you also need chrome and maybe these other things...? (I'll have to verify!)
That said though we could definitely do a simpler thing though. I'll take a look at this!
| runs-on: ubuntu-latest | ||
| services: | ||
| mysql: | ||
| image: mysql:8.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth setting up a matrix to test against 8 and 9? And maybe even patch releases like 8.4?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could! I'm not that worried that we're doing anything too novel that would break b/w versions of mysql though. Probably testing against the latest version is good enough 🤷♂️. But I'm very open to doing that if you think it's a good idea.
We're really only doing "SHOW DATABASES LIKE '#{database_path}' and DROP DATABASE IF EXISTS which will work across all versions. The rest of the stuff we're just calling the built-in Rails methods which are already well tested. Just not too worried about it I guess!
| puts "Creating integration app for #{COLOR_FG_BLUE}#{scenario}#{COLOR_RESET} at #{app_path}" | ||
|
|
||
| # Generate a unique database prefix for this scenario to avoid conflicts | ||
| db_prefix = "test_#{scenario[:database]}_#{scenario[:models]}_#{Process.pid}".gsub(/[^a-zA-Z0-9_]/, "_") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious what conflicts you're avoiding with this that DatabaseConfiguration#test_worker_id= isn't sufficient to prevent. Can you explain a bit more?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was having a bunch of contention issues before this when running integration tests in parallel.
In SQLite each test run uses a unique temporary directory which means that multiple test runs never touched the same database file.
But with MySQL it's different b/c we have the same shared server. What this means is that all parallel test runs originally used the same database name which caused collisions and contention. Different tests were trying to drop, create, or migrate using the same schema.
There are different ways we could set this up but I just went with an env variable that gets evaluated at runtime (<%= ENV.fetch('MYSQL_DB_PREFIX', 'primary') %>) from the database.yml. This happens when we db:drop, db:prepare, etc. and just ensures that each test database is unique from any others.
I actually didn't realize you could run the unit tests in parallel and I didn't implement something similar for them. But the problem you're seeing running the unit tests is the same problem that this was supposed to address.
| # create a fake tenant database to validate that it is deleted in the test suite | ||
| dev_db = Dir.glob("storage/development/*/development-tenant/main.sqlite3").first | ||
| test_db = dev_db.gsub("/development/", "/test/").gsub("development-tenant", "delete-me") | ||
| FileUtils.mkdir_p(File.dirname(test_db)) | ||
| FileUtils.touch(test_db) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this being removed? This test database needs to be here to make this test meaningful:
And I think the mysql implementation also needs to delete existing tenant databases at the start of the test suite, though you may need to change this setup to something that will work for both databases, maybe:
run_cmd(scenario, "bin/rails db:migrate", env: { ARTENANT: "delete-me" })There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may have misunderstood what this does. I just added in a db:drop at the start of the test run which I thought was a bit simpler / handles all database types. But the test you mentioned would be redundant then: https://github.com/basecamp/activerecord-tenanted/blob/main/test/integration/test/testcase_test.rb#L12-L15.
We can keep this in if you want! But we still need to db:drop with mysql just to ensure that we're starting from a clean slate every time.
|
|
||
| # set up the database and schema files | ||
| run_cmd(scenario, "bin/rails db:prepare") | ||
| adapter_config = {"RAILS_ENV" => "test","ARTENANT_SCHEMA_DUMP" => "1"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On main, the integration test suite runs rails db:prepare in the development environment, and then the tests get run in the test environment (implicitly setting up the databases).
Can you say a bit more about this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this in b/c I kept getting this error when trying to run any database tasks.
bin/rails db:drop (1.89s)
bin/rails aborted!
ActiveRecord::EnvironmentMismatchError: You are attempting to modify a database that was last run in `test` environment. (ActiveRecord::EnvironmentMismatchError)
You are running in `development` environment. If you are sure you want to continue, first set the environment using:
bin/rails db:environment:set RAILS_ENV=development
Tasks: TOP => db:drop => db:check_protected_environments
(See full trace by running task with --trace)
ERROR: Command failed
But I could easily be doing something wrong!
| rescue ActiveRecord::NoDatabaseError | ||
| database_already_initialized = false | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't feel like we should be rescuing this here. If you know this is a condition that will often happen, where table_exists? should return false, I'd prefer if we either fixed the method upstream and/or patched the method.
Looking at table_exists? I'm not sure if this error is raised by ConnectionPool#with_connection or ConnectionAdapter#data_source_exists?, do you know?
| # dump the schema and schema cache | ||
| if Rails.env.development? || ENV["ARTENANT_SCHEMA_DUMP"].present? | ||
| if migrated | ||
| if migrated || !database_already_initialized |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to add the check for !database_already_initialized, unless I'm misunderstanding the intent. The tests all pass without it.
| rescue Capybara::ElementNotFound | ||
| sleep 0.1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this method rescuing and sleeping? Seems like it should raise an exception if the assert_selector failed.
| end | ||
| end | ||
|
|
||
| describe "new_tenant_config" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oof, this made me realize how little test coverage the gem has for this method. 😬
| test "only returns tenants from tenanted databases, not shared databases" do | ||
| assert_empty(base_config.tenants) | ||
|
|
||
| TenantedApplicationRecord.create_tenant("foo") | ||
|
|
||
| assert_equal([ "foo" ], base_config.tenants) | ||
|
|
||
| all_databases = ActiveRecord::Base.configurations.configs_for(env_name: base_config.env_name) | ||
|
|
||
| untenanted_config = all_databases.reject { |c| c.configuration_hash[:tenanted] } | ||
| untenanted_config.each do |shared_config| | ||
| assert_not_includes(base_config.tenants, shared_config.database) | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 This test implicitly depends on the naming in the mysql scenarios' database.yml files, so it's not testing anything under sqlite.
Instead, I'd like to rewrite this test to explicitly modify ActiveRecord::Base.configurations so there is an additional untenanted config that is named like a tenant.
|
Thanks Mike! Feel free to make any changes as you see fit. I won’t have time to go through the feedback today but I will circle back this week. 👌 |
|
@andrewmarkle I'm dealing with a fire drill at work, so I may not have much time this week. I'm very keen to land this so it's at the top of my "OSS to-do list". 🙌 |
Add MySQL Support
This PR adds full MySQL support enabling the gem to work with both
mysql2andtrilogyadapters alongside the existing SQLite support.This is a bit of a big PR. If you prefer I would happily split this up into smaller chunks:
What's New
MySQL Database Adapter
ActiveRecord::Tenanted::DatabaseAdapters::MySQLwith full feature parity to the SQLite adaptermysql2andtrilogyadapters in the adapter registryDevcontainer support
Testing Infrastructure
bin/test-integration) to:MYSQL_UNIQUE_PREFIX,MYSQL_HOST)Dependencies
mysql2gem to development/test dependencies in GemfileTechnical Details
The MySQL adapter implements the same interface as the SQLite adapter but with MySQL-specific behavior:
SHOW DATABASES LIKE 'pattern') instead of file globbingTesting
All existing tests should pass with both SQLite and MySQL adapters. The integration test suite now runs scenarios against both database types, ensuring feature parity.
To test locally I've been using the devcontainer.
Known Limitations
There are some features still missing to fully support MySQL. The main one is supporting the use-case for using a DATABASE_URL to set everything up. In theory this should just work but I haven't tested yet and want to fully test this in the gem. I'll open a PR for this later!
Breaking Changes
None. This is purely additive functionality.