Module: Mutineer::RailsWorkerDb
- Defined in:
- lib/mutineer/rails_worker_db.rb
Overview
Per-worker database isolation for the daemon path.
Loaded APP-SIDE by DaemonServer (a sibling gem file, pulled in by absolute
path so it bypasses the app bundle, the same trick DaemonClient uses to run
daemon_server.rb under a bundle that has no mutineer). It uses the app's
OWN already-booted ActiveRecord and NEVER require "active_record": every
method that touches AR first confirms RailsWorkerDb.available?, so the daemon core stays
framework-agnostic and the gem keeps its zero-runtime-dependency promise.
Isolation model: each parallel worker gets its OWN database so concurrent forks cannot clobber each other's transactional fixtures. RailsWorkerDb.after_fork runs inside a freshly-forked child and points that child's connection at the worker's database BEFORE any test loads; transactional fixtures then repopulate that isolated database per test.
Scope: SQLite adapter only (per-worker file, hermetic). Postgres per-worker
DBs (CREATE DATABASE <db>-<worker>) are not implemented yet; a non-SQLite
config raises a clear NotImplementedError rather than silently mis-routing.
Routing failures surface as error via RailsWorkerDb.verify_connection!. Tagging an
in-test DB failure as error (not killed) is only observable under
concurrent load and is not yet implemented.
Class Method Summary collapse
-
.after_fork(worker, schema_path = nil) ⇒ void
Child-side (after fork): route this process's ActiveRecord at the worker's own database and confirm it is reachable, so a routing failure reads as
error(via the daemon's child rescue) rather than a false verdict. -
.available? ⇒ Boolean
True when the app has ActiveRecord loaded.
-
.load_schema(schema_path) ⇒ void
Load a Rails
schema.rbinto the current connection with output silenced (fork child stdout is already File::NULL; this is belt-and-braces). -
.per_worker_config(config_hash, worker) ⇒ Hash
Pure config-shaping (no AR): given a connection config hash, return the per-worker variant with its database swapped to the worker's own name.
-
.verify_connection! ⇒ void
Force a round-trip to the freshly-routed connection so a broken route fails HERE (→
error) instead of later masquerading as a test failure (→ falsekilled). -
.worker_database_path(database, worker) ⇒ String
Derive a per-worker database path from a base path by inserting
-<worker>before the extension. -
.worker_db_config(worker) ⇒ Hash
Build the AR connection config for one worker by copying the app's current (default test) config and swapping in the per-worker database path.
Class Method Details
.after_fork(worker, schema_path = nil) ⇒ void
This method returns an undefined value.
Child-side (after fork): route this process's ActiveRecord at the worker's
own database and confirm it is reachable, so a routing failure reads as
error (via the daemon's child rescue) rather than a false verdict. Loads
the schema into the worker database when a schema path is given
(idempotent: schema.rb runs with force: true), covering a fresh worker
file.
106 107 108 109 110 111 112 |
# File 'lib/mutineer/rails_worker_db.rb', line 106 def self.after_fork(worker, schema_path = nil) return unless available? ActiveRecord::Base.establish_connection(worker_db_config(worker)) load_schema(schema_path) if schema_path verify_connection! end |
.available? ⇒ Boolean
True when the app has ActiveRecord loaded. The only condition under which any other method here may touch AR. Never triggers an autoload/require of AR itself.
32 33 34 |
# File 'lib/mutineer/rails_worker_db.rb', line 32 def self.available? defined?(ActiveRecord::Base) ? true : false end |
.load_schema(schema_path) ⇒ void
This method returns an undefined value.
Load a Rails schema.rb into the current connection with output silenced
(fork child stdout is already File::NULL; this is belt-and-braces).
119 120 121 122 123 124 125 126 127 |
# File 'lib/mutineer/rails_worker_db.rb', line 119 def self.load_schema(schema_path) ActiveRecord::Migration.verbose = false if defined?(ActiveRecord::Migration) original = $stdout $stdout = File.open(File::NULL, "w") load schema_path ensure $stdout.close unless $stdout.equal?(original) $stdout = original end |
.per_worker_config(config_hash, worker) ⇒ Hash
Pure config-shaping (no AR): given a connection config hash, return the
per-worker variant with its database swapped to the worker's own name.
Adapter-general: SQLite (storage/test.sqlite3 -> storage/test-<w>.sqlite3)
and Postgres (myapp_test -> myapp_test-<w>, Rails parallelize naming)
both fall out of worker_database_path. Extracted and unit-tested so the
Postgres shape is proven ready without a live database.
85 86 87 88 89 90 91 92 93 94 |
# File 'lib/mutineer/rails_worker_db.rb', line 85 def self.per_worker_config(config_hash, worker) hash = config_hash.transform_keys(&:to_sym) database = hash[:database].to_s if database.empty? || database == ":memory:" raise NotImplementedError, "worker-DB isolation needs a file/name-backed database (got #{database.inspect})." end hash.merge(database: worker_database_path(database, worker)) end |
.verify_connection! ⇒ void
This method returns an undefined value.
Force a round-trip to the freshly-routed connection so a broken route fails
HERE (→ error) instead of later masquerading as a test failure
(→ false killed).
134 135 136 |
# File 'lib/mutineer/rails_worker_db.rb', line 134 def self.verify_connection! ActiveRecord::Base.connection.execute("SELECT 1") end |
.worker_database_path(database, worker) ⇒ String
Derive a per-worker database path from a base path by inserting -<worker>
before the extension. Pure string transform (no AR) so it is unit-testable
in the zero-dep suite. storage/test.sqlite3, worker 1 ->
storage/test-1.sqlite3.
44 45 46 47 |
# File 'lib/mutineer/rails_worker_db.rb', line 44 def self.worker_database_path(database, worker) ext = File.extname(database) "#{database.delete_suffix(ext)}-#{worker}#{ext}" end |
.worker_db_config(worker) ⇒ Hash
Build the AR connection config for one worker by copying the app's current (default test) config and swapping in the per-worker database path. SQLite only this pass: a non-SQLite adapter raises so the SQLite-first scope fails loud instead of mis-routing.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/mutineer/rails_worker_db.rb', line 57 def self.worker_db_config(worker) hash = ActiveRecord::Base.connection_db_config.configuration_hash adapter = hash[:adapter].to_s # Config shaping (per_worker_config) is already adapter-general: it derives # correct SQLite and Postgres worker-DB names. What is gated is runtime # provisioning: SQLite files are created on connect, but Postgres needs an # explicit `CREATE DATABASE` per worker. Until that lands, refuse non-SQLite # loudly rather than route to a database that does not exist. unless adapter.start_with?("sqlite") raise NotImplementedError, "worker-DB isolation currently provisions SQLite only (got adapter #{adapter.inspect}); " \ "Postgres per-worker provisioning is not yet supported. Use a SQLite test DB, or drop --jobs." end per_worker_config(hash, worker) end |