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.
- checksums.yaml +4 -4
- data/README.md +213 -5
- 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 +115 -9
- data/lib/magick/documentation.rb +153 -0
- data/lib/magick/feature.rb +62 -23
- data/lib/magick/rails/railtie.rb +18 -7
- 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, :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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|