launchdarkly-server-sdk 6.2.5 → 7.0.0

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/lib/ldclient-rb/config.rb +203 -43
  4. data/lib/ldclient-rb/context.rb +487 -0
  5. data/lib/ldclient-rb/evaluation_detail.rb +85 -26
  6. data/lib/ldclient-rb/events.rb +185 -146
  7. data/lib/ldclient-rb/flags_state.rb +25 -14
  8. data/lib/ldclient-rb/impl/big_segments.rb +117 -0
  9. data/lib/ldclient-rb/impl/context.rb +96 -0
  10. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  11. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  12. data/lib/ldclient-rb/impl/evaluator.rb +428 -132
  13. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  14. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  15. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  16. data/lib/ldclient-rb/impl/event_sender.rb +6 -6
  17. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  18. data/lib/ldclient-rb/impl/event_types.rb +78 -0
  19. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +7 -7
  20. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +92 -28
  21. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
  22. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +165 -32
  23. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
  24. data/lib/ldclient-rb/impl/model/clause.rb +39 -0
  25. data/lib/ldclient-rb/impl/model/feature_flag.rb +213 -0
  26. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  27. data/lib/ldclient-rb/impl/model/segment.rb +126 -0
  28. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  29. data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
  30. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  31. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  32. data/lib/ldclient-rb/impl/util.rb +62 -1
  33. data/lib/ldclient-rb/in_memory_store.rb +2 -2
  34. data/lib/ldclient-rb/integrations/consul.rb +9 -2
  35. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -2
  36. data/lib/ldclient-rb/integrations/file_data.rb +108 -0
  37. data/lib/ldclient-rb/integrations/redis.rb +43 -3
  38. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +594 -0
  39. data/lib/ldclient-rb/integrations/test_data.rb +213 -0
  40. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +14 -9
  41. data/lib/ldclient-rb/integrations.rb +2 -51
  42. data/lib/ldclient-rb/interfaces.rb +151 -1
  43. data/lib/ldclient-rb/ldclient.rb +175 -133
  44. data/lib/ldclient-rb/memoized_value.rb +1 -1
  45. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  46. data/lib/ldclient-rb/polling.rb +22 -41
  47. data/lib/ldclient-rb/reference.rb +274 -0
  48. data/lib/ldclient-rb/requestor.rb +7 -7
  49. data/lib/ldclient-rb/stream.rb +9 -9
  50. data/lib/ldclient-rb/util.rb +11 -17
  51. data/lib/ldclient-rb/version.rb +1 -1
  52. data/lib/ldclient-rb.rb +2 -4
  53. metadata +49 -23
  54. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  55. data/lib/ldclient-rb/file_data_source.rb +0 -314
  56. data/lib/ldclient-rb/impl/event_factory.rb +0 -126
  57. data/lib/ldclient-rb/newrelic.rb +0 -17
  58. data/lib/ldclient-rb/redis_store.rb +0 -88
  59. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -1,6 +1,6 @@
1
+ require "ldclient-rb/impl/big_segments"
1
2
  require "ldclient-rb/impl/diagnostic_events"
2
3
  require "ldclient-rb/impl/evaluator"
3
- require "ldclient-rb/impl/event_factory"
4
4
  require "ldclient-rb/impl/store_client_wrapper"
5
5
  require "concurrent/atomics"
6
6
  require "digest/sha1"
@@ -45,9 +45,6 @@ module LaunchDarkly
45
45
 
46
46
  @sdk_key = sdk_key
47
47
 
48
- @event_factory_default = EventFactory.new(false)
49
- @event_factory_with_reasons = EventFactory.new(true)
50
-
51
48
  # We need to wrap the feature store object with a FeatureStoreClientWrapper in order to add
52
49
  # some necessary logic around updates. Unfortunately, we have code elsewhere that accesses
53
50
  # the feature store through the Config object, so we need to make a new Config that uses
@@ -57,9 +54,13 @@ module LaunchDarkly
57
54
  updated_config.instance_variable_set(:@feature_store, @store)
58
55
  @config = updated_config
59
56
 
