Module: Mutineer::RailsWorkerDb
- Defined in:
- lib/mutineer/rails_worker_db.rb
Overview
#26/#27 Phase 2b (U5) — 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" (R10/KTD-8): 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 (KTD-7): each parallel worker gets its OWN database so concurrent forks can't clobber each other's transactional fixtures (the measured #26 corruption). 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: this pass ships the SQLite adapter (per-worker file, hermetic,
spike-proven). Postgres per-worker provisioning (CREATE DATABASE <db>-<worker> —
the measured corruption case) extends RailsWorkerDb.worker_db_config/RailsWorkerDb.provision at the marked
seam in U10; until then a non-SQLite config raises a clear NotImplementedError
rather than silently mis-routing.
Honest limit (KTD-5): routing failures surface as error via RailsWorkerDb.verify_connection!.
Re-raising an AR error that fires inside a test body past Minitest (so an in-test
DB failure is error, not killed) is only observable under concurrent load and
is deferred to U6 with the parallel gate — noted, not silently skipped.
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 — the only condition under which any other method here may touch AR.
-
.load_schema(schema_path) ⇒ void
Load a Rails
schema.rbinto the current connection with its output silenced, so a parent-side RailsWorkerDb.provision call can never spill schema chatter onto the daemon IPC pipe. -
.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.
-
.provision(worker_count, schema_path) ⇒ void
Explicit, parent-side provisioning: create + schema-load every worker database up front, then restore the app's default connection.
-
.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.
107 108 109 110 111 112 113 |
# File 'lib/mutineer/rails_worker_db.rb', line 107 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.
34 35 36 |
# File 'lib/mutineer/rails_worker_db.rb', line 34 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 its output silenced,
so a parent-side provision call can never spill schema chatter onto the daemon
IPC pipe. (In a fork the child's stdout is already File::NULL; this guards the
parent path too.)
143 144 145 146 147 148 149 150 151 |
# File 'lib/mutineer/rails_worker_db.rb', line 143 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 + unit-tested so the Postgres
SHAPE is proven ready for U10 without a live database.
87 88 89 90 91 92 93 94 95 96 |
# File 'lib/mutineer/rails_worker_db.rb', line 87 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 |
.provision(worker_count, schema_path) ⇒ void
This method returns an undefined value.
Explicit, parent-side provisioning: create + schema-load every worker database
up front, then restore the app's default connection. This is the EXPLICIT
provisioning entry point (V2 — never silent auto-create mid-audit) and the seam
U10 extends for Postgres (CREATE DATABASE per worker). For SQLite the files are
created on connect and the schema load makes them ready; idempotent to re-run.
124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/mutineer/rails_worker_db.rb', line 124 def self.provision(worker_count, schema_path) return unless available? original = ActiveRecord::Base.connection_db_config (0...worker_count).each do |worker| ActiveRecord::Base.establish_connection(worker_db_config(worker)) load_schema(schema_path) if schema_path end ensure ActiveRecord::Base.establish_connection(original) if original 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).
157 158 159 |
# File 'lib/mutineer/rails_worker_db.rb', line 157 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.
45 46 47 48 |
# File 'lib/mutineer/rails_worker_db.rb', line 45 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 (Postgres is U10).
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/mutineer/rails_worker_db.rb', line 58 def self.worker_db_config(worker) hash = ActiveRecord::Base.connection_db_config.configuration_hash adapter = hash[:adapter].to_s # U10 seam: the config-SHAPING below (per_worker_config) is already # adapter-general — it derives correct SQLite *and* Postgres worker-DB names. # What's gated is runtime PROVISIONING: SQLite files are created on connect, # but Postgres needs an explicit `CREATE DATABASE` per worker (U10). Until that # lands, refuse non-SQLite loudly rather than route to a database that doesn't # exist. To finish U10: implement provision() for PG and drop this guard. unless adapter.start_with?("sqlite") raise NotImplementedError, "worker-DB isolation currently provisions SQLite only (got adapter #{adapter.inspect}); " \ "Postgres per-worker provisioning is U10 (#26/#35) — use a SQLite test DB, or drop --jobs." end per_worker_config(hash, worker) end |