magick-feature-flags 0.9.23 → 0.9.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,11 +5,14 @@ module Magick
5
5
  class Registry
6
6
  CACHE_INVALIDATION_CHANNEL = 'magick:cache:invalidate'
7
7
 
8
- def initialize(memory_adapter, redis_adapter = nil, circuit_breaker: nil, async: false)
8
+ def initialize(memory_adapter, redis_adapter = nil, active_record_adapter: nil, circuit_breaker: nil,
9
+ async: false, primary: nil)
9
10
  @memory_adapter = memory_adapter
10
11
  @redis_adapter = redis_adapter
12
+ @active_record_adapter = active_record_adapter
11
13
  @circuit_breaker = circuit_breaker || Magick::CircuitBreaker.new
12
14
  @async = async
15
+ @primary = primary || :memory # :memory, :redis, or :active_record
13
16
  @subscriber_thread = nil
14
17
  @subscriber = nil
15
18
  # Only start Pub/Sub subscriber if Redis is available
@@ -19,7 +22,7 @@ module Magick
19
22
 
20
23
  def get(feature_name, key)
21
24
  # Try memory first (fastest) - no Redis calls needed thanks to Pub/Sub invalidation
22
- value = memory_adapter.get(feature_name, key)
25
+ value = memory_adapter.get(feature_name, key) if memory_adapter
23
26
  return value unless value.nil?
24
27
 
25
28
  # Fall back to Redis if available
@@ -27,10 +30,25 @@ module Magick
27
30
  begin
28
31
  value = redis_adapter.get(feature_name, key)
29
32
  # Update memory cache if found in Redis
30
- memory_adapter.set(feature_name, key, value) if value
33
+ if value && memory_adapter
34
+ memory_adapter.set(feature_name, key, value)
35
+ return value
36
+ end
37
+ # If Redis returns nil, continue to next adapter
38
+ rescue StandardError, AdapterError
39
+ # Redis failed, continue to next adapter
40
+ end
41
+ end
42
+
43
+ # Fall back to Active Record if available
44
+ if active_record_adapter
45
+ begin
46
+ value = active_record_adapter.get(feature_name, key)
47
+ # Update memory cache if found in Active Record
48
+ memory_adapter.set(feature_name, key, value) if value && memory_adapter
31
49
  return value
32
- rescue AdapterError
33
- # Redis failed, return nil
50
+ rescue StandardError, AdapterError
51
+ # Active Record failed, return nil
34
52
  nil
35
53
  end
36
54
  end
@@ -40,55 +58,88 @@ module Magick
40
58
 
41
59
  def set(feature_name, key, value)
42
60
  # Update memory first (always synchronous)
43
- memory_adapter.set(feature_name, key, value)
61
+ memory_adapter&.set(feature_name, key, value)
44
62
 
45
63
  # Update Redis if available
46
- return unless redis_adapter
64
+ if redis_adapter
65
+ update_redis = proc do
66
+ circuit_breaker.call do
67
+ redis_adapter.set(feature_name, key, value)
68
+ end
69
+ rescue AdapterError => e
70
+ # Log error but don't fail - memory is updated
71
+ warn "Failed to update Redis: #{e.message}" if defined?(Rails) && Rails.env.development?
72
+ end
47
73
 
48
- update_redis = proc do
49
- circuit_breaker.call do
50
- redis_adapter.set(feature_name, key, value)
74
+ if @async && defined?(Thread)
75
+ # For async updates, publish cache invalidation synchronously
76
+ # This ensures other processes are notified immediately, even if Redis update is delayed
77
+ publish_cache_invalidation(feature_name)
78
+ Thread.new { update_redis.call }
79
+ else
80
+ update_redis.call
51
81
  # Publish cache invalidation message to notify other processes
52
82
  publish_cache_invalidation(feature_name)
53
83
  end
54
- rescue AdapterError => e
55
- # Log error but don't fail - memory is updated
56
- warn "Failed to update Redis: #{e.message}" if defined?(Rails) && Rails.env.development?
57
84
  end
58
85
 
59
- if @async && defined?(Thread)
60
- Thread.new { update_redis.call }
61
- else
62
- update_redis.call
86
+ # Always update Active Record if available (as fallback/persistence layer)
87
+ return unless active_record_adapter
88
+
89
+ begin
90
+ active_record_adapter.set(feature_name, key, value)
91
+ rescue AdapterError => e
92
+ # Log error but don't fail
93
+ warn "Failed to update Active Record: #{e.message}" if defined?(Rails) && Rails.env.development?
63
94
  end
