launchdarkly-server-sdk 7.0.2 → 8.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -4
  3. data/lib/ldclient-rb/config.rb +50 -70
  4. data/lib/ldclient-rb/context.rb +65 -50
  5. data/lib/ldclient-rb/evaluation_detail.rb +5 -1
  6. data/lib/ldclient-rb/events.rb +81 -8
  7. data/lib/ldclient-rb/impl/big_segments.rb +1 -1
  8. data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
  9. data/lib/ldclient-rb/impl/context.rb +3 -3
  10. data/lib/ldclient-rb/impl/context_filter.rb +30 -9
  11. data/lib/ldclient-rb/impl/data_source.rb +188 -0
  12. data/lib/ldclient-rb/impl/data_store.rb +59 -0
  13. data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
  14. data/lib/ldclient-rb/impl/evaluation_with_hook_result.rb +34 -0
  15. data/lib/ldclient-rb/impl/event_sender.rb +1 -0
  16. data/lib/ldclient-rb/impl/event_types.rb +61 -3
  17. data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
  18. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +12 -0
  19. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +8 -0
  20. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +16 -3
  21. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +19 -2
  22. data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
  23. data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
  24. data/lib/ldclient-rb/impl/model/feature_flag.rb +25 -3
  25. data/lib/ldclient-rb/impl/repeating_task.rb +2 -3
  26. data/lib/ldclient-rb/impl/sampler.rb +25 -0
  27. data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
  28. data/lib/ldclient-rb/in_memory_store.rb +7 -0
  29. data/lib/ldclient-rb/integrations/file_data.rb +1 -1
  30. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +84 -15
  31. data/lib/ldclient-rb/integrations/test_data.rb +3 -3
  32. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +11 -0
  33. data/lib/ldclient-rb/interfaces.rb +671 -0
  34. data/lib/ldclient-rb/ldclient.rb +313 -22
  35. data/lib/ldclient-rb/migrations.rb +230 -0
  36. data/lib/ldclient-rb/polling.rb +51 -5
  37. data/lib/ldclient-rb/reference.rb +11 -0
  38. data/lib/ldclient-rb/requestor.rb +5 -5
  39. data/lib/ldclient-rb/stream.rb +91 -29
  40. data/lib/ldclient-rb/util.rb +89 -0
  41. data/lib/ldclient-rb/version.rb +1 -1
  42. data/lib/ldclient-rb.rb +1 -0
  43. metadata +44 -6
@@ -0,0 +1,25 @@
1
+ module LaunchDarkly
2
+ module Impl
3
+ class Sampler
4
+ #
5
+ # @param random [Random]
6
+ #
7
+ def initialize(random)
8
+ @random = random
9
+ end
10
+
11
+ #
12
+ # @param ratio [Int]
13
+ #
14
+ # @return [Boolean]
15
+ #
16
+ def sample(ratio)
17
+ return false unless ratio.is_a? Integer
18
+ return false if ratio <= 0
19
+ return true if ratio == 1
20
+
21
+ @random.rand(1.0) < 1.0 / ratio
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,3 +1,4 @@
1
+ require "concurrent"
1
2
  require "ldclient-rb/interfaces"
2
3
  require "ldclient-rb/impl/store_data_set_sorter"
3
4
 
@@ -5,34 +6,45 @@ module LaunchDarkly
5
6
  module Impl
6
7
  #
7
8
  # Provides additional behavior that the client requires before or after feature store operations.
8
- # Currently this just means sorting the data set for init(). In the future we may also use this
9
- # to provide an update listener capability.
9
+ # This just means sorting the data set for init() and dealing with data store status listeners.
10
10
  #
11
11
  class FeatureStoreClientWrapper
12
12
  include Interfaces::FeatureStore
13
13
 
14
- def initialize(store)
14
+ def initialize(store, store_update_sink, logger)
15
+ # @type [LaunchDarkly::Interfaces::FeatureStore]
15
16
  @store = store
