Module: Raif::EvalsDatabase
- Defined in:
- lib/raif/evals_database.rb
Overview
Points raif evals at its own database, so a run neither collides with the host's test suite on
the test database nor leaves a spec waiting on an eval's uncommitted rows. Evals still boot in
the test environment, because that is where host apps load FactoryBot, WebMock and friends; only
the database name changes, and only when Raif.config.evals_database_suffix is set.
Defined Under Namespace
Classes: InUseError
Constant Summary collapse
- IN_MEMORY_DATABASES =
Nothing to name: an in-memory SQLite database is already private to the process.
[":memory:"].freeze
Class Method Summary collapse
-
.configure! ⇒ Object
Renames every database the test environment connects to, replicas included, so a host with more than one database cannot leave some of its models writing to the test database.
- .database_names ⇒ Object
- .enabled? ⇒ Boolean
- .in_memory?(database) ⇒ Boolean
-
.lock! ⇒ Object
Held for the life of the run, so a second run cannot truncate this one's databases under it.
-
.name_for(database, suffix:, replacing: "_test") ⇒ Object
app_test becomes app_raif_evals.
-
.prepare! ⇒ Object
Creates each evals database that does not exist and loads the schema into any whose schema is out of date, the same way Rails prepares its parallel test databases.
- .rename(configurations, env_name:, suffix:, replacing: "_test") ⇒ Object
- .switch_to(suffix, replacing:, lock: false) ⇒ Object
-
.use_worker_database!(worker_number) ⇒ Object
Moves a forked eval worker onto a database of its own - app_raif_evals becomes app_raif_evals_2 - so its uncommitted rows cannot hold up the other workers.
Class Method Details
.configure! ⇒ Object
Renames every database the test environment connects to, replicas included, so a host with more than one database cannot leave some of its models writing to the test database. Runs in after_initialize, once the host's Raif initializer has set the suffix. Pools established before then - eager loading runs connects_to during boot - share these config objects, so renaming in place and dropping any open connections moves them too.
Prepared in the same hook as the rename, not later: the rest of boot can already query the renamed database - routes load after after_initialize and can load models whose class bodies read the schema - and a missing or empty database fails the boot there.
33 34 35 36 37 38 39 |
# File 'lib/raif/evals_database.rb', line 33 def configure! return unless enabled? ActiveSupport.on_load(:active_record) do Raif::EvalsDatabase.switch_to(Raif.config.evals_database_suffix, replacing: "_test", lock: true) end end |
.database_names ⇒ Object
115 116 117 |
# File 'lib/raif/evals_database.rb', line 115 def database_names managed_configs(ActiveRecord::Base.configurations, env_name: Rails.env).map(&:database).uniq end |
.enabled? ⇒ Boolean
16 17 18 |
# File 'lib/raif/evals_database.rb', line 16 def enabled? Raif.running_evals? && Rails.env.test? && Raif.config.evals_database_suffix.present? end |
.in_memory?(database) ⇒ Boolean
20 21 22 |
# File 'lib/raif/evals_database.rb', line 20 def in_memory?(database) IN_MEMORY_DATABASES.include?(database) || database.include?("mode=memory") end |
.lock! ⇒ Object
Held for the life of the run, so a second run cannot truncate this one's databases under it. Preparing a database truncates it, and so does each worker preparing its own. The workers inherit the lock when they fork, and the worker databases are named after the locked one, so this one lock covers them too.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/raif/evals_database.rb', line 96 def lock! database = database_names.first return if database.blank? || in_memory?(database) path = Rails.root.join("tmp", "raif_evals-#{database.gsub(/[^\w.-]+/, "_")}.lock") FileUtils.mkdir_p(path.dirname) file = File.open(path, File::RDWR | File::CREAT) unless file.flock(File::LOCK_EX | File::LOCK_NB) file.close raise InUseError, "Another raif evals run is using the #{database} database. Starting this run would " \ "truncate its tables under it. Wait for that run to finish, or give this run a database of its own " \ "with a different Raif.config.evals_database_suffix." end # Kept open: closing the file, or letting it be garbage collected, releases the lock. @lock_file = file end |
.name_for(database, suffix:, replacing: "_test") ⇒ Object
app_test becomes app_raif_evals. A name without a _test suffix keeps its name and gains the suffix, and a SQLite file keeps its extension: db/test.sqlite3 becomes db/test_raif_evals.sqlite3. A worker database passes replacing: nil, since its name only gains the worker number. A SQLite URI keeps its query: file:db/test.sqlite3?mode=rwc becomes file:db/test_raif_evals.sqlite3?mode=rwc.
66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/raif/evals_database.rb', line 66 def name_for(database, suffix:, replacing: "_test") return database if in_memory?(database) path, separator, query = database.start_with?("file:") ? database.partition("?") : [database, "", ""] extension = File.extname(path) extension = "" unless extension.match?(/\A\.sqlite3?\z/) base = path.delete_suffix(extension) base = base.delete_suffix(replacing) if replacing "#{base}#{suffix}#{extension}#{separator}#{query}" end |
.prepare! ⇒ Object
Creates each evals database that does not exist and loads the schema into any whose schema is out of date, the same way Rails prepares its parallel test databases. An up-to-date database is truncated instead, so every run starts from empty tables.
81 82 83 84 85 86 87 88 89 90 |
# File 'lib/raif/evals_database.rb', line 81 def prepare! verbose_was, ENV["VERBOSE"] = ENV["VERBOSE"], "false" managed_configs(ActiveRecord::Base.configurations, env_name: Rails.env).uniq(&:database).each do |db_config| reconstruct_from_schema(db_config) end ensure ActiveRecord::Base.establish_connection ENV["VERBOSE"] = verbose_was end |
.rename(configurations, env_name:, suffix:, replacing: "_test") ⇒ Object
56 57 58 59 60 |
# File 'lib/raif/evals_database.rb', line 56 def rename(configurations, env_name:, suffix:, replacing: "_test") managed_configs(configurations, env_name: env_name).each do |db_config| db_config._database = name_for(db_config.database, suffix: suffix, replacing: replacing) end end |
.switch_to(suffix, replacing:, lock: false) ⇒ Object
49 50 51 52 53 54 |
# File 'lib/raif/evals_database.rb', line 49 def switch_to(suffix, replacing:, lock: false) rename(ActiveRecord::Base.configurations, env_name: Rails.env, suffix: suffix, replacing: replacing) lock! if lock ActiveRecord::Base.connection_handler.clear_all_connections!(:all) prepare! end |
.use_worker_database!(worker_number) ⇒ Object
Moves a forked eval worker onto a database of its own - app_raif_evals becomes app_raif_evals_2 - so its uncommitted rows cannot hold up the other workers. Two workers sharing one database block on any unique index both write the same key to, which a shared fixture or a repeated case does on every execution.
45 46 47 |
# File 'lib/raif/evals_database.rb', line 45 def use_worker_database!(worker_number) switch_to("_#{worker_number}", replacing: nil) end |