Class: Mixpanel::Flags::LocalFlagsProvider
- Inherits:
-
FlagsProvider
- Object
- FlagsProvider
- Mixpanel::Flags::LocalFlagsProvider
- Defined in:
- lib/mixpanel-ruby/flags/local_flags_provider.rb
Overview
Local feature flags provider Evaluates flags client-side with cached flag definitions
Constant Summary collapse
- DEFAULT_CONFIG =
{ api_host: 'api.mixpanel.com', request_timeout_in_seconds: 10, enable_polling: true, polling_interval_in_seconds: 60 }.freeze
Instance Method Summary collapse
-
#get_all_variants(context) ⇒ Hash
Get all variants for user context Exposure events NOT tracked automatically.
-
#get_variant(flag_key, fallback_variant, context, report_exposure: true) ⇒ SelectedVariant
Get complete variant information.
-
#get_variant_value(flag_key, fallback_value, context, report_exposure: true) ⇒ Object
Get variant value for a flag.
-
#initialize(token, config, tracker_callback, error_handler, credentials = nil) ⇒ LocalFlagsProvider
constructor
A new instance of LocalFlagsProvider.
-
#is_enabled?(flag_key, context) ⇒ Boolean
Check if flag is enabled (for boolean flags).
- #shutdown ⇒ Object
-
#start_polling_for_definitions! ⇒ Object
Start polling for flag definitions Fetches immediately, then at regular intervals if polling enabled.
- #stop_polling_for_definitions! ⇒ Object
Methods inherited from FlagsProvider
#call_flags_endpoint, #track_exposure_event
Constructor Details
#initialize(token, config, tracker_callback, error_handler, credentials = nil) ⇒ LocalFlagsProvider
Returns a new instance of LocalFlagsProvider.
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/mixpanel-ruby/flags/local_flags_provider.rb', line 22 def initialize(token, config, tracker_callback, error_handler, credentials = nil) # compact: an explicit nil from the caller (e.g. # polling_interval_in_seconds: nil) must not override a sane default. # Both the previous sleep(nil) and the current # ConditionVariable#wait(mutex, nil) block indefinitely, which would # silently disable polling — fall back to the default instead. @config = DEFAULT_CONFIG.merge((config || {}).compact) interval = @config[:polling_interval_in_seconds] unless interval.is_a?(Numeric) && interval > 0 raise ArgumentError, "polling_interval_in_seconds must be a positive number, got: #{interval.inspect}" end provider_config = { token: token, api_host: @config[:api_host], request_timeout_in_seconds: @config[:request_timeout_in_seconds], credentials: credentials } super(provider_config, '/flags/definitions', tracker_callback, 'local', error_handler) @flag_definitions = {} @polling_thread = nil @stop_polling = false @polling_mutex = Mutex.new @polling_condition = ConditionVariable.new # Separate from @polling_mutex: serializes the full start/stop lifecycle # (including the join inside stop) so a concurrent start can't observe a # mid-stop state and spawn a new polling thread before the old one # finishes exiting. We can't hold @polling_mutex across the join — the # polling thread needs it to wake from the condition wait. @lifecycle_mutex = Mutex.new end |
Instance Method Details
#get_all_variants(context) ⇒ Hash
Get all variants for user context Exposure events NOT tracked automatically
188 189 190 191 192 193 194 195 196 197 |
# File 'lib/mixpanel-ruby/flags/local_flags_provider.rb', line 188 def get_all_variants(context) variants = {} @flag_definitions.each_key do |flag_key| variant = get_variant(flag_key, nil, context, report_exposure: false) variants[flag_key] = variant if variant end variants end |
#get_variant(flag_key, fallback_variant, context, report_exposure: true) ⇒ SelectedVariant
Get complete variant information
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/mixpanel-ruby/flags/local_flags_provider.rb', line 159 def get_variant(flag_key, fallback_variant, context, report_exposure: true) flag = @flag_definitions[flag_key] return fallback_variant unless flag context_key = flag['context'] unless context.key?(context_key) || context.key?(context_key.to_sym) return fallback_variant end context_value = context[context_key] || context[context_key.to_sym] selected_variant = get_variant_override_for_test_user(flag, context) unless selected_variant rollout = get_assigned_rollout(flag, context_value, context) selected_variant = get_assigned_variant(flag, context_value, flag_key, rollout) if rollout end return fallback_variant unless selected_variant track_exposure_event(flag_key, selected_variant, context) if report_exposure selected_variant end |
#get_variant_value(flag_key, fallback_value, context, report_exposure: true) ⇒ Object
Get variant value for a flag
143 144 145 146 147 148 149 150 151 |
# File 'lib/mixpanel-ruby/flags/local_flags_provider.rb', line 143 def get_variant_value(flag_key, fallback_value, context, report_exposure: true) result = get_variant( flag_key, SelectedVariant.new(variant_value: fallback_value), context, report_exposure: report_exposure ) result.variant_value end |
#is_enabled?(flag_key, context) ⇒ Boolean
Check if flag is enabled (for boolean flags)
132 133 134 135 |
# File 'lib/mixpanel-ruby/flags/local_flags_provider.rb', line 132 def is_enabled?(flag_key, context) value = get_variant_value(flag_key, false, context) value == true end |
#shutdown ⇒ Object
124 125 126 |
# File 'lib/mixpanel-ruby/flags/local_flags_provider.rb', line 124 def shutdown stop_polling_for_definitions! end |
#start_polling_for_definitions! ⇒ Object
Start polling for flag definitions Fetches immediately, then at regular intervals if polling enabled
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/mixpanel-ruby/flags/local_flags_provider.rb', line 60 def start_polling_for_definitions! # Clear @stop_polling BEFORE the initial fetch so a concurrent # stop_polling_for_definitions! arriving during the fetch can flip it # back to true and signal "abandon start". Under @polling_mutex, writes # to @stop_polling are linearized: whichever of {start's clear, stop's # set} happened latest is what we observe under the lifecycle lock # below — i.e. last-writer-wins semantics preserve stop's postcondition # (polling is off when stop returns) even across the unguarded fetch. @polling_mutex.synchronize { @stop_polling = false } # Initial fetch is intentionally outside @lifecycle_mutex: it's a # blocking HTTP call, and holding the lifecycle lock across it would # block a concurrent stop_polling_for_definitions! for the full request # timeout. fetch_flag_definitions @lifecycle_mutex.synchronize do # If a stop arrived during/after our @stop_polling clear above, abort # — the user's stop "wins" because it's the most recent intent. stop_requested = @polling_mutex.synchronize { @stop_polling } # .alive? guards against a prior polling thread that died abnormally # (e.g. error_handler itself raised); without it @polling_thread would # be non-nil-but-dead and we'd silently refuse to restart polling. if !stop_requested && @config[:enable_polling] && (@polling_thread.nil? || !@polling_thread.alive?) @polling_thread = Thread.new do loop do # Check @stop_polling INSIDE the mutex (before and after wait) # so a broadcast from stop_polling_for_definitions! can't be # lost if it arrives while we're outside the synchronized region # (e.g. during fetch_flag_definitions below). stopped = @polling_mutex.synchronize do next true if @stop_polling @polling_condition.wait(@polling_mutex, @config[:polling_interval_in_seconds]) @stop_polling end break if stopped begin fetch_flag_definitions rescue StandardError => e safe_handle_error(e) end end end end end rescue StandardError => e safe_handle_error(e) end |
#stop_polling_for_definitions! ⇒ Object
113 114 115 116 117 118 119 120 121 122 |
# File 'lib/mixpanel-ruby/flags/local_flags_provider.rb', line 113 def stop_polling_for_definitions! @lifecycle_mutex.synchronize do @polling_mutex.synchronize do @stop_polling = true @polling_condition.broadcast end @polling_thread&.join @polling_thread = nil end end |