64
95
  end
65
96
 
66
97
  def delete(feature_name)
67
- memory_adapter.delete(feature_name)
68
- return unless redis_adapter
98
+ memory_adapter&.delete(feature_name)
99
+
100
+ if redis_adapter
101
+ begin
102
+ redis_adapter.delete(feature_name)
103
+ # Publish cache invalidation message
104
+ publish_cache_invalidation(feature_name)
105
+ rescue AdapterError
106
+ # Continue even if Redis fails
107
+ end
108
+ end
109
+
110
+ return unless active_record_adapter
69
111
 
70
112
  begin
71
- redis_adapter.delete(feature_name)
72
- # Publish cache invalidation message
73
- publish_cache_invalidation(feature_name)
113
+ active_record_adapter.delete(feature_name)
74
114
  rescue AdapterError
75
- # Continue even if Redis fails
115
+ # Continue even if Active Record fails
76
116
  end
77
117
  end
78
118
 
79
119
  def exists?(feature_name)
80
- memory_adapter.exists?(feature_name) || (redis_adapter&.exists?(feature_name) == true)
120
+ return true if memory_adapter&.exists?(feature_name)
121
+ return true if redis_adapter&.exists?(feature_name) == true
122
+ return true if active_record_adapter&.exists?(feature_name) == true
123
+
124
+ false
81
125
  end
82
126
 
83
127
  def all_features
84
- memory_features = memory_adapter.all_features
85
- redis_features = redis_adapter&.all_features || []
86
- (memory_features + redis_features).uniq
128
+ features = []
129
+ features += memory_adapter.all_features if memory_adapter
130
+ features += redis_adapter.all_features if redis_adapter
131
+ features += active_record_adapter.all_features if active_record_adapter
132
+ features.uniq
87
133
  end
88
134
 
89
135
  # Explicitly trigger cache invalidation for a feature
90
136
  # This is useful for targeting updates that need immediate cache invalidation
137
+ # Invalidates memory cache in current process AND publishes to Redis for other processes
91
138
  def invalidate_cache(feature_name)
139
+ # Invalidate memory cache in current process immediately
140
+ memory_adapter&.delete(feature_name)
141
+
142
+ # Publish to Redis Pub/Sub to invalidate cache in other processes
92
143
  publish_cache_invalidation(feature_name)
93
144
  end
94
145
 
@@ -104,11 +155,9 @@ module Magick
104
155
  redis_adapter.instance_variable_get(:@redis)
105
156
  end
106
157
 
107
- private
108
-
109
- attr_reader :memory_adapter, :redis_adapter, :circuit_breaker
110
-
111
- # Publish cache invalidation message to Redis Pub/Sub
158
+ # Publish cache invalidation message to Redis Pub/Sub (without deleting local memory cache)
159
+ # This is useful when you've just updated the cache and want to notify other processes
160
+ # but keep the local memory cache intact
112
161
  def publish_cache_invalidation(feature_name)
113
162
  return unless redis_adapter
114
163
 
@@ -121,6 +170,10 @@ module Magick
121
170
  end
122
171
  end
123
172
 
173
+ private
174
+
175
+ attr_reader :memory_adapter, :redis_adapter, :active_record_adapter, :circuit_breaker
176
+
124
177
  # Start a background thread to listen for cache invalidation messages
125
178
  def start_cache_invalidation_subscriber
126
179
  return unless redis_adapter && defined?(Thread)
@@ -132,8 +185,28 @@ module Magick
132
185
  @subscriber = redis_client.dup
133
186
  @subscriber.subscribe(CACHE_INVALIDATION_CHANNEL) do |on|
134
187
  on.message do |_channel, feature_name|
188
+ feature_name_str = feature_name.to_s
189
+
135
190
  # Invalidate memory cache for this feature
136
- memory_adapter.delete(feature_name)
191
+ memory_adapter.delete(feature_name_str) if memory_adapter
192
+
193
+ # Also reload the feature instance in Magick.features if it exists
194
+ # This ensures the feature instance has the latest targeting and values
195
+ if defined?(Magick) && Magick.features.key?(feature_name_str)
196
+ feature = Magick.features[feature_name_str]
197
+ if feature.respond_to?(:reload)
198
+ feature.reload
199
+ # Log for debugging (only in development)
200
+ if defined?(Rails) && Rails.env.development?
201
+ Rails.logger.debug "Magick: Reloaded feature '#{feature_name_str}' after cache invalidation"
202
+ end
203
+ end
204
+ end
205
+ rescue StandardError => e
206
+ # Log error but don't crash the subscriber thread
207
+ if defined?(Rails) && Rails.env.development?
208
+ warn "Magick: Error processing cache invalidation for '#{feature_name}': #{e.message}"
209
+ end
137
210
  end