17
+
18
+ @monitoring_enabled = does_store_support_monitoring?
19
+
20
+ # @type [LaunchDarkly::Impl::DataStore::UpdateSink]
21
+ @store_update_sink = store_update_sink
22
+ @logger = logger
23
+
24
+ @mutex = Mutex.new # Covers the following variables
25
+ @last_available = true
26
+ # @type [LaunchDarkly::Impl::RepeatingTask, nil]
27
+ @poller = nil
16
28
  end
17
29
 
18
30
  def init(all_data)
19
- @store.init(FeatureStoreDataSetSorter.sort_all_collections(all_data))
31
+ wrapper { @store.init(FeatureStoreDataSetSorter.sort_all_collections(all_data)) }
20
32
  end
21
33
 
22
34
  def get(kind, key)
23
- @store.get(kind, key)
35
+ wrapper { @store.get(kind, key) }
24
36
  end
25
37
 
26
38
  def all(kind)
27
- @store.all(kind)
39
+ wrapper { @store.all(kind) }
28
40
  end
29
41
 
30
42
  def upsert(kind, item)
31
- @store.upsert(kind, item)
43
+ wrapper { @store.upsert(kind, item) }
32
44
  end
33
45
 
34
46
  def delete(kind, key, version)
35
- @store.delete(kind, key, version)
47
+ wrapper { @store.delete(kind, key, version) }
36
48
  end
37
49
 
38
50
  def initialized?
@@ -41,6 +53,88 @@ module LaunchDarkly
41
53
 
42
54
  def stop
43
55
  @store.stop
56
+ @mutex.synchronize do
57
+ return if @poller.nil?
58
+
59
+ @poller.stop
60
+ @poller = nil
61
+ end
62
+ end
63
+
64
+ def monitoring_enabled?
65
+ @monitoring_enabled
66
+ end
67
+
68
+ private def wrapper()
69
+ begin
70
+ yield
71
+ rescue => e
72
+ update_availability(false) if @monitoring_enabled
73
+ raise
74
+ end
75
+ end
76
+
77
+ private def update_availability(available)
78
+ @mutex.synchronize do
79
+ return if available == @last_available
80
+ @last_available = available
81
+ end
82
+
83
+ status = LaunchDarkly::Interfaces::DataStore::Status.new(available, false)
84
+
85
+ @logger.warn("Persistent store is available again") if available
86
+
87
+ @store_update_sink.update_status(status)
88
+
89
+ if available
90
+ @mutex.synchronize do
91
+ return if @poller.nil?
92
+
93
+ @poller.stop
94
+ @poller = nil
95
+ end
96
+
97
+ return
98
+ end
99
+
100
+ @logger.warn("Detected persistent store unavailability; updates will be cached until it recovers.")
101
+
102
+ task = Impl::RepeatingTask.new(0.5, 0, -> { self.check_availability }, @logger)
103
+
104
+ @mutex.synchronize do
105
+ @poller = task
106
+ @poller.start
107
+ end
108
+ end
109
+
110
+ private def check_availability
111
+ begin
112
+ update_availability(true) if @store.available?
113
+ rescue => e
114
+ @logger.error("Unexpected error from data store status function: #{e}")
115
+ end
116
+ end
117
+
118
+ # This methods determines whether the wrapped store can support enabling monitoring.
119
+ #
120
+ # The wrapped store must provide a monitoring_enabled method, which must
121
+ # be true. But this alone is not sufficient.
122
+ #
123
+ # Because this class wraps all interactions with a provided store, it can
124
+ # technically "monitor" any store. However, monitoring also requires that
125
+ # we notify listeners when the store is available again.
126
+ #
127
+ # We determine this by checking the store's `available?` method, so this
128
+ # is also a requirement for monitoring support.
129
+ #
130
+ # These extra checks won't be necessary once `available` becomes a part
131
+ # of the core interface requirements and this class no longer wraps every
132
+ # feature store.
133
+ private def does_store_support_monitoring?
134
+ return false unless @store.respond_to? :monitoring_enabled?
135
+ return false unless @store.respond_to? :available?
136
+
137
+ @store.monitoring_enabled?
44
138
  end
