launchdarkly-server-sdk 6.2.5 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
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