57
+ @big_segment_store_manager = Impl::BigSegmentStoreManager.new(config.big_segments, @config.logger)
58
+ @big_segment_store_status_provider = @big_segment_store_manager.status_provider
59
+
60
60
  get_flag = lambda { |key| @store.get(FEATURES, key) }
61
61
  get_segment = lambda { |key| @store.get(SEGMENTS, key) }
62
- @evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, @config.logger)
62
+ get_big_segments_membership = lambda { |key| @big_segment_store_manager.get_context_membership(key) }
63
+ @evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, get_big_segments_membership, @config.logger)
63
64
 
64
65
  if !@config.offline? && @config.send_events && !@config.diagnostic_opt_out?
65
66
  diagnostic_accumulator = Impl::DiagnosticAccumulator.new(Impl::DiagnosticAccumulator.create_diagnostic_id(sdk_key))
@@ -119,26 +120,20 @@ module LaunchDarkly
119
120
  end
120
121
 
121
122
  #
122
- # @param key [String] the feature flag key
123
- # @param user [Hash] the user properties
124
- # @param default [Boolean] (false) the value to use if the flag cannot be evaluated
125
- # @return [Boolean] the flag value
126
- # @deprecated Use {#variation} instead.
127
- #
128
- def toggle?(key, user, default = false)
129
- @config.logger.warn { "[LDClient] toggle? is deprecated. Use variation instead" }
130
- variation(key, user, default)
131
- end
132
-
133
- #
134
- # Creates a hash string that can be used by the JavaScript SDK to identify a user.
123
+ # Creates a hash string that can be used by the JavaScript SDK to identify a context.
135
124
  # For more information, see [Secure mode](https://docs.launchdarkly.com/sdk/features/secure-mode#ruby).
136
125
  #
137
- # @param user [Hash] the user properties
138
- # @return [String] a hash string
126
+ # @param context [Hash, LDContext]
127
+ # @return [String, nil] a hash string or nil if the provided context was invalid
139
128
  #
140
- def secure_mode_hash(user)
141
- OpenSSL::HMAC.hexdigest("sha256", @sdk_key, user[:key].to_s)
129
+ def secure_mode_hash(context)
130
+ context = Impl::Context::make_context(context)
131
+ unless context.valid?
132
+ @config.logger.warn("secure_mode_hash called with invalid context: #{context.error}")
133
+ return nil
134
+ end
135
+
136
+ OpenSSL::HMAC.hexdigest("sha256", @sdk_key, context.fully_qualified_key)
142
137
  end
143
138
 
144
139
  #
@@ -164,44 +159,22 @@ module LaunchDarkly
164
159
  end
165
160
 
166
161
  #
167
- # Determines the variation of a feature flag to present to a user.
168
- #
169
- # At a minimum, the user hash should contain a `:key`, which should be the unique
170
- # identifier for your user (or, for an anonymous user, a session identifier or
171
- # cookie).
172
- #
173
- # Other supported user attributes include IP address, country code, and an arbitrary hash of
174
- # custom attributes. For more about the supported user properties and how they work in
175
- # LaunchDarkly, see [Targeting users](https://docs.launchdarkly.com/home/flags/targeting-users).
176
- #
177
- # The optional `:privateAttributeNames` user property allows you to specify a list of
178
- # attribute names that should not be sent back to LaunchDarkly.
179
- # [Private attributes](https://docs.launchdarkly.com/home/users/attributes#creating-private-user-attributes)
180
- # can also be configured globally in {Config}.
181
- #
182
- # @example Basic user hash
183
- # {key: "my-user-id"}
184
- #
185
- # @example More complete user hash
186
- # {key: "my-user-id", ip: "127.0.0.1", country: "US", custom: {customer_rank: 1000}}
187
- #
188
- # @example User with a private attribute
189
- # {key: "my-user-id", email: "email@example.com", privateAttributeNames: ["email"]}
162
+ # Determines the variation of a feature flag to present for a context.
190
163
  #
191
164
  # @param key [String] the unique feature key for the feature flag, as shown
192
165
  # on the LaunchDarkly dashboard
193
- # @param user [Hash] a hash containing parameters for the end user requesting the flag
166
+ # @param context [Hash, LDContext] a hash or LDContext instance describing the context requesting the flag
194
167
  # @param default the default value of the flag; this is used if there is an error
