Class: Apartment::Config

Inherits:
Object
  • Object
show all
Defined in:
lib/apartment/config.rb

Overview

Configuration object for Apartment v4. Created via Apartment.configure block; validated after the block yields.

Constant Summary collapse

VALID_STRATEGIES =

rubocop:disable Metrics/ClassLength

%i[schema database_name shard database_config].freeze
VALID_ENVIRONMENTIFY_STRATEGIES =
[nil, :prepend, :append].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConfig

rubocop:disable Metrics/AbcSize



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/apartment/config.rb', line 32

def initialize # rubocop:disable Metrics/AbcSize
  @tenant_strategy = nil
  @tenants_provider = nil
  @default_tenant = nil
  @default_tenant_switch_allowed = true
  @excluded_models = []
  @tenant_pool_size = nil
  @pool_idle_timeout = 300
  @reaper_interval = nil
  @max_total_connections = nil
  @max_tenant_pools = nil
  @max_tenant_connections = nil
  @pool_overflow_policy = :evict_idle
  @seed_after_create = false
  @seed_data_file = nil
  @schema_load_strategy = nil
  @schema_file = nil
  @parallel_migration_threads = 0
  @environmentify_strategy = nil
  @elevator = nil
  @elevator_options = {}
  @tenant_not_found_handler = nil
  @tenant_validator = nil
  @active_record_log = false
  @sql_query_tags = false
  @postgres_config = nil
  @mysql_config = nil
  @shard_key_prefix = 'apartment'
  @migration_role = nil
  @app_role = nil
  @schema_cache_per_tenant = false
  @check_pending_migrations = true
  @force_separate_pinned_pool = false
  @reap_in_test = false
  @test_fixture_cleanup = true
end

Instance Attribute Details

#active_record_logObject

Returns the value of attribute active_record_log.



17
18
19
# File 'lib/apartment/config.rb', line 17

def active_record_log
  @active_record_log
end

#app_roleObject

Returns the value of attribute app_role.



17
18
19
# File 'lib/apartment/config.rb', line 17

def app_role
  @app_role
end

#check_pending_migrationsObject

Returns the value of attribute check_pending_migrations.



17
18
19
# File 'lib/apartment/config.rb', line 17

def check_pending_migrations
  @check_pending_migrations
end

#default_tenantObject

Returns the value of attribute default_tenant.



17
18
19
# File 'lib/apartment/config.rb', line 17

def default_tenant
  @default_tenant
end

#default_tenant_switch_allowedObject

Returns the value of attribute default_tenant_switch_allowed.



17
18
19
# File 'lib/apartment/config.rb', line 17

def default_tenant_switch_allowed
  @default_tenant_switch_allowed
end

#elevatorObject

Returns the value of attribute elevator.



17
18
19
# File 'lib/apartment/config.rb', line 17

def elevator
  @elevator
end

#elevator_optionsObject

Returns the value of attribute elevator_options.



17
18
19
# File 'lib/apartment/config.rb', line 17

def elevator_options
  @elevator_options
end

#environmentify_strategyObject

Returns the value of attribute environmentify_strategy.



14
15
16
# File 'lib/apartment/config.rb', line 14

def environmentify_strategy
  @environmentify_strategy
end

#excluded_modelsObject

Returns the value of attribute excluded_models.



14
15
16
# File 'lib/apartment/config.rb', line 14

def excluded_models
  @excluded_models
end

#force_separate_pinned_poolObject

Returns the value of attribute force_separate_pinned_pool.



17
18
19
# File 'lib/apartment/config.rb', line 17

def force_separate_pinned_pool
  @force_separate_pinned_pool
end

#max_tenant_connectionsObject

Returns the value of attribute max_tenant_connections.



17
18
19
# File 'lib/apartment/config.rb', line 17

def max_tenant_connections
  @max_tenant_connections
end

#max_tenant_poolsObject

Returns the value of attribute max_tenant_pools.



17
18
19
# File 'lib/apartment/config.rb', line 17

def max_tenant_pools
  @max_tenant_pools
end

#max_total_connectionsObject

Returns the value of attribute max_total_connections.



17
18
19
# File 'lib/apartment/config.rb', line 17

def max_total_connections
  @max_total_connections
end

#migration_roleObject

Returns the value of attribute migration_role.



17
18
19
# File 'lib/apartment/config.rb', line 17

def migration_role
  @migration_role
end

#mysql_configObject (readonly)

Returns the value of attribute mysql_config.