138
211
  end
139
212
  rescue StandardError => e
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Magick::AdminUI::Engine.routes.draw do
4
+ root 'features#index'
5
+ resources :features, only: %i[index show edit update] do
6
+ member do
7
+ put :enable
8
+ put :disable
9
+ put :enable_for_user
10
+ end
11
+ end
12
+ resources :stats, only: [:show]
13
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configure inflector immediately when this file loads
4
+ # This ensures AdminUI stays as AdminUI (not AdminUi) before Rails processes routes
5
+ if defined?(ActiveSupport::Inflector)
6
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
7
+ inflect.acronym 'AdminUI'
8
+ inflect.acronym 'UI'
9
+ end
10
+ end
11
+
12
+ module Magick
13
+ module AdminUI
14
+ class Engine < ::Rails::Engine
15
+ isolate_namespace Magick::AdminUI
16
+
17
+ engine_name 'magick_admin_ui'
18
+
19
+ # Rails engines automatically detect app/views and app/controllers directories
20
+ # With isolate_namespace, views should be at:
21
+ # app/views/magick/adminui/[controller]/[action].html.erb
22
+ # Controllers should be at:
23
+ # app/controllers/magick/adminui/[controller]_controller.rb
24
+ # Rails handles this automatically, but we explicitly add app/controllers to autoload paths
25
+ # to ensure controllers are found
26
+ config.autoload_paths += %W[#{root}/app/controllers] if root.join('app', 'controllers').exist?
27
+
28
+ # Explicitly add app/views to view paths
29
+ # Rails engines should do this automatically, but we ensure it's configured
30
+ initializer 'magick.admin_ui.append_view_paths', after: :add_view_paths do |app|
31
+ app.paths['app/views'] << root.join('app', 'views').to_s if root.join('app', 'views').exist?
32
+ end
33
+
34
+ # Also ensure view paths are added when ActionController loads
35
+ initializer 'magick.admin_ui.append_view_paths_to_controller', after: :add_view_paths do
36
+ ActiveSupport.on_load(:action_controller) do
37
+ view_path = Magick::AdminUI::Engine.root.join('app', 'views').to_s
38
+ append_view_path view_path unless view_paths.include?(view_path)
39
+ end
40
+ end
41
+
42
+ # Ensure view paths are added in to_prepare (runs before each request in development)
43
+ config.to_prepare do
44
+ view_path = Magick::AdminUI::Engine.root.join('app', 'views').to_s
45
+ if File.directory?(view_path)
46
+ if defined?(Magick::AdminUI::FeaturesController)
47
+ Magick::AdminUI::FeaturesController.append_view_path(view_path)
48
+ end
49
+ Magick::AdminUI::StatsController.append_view_path(view_path) if defined?(Magick::AdminUI::StatsController)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # Draw routes directly - Rails engines should auto-load config/routes.rb
57
+ # but for gems we need to ensure routes are drawn at the right time
58
+ # Use both to_prepare (for development reloading) and an initializer (for production)
59
+ if defined?(Rails)
60
+ # Initializer runs once during app initialization
61
+ Magick::AdminUI::Engine.initializer 'magick.admin_ui.draw_routes', after: :load_config_initializers do
62
+ Magick::AdminUI::Engine.routes.draw do
63
+ root 'features#index'
64
+ resources :features, only: %i[index show edit update] do
65
+ member do
66
+ put :enable
67
+ put :disable
68
+ put :enable_for_user
69
+ put :enable_for_role
70
+ put :disable_for_role
71
+ put :update_targeting
72
+ end
73
+ end
74
+ resources :stats, only: [:show]
75
+ end
76
+ end
77
+
78
+ # to_prepare runs before each request in development (for code reloading)
79
+ Magick::AdminUI::Engine.config.to_prepare do
80
+ # Routes are already drawn by initializer, but redraw if needed for development reloading
81
+ if Magick::AdminUI::Engine.routes.routes.empty?
82
+ Magick::AdminUI::Engine.routes.draw do
83
+ root 'features#index'
84
+ resources :features, only: %i[index show edit update] do
85
+ member do
86
+ put :enable
87
+ put :disable
88
+ put :enable_for_user
89
+ put :enable_for_role
90
+ put :disable_for_role
91
+ put :update_targeting
92
+ end
93
+ end
94
+ resources :stats, only: [:show]
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Magick
4
+ module AdminUI
5
+ module Helpers
6
+ def self.feature_status_badge(status)
7
+ case status.to_sym
8
+ when :active
9
+ '<span class="badge badge-success">Active</span>'
10
+ when :deprecated
11
+ '<span class="badge badge-warning">Deprecated</span>'
12
+ when :inactive
13
+ '<span class="badge badge-danger">Inactive</span>'
14
+ else
15
+ '<span class="badge">Unknown</span>'
16
+ end
17
+ end
18
+
19
+ def self.feature_type_label(type)
20
+ case type.to_sym
21
+ when :boolean
22
+ 'Boolean'
23
+ when :string
24
+ 'String'
25
+ when :number
26
+ 'Number'
27
+ else
28
+ type.to_s.capitalize
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Routes file - users should add this to their Rails app's config/routes.rb:
4
+ # mount Magick::AdminUI::Engine, at: '/magick'
5
+ #
6
+ # Or define routes manually:
7
+ # Magick::AdminUI::Engine.routes.draw do
8
+ # root 'features#index'
9
+ # resources :features, only: [:index, :show, :edit, :update] do
10
+ # member do
11
+ # put :enable
12
+ # put :disable
13
+ # put :enable_for_user
14
+ # end
15
+ # end
16
+ # resources :stats, only: [:show]
17
+ # end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'admin_ui/engine'
4
+ # Controllers are in app/controllers and will be auto-loaded by Rails engine
5
+ # But we explicitly require them to ensure they're available when needed
6
+ if defined?(Rails) && Rails.env
7
+ # In Rails, controllers are auto-loaded from app/controllers
8
+ # But we can explicitly require them if needed for console access
9
+ engine_root = Magick::AdminUI::Engine.root
10
+ if engine_root.join('app', 'controllers', 'magick', 'adminui', 'features_controller.rb').exist?
11
+ require engine_root.join('app', 'controllers', 'magick', 'adminui', 'features_controller').to_s
12
+ end
13
+ if engine_root.join('app', 'controllers', 'magick', 'adminui', 'stats_controller.rb').exist?
14
+ require engine_root.join('app', 'controllers', 'magick', 'adminui', 'stats_controller').to_s
15
+ end
16
+ end
17
+ require_relative 'admin_ui/helpers'
18
+
19
+ module Magick
20
+ module AdminUI
21
+ class << self
22
+ def configure
23
+ yield config if block_given?
24
+ end
25
+
26
+ def config
27
+ @config ||= Configuration.new
28
+ end
29
+
30
+ class Configuration
31
+ attr_accessor :theme, :brand_name, :require_role, :available_roles
32
+
33
+ def initialize
34
+ @theme = :light
35
+ @brand_name = 'Magick'
36
+ @require_role = nil
37
+ @available_roles = [] # Can be populated via DSL: admin_ui { roles ['admin', 'user', 'manager'] }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
data/lib/magick/config.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Magick
4
4
  class Config
5
5
  attr_accessor :adapter_registry, :performance_metrics, :audit_log, :versioning, :warn_on_deprecated,
6
- :async_updates, :memory_ttl, :circuit_breaker_threshold, :circuit_breaker_timeout, :redis_url, :redis_namespace, :environment
6
+ :async_updates, :memory_ttl, :circuit_breaker_threshold, :circuit_breaker_timeout, :redis_url, :redis_namespace, :redis_db, :environment, :active_record_model_class, :enable_admin_ui
7
7
 
8
8
  def initialize
9
9
  @warn_on_deprecated = false
@@ -12,7 +12,9 @@ module Magick
12
12
  @circuit_breaker_threshold = 5
13
13
  @circuit_breaker_timeout = 60
14
14
  @redis_namespace = 'magick:features'
15
+ @redis_db = nil # Use default database (0) unless specified
15
16
  @environment = defined?(Rails) ? Rails.env.to_s : 'development'
17
+ @enable_admin_ui = false # Admin UI disabled by default
16
18
  end
17
19
 
18
20
  # DSL methods for configuration
@@ -22,6 +24,8 @@ module Magick
22
24
  configure_memory_adapter(**options)
23
25
  when :redis
24
26
  configure_redis_adapter(**options)
27
+ when :active_record
28
+ configure_active_record_adapter(**options)
25
29
  when :registry
26
30
  if block_given?
27
31
  instance_eval(&block)
@@ -38,10 +42,11 @@ module Magick
38
42
  configure_memory_adapter(**options)
39
43
  end
40
44
 
41
- def redis(url: nil, namespace: nil, **options)
45
+ def redis(url: nil, namespace: nil, db: nil, **options)
42
46
  @redis_url = url if url
43
47
  @redis_namespace = namespace if namespace
44
- redis_adapter = configure_redis_adapter(url: url, namespace: namespace, **options)
48
+ @redis_db = db if db
49
+ redis_adapter = configure_redis_adapter(url: url, namespace: namespace, db: db, **options)
45
50
 
46
51
  # Automatically create Registry adapter if it doesn't exist
47
52
  # This allows users to just call `redis url: ...` without needing to call `adapter :registry`
@@ -56,6 +61,7 @@ module Magick
56
61
  end
57
62
  else
58
63
  memory_adapter = configure_memory_adapter
64
+ active_record_adapter = configure_active_record_adapter if defined?(::ActiveRecord::Base)
59
65
  cb = Magick::CircuitBreaker.new(
60
66
  failure_threshold: @circuit_breaker_threshold,
61
67
  timeout: @circuit_breaker_timeout
@@ -63,6 +69,7 @@ module Magick
63
69
  @adapter_registry = Adapters::Registry.new(
64
70
  memory_adapter,
65
71
  redis_adapter,
72
+ active_record_adapter: active_record_adapter,
66
73
  circuit_breaker: cb,
67
74
  async: @async_updates
68
75
  )
@@ -71,6 +78,40 @@ module Magick
71
78
  redis_adapter
72
79
  end
73
80
 
81
+ def active_record(model_class: nil, primary: false, **options)
82
+ @active_record_model_class = model_class if model_class
83
+ @active_record_primary = primary
84
+ active_record_adapter = configure_active_record_adapter(model_class: model_class, **options)
85
+
86
+ # Automatically create Registry adapter if it doesn't exist
87
+ if @adapter_registry
88
+ # If registry already exists, update it with the new Active Record adapter
89
+ if active_record_adapter && @adapter_registry.is_a?(Adapters::Registry)
90
+ @adapter_registry.instance_variable_set(:@active_record_adapter, active_record_adapter)
91
+ # Update primary if specified
92
+ @adapter_registry.instance_variable_set(:@primary, :active_record) if primary
93
+ end
94
+ else
95
+ memory_adapter = configure_memory_adapter
96
+ redis_adapter = configure_redis_adapter
97
+ cb = Magick::CircuitBreaker.new(
98
+ failure_threshold: @circuit_breaker_threshold,
99
+ timeout: @circuit_breaker_timeout
100
+ )
101
+ primary_adapter = primary ? :active_record : :memory
102
+ @adapter_registry = Adapters::Registry.new(
103
+ memory_adapter,
104
+ redis_adapter,
105
+ active_record_adapter: active_record_adapter,
106
+ circuit_breaker: cb,
107
+ async: @async_updates,
108
+ primary: primary_adapter
109
+ )
110
+ end
111
+
112
+ active_record_adapter
113
+ end
114
+
74
115
  def performance_metrics(enabled: true, redis_tracking: nil, batch_size: 100, flush_interval: 60, **_options)
75
116
  return unless enabled
76
117
 
@@ -120,6 +161,10 @@ module Magick
120
161
  @environment = name.to_s
121
162
  end
122
163
 
164
+ def admin_ui(enabled: true)
165
+ @enable_admin_ui = enabled
166
+ end
167
+
123
168
  def apply!
124
169
  # Apply configuration to Magick module
125
170
  Magick.adapter_registry = adapter_registry if adapter_registry
@@ -149,6 +194,11 @@ module Magick
149
194
  Magick.audit_log = audit_log if audit_log
150
195
  Magick.versioning = versioning if versioning
151
196
  Magick.warn_on_deprecated = warn_on_deprecated
197
+
198
+ # Load optional components if enabled
199
+ return unless @enable_admin_ui && defined?(Rails)
200
+
201
+ require_relative '../magick/admin_ui' unless defined?(Magick::AdminUI)
152
202
  end
153
203
 
154
204
  private
@@ -161,17 +211,37 @@ module Magick
161
211
  adapter
162
212
  end
163
213
 
164
- def configure_redis_adapter(url: nil, namespace: nil, client: nil)
214
+ def configure_redis_adapter(url: nil, namespace: nil, db: nil, client: nil)
165
215
  return nil unless defined?(Redis)
166
216
 
167
217
  url ||= @redis_url
168
218
  namespace ||= @redis_namespace
219
+ db ||= @redis_db
169
220
 
170
221
  redis_client = client || begin
222
+ redis_options = {}
223
+
171
224
  if url
172
- ::Redis.new(url: url)
225
+ # Parse URL to extract database number if present
226
+ parsed_url = begin
227
+ URI.parse(url)
228
+ rescue StandardError
229
+ nil
230
+ end
231
+ db_from_url = nil
232
+ if parsed_url && parsed_url.path && parsed_url.path.length > 1
233
+ # Redis URL format: redis://host:port/db_number
234
+ db_from_url = parsed_url.path[1..-1].to_i
235
+ end
236
+
237
+ # Use db parameter if provided, otherwise use db from URL, otherwise nil (default DB 0)
238
+ final_db = db || db_from_url
239
+ redis_options[:db] = final_db if final_db
240
+ redis_options[:url] = url
241
+ ::Redis.new(redis_options)
173
242
  else
174
- ::Redis.new
243
+ redis_options[:db] = db if db
244
+ ::Redis.new(redis_options)
175
245
  end
176
246
  rescue StandardError
177
247
  nil
@@ -179,14 +249,46 @@ module Magick
179
249
 
180
250
  return nil unless redis_client
181
251
 
252
+ # If db was specified but not in URL, select it explicitly
253
+ # This handles cases where URL doesn't include db number
254
+ if db && url
255
+ parsed_url = begin
256
+ URI.parse(url)
257
+ rescue StandardError
258
+ nil
259
+ end
260
+ url_has_db = parsed_url && parsed_url.path && parsed_url.path.length > 1
261
+ unless url_has_db
262
+ begin
263
+ redis_client.select(db)
264
+ rescue StandardError
265
+ # Ignore if SELECT fails (some Redis setups don't support SELECT, e.g., Redis Cluster)
266
+ end
267
+ end
268
+ end
269
+
182
270
  adapter = Adapters::Redis.new(redis_client)
183
271
  adapter.instance_variable_set(:@namespace, namespace) if namespace
184
272
  adapter
185
273
  end
186
274
 
187
- def configure_registry_adapter(memory: nil, redis: nil, async: nil, circuit_breaker: nil)
275
+ def configure_active_record_adapter(model_class: nil, **_options)
276
+ return nil unless defined?(::ActiveRecord::Base)
277
+
278
+ model_class ||= @active_record_model_class
279
+ Adapters::ActiveRecord.new(model_class: model_class)
280
+ rescue StandardError => e
281
+ if defined?(Rails) && Rails.env.development?
282
+ warn "Magick: Failed to initialize ActiveRecord adapter: #{e.message}"
283
+ end
284
+ nil
285
+ end
286
+
287
+ def configure_registry_adapter(memory: nil, redis: nil, active_record: nil, async: nil, circuit_breaker: nil,
288
+ primary: nil)
188
289
  memory_adapter = memory || configure_memory_adapter
189
290
  redis_adapter = redis || configure_redis_adapter
291
+ active_record_adapter = active_record || configure_active_record_adapter
190
292
 
191
293
  cb = circuit_breaker || Magick::CircuitBreaker.new(
192
294
  failure_threshold: @circuit_breaker_threshold,
@@ -194,12 +296,15 @@ module Magick
194
296
  )
195
297
 
196
298
  async_enabled = async.nil? ? @async_updates : async
299
+ primary_adapter = primary || (@active_record_primary ? :active_record : :memory)
197
300
 
198
301
  @adapter_registry = Adapters::Registry.new(
199
302
  memory_adapter,
200
303
  redis_adapter,
304
+ active_record_adapter: active_record_adapter,
201
305
  circuit_breaker: cb,
202
- async: async_enabled
306
+ async: async_enabled,
307
+ primary: primary_adapter
203
308
  )
204
309
  end
205
310
 
@@ -207,7 +312,8 @@ module Magick
207
312
  @default_adapter_registry ||= begin
208
313
  memory_adapter = Adapters::Memory.new
209
314
  redis_adapter = configure_redis_adapter
210
- Adapters::Registry.new(memory_adapter, redis_adapter)
315
+ active_record_adapter = configure_active_record_adapter if defined?(::ActiveRecord::Base)
316
+ Adapters::Registry.new(memory_adapter, redis_adapter, active_record_adapter: active_record_adapter)
211
317
  end
212
318
  end
213
319
  end