launchdarkly-server-sdk 7.0.2 → 8.4.2

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 (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)