14
15
16
# File 'lib/apartment/config.rb', line 14

def mysql_config
  @mysql_config
end

#parallel_migration_threadsObject

Returns the value of attribute parallel_migration_threads.



17
18
19
# File 'lib/apartment/config.rb', line 17

def parallel_migration_threads
  @parallel_migration_threads
end

#pool_idle_timeoutObject

Returns the value of attribute pool_idle_timeout.



17
18
19
# File 'lib/apartment/config.rb', line 17

def pool_idle_timeout
  @pool_idle_timeout
end

#pool_overflow_policyObject

Returns the value of attribute pool_overflow_policy.



17
18
19
# File 'lib/apartment/config.rb', line 17

def pool_overflow_policy
  @pool_overflow_policy
end

#postgres_configObject (readonly)

Returns the value of attribute postgres_config.



14
15
16
# File 'lib/apartment/config.rb', line 14

def postgres_config
  @postgres_config
end

#reap_in_testObject

Returns the value of attribute reap_in_test.



17
18
19
# File 'lib/apartment/config.rb', line 17

def reap_in_test
  @reap_in_test
end

#reaper_intervalObject

Returns the value of attribute reaper_interval.



17
18
19
# File 'lib/apartment/config.rb', line 17

def reaper_interval
  @reaper_interval
end

#schema_cache_per_tenantObject

Returns the value of attribute schema_cache_per_tenant.



17
18
19
# File 'lib/apartment/config.rb', line 17

def schema_cache_per_tenant
  @schema_cache_per_tenant
end

#schema_fileObject

Returns the value of attribute schema_file.



17
18
19
# File 'lib/apartment/config.rb', line 17

def schema_file
  @schema_file
end

#schema_load_strategyObject

Returns the value of attribute schema_load_strategy.



17
18
19
# File 'lib/apartment/config.rb', line 17

def schema_load_strategy
  @schema_load_strategy
end

#seed_after_createObject

Returns the value of attribute seed_after_create.



17
18
19
# File 'lib/apartment/config.rb', line 17

def seed_after_create
  @seed_after_create
end

#seed_data_fileObject

Returns the value of attribute seed_data_file.



17
18
19
# File 'lib/apartment/config.rb', line 17

def seed_data_file
  @seed_data_file
end

#shard_key_prefixObject

Returns the value of attribute shard_key_prefix.



17
18
19
# File 'lib/apartment/config.rb', line 17

def shard_key_prefix
  @shard_key_prefix
end

#sql_query_tagsObject

Returns the value of attribute sql_query_tags.



17
18
19
# File 'lib/apartment/config.rb', line 17

def sql_query_tags
  @sql_query_tags
end

#tenant_not_found_handlerObject

Returns the value of attribute tenant_not_found_handler.



17
18
19
# File 'lib/apartment/config.rb', line 17

def tenant_not_found_handler
  @tenant_not_found_handler
end

#tenant_pool_sizeObject

Returns the value of attribute tenant_pool_size.



17
18
19
# File 'lib/apartment/config.rb', line 17

def tenant_pool_size
  @tenant_pool_size
end

#tenant_strategyObject

Returns the value of attribute tenant_strategy.



14
15
16
# File 'lib/apartment/config.rb', line 14

def tenant_strategy
  @tenant_strategy
end

#tenant_validatorObject

Returns the value of attribute tenant_validator.



17
18
19
# File 'lib/apartment/config.rb', line 17

def tenant_validator
  @tenant_validator
end

#tenants_providerObject

Returns the value of attribute tenants_provider.



17
18
19
# File 'lib/apartment/config.rb', line 17

def tenants_provider
  @tenants_provider
end

#test_fixture_cleanupObject

Returns the value of attribute test_fixture_cleanup.



17
18
19
# File 'lib/apartment/config.rb', line 17

def test_fixture_cleanup
  @test_fixture_cleanup
end

Instance Method Details

#apply_defaults!Object

Apply derived defaults that depend on user-set values. Runs after the configure block yields and before validate!, so validate! stays read-only.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/apartment/config.rb', line 124