195
168
  # condition making it impossible to find or evaluate the flag
196
169
  #
197
- # @return the variation to show the user, or the default value if there's an an error
170
+ # @return the variation for the provided context, or the default value if there's an an error
198
171
  #
199
- def variation(key, user, default)
200
- evaluate_internal(key, user, default, @event_factory_default).value
172
+ def variation(key, context, default)
173
+ evaluate_internal(key, context, default, false).value
201
174
  end
202
175
 
203
176
  #
204
- # Determines the variation of a feature flag for a user, like {#variation}, but also
177
+ # Determines the variation of a feature flag for a context, like {#variation}, but also
205
178
  # provides additional information about how this value was calculated.
206
179
  #
207
180
  # The return value of `variation_detail` is an {EvaluationDetail} object, which has
@@ -217,43 +190,48 @@ module LaunchDarkly
217
190
  #
218
191
  # @param key [String] the unique feature key for the feature flag, as shown
219
192
  # on the LaunchDarkly dashboard
220
- # @param user [Hash] a hash containing parameters for the end user requesting the flag
193
+ # @param context [Hash, LDContext] a hash or object describing the context requesting the flag,
221
194
  # @param default the default value of the flag; this is used if there is an error
222
195
  # condition making it impossible to find or evaluate the flag
223
196
  #
224
197
  # @return [EvaluationDetail] an object describing the result
225
198
  #
226
- def variation_detail(key, user, default)
227
- evaluate_internal(key, user, default, @event_factory_with_reasons)
199
+ def variation_detail(key, context, default)
200
+ evaluate_internal(key, context, default, true)
228
201
  end
229
202
 
230
203
  #
231
- # Registers the user. This method simply creates an analytics event containing the user
232
- # properties, so that LaunchDarkly will know about that user if it does not already.
204
+ # Registers the context. This method simply creates an analytics event containing the context
205
+ # properties, so that LaunchDarkly will know about that context if it does not already.
233
206
  #
234
- # Calling {#variation} or {#variation_detail} also sends the user information to
207
+ # Calling {#variation} or {#variation_detail} also sends the context information to
235
208
  # LaunchDarkly (if events are enabled), so you only need to use {#identify} if you
236
- # want to identify the user without evaluating a flag.
209
+ # want to identify the context without evaluating a flag.
237
210
  #
238
211
  # Note that event delivery is asynchronous, so the event may not actually be sent
239
212
  # until later; see {#flush}.
240
213
  #
241
- # @param user [Hash] The user to register; this can have all the same user properties
242
- # described in {#variation}
214
+ # @param context [Hash, LDContext] a hash or object describing the context to register
243
215
  # @return [void]
244
216
  #
245
- def identify(user)
246
- if !user || user[:key].nil?
247
- @config.logger.warn("Identify called with nil user or nil user key!")
217
+ def identify(context)
218
+ context = LaunchDarkly::Impl::Context.make_context(context)
219
+ unless context.valid?
220
+ @config.logger.warn("Identify called with invalid context: #{context.error}")
221
+ return
222
+ end
223
+
224
+ if context.key == ""
225
+ @config.logger.warn("Identify called with empty key")
248
226
  return
249
227
  end
250
- sanitize_user(user)
251
- @event_processor.add_event(@event_factory_default.new_identify_event(user))
228
+
229
+ @event_processor.record_identify_event(context)
252
230
  end
253
231
 
254
232
  #
255
- # Tracks that a user performed an event. This method creates a "custom" analytics event
256
- # containing the specified event name (key), user properties, and optional data.
233
+ # Tracks that a context performed an event. This method creates a "custom" analytics event
234
+ # containing the specified event name (key), context properties, and optional data.
257
235
  #
258
236
  # Note that event delivery is asynchronous, so the event may not actually be sent
259
237
  # until later; see {#flush}.
@@ -264,8 +242,7 @@ module LaunchDarkly
264
242
  # for the latest status.
265
243
  #
266
244
  # @param event_name [String] The name of the event
