Module: Apartment::MigrationRole
- Defined in:
- lib/apartment/migration_role.rb
Overview
Runs a block on the connection role configured for DDL (+config.ddl_role+).
This lives outside Migrator because two layers need it and they sit on opposite
sides of the dependency arrow: Migrator drives Tenant.switch, and the adapters
it switches into issue DDL of their own at tenant-create time. Migrator keeps
with_migration_role as its documented entry point and delegates here, so the
CLI and the adapters share one implementation without an adapter reaching up
into Migrator.
Class Method Summary collapse
-
.role_pool_registered?(role) ⇒ Boolean
Whether ActiveRecord can resolve a pool for
roleon the current shard. -
.wrap { ... } ⇒ Object
The resolution check lives here rather than at
activate!becauseactivate!runs inafter_initialize, which railties fire after the eager-load initializer: under lazy loading (development, most test setups) no model has runconnects_toyet, so "configured but not loaded" is indistinguishable from "missing" and a boot-time check would fail on every boot.
Class Method Details
.role_pool_registered?(role) ⇒ Boolean
Whether ActiveRecord can resolve a pool for role on the current shard.
retrieve_connection_pool returns nil for an unregistered role rather than
raising, which is what makes it usable as a classifier here.
Conservative on its own failure: if the probe cannot answer, report the role as registered so the original error re-raises as itself. Misreporting the other way would blame ddl_role for someone else's problem.
87 88 89 90 91 92 93 94 95 |
# File 'lib/apartment/migration_role.rb', line 87 def role_pool_registered?(role) !ActiveRecord::Base.connection_handler.retrieve_connection_pool( ActiveRecord::Base.connection_specification_name, role: role, shard: ActiveRecord::Base.current_shard ).nil? rescue StandardError true end |
.wrap { ... } ⇒ Object
The resolution check lives here rather than at activate! because activate!
runs in after_initialize, which railties fire after the eager-load
initializer: under lazy loading (development, most test setups) no model has run
connects_to yet, so "configured but not loaded" is indistinguishable from
"missing" and a boot-time check would fail on every boot.
Nothing is probed before yielding, and that is deliberate twice over.
connected_to resolves no pool — with_role_and_shard pushes onto
connected_to_stack and yields — so an unregistered role cannot be detected
until the block asks for a connection. A wrap whose block never touches the
database therefore stays SILENT even with an unresolvable role, which is
correct: it did not need the role to exist. Probing up front would invent that
failure, and it would reintroduce the boot-order problem above for a role
registered lazily on a model load that has not happened yet.
So the error is classified after the fact, in the shape
AbstractAdapter#tenant_container_gone? uses: a cheap error-shape check, then an
authoritative probe of whether OUR role resolves. Nil means the failure is ours to
explain. Non-nil means the role is fine and the error belongs to something the
caller did inside the block, so it re-raises untouched.
The probe is the discriminator; the class is only a pre-filter, and on the Rails
floor it cannot filter at all. ActiveRecord::ConnectionNotDefined does not exist
before Rails 8.0 (absent on 7.2.3.1, present on 8.0.5 and 8.1.3), and Ruby
resolves a rescue clause's constants at raise time — so naming it here raised
NameError on 7.2 and destroyed the error it was meant to classify. 7.2 raises
ConnectionNotEstablished for an unregistered role and 8.0+ raises the subclass,
so the superclass alone covers the whole matrix. Do not narrow it back.
The cost of the wider clause is that a bare ConnectionNotEstablished from the
caller's own code now reaches the probe instead of passing straight through. That
is harmless: a registered role re-raises it exactly as before, and if our role is
missing then naming ddl_role is correct advice whatever the immediate error was.
No wrapped error still NEEDS translating here, which is why the clause names only
ActiveRecord's. Errors can still arrive wrapped in Apartment::ApartmentError —
Patches::ConnectionHandling#connection_pool relabels failures raised while
resolving a TENANT pool — but those are genuine tenant-pool failures and belong to
the tenant, not to ddl_role; translating them would misattribute. A ddl_role
failure is not among them: it surfaces from the default-path lookup the early
guards hand to super, outside that method's rescue boundary, and the tenant path
cannot produce one at all because it establishes a pool for the role itself and so
never fails on an unregistered one (pinned by the base-config-fallback examples in
spec/unit/patches/connection_handling_spec.rb). The ApartmentError clause and the
one-layer unwrap that used to sit here existed only for the method-level rescue
that covered those guards, and went with it.
66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/apartment/migration_role.rb', line 66 def wrap(&) role = Apartment.config.ddl_role return yield unless role ActiveRecord::Base.connected_to(role: role, &) rescue ActiveRecord::ConnectionNotEstablished => e raise if role_pool_registered?(role) raise(Apartment::ConfigurationError, "ddl_role is set to #{role.inspect} but ActiveRecord has no connection " \ 'registered for that role. Declare it with connects_to on your ' \ "application record, or unset ddl_role. (#{e.class}: #{e.})") end |