def apply_defaults!
  # PostgreSQL's default schema is 'public'; avoid forcing every user to set it.
  @default_tenant ||= 'public' if @tenant_strategy == :schema
  # Reap on the idle-timeout cadence unless an explicit interval decouples
  # the two (reap more often without shrinking the idle window).
  @reaper_interval = @pool_idle_timeout if @reaper_interval.nil?

  # max_total_connections is deprecated: the name said "connections" but it
  # always capped tenant-pool COUNT. Alias it to its true meaning without
  # changing behavior. Only fill when max_tenant_pools was not set explicitly,
  # so validate!'s both-set guard can still catch a genuine double-spec.
  return unless @max_total_connections

  warn '[Apartment] DEPRECATION: config.max_total_connections is deprecated and will be ' \
       'removed in v5. It caps tenant-pool COUNT, not connections; rename it to ' \
       'max_tenant_pools. For a true connection ceiling, set max_tenant_connections.'
  @max_tenant_pools = @max_total_connections if @max_tenant_pools.nil?
end

#configure_mysql {|@mysql_config| ... } ⇒ Object

Configure MySQL-specific options via block.

Yields:



104
105
106
107
108
# File 'lib/apartment/config.rb', line 104

def configure_mysql
  @mysql_config = Configs::MysqlConfig.new
  yield(@mysql_config) if block_given?
  @mysql_config
end

#configure_postgres {|@postgres_config| ... } ⇒ Object

Configure PostgreSQL-specific options via block.

Yields:



97
98
99
100
101
# File 'lib/apartment/config.rb', line 97

def configure_postgres
  @postgres_config = Configs::PostgresqlConfig.new
  yield(@postgres_config) if block_given?
  @postgres_config
end

#effective_pool_budgetObject

The pool-count bound the admission controller enforces: the stricter of the explicit pool cap (max_tenant_pools) and the pool budget derived from the connection ceiling (max_tenant_connections / tenant_pool_size, floored). Returns nil (uncapped) when neither knob is set. Relies on a global tenant_pool_size, so tenant-pool connections <= budget * tenant_pool_size.



290
291
292
293
# File 'lib/apartment/config.rb', line 290

def effective_pool_budget
  derived = @max_tenant_connections && @tenant_pool_size ? @max_tenant_connections / @tenant_pool_size : nil
  [@max_tenant_pools, derived].compact.min
end

#freeze!Object

Deep-freeze the config after validation to prevent post-boot mutation. Freezes mutable collections and sub-configs, then freezes self.



112
113
114
115
116
117
118
119
120
# File 'lib/apartment/config.rb', line 112

def freeze!
  @excluded_models.freeze
  @elevator_options.freeze
  @postgres_config&.freeze!
  @mysql_config&.freeze!
  # schema_file is a simple string, no deep freeze needed
  @app_role.freeze if @app_role.is_a?(String)
  freeze
end

#rails_env_nameObject

Returns the current Rails environment name, falling back to env vars and a safe default.



281
282
283
# File 'lib/apartment/config.rb', line 281

def rails_env_name
  (Rails.env if defined?(Rails.env)) || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'default_env'
end

#validate!Object

Validate configuration completeness and consistency. Raises ConfigurationError on invalid state.

Raises:



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/apartment/config.rb', line 145