267
- # @param user [Hash] The user to register; this can have all the same user properties
268
- # described in {#variation}
245
+ # @param context [Hash, LDContext] a hash or object describing the context to track
269
246
  # @param data [Hash] An optional hash containing any additional data associated with the event
270
247
  # @param metric_value [Number] A numeric value used by the LaunchDarkly experimentation
271
248
  # feature in numeric custom metrics. Can be omitted if this event is used by only
@@ -273,52 +250,22 @@ module LaunchDarkly
273
250
  # for Data Export.
274
251
  # @return [void]
275
252
  #
276
- def track(event_name, user, data = nil, metric_value = nil)
277
- if !user || user[:key].nil?
278
- @config.logger.warn("Track called with nil user or nil user key!")
279
- return
280
- end
281
- sanitize_user(user)
282
- @event_processor.add_event(@event_factory_default.new_custom_event(event_name, user, data, metric_value))
283
- end
284
-
285
- #
286
- # Associates a new and old user object for analytics purposes via an alias event.
287
- #
288
- # @param current_context [Hash] The current version of a user.
289
- # @param previous_context [Hash] The previous version of a user.
290
- # @return [void]
291
- #
292
- def alias(current_context, previous_context)
293
- if !current_context || current_context[:key].nil? || !previous_context || previous_context[:key].nil?
294
- @config.logger.warn("Alias called with nil user or nil user key!")
253
+ def track(event_name, context, data = nil, metric_value = nil)
254
+ context = LaunchDarkly::Impl::Context.make_context(context)
255
+ unless context.valid?
256
+ @config.logger.warn("Track called with invalid context: #{context.error}")
295
257
  return
296
258
  end
297
- sanitize_user(current_context)
298
- sanitize_user(previous_context)
299
- @event_processor.add_event(@event_factory_default.new_alias_event(current_context, previous_context))
300
- end
301
259
 
302
- #
303
- # Returns all feature flag values for the given user.
304
- #
305
- # @deprecated Please use {#all_flags_state} instead. Current versions of the
306
- # client-side SDK will not generate analytics events correctly if you pass the
307
- # result of `all_flags`.
308
- #
309
- # @param user [Hash] The end user requesting the feature flags
310
- # @return [Hash] a hash of feature flag keys to values
311
- #
312
- def all_flags(user)
313
- all_flags_state(user).values_map
260
+ @event_processor.record_custom_event(context, event_name, data, metric_value)
314
261
  end
315
262
 
316
263
  #
317
- # Returns a {FeatureFlagsState} object that encapsulates the state of all feature flags for a given user,
264
+ # Returns a {FeatureFlagsState} object that encapsulates the state of all feature flags for a given context,
318
265
  # including the flag values and also metadata that can be used on the front end. This method does not
319
266
  # send analytics events back to LaunchDarkly.
320
267
  #
321
- # @param user [Hash] The end user requesting the feature flags
268
+ # @param context [Hash, LDContext] a hash or object describing the context requesting the flags,
322
269
  # @param options [Hash] Optional parameters to control how the state is generated
323
270
  # @option options [Boolean] :client_side_only (false) True if only flags marked for use with the
324
271
  # client-side SDK should be included in the state. By default, all flags are included.
@@ -330,11 +277,21 @@ module LaunchDarkly
330
277
  # of the JSON data if you are passing the flag state to the front end.
331
278
  # @return [FeatureFlagsState] a {FeatureFlagsState} object which can be serialized to JSON
332
279
  #
333
- def all_flags_state(user, options={})
280
+ def all_flags_state(context, options={})
334
281
  return FeatureFlagsState.new(false) if @config.offline?
335
282
 
336
- unless user && !user[:key].nil?
337
- @config.logger.error { "[LDClient] User and user key must be specified in all_flags_state" }
283
+ unless initialized?
284
+ if @store.initialized?
285
+ @config.logger.warn { "Called all_flags_state before client initialization; using last known values from data store" }
286
+ else
287
+ @config.logger.warn { "Called all_flags_state before client initialization. Data store not available; returning empty state" }
288
+ return FeatureFlagsState.new(false)
289
+ end
290
+ end
291
+
292
+ context = Impl::Context::make_context(context)
293
+ unless context.valid?
294
+ @config.logger.error { "[LDClient] Context was invalid for all_flags_state (#{context.error})" }
338
295
  return FeatureFlagsState.new(false)
