Module: Magick
- Defined in:
- lib/magick/rails/railtie.rb,
lib/magick.rb,
lib/magick/dsl.rb,
lib/magick/config.rb,
lib/magick/errors.rb,
lib/magick/feature.rb,
lib/magick/version.rb,
lib/magick/admin_ui.rb,
lib/magick/log_safe.rb,
lib/magick/audit_log.rb,
lib/magick/versioning.rb,
lib/magick/bulk_result.rb,
lib/magick/rails/events.rb,
lib/magick/adapters/base.rb,
lib/magick/documentation.rb,
lib/magick/export_import.rb,
lib/magick/adapters/redis.rb,
lib/magick/targeting/base.rb,
lib/magick/targeting/role.rb,
lib/magick/targeting/user.rb,
lib/magick/adapter_failure.rb,
lib/magick/adapters/memory.rb,
lib/magick/admin_ui/engine.rb,
lib/magick/circuit_breaker.rb,
lib/magick/feature_variant.rb,
lib/magick/targeting/group.rb,
lib/magick/testing_helpers.rb,
lib/magick/admin_ui/helpers.rb,
lib/magick/adapters/registry.rb,
lib/magick/targeting/complex.rb,
lib/magick/targeting_payload.rb,
lib/magick/performance_metrics.rb,
lib/magick/targeting/date_range.rb,
lib/magick/targeting/ip_address.rb,
lib/magick/targeting/percentage.rb,
lib/magick/adapters/async_writer.rb,
lib/magick/adapters/active_record.rb,
lib/magick/rails/event_subscriber.rb,
lib/magick/admin_ui/authentication.rb,
lib/magick/request_store_integration.rb,
lib/magick/targeting/custom_attribute.rb,
lib/magick/targeting/request_percentage.rb,
app/controllers/magick/adminui/stats_controller.rb,
lib/generators/magick/install/install_generator.rb,
app/controllers/magick/adminui/features_controller.rb,
lib/generators/magick/active_record/active_record_generator.rb
Overview
Admin UI engine is now loaded in magick.rb when Rails is detected
Defined Under Namespace
Modules: AdapterFailure, Adapters, AdminUI, ConfigDSL, DSL, Generators, LogSafe, Rails, RequestStoreIntegration, Targeting, TargetingPayload, TestingHelpers Classes: AdapterError, AuditLog, BulkResult, CircuitBreaker, CircuitOpenError, Config, ConfigurationError, Documentation, Error, ExportImport, Feature, FeatureNotFoundError, FeatureVariant, InvalidFeatureTypeError, InvalidFeatureValueError, InvalidTargetingError, PerformanceMetrics, Versioning
Constant Summary collapse
- UNKNOWN_DEPENDENCY_POLICIES =
Accepted values for Magick.unknown_dependency_policy (see below).
%i[satisfied unsatisfied].freeze
- VERSION =
'1.7.0'
Class Attribute Summary collapse
-
.adapter_registry ⇒ Object
Returns the value of attribute adapter_registry.
-
.audit_log ⇒ Object
Returns the value of attribute audit_log.
-
.default_adapter ⇒ Object
Returns the value of attribute default_adapter.
-
.versioning ⇒ Object
Returns the value of attribute versioning.
-
.versioning_enabled ⇒ Object
writeonly
When false, Feature#record_change skips version snapshots (audit log entries are still written).
-
.warn_on_deprecated ⇒ Object
Returns the value of attribute warn_on_deprecated.
Class Method Summary collapse
- .[](feature_name) ⇒ Object
-
.bulk_disable(feature_names, _context = {}) ⇒ Object
Disable every named feature globally, with the same semantics as Feature#disable: the off value for the feature's type AND targeting cleared, so a flag that was enabled for one user is off for that user too.
-
.bulk_enable(feature_names, _context = {}) ⇒ Object
Enable every named feature globally, with the same semantics as Feature#enable: value true AND targeting cleared.
- .change_recording_suppressed? ⇒ Boolean
- .configure(&block) ⇒ Object
- .current_actor ⇒ Object
-
.default_adapter_registry ⇒ Object
Get default adapter registry (public method for use by other classes).
-
.definition_mode ⇒ Object
Suppress audit/version recording while declarative feature definitions are (re)applied.
- .definition_mode? ⇒ Boolean
- .disabled?(feature_name, context = {}) ⇒ Boolean
- .disabled_for?(feature_name, object, **additional_context) ⇒ Boolean
-
.enable_redis_tracking(enable: true) ⇒ Object
Manually enable Redis tracking for performance metrics Useful if Redis adapter becomes available after initial configuration.
- .enabled?(feature_name, context = {}) ⇒ Boolean
- .enabled_for?(feature_name, object, **additional_context) ⇒ Boolean
- .exists?(feature_name) ⇒ Boolean
- .export(format: :json) ⇒ Object
-
.feature_average_duration(feature_name, operation: nil) ⇒ Object
Get average duration for a feature (optionally filtered by operation).
-
.feature_stats(feature_name) ⇒ Object
Get total usage count for a feature (combines memory and Redis counts).
-
.feature_usage_count(feature_name) ⇒ Object
Get usage count for a feature.
- .features ⇒ Object
-
.health ⇒ Object
What a host health check wants to know about this process's view of the flags: whether it is listening for cross-process invalidations, when it last confirmed its state against the shared backend, and whether async writes are queued.
- .import(data, format: :json) ⇒ Object
-
.most_used_features(limit: 10) ⇒ Object
Get most used features.
-
.performance_metrics ⇒ Object
Getter for performance_metrics.
-
.performance_metrics=(value) ⇒ Object
Override performance_metrics setter to auto-enable Redis tracking.
-
.preload! ⇒ Object
Preload all features into memory cache in minimal queries.
-
.refresh! ⇒ Object
Re-read every feature from the shared backend now, without waiting for the next scheduled refresh: memory is brought in line and registered features whose stored state changed are reloaded.
- .register_feature(name, **options) ⇒ Object
-
.reload_feature(feature_name) ⇒ Object
Reload a feature from the adapter (useful when feature is changed externally).
- .reset! ⇒ Object
-
.shutdown!(timeout: 5) ⇒ Object
Gracefully terminate background threads (Redis Pub/Sub subscriber, async metrics processor) so the host process can exit promptly.
-
.suppress_change_recording ⇒ Object
Reentrancy guard for Feature#record_change: the outermost public mutator records once; nested mutator calls (enable -> set_value) run silently so one logical operation never produces multiple entries.
-
.unknown_dependency_policy ⇒ Object
What to do when a feature depends on a prerequisite that is registered nowhere in this process AND absent from the shared backend.
- .unknown_dependency_policy=(policy) ⇒ Object
- .unknown_dependency_satisfied? ⇒ Boolean
- .variant(feature_name, context = {}) ⇒ Object
- .variant_value(feature_name, context = {}) ⇒ Object
- .versioning_enabled? ⇒ Boolean
-
.with_actor(actor) ⇒ Object
Attribute all changes made inside the block to the given actor.
Class Attribute Details
.adapter_registry ⇒ Object
Returns the value of attribute adapter_registry.
65 66 67 |
# File 'lib/magick.rb', line 65 def adapter_registry @adapter_registry end |
.audit_log ⇒ Object
Returns the value of attribute audit_log.
65 66 67 |
# File 'lib/magick.rb', line 65 def audit_log @audit_log end |
.default_adapter ⇒ Object
Returns the value of attribute default_adapter.
65 66 67 |
# File 'lib/magick.rb', line 65 def default_adapter @default_adapter end |
.versioning ⇒ Object
Returns the value of attribute versioning.
65 66 67 |
# File 'lib/magick.rb', line 65 def versioning @versioning end |
.versioning_enabled=(value) ⇒ Object (writeonly)
When false, Feature#record_change skips version snapshots (audit log
entries are still written). Set via versioning enabled: false in the
configuration DSL.
302 303 304 |
# File 'lib/magick.rb', line 302 def versioning_enabled=(value) @versioning_enabled = value end |
.warn_on_deprecated ⇒ Object
Returns the value of attribute warn_on_deprecated.
65 66 67 |
# File 'lib/magick.rb', line 65 def warn_on_deprecated @warn_on_deprecated end |
Class Method Details
.[](feature_name) ⇒ Object
168 169 170 171 |
# File 'lib/magick.rb', line 168 def [](feature_name) # Return registered feature if it exists, otherwise create new instance features[feature_name.to_s] || Feature.new(feature_name, adapter_registry || default_adapter_registry) end |
.bulk_disable(feature_names, _context = {}) ⇒ Object
Disable every named feature globally, with the same semantics as Feature#disable: the off value for the feature's type AND targeting cleared, so a flag that was enabled for one user is off for that user too. Every feature type has an "off", so nothing is skipped.
272 273 274 |
# File 'lib/magick.rb', line 272 def bulk_disable(feature_names, _context = {}) bulk_toggle(feature_names, &:disable) end |
.bulk_enable(feature_names, _context = {}) ⇒ Object
Enable every named feature globally, with the same semantics as Feature#enable: value true AND targeting cleared. Only a boolean feature has an "on"; a string or number one is left untouched and named in the result rather than silently passed over. Returns a BulkResult, which still iterates as the array of features this used to return.
260 261 262 263 264 265 266 |
# File 'lib/magick.rb', line 260 def bulk_enable(feature_names, _context = {}) bulk_toggle(feature_names) do |feature| next "cannot enable a #{feature.type} feature; use set_value" unless feature.type == :boolean feature.enable end end |
.change_recording_suppressed? ⇒ Boolean
353 354 355 |
# File 'lib/magick.rb', line 353 def change_recording_suppressed? Thread.current[:magick_change_recording] == true || definition_mode? end |
.configure(&block) ⇒ Object
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/magick.rb', line 137 def configure(&block) @performance_metrics ||= PerformanceMetrics.new @audit_log ||= AuditLog.new @warn_on_deprecated ||= false # Ensure adapter_registry is set (fallback to default if not configured) @adapter_registry ||= default_adapter_registry # Support both old style and new DSL style return unless block_given? if block.arity.zero? # New DSL style - calls apply! automatically Magick::ConfigDSL.configure(&block) else # Old style - need to manually reapply Redis tracking after configuration yield self # Ensure adapter_registry is still set after configuration @adapter_registry ||= default_adapter_registry # Enable Redis tracking if adapter is available and performance_metrics exists # Only enable if not already enabled (to avoid overriding explicit false setting) if @performance_metrics && @adapter_registry.is_a?(Adapters::Registry) && @adapter_registry.redis_available? unless @performance_metrics.instance_variable_get(:@redis_enabled) @performance_metrics.enable_redis_tracking(enable: true) end end end # Final check: ensure adapter_registry is set @adapter_registry ||= default_adapter_registry end |
.current_actor ⇒ Object
321 322 323 |
# File 'lib/magick.rb', line 321 def current_actor Thread.current[:magick_actor] end |
.default_adapter_registry ⇒ Object
Get default adapter registry (public method for use by other classes)
438 439 440 441 442 443 444 445 446 447 448 |
# File 'lib/magick.rb', line 438 def default_adapter_registry @default_adapter_registry ||= begin memory_adapter = Adapters::Memory.new redis_adapter = begin Adapters::Redis.new if defined?(Redis) rescue AdapterError nil end Adapters::Registry.new(memory_adapter, redis_adapter) end end |
.definition_mode ⇒ Object
Suppress audit/version recording while declarative feature definitions are (re)applied. Process boot replays config/features.rb in every container; recording those replays would flood history with identical snapshots. The Rails railtie wraps the features file load in this; non-Rails apps should do the same around their definition file.
330 331 332 333 334 335 336 |
# File 'lib/magick.rb', line 330 def definition_mode previous = Thread.current[:magick_definition_mode] Thread.current[:magick_definition_mode] = true yield ensure Thread.current[:magick_definition_mode] = previous end |
.definition_mode? ⇒ Boolean
338 339 340 |
# File 'lib/magick.rb', line 338 def definition_mode? Thread.current[:magick_definition_mode] == true end |
.disabled?(feature_name, context = {}) ⇒ Boolean
237 238 239 |
# File 'lib/magick.rb', line 237 def disabled?(feature_name, context = {}) !enabled?(feature_name, context) end |
.disabled_for?(feature_name, object, **additional_context) ⇒ Boolean
204 205 206 |
# File 'lib/magick.rb', line 204 def disabled_for?(feature_name, object, **additional_context) !enabled_for?(feature_name, object, **additional_context) end |
.enable_redis_tracking(enable: true) ⇒ Object
Manually enable Redis tracking for performance metrics Useful if Redis adapter becomes available after initial configuration
359 360 361 362 363 |
# File 'lib/magick.rb', line 359 def enable_redis_tracking(enable: true) return unless performance_metrics performance_metrics.enable_redis_tracking(enable: enable) end |
.enabled?(feature_name, context = {}) ⇒ Boolean
192 193 194 195 196 197 |
# File 'lib/magick.rb', line 192 def enabled?(feature_name, context = {}) # Fast path: use string key directly (avoid repeated to_s conversion) feature_name_str = feature_name.to_s feature = features[feature_name_str] || self[feature_name] feature.enabled?(context) end |
.enabled_for?(feature_name, object, **additional_context) ⇒ Boolean
199 200 201 202 |
# File 'lib/magick.rb', line 199 def enabled_for?(feature_name, object, **additional_context) feature = features[feature_name.to_s] || self[feature_name] feature.enabled_for?(object, **additional_context) end |
.exists?(feature_name) ⇒ Boolean
251 252 253 |
# File 'lib/magick.rb', line 251 def exists?(feature_name) features.key?(feature_name.to_s) || (adapter_registry || default_adapter_registry).exists?(feature_name) end |
.export(format: :json) ⇒ Object
276 277 278 279 280 281 282 283 284 285 |
# File 'lib/magick.rb', line 276 def export(format: :json) case format when :json ExportImport.export_json(features) when :hash ExportImport.export(features) else ExportImport.export(features) end end |
.feature_average_duration(feature_name, operation: nil) ⇒ Object
Get average duration for a feature (optionally filtered by operation)
386 387 388 389 390 |
# File 'lib/magick.rb', line 386 def feature_average_duration(feature_name, operation: nil) return 0.0 unless performance_metrics performance_metrics.average_duration(feature_name: feature_name, operation: operation) end |
.feature_stats(feature_name) ⇒ Object
Get total usage count for a feature (combines memory and Redis counts)
366 367 368 369 370 371 372 373 374 375 376 377 378 |
# File 'lib/magick.rb', line 366 def feature_stats(feature_name) return {} unless performance_metrics { usage_count: performance_metrics.usage_count(feature_name), average_duration: performance_metrics.average_duration(feature_name: feature_name), average_duration_by_operation: { enabled: performance_metrics.average_duration(feature_name: feature_name, operation: 'enabled?'), value: performance_metrics.average_duration(feature_name: feature_name, operation: 'value'), get_value: performance_metrics.average_duration(feature_name: feature_name, operation: 'get_value') } } end |
.feature_usage_count(feature_name) ⇒ Object
Get usage count for a feature
381 382 383 |
# File 'lib/magick.rb', line 381 def feature_usage_count(feature_name) performance_metrics&.usage_count(feature_name) || 0 end |
.features ⇒ Object
180 181 182 |
# File 'lib/magick.rb', line 180 def features @features ||= {} end |
.health ⇒ Object
What a host health check wants to know about this process's view of the flags: whether it is listening for cross-process invalidations, when it last confirmed its state against the shared backend, and whether async writes are queued. See Adapters::Registry#health for the keys.
230 231 232 233 234 235 |
# File 'lib/magick.rb', line 230 def health registry = adapter_registry || default_adapter_registry return {} unless registry.respond_to?(:health) registry.health end |
.import(data, format: :json) ⇒ Object
287 288 289 290 291 292 293 |
# File 'lib/magick.rb', line 287 def import(data, format: :json) imported = ExportImport.import(data, adapter_registry || default_adapter_registry) FEATURES_MUTEX.synchronize do @features = (@features || {}).merge(imported) end imported end |
.most_used_features(limit: 10) ⇒ Object
Get most used features
393 394 395 |
# File 'lib/magick.rb', line 393 def most_used_features(limit: 10) performance_metrics&.most_used_features(limit: limit) || {} end |
.performance_metrics ⇒ Object
Getter for performance_metrics
102 103 104 |
# File 'lib/magick.rb', line 102 def performance_metrics @performance_metrics end |
.performance_metrics=(value) ⇒ Object
Override performance_metrics setter to auto-enable Redis tracking
69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/magick.rb', line 69 def performance_metrics=(value) @performance_metrics = value # Auto-enable Redis tracking if Redis adapter is available if value && adapter_registry.is_a?(Adapters::Registry) && adapter_registry.redis_available? value.enable_redis_tracking(enable: true) end # Update all existing features to enable performance metrics tracking if value features.each_value do |feature| feature.instance_variable_set(:@_perf_metrics_enabled, true) end end value end |
.preload! ⇒ Object
Preload all features into memory cache in minimal queries. Call after configuration and feature registration to avoid per-feature cache misses.
399 400 401 402 403 404 405 406 407 408 409 410 411 412 |
# File 'lib/magick.rb', line 399 def preload! registry = adapter_registry || default_adapter_registry return unless registry # Bulk load all feature data into memory (1-2 queries total) all_data = registry.preload! # Also preload registered features' state from the cached data features.each_value do |feature| feature.reload end all_data end |
.refresh! ⇒ Object
Re-read every feature from the shared backend now, without waiting for the next scheduled refresh: memory is brought in line and registered features whose stored state changed are reloaded. For a console or a rake task that just changed flags behind the gem's back. Returns the names of the features that changed, or nil when no shared backend answered.
219 220 221 222 223 224 |
# File 'lib/magick.rb', line 219 def refresh! registry = adapter_registry || default_adapter_registry return nil unless registry.respond_to?(:refresh_from_source!) registry.refresh_from_source! end |
.register_feature(name, **options) ⇒ Object
184 185 186 187 188 189 190 |
# File 'lib/magick.rb', line 184 def register_feature(name, **) feature = Feature.new(name, adapter_registry || default_adapter_registry, **) FEATURES_MUTEX.synchronize do @features = (@features || {}).merge(name.to_s => feature) end feature end |
.reload_feature(feature_name) ⇒ Object
Reload a feature from the adapter (useful when feature is changed externally)
209 210 211 212 |
# File 'lib/magick.rb', line 209 def reload_feature(feature_name) feature = features[feature_name.to_s] || self[feature_name] feature.reload end |
.reset! ⇒ Object
414 415 416 417 418 419 420 421 422 423 424 425 426 |
# File 'lib/magick.rb', line 414 def reset! safely_shutdown(@adapter_registry) { |r| r.shutdown } safely_shutdown(@default_adapter_registry) { |r| r.shutdown } @features = {} @adapter_registry = nil @default_adapter = nil @default_adapter_registry = nil @versioning = nil @audit_log = nil @versioning_enabled = nil @unknown_dependency_policy = nil @performance_metrics&.clear! end |
.shutdown!(timeout: 5) ⇒ Object
Gracefully terminate background threads (Redis Pub/Sub subscriber,
async metrics processor) so the host process can exit promptly.
Intended for use in Rails shutdown hooks, at_exit, or tests.
431 432 433 434 435 |
# File 'lib/magick.rb', line 431 def shutdown!(timeout: 5) safely_shutdown(@adapter_registry) { |r| r.shutdown(timeout: timeout) } safely_shutdown(@performance_metrics, &:stop_async_processor) true end |
.suppress_change_recording ⇒ Object
Reentrancy guard for Feature#record_change: the outermost public mutator records once; nested mutator calls (enable -> set_value) run silently so one logical operation never produces multiple entries.
345 346 347 348 349 350 351 |
# File 'lib/magick.rb', line 345 def suppress_change_recording previous = Thread.current[:magick_change_recording] Thread.current[:magick_change_recording] = true yield ensure Thread.current[:magick_change_recording] = previous end |
.unknown_dependency_policy ⇒ Object
What to do when a feature depends on a prerequisite that is registered nowhere in this process AND absent from the shared backend.
:satisfied (default) the unknown prerequisite is ignored; the
dependent feature falls back to its own value/targeting.
A prerequisite that has not been deployed yet, or a typo,
cannot switch off correctly configured features.
:unsatisfied the dependent feature evaluates false until the
prerequisite exists. Choose this when a missing
prerequisite means the dependent feature is unsafe to run.
Either way the unknown name is reported (once per process, per feature) on stderr, so the condition is never silent.
119 120 121 |
# File 'lib/magick.rb', line 119 def unknown_dependency_policy @unknown_dependency_policy || :satisfied end |
.unknown_dependency_policy=(policy) ⇒ Object
123 124 125 126 127 128 129 130 131 |
# File 'lib/magick.rb', line 123 def unknown_dependency_policy=(policy) normalized = policy.to_sym unless UNKNOWN_DEPENDENCY_POLICIES.include?(normalized) raise ArgumentError, "unknown_dependency_policy must be one of #{UNKNOWN_DEPENDENCY_POLICIES.inspect}, got #{policy.inspect}" end @unknown_dependency_policy = normalized end |
.unknown_dependency_satisfied? ⇒ Boolean
133 134 135 |
# File 'lib/magick.rb', line 133 def unknown_dependency_satisfied? unknown_dependency_policy == :satisfied end |
.variant(feature_name, context = {}) ⇒ Object
241 242 243 244 |
# File 'lib/magick.rb', line 241 def variant(feature_name, context = {}) feature = features[feature_name.to_s] || self[feature_name] feature.get_variant(context) end |
.variant_value(feature_name, context = {}) ⇒ Object
246 247 248 249 |
# File 'lib/magick.rb', line 246 def variant_value(feature_name, context = {}) feature = features[feature_name.to_s] || self[feature_name] feature.get_variant_value(context) end |
.versioning_enabled? ⇒ Boolean
304 305 306 |
# File 'lib/magick.rb', line 304 def versioning_enabled? @versioning_enabled.nil? || @versioning_enabled != false end |
.with_actor(actor) ⇒ Object
313 314 315 316 317 318 319 |
# File 'lib/magick.rb', line 313 def with_actor(actor) previous = Thread.current[:magick_actor] Thread.current[:magick_actor] = actor yield ensure Thread.current[:magick_actor] = previous end |