45
139
  end
46
140
  end
@@ -23,6 +23,9 @@ module LaunchDarkly
23
23
  priority: 0,
24
24
  }.freeze
25
25
 
26
+ # @private
27
+ ALL_KINDS = [FEATURES, SEGMENTS].freeze
28
+
26
29
  #
27
30
  # Default implementation of the LaunchDarkly client's feature store, using an in-memory
28
31
  # cache. This object holds feature flags and related data received from LaunchDarkly.
@@ -37,6 +40,10 @@ module LaunchDarkly
37
40
  @initialized = Concurrent::AtomicBoolean.new(false)
38
41
  end
39
42
 
43
+ def monitoring_enabled?
44
+ false
45
+ end
46
+
40
47
  def get(kind, key)
41
48
  @lock.with_read_lock do
42
49
  coll = @items[kind]
@@ -101,7 +101,7 @@ module LaunchDarkly
101
101
  #
102
102
  def self.data_source(options={})
103
103
  lambda { |sdk_key, config|
104
- Impl::Integrations::FileDataSourceImpl.new(config.feature_store, config.logger, options) }
104
+ Impl::Integrations::FileDataSourceImpl.new(config.feature_store, config.data_source_update_sink, config.logger, options) }
105
105
  end
106
106
  end
107
107
  end
@@ -43,6 +43,47 @@ module LaunchDarkly
43
43
  self
44
44
  end
45
45
 
46
+ #
47
+ # Set the migration related settings for this feature flag.
48
+ #
49
+ # The settings hash should be built using the {FlagMigrationSettingsBuilder}.
50
+ #
51
+ # @param settings [Hash]
52
+ # @return [FlagBuilder] the builder
53
+ #
54
+ def migration_settings(settings)
55
+ @migration_settings = settings
56
+ self
57
+ end
58
+
59
+ #
60
+ # Set the sampling ratio for this flag. This ratio is used to control the emission rate of feature, debug, and
61
+ # migration op events.
62
+ #
63
+ # General usage should not require interacting with this method.
64
+ #
65
+ # @param ratio [Integer]
66
+ # @return [FlagBuilder]
67
+ #
68
+ def sampling_ratio(ratio)
69
+ @sampling_ratio = ratio
70
+ self
71
+ end
72
+
73
+ #
74
+ # Set the option to exclude this flag from summary events. This is used to control the size of the summary event
75
+ # in the event certain flag payloads are large.
76
+ #
77
+ # General usage should not require interacting with this method.
78
+ #
79
+ # @param exclude [Boolean]
80
+ # @return [FlagBuilder]
81
+ #
82
+ def exclude_from_summaries(exclude)
83
+ @exclude_from_summaries = exclude
84
+ self
85
+ end
86
+
46
87
  #
47
88
  # Specifies the fallthrough variation. The fallthrough is the value
48
89
  # that is returned if targeting is on and the context was not matched by a more specific
@@ -128,11 +169,6 @@ module LaunchDarkly
128
169
  end
129
170
  end
130
171
 
131
- #
132
- # @deprecated Backwards compatibility alias for #variation_for_all
133
- #
134
- alias_method :variation_for_all_users, :variation_for_all
135
-
136
172
  #
137
173
  # Sets the flag to always return the specified variation value for all context.
138
174
  #
@@ -148,11 +184,6 @@ module LaunchDarkly
148
184
  variations(value).variation_for_all(0)
149
185
  end
150
186
 
151
- #
152
- # @deprecated Backwards compatibility alias for #value_for_all
153
- #
154
- alias_method :value_for_all_users, :value_for_all
155
-
156
187
  #
157
188
  # Sets the flag to return the specified variation for a specific context key when targeting
158
189
  # is on.
@@ -315,11 +346,6 @@ module LaunchDarkly
315
346
  self
316
347
  end
317
348
 