339
296
  end
340
297
 
@@ -354,14 +311,25 @@ module LaunchDarkly
354
311
  next
355
312
  end
356
313
  begin
357
- result = @evaluator.evaluate(f, user, @event_factory_default)
358
- state.add_flag(f, result.detail.value, result.detail.variation_index, with_reasons ? result.detail.reason : nil,
359
- details_only_if_tracked)
314
+ detail = @evaluator.evaluate(f, context).detail
360
315
  rescue => exn
316
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION))
361
317
  Util.log_exception(@config.logger, "Error evaluating flag \"#{k}\" in all_flags_state", exn)
362
- state.add_flag(f, nil, nil, with_reasons ? EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION) : nil,
363
- details_only_if_tracked)
364
318
  end
319
+
320
+ requires_experiment_data = experiment?(f, detail.reason)
321
+ flag_state = {
322
+ key: f[:key],
323
+ value: detail.value,
324
+ variation: detail.variation_index,
325
+ reason: detail.reason,
326
+ version: f[:version],
327
+ trackEvents: f[:trackEvents] || requires_experiment_data,
328
+ trackReason: requires_experiment_data,
329
+ debugEventsUntilDate: f[:debugEventsUntilDate],
330
+ }
331
+
332
+ state.add_flag(flag_state, with_reasons, details_only_if_tracked)
365
333
  end
366
334
 
367
335
  state
@@ -375,9 +343,18 @@ module LaunchDarkly
375
343
  @config.logger.info { "[LDClient] Closing LaunchDarkly client..." }
376
344
  @data_source.stop
377
345
  @event_processor.stop
346
+ @big_segment_store_manager.stop
378
347
  @store.stop
379
348
  end
380
349
 
350
+ #
351
+ # Returns an interface for tracking the status of a Big Segment store.
352
+ #
353
+ # The {BigSegmentStoreStatusProvider} has methods for checking whether the Big Segment store
354
+ # is (as far as the SDK knows) currently operational and tracking changes in this status.
355
+ #
356
+ attr_reader :big_segment_store_status_provider
357
+
381
358
  private
382
359
 
383
360
  def create_default_data_source(sdk_key, config, diagnostic_accumulator)
@@ -395,32 +372,34 @@ module LaunchDarkly
395
372
  end
396
373
  end
397
374
 
375
+ # @param context [Hash, LDContext]
398
376
  # @return [EvaluationDetail]
399
- def evaluate_internal(key, user, default, event_factory)
377
+ def evaluate_internal(key, context, default, with_reasons)
400
378
  if @config.offline?
401
379
  return Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
402
380
  end
403
381
 
404
- unless user
405
- @config.logger.error { "[LDClient] Must specify user" }
382
+ if context.nil?
383
+ @config.logger.error { "[LDClient] Must specify context" }
406
384
  detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default)
407
385
  return detail
408
386
  end
409
387
 
410
- if user[:key].nil?
411
- @config.logger.warn { "[LDClient] Variation called with nil user key; returning default value" }
388
+ context = Impl::Context::make_context(context)
389
+ unless context.valid?
390
+ @config.logger.error { "[LDClient] Context was invalid for flag evaluation (#{context.error}); returning default value" }
412
391
  detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default)
413
392
  return detail
414
393
  end
415
394
 
416
- if !initialized?
395
+ unless initialized?
417
396
  if @store.initialized?
418
397
  @config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
419
398
  else
420
399
  @config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
421
400
  detail = Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
422
- @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
423
- return detail
401
+ record_unknown_flag_eval(key, context, default, detail.reason, with_reasons)
402
+ return detail
424
403
  end
425
404
  end
426
405
 
@@ -429,35 +408,98 @@ module LaunchDarkly
429
408
  if feature.nil?
430
409
  @config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" }
431
410
  detail = Evaluator.error_result(EvaluationReason::ERROR_FLAG_NOT_FOUND, default)
432
- @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
411
+ record_unknown_flag_eval(key, context, default, detail.reason, with_reasons)
433
412
  return detail
