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

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.

Parameters:

  • worker (Integer)

    the worker slot index.

  • schema_path (String, nil) (defaults to: nil)

    absolute path to db/schema.rb, or nil to skip.



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.

Returns:

  • (Boolean)


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).

Parameters:

  • schema_path (String)

    absolute path to db/schema.rb.



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.

Parameters:

  • config_hash (Hash)

    a connection config hash (symbol or string keys).

  • worker (Integer)

    the worker slot index.

Returns:

  • (Hash)

    the per-worker config (symbol keys), database swapped.

Raises:

  • (NotImplementedError)

    for an in-memory or empty database (no per-worker split).



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.

Parameters:

  • database (String)

    the base database path.

  • worker (Integer)

    the worker slot index (0..N-1).

Returns:

  • (String)

    the per-worker database path.



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.

Parameters:

  • worker (Integer)

    the worker slot index.

Returns:

  • (Hash)

    a symbol-keyed AR configuration hash for the worker database.

Raises:

  • (NotImplementedError)

    when the app's database is non-SQLite or in-memory.



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