318
- #
319
- # @deprecated Backwards compatibility alias for #clear_targets
320
- #
321
- alias_method :clear_user_targets, :clear_targets
322
-
323
349
  #
324
350
  # Removes any existing rules from the flag.
325
351
  # This undoes the effect of methods like {#if_match}
@@ -376,6 +402,18 @@ module LaunchDarkly
376
402
  res[:fallthrough] = { variation: @fallthrough_variation }
377
403
  end
378
404
 
405
+ unless @migration_settings.nil?
406
+ res[:migration] = @migration_settings
407
+ end
408
+
409
+ unless @sampling_ratio.nil? || @sampling_ratio == 1
410
+ res[:samplingRatio] = @sampling_ratio
411
+ end
412
+
413
+ unless @exclude_from_summaries.nil? || !@exclude_from_summaries
414
+ res[:excludeFromSummaries] = @exclude_from_summaries
415
+ end
416
+
379
417
  unless @targets.nil?
380
418
  targets = []
381
419
  context_targets = []
@@ -403,6 +441,37 @@ module LaunchDarkly
403
441
  res
404
442
  end
405
443
 
444
+ #
445
+ # A builder for feature flag migration settings to be used with {FlagBuilder}.
446
+ #
447
+ # In the LaunchDarkly model, a flag can be a standard feature flag, or it can be a migration-related flag, in
448
+ # which case it has migration-specified related settings. These settings control things like the rate at which
449
+ # reads are tested for consistency between origins.
450
+ #
451
+ class FlagMigrationSettingsBuilder
452
+ def initialize()
453
+ @check_ratio = nil
454
+ end
455
+
456
+ #
457
+ # @param ratio [Integer]
458
+ # @return [FlagMigrationSettingsBuilder]
459
+ #
460
+ def check_ratio(ratio)
461
+ return unless ratio.is_a? Integer
462
+ @check_ratio = ratio
463
+ self
464
+ end
465
+
466
+ def build
467
+ return nil if @check_ratio.nil? || @check_ratio == 1
468
+
469
+ {
470
+ "checkRatio": @check_ratio,
471
+ }
472
+ end
473
+ end
474
+
406
475
  #
407
476
  # A builder for feature flag rules to be used with {FlagBuilder}.
408
477
  #
@@ -90,7 +90,7 @@ module LaunchDarkly
90
90
  #
91
91
  def flag(key)
92
92
  existing_builder = @lock.with_read_lock { @flag_builders[key] }
93
- if existing_builder.nil? then
93
+ if existing_builder.nil?
94
94
  FlagBuilder.new(key).boolean_flag
95
95
  else
96
96
  existing_builder.clone
@@ -118,7 +118,7 @@ module LaunchDarkly
118
118
  @flag_builders[flag_builder.key] = flag_builder
119
119
  version = 0
120
120
  flag_key = flag_builder.key.to_sym
121
- if @current_flags[flag_key] then
121
+ if @current_flags[flag_key]
122
122
  version = @current_flags[flag_key][:version]
123
123
  end
124
124
  new_flag = Impl::Model.deserialize(FEATURES, flag_builder.build(version+1))
@@ -175,7 +175,7 @@ module LaunchDarkly
175
175
  key = item.key.to_sym
176
176
  @lock.with_write_lock do
177
177
  old_item = current[key]
178
- unless old_item.nil? then
178
+ unless old_item.nil?
179
179
  data = item.as_json
180
180
  data[:version] = old_item.version + 1
181
181
  item = Impl::Model.deserialize(kind, data)
@@ -43,6 +43,17 @@ module LaunchDarkly
43
43
  end
44
44
 
45
45
  @inited = Concurrent::AtomicBoolean.new(false)
46
+ @has_available_method = @core.respond_to? :available?
47
+ end
48
+
49
+ def monitoring_enabled?
50
+ @has_available_method
51
+ end
52
+
53
+ def available?
54
+ return false unless @has_available_method
55
+
56
+ @core.available?
46
57
  end
47
58
 
48
59
  def init(all_data)