434
413
  end
435
414
 
436
415
  begin
437
- res = @evaluator.evaluate(feature, user, event_factory)
438
- if !res.events.nil?
439
- res.events.each do |event|
440
- @event_processor.add_event(event)
416
+ res = @evaluator.evaluate(feature, context)
417
+ unless res.prereq_evals.nil?
418
+ res.prereq_evals.each do |prereq_eval|
419
+ record_prereq_flag_eval(prereq_eval.prereq_flag, prereq_eval.prereq_of_flag, context, prereq_eval.detail, with_reasons)
441
420
  end
442
421
  end
443
422
  detail = res.detail
444
423
  if detail.default_value?
445
424
  detail = EvaluationDetail.new(default, nil, detail.reason)
446
425
  end
447
- @event_processor.add_event(event_factory.new_eval_event(feature, user, detail, default))
448
- return detail
426
+ record_flag_eval(feature, context, detail, default, with_reasons)
427
+ detail
449
428
  rescue => exn
450
429
  Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn)
451
430
  detail = Evaluator.error_result(EvaluationReason::ERROR_EXCEPTION, default)
452
- @event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
453
- return detail
431
+ record_flag_eval_error(feature, context, default, detail.reason, with_reasons)
432
+ detail
454
433
  end
455
434
  end
456
435
 
457
- def sanitize_user(user)
458
- if user[:key]
459
- user[:key] = user[:key].to_s
436
+ private def record_flag_eval(flag, context, detail, default, with_reasons)
437
+ add_experiment_data = experiment?(flag, detail.reason)
438
+ @event_processor.record_eval_event(
439
+ context,
440
+ flag[:key],
441
+ flag[:version],
442
+ detail.variation_index,
443
+ detail.value,
444
+ (add_experiment_data || with_reasons) ? detail.reason : nil,
445
+ default,
446
+ add_experiment_data || flag[:trackEvents] || false,
447
+ flag[:debugEventsUntilDate],
448
+ nil
449
+ )
450
+ end
451
+
452
+ private def record_prereq_flag_eval(prereq_flag, prereq_of_flag, context, detail, with_reasons)
453
+ add_experiment_data = experiment?(prereq_flag, detail.reason)
454
+ @event_processor.record_eval_event(
455
+ context,
456
+ prereq_flag[:key],
457
+ prereq_flag[:version],
458
+ detail.variation_index,
459
+ detail.value,
460
+ (add_experiment_data || with_reasons) ? detail.reason : nil,
461
+ nil,
462
+ add_experiment_data || prereq_flag[:trackEvents] || false,
463
+ prereq_flag[:debugEventsUntilDate],
464
+ prereq_of_flag[:key]
465
+ )
466
+ end
467
+
468
+ private def record_flag_eval_error(flag, context, default, reason, with_reasons)
469
+ @event_processor.record_eval_event(context, flag[:key], flag[:version], nil, default, with_reasons ? reason : nil, default,
470
+ flag[:trackEvents], flag[:debugEventsUntilDate], nil)
471
+ end
472
+
473
+ #
474
+ # @param flag_key [String]
475
+ # @param context [LaunchDarkly::LDContext]
476
+ # @param default [any]
477
+ # @param reason [LaunchDarkly::EvaluationReason]
478
+ # @param with_reasons [Boolean]
479
+ #
480
+ private def record_unknown_flag_eval(flag_key, context, default, reason, with_reasons)
481
+ @event_processor.record_eval_event(context, flag_key, nil, nil, default, with_reasons ? reason : nil, default,
482
+ false, nil, nil)
483
+ end
484
+
485
+ private def experiment?(flag, reason)
486
+ return false unless reason
487
+
488
+ if reason.in_experiment
489
+ return true
490
+ end
491
+
492
+ case reason[:kind]
493
+ when 'RULE_MATCH'
494
+ index = reason[:ruleIndex]
495
+ unless index.nil?
496
+ rules = flag[:rules] || []
497
+ return index >= 0 && index < rules.length && rules[index][:trackEvents]
498
+ end
499
+ when 'FALLTHROUGH'
500
+ return !!flag[:trackEventsFallthrough]
460
501
  end