def validate! # rubocop:disable Metrics/AbcSize
  raise(ConfigurationError, 'tenant_strategy is required') unless @tenant_strategy

  if @default_tenant.is_a?(String) && @default_tenant.strip.empty?
    raise(ConfigurationError, 'default_tenant cannot be an empty string')
  end

  unless [true, false].include?(@default_tenant_switch_allowed)
    raise(ConfigurationError,
          'default_tenant_switch_allowed must be true or false, got: ' \
          "#{@default_tenant_switch_allowed.inspect}")
  end

  unless @tenants_provider.respond_to?(:call)
    raise(ConfigurationError, 'tenants_provider must be a callable (e.g., -> { Tenant.pluck(:name) })')
  end

  if @postgres_config && @mysql_config
    raise(ConfigurationError, 'Cannot configure both Postgres and MySQL at the same time')
  end

  @postgres_config&.validate!

  if @tenant_pool_size && (!@tenant_pool_size.is_a?(Integer) || @tenant_pool_size < 1)
    raise(ConfigurationError,
          "tenant_pool_size must be a positive integer or nil, got: #{@tenant_pool_size.inspect}")
  end

  unless @pool_idle_timeout.is_a?(Numeric) && @pool_idle_timeout.positive?
    raise(ConfigurationError, "pool_idle_timeout must be a positive number, got: #{@pool_idle_timeout.inspect}")
  end

  # nil is valid pre-apply_defaults! (it derives from pool_idle_timeout); a
  # set value must be a positive number.
  if @reaper_interval && (!@reaper_interval.is_a?(Numeric) || !@reaper_interval.positive?)
    raise(ConfigurationError, "reaper_interval must be a positive number or nil, got: #{@reaper_interval.inspect}")
  end

  if @max_total_connections && (!@max_total_connections.is_a?(Integer) || @max_total_connections < 1)
    raise(ConfigurationError,
          "max_total_connections must be a positive integer or nil, got: #{@max_total_connections.inspect}")
  end

  if @max_tenant_pools && (!@max_tenant_pools.is_a?(Integer) || @max_tenant_pools < 1)
    raise(ConfigurationError,
          "max_tenant_pools must be a positive integer or nil, got: #{@max_tenant_pools.inspect}")
  end

  if @max_total_connections && @max_tenant_pools && @max_total_connections != @max_tenant_pools
    raise(ConfigurationError,
          'max_total_connections and max_tenant_pools are the same setting under two names; ' \
          "set only one. Got max_total_connections=#{@max_total_connections}, " \
          "max_tenant_pools=#{@max_tenant_pools}")
  end

  if @max_tenant_connections && (!@max_tenant_connections.is_a?(Integer) || @max_tenant_connections < 1)
    raise(ConfigurationError,
          "max_tenant_connections must be a positive integer or nil, got: #{@max_tenant_connections.inspect}")
  end

  if @max_tenant_connections && @tenant_pool_size.nil?
    raise(ConfigurationError,
          'max_tenant_connections requires tenant_pool_size to be set (the pool budget is ' \
          'derived as max_tenant_connections / tenant_pool_size)')
  end

  if @max_tenant_connections && @tenant_pool_size && @max_tenant_connections < @tenant_pool_size
    raise(ConfigurationError,
          "max_tenant_connections (#{@max_tenant_connections}) must be >= tenant_pool_size " \
          "(#{@tenant_pool_size}); it cannot fit a single tenant pool")
  end

  unless %i[evict_idle raise].include?(@pool_overflow_policy)
    raise(ConfigurationError,
          'pool_overflow_policy must be :evict_idle or :raise, ' \
          "got: #{@pool_overflow_policy.inspect}")
  end

  unless [nil, :schema_rb, :sql].include?(@schema_load_strategy)
    raise(ConfigurationError, "Invalid schema_load_strategy: #{@schema_load_strategy.inspect}. " \
                              'Must be nil, :schema_rb, or :sql')
  end

  if @migration_role && !@migration_role.is_a?(Symbol)
    raise(ConfigurationError, "migration_role must be nil or a Symbol, got: #{@migration_role.inspect}")
  end

  if @app_role && !@app_role.is_a?(String) && !@app_role.respond_to?(:call)
    raise(ConfigurationError, "app_role must be nil, a String, or a callable, got: #{@app_role.inspect}")
  end

  unless [true, false].include?(@schema_cache_per_tenant)
    raise(ConfigurationError,
          "schema_cache_per_tenant must be true or false, got: #{@schema_cache_per_tenant.inspect}")
  end

  unless [true, false].include?(@check_pending_migrations)
    raise(ConfigurationError,
          "check_pending_migrations must be true or false, got: #{@check_pending_migrations.inspect}")
  end

  unless [true, false].include?(@force_separate_pinned_pool)
    raise(ConfigurationError,
          "force_separate_pinned_pool must be true or false, got: #{@force_separate_pinned_pool.inspect}")
  end

  unless [true, false].include?(@test_fixture_cleanup)
    raise(ConfigurationError,
          "test_fixture_cleanup must be true or false, got: #{@test_fixture_cleanup.inspect}")
  end

  unless [true, false].include?(@reap_in_test)
    raise(ConfigurationError,
          "reap_in_test must be true or false, got: #{@reap_in_test.inspect}")
  end

  unless @tenant_validator.nil? || @tenant_validator == false || @tenant_validator.respond_to?(:call)
    raise(ConfigurationError,
          'tenant_validator must be nil, false, or a callable, ' \
          "got: #{@tenant_validator.inspect}")
  end

  if @tenant_not_found_handler && !@tenant_not_found_handler.respond_to?(:call)
    raise(ConfigurationError,
          'tenant_not_found_handler must be nil or a callable, ' \
          "got: #{@tenant_not_found_handler.inspect}")
  end

  return if @shard_key_prefix.is_a?(String) && @shard_key_prefix.match?(/\A[a-z_][a-z0-9_]*\z/)

  raise(ConfigurationError,
        'shard_key_prefix must be a lowercase string matching /[a-z_][a-z0-9_]*/, ' \
        "got: #{@shard_key_prefix.inspect}")
end