magick-feature-flags 0.9.24 → 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.
- checksums.yaml +4 -4
- data/README.md +200 -1
- data/lib/generators/magick/active_record/active_record_generator.rb +44 -0
- data/lib/generators/magick/active_record/templates/create_magick_features.rb +20 -0
- data/lib/magick/adapters/active_record.rb +267 -0
- data/lib/magick/adapters/registry.rb +106 -33
- data/lib/magick/admin_ui/config/routes.rb +13 -0
- data/lib/magick/admin_ui/engine.rb +98 -0
- data/lib/magick/admin_ui/helpers.rb +33 -0
- data/lib/magick/admin_ui/routes.rb +17 -0
- data/lib/magick/admin_ui.rb +42 -0
- data/lib/magick/config.rb +80 -6
- data/lib/magick/documentation.rb +153 -0
- data/lib/magick/feature.rb +62 -23
- data/lib/magick/rails/railtie.rb +11 -6
- data/lib/magick/version.rb +1 -1
- data/lib/magick.rb +9 -0
- metadata +46 -6
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
61
|
+
memory_adapter&.set(feature_name, key, value)
|
|
44
62
|
|
|
45
63
|
# Update Redis if available
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
115
|
+
# Continue even if Active Record fails
|
|
76
116
|
end
|
|
77
117
|
end
|
|
78
118
|
|
|
79
119
|
def exists?(feature_name)
|
|
80
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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(
|
|
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, :redis_db, :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
|
|
@@ -14,6 +14,7 @@ module Magick
|
|
|
14
14
|
@redis_namespace = 'magick:features'
|
|
15
15
|
@redis_db = nil # Use default database (0) unless specified
|
|
16
16
|
@environment = defined?(Rails) ? Rails.env.to_s : 'development'
|
|
17
|
+
@enable_admin_ui = false # Admin UI disabled by default
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
# DSL methods for configuration
|
|
@@ -23,6 +24,8 @@ module Magick
|
|
|
23
24
|
configure_memory_adapter(**options)
|
|
24
25
|
when :redis
|
|
25
26
|
configure_redis_adapter(**options)
|
|
27
|
+
when :active_record
|
|
28
|
+
configure_active_record_adapter(**options)
|
|
26
29
|
when :registry
|
|
27
30
|
if block_given?
|
|
28
31
|
instance_eval(&block)
|
|
@@ -58,6 +61,7 @@ module Magick
|
|
|
58
61
|
end
|
|
59
62
|
else
|
|
60
63
|
memory_adapter = configure_memory_adapter
|
|
64
|
+
active_record_adapter = configure_active_record_adapter if defined?(::ActiveRecord::Base)
|
|
61
65
|
cb = Magick::CircuitBreaker.new(
|
|
62
66
|
failure_threshold: @circuit_breaker_threshold,
|
|
63
67
|
timeout: @circuit_breaker_timeout
|
|
@@ -65,6 +69,7 @@ module Magick
|
|
|
65
69
|
@adapter_registry = Adapters::Registry.new(
|
|
66
70
|
memory_adapter,
|
|
67
71
|
redis_adapter,
|
|
72
|
+
active_record_adapter: active_record_adapter,
|
|
68
73
|
circuit_breaker: cb,
|
|
69
74
|
async: @async_updates
|
|
70
75
|
)
|
|
@@ -73,6 +78,40 @@ module Magick
|
|
|
73
78
|
redis_adapter
|
|
74
79
|
end
|
|
75
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
|
+
|
|
76
115
|
def performance_metrics(enabled: true, redis_tracking: nil, batch_size: 100, flush_interval: 60, **_options)
|
|
77
116
|
return unless enabled
|
|
78
117
|
|
|
@@ -122,6 +161,10 @@ module Magick
|
|
|
122
161
|
@environment = name.to_s
|
|
123
162
|
end
|
|
124
163
|
|
|
164
|
+
def admin_ui(enabled: true)
|
|
165
|
+
@enable_admin_ui = enabled
|
|
166
|
+
end
|
|
167
|
+
|
|
125
168
|
def apply!
|
|
126
169
|
# Apply configuration to Magick module
|
|
127
170
|
Magick.adapter_registry = adapter_registry if adapter_registry
|
|
@@ -151,6 +194,11 @@ module Magick
|
|
|
151
194
|
Magick.audit_log = audit_log if audit_log
|
|
152
195
|
Magick.versioning = versioning if versioning
|
|
153
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)
|
|
154
202
|
end
|
|
155
203
|
|
|
156
204
|
private
|
|
@@ -175,7 +223,11 @@ module Magick
|
|
|
175
223
|
|
|
176
224
|
if url
|
|
177
225
|
# Parse URL to extract database number if present
|
|
178
|
-
parsed_url =
|
|
226
|
+
parsed_url = begin
|
|
227
|
+
URI.parse(url)
|
|
228
|
+
rescue StandardError
|
|
229
|
+
nil
|
|
230
|
+
end
|
|
179
231
|
db_from_url = nil
|
|
180
232
|
if parsed_url && parsed_url.path && parsed_url.path.length > 1
|
|
181
233
|
# Redis URL format: redis://host:port/db_number
|
|
@@ -200,7 +252,11 @@ module Magick
|
|
|
200
252
|
# If db was specified but not in URL, select it explicitly
|
|
201
253
|
# This handles cases where URL doesn't include db number
|
|
202
254
|
if db && url
|
|
203
|
-
parsed_url =
|
|
255
|
+
parsed_url = begin
|
|
256
|
+
URI.parse(url)
|
|
257
|
+
rescue StandardError
|
|
258
|
+
nil
|
|
259
|
+
end
|
|
204
260
|
url_has_db = parsed_url && parsed_url.path && parsed_url.path.length > 1
|
|
205
261
|
unless url_has_db
|
|
206
262
|
begin
|
|
@@ -216,9 +272,23 @@ module Magick
|
|
|
216
272
|
adapter
|
|
217
273
|
end
|
|
218
274
|
|
|
219
|
-
def
|
|
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)
|
|
220
289
|
memory_adapter = memory || configure_memory_adapter
|
|
221
290
|
redis_adapter = redis || configure_redis_adapter
|
|
291
|
+
active_record_adapter = active_record || configure_active_record_adapter
|
|
222
292
|
|
|
223
293
|
cb = circuit_breaker || Magick::CircuitBreaker.new(
|
|
224
294
|
failure_threshold: @circuit_breaker_threshold,
|
|
@@ -226,12 +296,15 @@ module Magick
|
|
|
226
296
|
)
|
|
227
297
|
|
|
228
298
|
async_enabled = async.nil? ? @async_updates : async
|
|
299
|
+
primary_adapter = primary || (@active_record_primary ? :active_record : :memory)
|
|
229
300
|
|
|
230
301
|
@adapter_registry = Adapters::Registry.new(
|
|
231
302
|
memory_adapter,
|
|
232
303
|
redis_adapter,
|
|
304
|
+
active_record_adapter: active_record_adapter,
|
|
233
305
|
circuit_breaker: cb,
|
|
234
|
-
async: async_enabled
|
|
306
|
+
async: async_enabled,
|
|
307
|
+
primary: primary_adapter
|
|
235
308
|
)
|
|
236
309
|
end
|
|
237
310
|
|
|
@@ -239,7 +312,8 @@ module Magick
|
|
|
239
312
|
@default_adapter_registry ||= begin
|
|
240
313
|
memory_adapter = Adapters::Memory.new
|
|
241
314
|
redis_adapter = configure_redis_adapter
|
|
242
|
-
|
|
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)
|
|
243
317
|
end
|
|
244
318
|
end
|
|
245
319
|
end
|