502
+ false
461
503
  end
462
504
  end
463
505
 
@@ -14,7 +14,7 @@ module LaunchDarkly
14
14
 
15
15
  def get
16
16
  @mutex.synchronize do
17
- if !@inited
17
+ unless @inited
18
18
  @value = @generator.call
19
19
  @inited = true
20
20
  end
@@ -17,7 +17,7 @@ module LaunchDarkly
17
17
  # Attempts to submit a job, but only if a worker is available. Unlike the regular post method,
18
18
  # this returns a value: true if the job was submitted, false if all workers are busy.
19
19
  def post
20
- if !@semaphore.try_acquire(1)
20
+ unless @semaphore.try_acquire(1)
21
21
  return
22
22
  end
23
23
  @pool.post do
@@ -1,3 +1,5 @@
1
+ require "ldclient-rb/impl/repeating_task"
2
+
1
3
  require "concurrent/atomics"
2
4
  require "thread"
3
5
 
@@ -9,8 +11,8 @@ module LaunchDarkly
9
11
  @requestor = requestor
10
12
  @initialized = Concurrent::AtomicBoolean.new(false)
11
13
  @started = Concurrent::AtomicBoolean.new(false)
12
- @stopped = Concurrent::AtomicBoolean.new(false)
13
14
  @ready = Concurrent::Event.new
15
+ @task = Impl::RepeatingTask.new(@config.poll_interval, 0, -> { self.poll }, @config.logger)
14
16
  end
15
17
 
16
18
  def initialized?
@@ -20,56 +22,35 @@ module LaunchDarkly
20
22
  def start
21
23
  return @ready unless @started.make_true
22
24
  @config.logger.info { "[LDClient] Initializing polling connection" }
23
- create_worker
25
+ @task.start
24
26
  @ready
25
27
  end
26
28
 
27
29
  def stop
28
- if @stopped.make_true
29
- if @worker && @worker.alive? && @worker != Thread.current
30
- @worker.run # causes the thread to wake up if it's currently in a sleep
31
- @worker.join
32
- end
33
- @config.logger.info { "[LDClient] Polling connection stopped" }
34
- end
30
+ @task.stop
31
+ @config.logger.info { "[LDClient] Polling connection stopped" }
35
32
  end
36
33
 
37
34
  def poll
38
- all_data = @requestor.request_all_data
39
- if all_data
40
- @config.feature_store.init(all_data)
41
- if @initialized.make_true
42
- @config.logger.info { "[LDClient] Polling connection initialized" }
43
- @ready.set
44
- end
45
- end
46
- end
47
-
48
- def create_worker
49
- @worker = Thread.new do
50
- @config.logger.debug { "[LDClient] Starting polling worker" }
51
- while !@stopped.value do
52
- started_at = Time.now
53
- begin
54
- poll
55
- rescue UnexpectedResponseError => e
56
- message = Util.http_error_message(e.status, "polling request", "will retry")
57
- @config.logger.error { "[LDClient] #{message}" };
58
- if !Util.http_error_recoverable?(e.status)
59
- @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
60
- stop
61
- end
62
- rescue StandardError => exn
63
- Util.log_exception(@config.logger, "Exception while polling", exn)
64
- end
65
- delta = @config.poll_interval - (Time.now - started_at)
66
- if delta > 0
67
- sleep(delta)
35
+ begin
36
+ all_data = @requestor.request_all_data
37
+ if all_data
38
+ @config.feature_store.init(all_data)
39
+ if @initialized.make_true
40
+ @config.logger.info { "[LDClient] Polling connection initialized" }
41
+ @ready.set
68
42
  end
69
43
  end
44
+ rescue UnexpectedResponseError => e
45
+ message = Util.http_error_message(e.status, "polling request", "will retry")
46
+ @config.logger.error { "[LDClient] #{message}" }
47
+ unless Util.http_error_recoverable?(e.status)
48
+ @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
49
+ stop
50
+ end
51
+ rescue StandardError => e
52
+ Util.log_exception(@config.logger, "Exception while polling", e)
70
53
  end
71
54
  end
72
-
73
- private :poll, :create_worker
74
55
  end
75
56
  end