Module: TalkToYourApp::ConnectionRegistry

Defined in:
lib/talk_to_your_app/connection_registry.rb

Overview

The named-connection registry. Operators declare connections in the initializer (‘config.connection :name, database:, role:`); plugins reference them by the gem-internal name. The registry’s job is twofold:

1. Fail closed at boot — if a plugin requires a connection that was never
   declared, or a declared connection points at a database.yml key that
   does not exist, raise during boot rather than at the first request.
2. Switch connections at call time via Rails' `connected_to`, so each tool
   runs against the role its plugin declared (reads on a reader, writes on
   a writer) without coupling plugin code to the host's database.yml.

Defined Under Namespace

Classes: ConnectionSpec

Constant Summary collapse

BUILD_MUTEX =

Guards lazy pool-class construction against concurrent first-calls.

Mutex.new

Class Method Summary collapse

Class Method Details

.build_connection_class(spec, index) ⇒ Object

‘connects_to` rejects anonymous classes, so each pool class gets a stable constant name. The index suffix guarantees uniqueness even when two database keys differ only in characters `W`-normalization would collapse.



98
99
100
101
102
103
104
105
# File 'lib/talk_to_your_app/connection_registry.rb', line 98

def build_connection_class(spec, index)
  const_name = "Conn#{index}_#{spec.database}_#{spec.role}".gsub(/\W/, "_")
  remove_const(const_name) if const_defined?(const_name, false)
  klass = Class.new(ActiveRecord::Base) { self.abstract_class = true }
  const_set(const_name, klass)
  klass.connects_to(database: { spec.role => spec.database })
  klass
end

.connection_class_for(spec) ⇒ Object

Lazily builds (and caches) an abstract ActiveRecord class wired to the spec’s database under its role. Cached by (database, role) so the same declared connection reuses one pool across calls. The build is guarded by a mutex so concurrent first-calls under a threaded server cannot create two pools for the same key.



85
86
87
88
89
90
91
92
93
# File 'lib/talk_to_your_app/connection_registry.rb', line 85

def connection_class_for(spec)
  cache_key = [spec.database, spec.role]
  existing = connection_classes[cache_key]
  return existing if existing

  BUILD_MUTEX.synchronize do
    connection_classes[cache_key] ||= build_connection_class(spec, connection_classes.size)
  end
end

.connection_classesObject



127
128
129
# File 'lib/talk_to_your_app/connection_registry.rb', line 127

def connection_classes
  @connection_classes ||= {}
end

.database_config_exists?(db_key) ⇒ Boolean

Returns:

  • (Boolean)


117
118
119
120
121
# File 'lib/talk_to_your_app/connection_registry.rb', line 117

def database_config_exists?(db_key)
  ActiveRecord::Base.configurations
    .configs_for(env_name: env_name, include_hidden: true)
    .any? { |config| config.name.to_sym == db_key.to_sym }
end

.env_nameObject



123
124
125
# File 'lib/talk_to_your_app/connection_registry.rb', line 123

def env_name
  (defined?(Rails) && Rails.respond_to?(:env) && Rails.env.to_s) || ENV["RAILS_ENV"] || "test"
end

.fetch(name) ⇒ Object



41
42
43
44
45
# File 'lib/talk_to_your_app/connection_registry.rb', line 41

def fetch(name)
  specs[name.to_sym] ||
    raise(ConfigurationError, "talk_to_your_app: connection #{name.inspect} is not registered. " \
      "Declare it with `config.connection #{name.inspect}, database: ..., role: ...` in your initializer.")
end

.registered?(name) ⇒ Boolean

Returns:

  • (Boolean)


37
38
39
# File 'lib/talk_to_your_app/connection_registry.rb', line 37

def registered?(name)
  specs.key?(name.to_sym)
end

.reset!Object

Test seam: drop the cached pool classes and their constants so a reconfiguration in a later test does not reuse a stale pool. Wired into TalkToYourApp.reset_configuration!.



110
111
112
113
114
115
# File 'lib/talk_to_your_app/connection_registry.rb', line 110

def reset!
  BUILD_MUTEX.synchronize do
    constants(false).grep(/\AConn\d+_/).each { |const| remove_const(const) }
    @connection_classes = {}
  end
end

.specsObject



33
34
35
# File 'lib/talk_to_your_app/connection_registry.rb', line 33

def specs
  TalkToYourApp.configuration.connections
end

.validate!(requirements) ⇒ Object

Fail-closed boot check. ‘requirements` is an array of

connection_name, requester_label

pairs gathered from enabled plugins.

Raises ConfigurationError naming every missing connection and who needs it.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/talk_to_your_app/connection_registry.rb', line 50

def validate!(requirements)
  missing = requirements.reject { |name, _requester| registered?(name) }
  unless missing.empty?
    details = missing.map { |name, requester| "#{name.inspect} (required by #{requester})" }.join(", ")
    raise ConfigurationError,
      "talk_to_your_app: missing required connection(s): #{details}. " \
      "Declare them with `config.connection ...` in config/initializers/talk_to_your_app.rb."
  end

  requirements.each do |name, requester|
    spec = fetch(name)
    next if database_config_exists?(spec.database)

    raise ConfigurationError,
      "talk_to_your_app: connection #{name.inspect} (required by #{requester}) references database " \
      "#{spec.database.inspect}, which is not configured in database.yml for the #{env_name.inspect} environment."
  end
end

.with(name) ⇒ Object

Runs the block against the connection declared under ‘name`, switched to the spec’s role. The connection is yielded; the role switch is unwound when the block returns, so nothing leaks into the surrounding request.



72
73
74
75
76
77
78
# File 'lib/talk_to_your_app/connection_registry.rb', line 72

def with(name)
  spec = fetch(name)
  klass = connection_class_for(spec)
  klass.connected_to(role: spec.role) do
    yield klass.connection
  end
end