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,58 @@
1
+ require "concurrent"
2
+ require "ldclient-rb/interfaces"
3
+ require "forwardable"
4
+
5
+ module LaunchDarkly
6
+ module Impl
7
+ class FlagTracker
8
+ include LaunchDarkly::Interfaces::FlagTracker
9
+
10
+ extend Forwardable
11
+ def_delegators :@broadcaster, :add_listener, :remove_listener
12
+
13
+ def initialize(broadcaster, eval_fn)
14
+ @broadcaster = broadcaster
15
+ @eval_fn = eval_fn
16
+ end
17
+
18
+ def add_flag_value_change_listener(key, context, listener)
19
+ flag_change_listener = FlagValueChangeAdapter.new(key, context, listener, @eval_fn)
20
+ add_listener(flag_change_listener)
21
+
22
+ flag_change_listener
23
+ end
24
+
25
+ #
26
+ # An adapter which turns a normal flag change listener into a flag value change listener.
27
+ #
28
+ class FlagValueChangeAdapter
29
+ # @param [Symbol] flag_key
30
+ # @param [LaunchDarkly::LDContext] context
31
+ # @param [#update] listener
32
+ # @param [#call] eval_fn
33
+ def initialize(flag_key, context, listener, eval_fn)
34
+ @flag_key = flag_key
35
+ @context = context
36
+ @listener = listener
37
+ @eval_fn = eval_fn
38
+ @value = Concurrent::AtomicReference.new(@eval_fn.call(@flag_key, @context))
39
+ end
40
+
41
+ #
42
+ # @param [LaunchDarkly::Interfaces::FlagChange] flag_change
43
+ #
44
+ def update(flag_change)
45
+ return unless flag_change.key == @flag_key
46
+
47
+ new_eval = @eval_fn.call(@flag_key, @context)
48
+ old_eval = @value.get_and_set(new_eval)
49
+
50
+ return if new_eval == old_eval
51
+
52
+ @listener.update(
53
+ LaunchDarkly::Interfaces::FlagValueChange.new(@flag_key, old_eval, new_eval))
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -119,6 +119,18 @@ module LaunchDarkly
119
119
  end
120
120
  end
121
121
 
122
+ def available?
123
+ # Most implementations use the initialized_internal? method as a
124
+ # proxy for this check. However, since `initialized_internal?`
125
+ # catches a KeyNotFound exception, and that exception can be raised
126
+ # when the server goes away, we have to modify our behavior
127
+ # slightly.
128
+ Diplomat::Kv.get(inited_key, {}, :return, :return)
129
+ true
130
+ rescue
131
+ false
132
+ end
133
+
122
134
  def stop
123
135
  # There's no Consul client instance to dispose of
124
136
  end
@@ -62,6 +62,14 @@ module LaunchDarkly
62
62
  "DynamoDBFeatureStore"
63
63
  end
64
64
 
65
+ def available?
66
+ resp = get_item_by_keys(inited_key, inited_key)
67
+ !resp.item.nil? && resp.item.length > 0
68
+ true
69
+ rescue
70
+ false
71
+ end
72
+
65
73
  def init_internal(all_data)
66
74
  # Start by reading the existing keys; we will later delete any of these that weren't in all_data.
67
75
  unused_old_keys = read_existing_keys(all_data.keys)
@@ -18,10 +18,18 @@ module LaunchDarkly
18
18
  require 'listen'
19
19
  @@have_listen = true
20
20
  rescue LoadError
21
+ # Ignored
21
22
  end
22
23
 
23
- def initialize(feature_store, logger, options={})
24
- @feature_store = feature_store
24
+ #
25
+ # @param data_store [LaunchDarkly::Interfaces::FeatureStore]
26
+ # @param data_source_update_sink [LaunchDarkly::Interfaces::DataSource::UpdateSink, nil] Might be nil for backwards compatibility reasons.
27
+ # @param logger [Logger]
28
+ # @param options [Hash]
29
+ #
30
+ def initialize(data_store, data_source_update_sink, logger, options={})
31
+ @data_store = data_source_update_sink || data_store
32
+ @data_source_update_sink = data_source_update_sink
25
33
  @logger = logger
26
34
  @paths = options[:paths] || []
27
35
  if @paths.is_a? String
@@ -80,10 +88,15 @@ module LaunchDarkly
80
88
  load_file(path, all_data)
81
89
  rescue => exn
82
90
  LaunchDarkly::Util.log_exception(@logger, "Unable to load flag data from \"#{path}\"", exn)
91
+ @data_source_update_sink&.update_status(
92
+ LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
93
+ LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA, 0, exn.to_s, Time.now)
94
+ )
83
95
  return
84
96
  end
85
97
  end
86
- @feature_store.init(all_data)
98
+ @data_store.init(all_data)
99
+ @data_source_update_sink&.update_status(LaunchDarkly::Interfaces::DataSource::Status::VALID, nil)
87
100
  @initialized.make_true
88
101
  end
89
102
 
@@ -1,3 +1,4 @@
1
+ require "ldclient-rb/interfaces"
1
2
  require "concurrent/atomics"
2
3
  require "json"
3
4
 
@@ -42,6 +43,14 @@ module LaunchDarkly
42
43
  @wrapper = LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
43
44
  end
44
45
 
46
+ def monitoring_enabled?
47
+ true
48
+ end
49
+
50
+ def available?
51
+ @wrapper.available?
52
+ end
53
+
45
54
  #
46
55
  # Default value for the `redis_url` constructor parameter; points to an instance of Redis
47
56
  # running at `localhost` with its default port.
@@ -103,13 +112,13 @@ module LaunchDarkly
103
112
  @pool = create_redis_pool(opts)
104
113
 
105
114
  # shutdown pool on close unless the client passed a custom pool and specified not to shutdown
106
- @pool_shutdown_on_close = (!opts[:pool] || opts.fetch(:pool_shutdown_on_close, true))
115
+ @pool_shutdown_on_close = !opts[:pool] || opts.fetch(:pool_shutdown_on_close, true)
107
116
 
108
117
  @prefix = opts[:prefix] || LaunchDarkly::Integrations::Redis::default_prefix
109
118
  @logger = opts[:logger] || Config.default_logger
110
119
  @test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
111
120
 
112
- @stopped = Concurrent::AtomicBoolean.new()
121
+ @stopped = Concurrent::AtomicBoolean.new
113
122
 
114
123
  with_connection do |redis|
115
124
  @logger.info("#{description}: using Redis instance at #{redis.connection[:host]}:#{redis.connection[:port]} and prefix: #{@prefix}")
@@ -154,6 +163,14 @@ module LaunchDarkly
154
163
  @test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
155
164
  end
156
165
 
166
+ def available?
167
+ # We don't care what the status is, only that we can connect
168
+ initialized_internal?
169
+ true
170
+ rescue
171
+ false
172
+ end
173
+
157
174
  def description
158
175
  "RedisFeatureStore"
159
176
  end
@@ -0,0 +1,287 @@
1
+ require 'thread'
2
+
3
+ module LaunchDarkly
4
+ module Impl
5
+ module Migrations
6
+
7
+ #
8
+ # A migration config stores references to callable methods which execute customer defined read or write
9
+ # operations on old or new origins of information. For read operations, an optional comparison function also be
10
+ # defined.
11
+ #
12
+ class MigrationConfig
13
+ #
14
+ # @param old [#call] Refer to {#old}
15
+ # @param new [#call] Refer to {#new}
16
+ # @param comparison [#call, nil] Refer to {#comparison}
17
+ #
18
+ def initialize(old, new, comparison)
19
+ @old = old
20
+ @new = new
21
+ @comparison = comparison
22
+ end
23
+
24
+ #
25
+ # Callable which receives a nullable payload parameter and returns an {LaunchDarkly::Result}.
26
+ #
27
+ # This function call should affect the old migration origin when called.
28
+ #
29
+ # @return [#call]
30
+ #
31
+ attr_reader :old
32
+
33
+ #
34
+ # Callable which receives a nullable payload parameter and returns an {LaunchDarkly::Result}.
35
+ #
36
+ # This function call should affect the new migration origin when called.
37
+ #
38
+ # @return [#call]
39
+ #
40
+ attr_reader :new
41
+
42
+ #
43
+ # Optional callable which receives two objects of any kind and returns a boolean representing equality.
44
+ #
45
+ # The result of this comparison can be sent upstream to LaunchDarkly to enhance migration observability.
46
+ #
47
+ # @return [#call, nil]
48
+ #
49
+ attr_reader :comparison
50
+ end
51
+
52
+ #
53
+ # An implementation of the [LaunchDarkly::Interfaces::Migrations::Migrator] interface, capable of supporting
54
+ # feature-flag backed technology migrations.
55
+ #
56
+ class Migrator
57
+ include LaunchDarkly::Interfaces::Migrations::Migrator
58
+
59
+ #
60
+ # @param client [LaunchDarkly::LDClient]
61
+ # @param read_execution_order [Symbol]
62
+ # @param read_config [MigrationConfig]
63
+ # @param write_config [MigrationConfig]
64
+ # @param measure_latency [Boolean]
65
+ # @param measure_errors [Boolean]
66
+ #
67
+ def initialize(client, read_execution_order, read_config, write_config, measure_latency, measure_errors)
68
+ @client = client
69
+ @read_execution_order = read_execution_order
70
+ @read_config = read_config
71
+ @write_config = write_config
72
+ @measure_latency = measure_latency
73
+ @measure_errors = measure_errors
74
+ @sampler = LaunchDarkly::Impl::Sampler.new(Random.new)
75
+ end
76
+
77
+ #
78
+ # Perform the configured read operations against the appropriate old and/or new origins.
79
+ #
80
+ # @param key [String] The migration-based flag key to use for determining migration stages
81
+ # @param context [LaunchDarkly::LDContext] The context to use for evaluating the migration flag
82
+ # @param default_stage [Symbol] The stage to fallback to if one could not be determined for the requested flag
83
+ # @param payload [String] An optional payload to pass through to the configured read operations.
84
+ #
85
+ # @return [LaunchDarkly::Migrations::OperationResult]
86
+ #
87
+ def read(key, context, default_stage, payload = nil)
88
+ stage, tracker = @client.migration_variation(key, context, default_stage)
89
+ tracker.operation(LaunchDarkly::Migrations::OP_READ)
90
+
91
+ old = Executor.new(@client.logger, LaunchDarkly::Migrations::ORIGIN_OLD, @read_config.old, tracker, @measure_latency, @measure_errors, payload)
92
+ new = Executor.new(@client.logger, LaunchDarkly::Migrations::ORIGIN_NEW, @read_config.new, tracker, @measure_latency, @measure_errors, payload)
93
+
94
+ case stage
95
+ when LaunchDarkly::Migrations::STAGE_OFF
96
+ result = old.run
97
+ when LaunchDarkly::Migrations::STAGE_DUALWRITE
98
+ result = old.run
99
+ when LaunchDarkly::Migrations::STAGE_SHADOW
100
+ result = read_both(old, new, @read_config.comparison, @read_execution_order, tracker)
101
+ when LaunchDarkly::Migrations::STAGE_LIVE
102
+ result = read_both(new, old, @read_config.comparison, @read_execution_order, tracker)
103
+ when LaunchDarkly::Migrations::STAGE_RAMPDOWN
104
+ result = new.run
105
+ when LaunchDarkly::Migrations::STAGE_COMPLETE
106
+ result = new.run
107
+ else
108
+ result = LaunchDarkly::Migrations::OperationResult.new(
109
+ LaunchDarkly::Migrations::ORIGIN_OLD,
110
+ LaunchDarkly::Result.fail("invalid stage #{stage}; cannot execute read")
111
+ )
112
+ end
113
+
114
+ @client.track_migration_op(tracker)
115
+
116
+ result
117
+ end
118
+
119
+ #
120
+ # Perform the configured write operations against the appropriate old and/or new origins.
121
+ #
122
+ # @param key [String] The migration-based flag key to use for determining migration stages
123
+ # @param context [LaunchDarkly::LDContext] The context to use for evaluating the migration flag
124
+ # @param default_stage [Symbol] The stage to fallback to if one could not be determined for the requested flag
125
+ # @param payload [String] An optional payload to pass through to the configured write operations.
126
+ #
127
+ # @return [LaunchDarkly::Migrations::WriteResult]
128
+ #
129
+ def write(key, context, default_stage, payload = nil)
130
+ stage, tracker = @client.migration_variation(key, context, default_stage)
131
+ tracker.operation(LaunchDarkly::Migrations::OP_WRITE)
132
+
133
+ old = Executor.new(@client.logger, LaunchDarkly::Migrations::ORIGIN_OLD, @write_config.old, tracker, @measure_latency, @measure_errors, payload)
134
+ new = Executor.new(@client.logger, LaunchDarkly::Migrations::ORIGIN_NEW, @write_config.new, tracker, @measure_latency, @measure_errors, payload)
135
+
136
+ case stage
137
+ when LaunchDarkly::Migrations::STAGE_OFF
138
+ result = old.run()
139
+ write_result = LaunchDarkly::Migrations::WriteResult.new(result)
140
+ when LaunchDarkly::Migrations::STAGE_DUALWRITE
141
+ authoritative_result, nonauthoritative_result = write_both(old, new, tracker)
142
+ write_result = LaunchDarkly::Migrations::WriteResult.new(authoritative_result, nonauthoritative_result)
143
+ when LaunchDarkly::Migrations::STAGE_SHADOW
144
+ authoritative_result, nonauthoritative_result = write_both(old, new, tracker)
145
+ write_result = LaunchDarkly::Migrations::WriteResult.new(authoritative_result, nonauthoritative_result)
146
+ when LaunchDarkly::Migrations::STAGE_LIVE
147
+ authoritative_result, nonauthoritative_result = write_both(new, old, tracker)
148
+ write_result = LaunchDarkly::Migrations::WriteResult.new(authoritative_result, nonauthoritative_result)
149
+ when LaunchDarkly::Migrations::STAGE_RAMPDOWN
150
+ authoritative_result, nonauthoritative_result = write_both(new, old, tracker)
151
+ write_result = LaunchDarkly::Migrations::WriteResult.new(authoritative_result, nonauthoritative_result)
152
+ when LaunchDarkly::Migrations::STAGE_COMPLETE
153
+ result = new.run()
154
+ write_result = LaunchDarkly::Migrations::WriteResult.new(result)
155
+ else
156
+ result = LaunchDarkly::Migrations::OperationResult.fail(
157
+ LaunchDarkly::Migrations::ORIGIN_OLD,
158
+ LaunchDarkly::Result.fail("invalid stage #{stage}; cannot execute write")
159
+ )
160
+ write_result = LaunchDarkly::Migrations::WriteResult.new(result)
161
+ end
162
+
163
+ @client.track_migration_op(tracker)
164
+
165
+ write_result
166
+ end
167
+
168
+ #
169
+ # Execute both read methods in accordance with the requested execution order.
170
+ #
171
+ # This method always returns the {LaunchDarkly::Migrations::OperationResult} from running the authoritative read operation. The
172
+ # non-authoritative executor may fail but it will not affect the return value.
173
+ #
174
+ # @param authoritative [Executor]
175
+ # @param nonauthoritative [Executor]
176
+ # @param comparison [#call]
177
+ # @param execution_order [Symbol]
178
+ # @param tracker [LaunchDarkly::Interfaces::Migrations::OpTracker]
179
+ #
180
+ # @return [LaunchDarkly::Migrations::OperationResult]
181
+ #
182
+ private def read_both(authoritative, nonauthoritative, comparison, execution_order, tracker)
183
+ authoritative_result = nil
184
+ nonauthoritative_result = nil
185
+
186
+ case execution_order
187
+ when LaunchDarkly::Migrations::MigratorBuilder::EXECUTION_PARALLEL
188
+ auth_handler = Thread.new { authoritative_result = authoritative.run }
189
+ nonauth_handler = Thread.new { nonauthoritative_result = nonauthoritative.run }
190
+
191
+ auth_handler.join()
192
+ nonauth_handler.join()
193
+ when LaunchDarkly::Migrations::MigratorBuilder::EXECUTION_RANDOM && @sampler.sample(2)
194
+ nonauthoritative_result = nonauthoritative.run
195
+ authoritative_result = authoritative.run
196
+ else
197
+ authoritative_result = authoritative.run
198
+ nonauthoritative_result = nonauthoritative.run
199
+ end
200
+
201
+ return authoritative_result if comparison.nil?
202
+
203
+ if authoritative_result.success? && nonauthoritative_result.success?
204
+ tracker.consistent(->{ comparison.call(authoritative_result.value, nonauthoritative_result.value) })
205
+ end
206
+
207
+ authoritative_result
208
+ end
209
+
210
+ #
211
+ # Execute both operations sequentially.
212
+ #
213
+ # If the authoritative executor fails, do not run the non-authoritative one. As a result, this method will
214
+ # always return an authoritative {LaunchDarkly::Migrations::OperationResult} as the first value, and optionally the non-authoritative
215
+ # {LaunchDarkly::Migrations::OperationResult} as the second value.
216
+ #
217
+ # @param authoritative [Executor]
218
+ # @param nonauthoritative [Executor]
219
+ # @param tracker [LaunchDarkly::Interfaces::Migrations::OpTracker]
220
+ #
221
+ # @return [Array<LaunchDarkly::Migrations::OperationResult, [LaunchDarkly::Migrations::OperationResult, nil]>]
222
+ #
223
+ private def write_both(authoritative, nonauthoritative, tracker)
224
+ authoritative_result = authoritative.run()
225
+ tracker.invoked(authoritative.origin)
226
+
227
+ return authoritative_result, nil unless authoritative_result.success?
228
+
229
+ nonauthoritative_result = nonauthoritative.run()
230
+ tracker.invoked(nonauthoritative.origin)
231
+
232
+ [authoritative_result, nonauthoritative_result]
233
+ end
234
+ end
235
+
236
+ #
237
+ # Utility class for executing migration operations while also tracking our built-in migration measurements.
238
+ #
239
+ class Executor
240
+ #
241
+ # @return [Symbol]
242
+ #
243
+ attr_reader :origin
244
+
245
+ #
246
+ # @param origin [Symbol]
247
+ # @param fn [#call]
248
+ # @param tracker [LaunchDarkly::Interfaces::Migrations::OpTracker]
249
+ # @param measure_latency [Boolean]
250
+ # @param measure_errors [Boolean]
251
+ # @param payload [Object, nil]
252
+ #
253
+ def initialize(logger, origin, fn, tracker, measure_latency, measure_errors, payload)
254
+ @logger = logger
255
+ @origin = origin
256
+ @fn = fn
257
+ @tracker = tracker
258
+ @measure_latency = measure_latency
259
+ @measure_errors = measure_errors
260
+ @payload = payload
261
+ end
262
+
263
+ #
264
+ # Execute the configured operation and track any available measurements.
265
+ #
266
+ # @return [LaunchDarkly::Migrations::OperationResult]
267
+ #
268
+ def run()
269
+ start = Time.now
270
+
271
+ begin
272
+ result = @fn.call(@payload)
273
+ rescue => e
274
+ LaunchDarkly::Util.log_exception(@logger, "Unexpected error running method for '#{origin}' origin", e)
275
+ result = LaunchDarkly::Result.fail("'#{origin}' operation raised an exception", e)
276
+ end
277
+
278
+ @tracker.latency(@origin, (Time.now - start) * 1_000) if @measure_latency
279
+ @tracker.error(@origin) if @measure_errors && !result.success?
280
+ @tracker.invoked(@origin)
281
+
282
+ LaunchDarkly::Migrations::OperationResult.new(@origin, result)
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,136 @@
1
+ require "set"
2
+ require "ldclient-rb/impl/sampler"
3
+ require "logger"
4
+
5
+ module LaunchDarkly
6
+ module Impl
7
+ module Migrations
8
+ class OpTracker
9
+ include LaunchDarkly::Interfaces::Migrations::OpTracker
10
+
11
+ #
12
+ # @param logger [Logger] logger
13
+ # @param key [string] key
14
+ # @param flag [LaunchDarkly::Impl::Model::FeatureFlag] flag
15
+ # @param context [LaunchDarkly::LDContext] context
16
+ # @param detail [LaunchDarkly::EvaluationDetail] detail
17
+ # @param default_stage [Symbol] default_stage
18
+ #
19
+ def initialize(logger, key, flag, context, detail, default_stage)
20
+ @logger = logger
21
+ @key = key
22
+ @flag = flag
23
+ @context = context
24
+ @detail = detail
25
+ @default_stage = default_stage
26
+ @sampler = LaunchDarkly::Impl::Sampler.new(Random.new)
27
+
28
+ @mutex = Mutex.new
29
+
30
+ # @type [Symbol, nil]
31
+ @operation = nil
32
+
33
+ # @type [Set<Symbol>]
34
+ @invoked = Set.new
35
+ # @type [Boolean, nil]
36
+ @consistent = nil
37
+
38
+ # @type [Int]
39
+ @consistent_ratio = @flag&.migration_settings&.check_ratio
40
+ @consistent_ratio = 1 if @consistent_ratio.nil?
41
+
42
+ # @type [Set<Symbol>]
43
+ @errors = Set.new
44
+ # @type [Hash<Symbol, Float>]
45
+ @latencies = Hash.new
46
+ end
47
+
48
+ def operation(operation)
49
+ return unless LaunchDarkly::Migrations::VALID_OPERATIONS.include? operation
50
+
51
+ @mutex.synchronize do
52
+ @operation = operation
53
+ end
54
+ end
55
+
56
+ def invoked(origin)
57
+ return unless LaunchDarkly::Migrations::VALID_ORIGINS.include? origin
58
+
59
+ @mutex.synchronize do
60
+ @invoked.add(origin)
61
+ end
62
+ end
63
+
64
+ def consistent(is_consistent)
65
+ @mutex.synchronize do
66
+ if @sampler.sample(@consistent_ratio)
67
+ begin
68
+ @consistent = is_consistent.call
69
+ rescue => e
70
+ LaunchDarkly::Util.log_exception(@logger, "Exception raised during consistency check; failed to record measurement", e)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def error(origin)
77
+ return unless LaunchDarkly::Migrations::VALID_ORIGINS.include? origin
78
+
79
+ @mutex.synchronize do
80
+ @errors.add(origin)
81
+ end
82
+ end
83
+
84
+ def latency(origin, duration)
85
+ return unless LaunchDarkly::Migrations::VALID_ORIGINS.include? origin
86
+ return unless duration.is_a? Numeric
87
+ return if duration < 0
88
+
89
+ @mutex.synchronize do
90
+ @latencies[origin] = duration
91
+ end
92
+ end
93
+
94
+ def build
95
+ @mutex.synchronize do
96
+ return "operation cannot contain an empty key" if @key.empty?
97
+ return "operation not provided" if @operation.nil?
98
+ return "no origins were invoked" if @invoked.empty?
99
+ return "provided context was invalid" unless @context.valid?
100
+
101
+ result = check_invoked_consistency
102
+ return result unless result == true
103
+
104
+ LaunchDarkly::Impl::MigrationOpEvent.new(
105
+ LaunchDarkly::Impl::Util.current_time_millis,
106
+ @context,
107
+ @key,
108
+ @flag,
109
+ @operation,
110
+ @default_stage,
111
+ @detail,
112
+ @invoked,
113
+ @consistent,
114
+ @consistent_ratio,
115
+ @errors,
116
+ @latencies
117
+ )
118
+ end
119
+ end
120
+
121
+ private def check_invoked_consistency
122
+ LaunchDarkly::Migrations::VALID_ORIGINS.each do |origin|
123
+ next if @invoked.include? origin
124
+
125
+ return "provided latency for origin '#{origin}' without recording invocation" if @latencies.include? origin
126
+ return "provided error for origin '#{origin}' without recording invocation" if @errors.include? origin
127
+ end
128
+
129
+ return "provided consistency without recording both invocations" if !@consistent.nil? && @invoked.size != 2
130
+
131
+ true
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -26,6 +26,10 @@ module LaunchDarkly
26
26
  @version = data[:version]
27
27
  @deleted = !!data[:deleted]
28
28
  return if @deleted
29
+ migration_settings = data[:migration] || {}
30
+ @migration_settings = MigrationSettings.new(migration_settings[:checkRatio])
31
+ @sampling_ratio = data[:samplingRatio]
32
+ @exclude_from_summaries = !!data[:excludeFromSummaries]
29
33
  @variations = data[:variations] || []
30
34
  @on = !!data[:on]
31
35
  fallthrough = data[:fallthrough] || {}
@@ -33,7 +37,7 @@ module LaunchDarkly
33
37
  @off_variation = data[:offVariation]
34
38
  check_variation_range(self, errors, @off_variation, "off variation")
35
39
  @prerequisites = (data[:prerequisites] || []).map do |prereq_data|
36
- Prerequisite.new(prereq_data, self, errors)
40
+ Prerequisite.new(prereq_data, self)
37
41
  end
38
42
  @targets = (data[:targets] || []).map do |target_data|
39
43
  Target.new(target_data, self, errors)
@@ -63,6 +67,12 @@ module LaunchDarkly
63
67
  attr_reader :version
64
68
  # @return [Boolean]
65
69
  attr_reader :deleted
70
+ # @return [MigrationSettings, nil]
71
+ attr_reader :migration_settings
72
+ # @return [Integer, nil]
73
+ attr_reader :sampling_ratio
74
+ # @return [Boolean, nil]
75
+ attr_reader :exclude_from_summaries
66
76
  # @return [Array]
67
77
  attr_reader :variations
68
78
  # @return [Boolean]
@@ -108,13 +118,12 @@ module LaunchDarkly
108
118
  end
109
119
 
110
120
  class Prerequisite
111
- def initialize(data, flag, errors_out = nil)
121
+ def initialize(data, flag)
112
122
  @data = data
113
123
  @key = data[:key]
114
124
  @variation = data[:variation]
115
125
  @failure_result = EvaluatorHelpers.evaluation_detail_for_off_variation(flag,
116
126
  EvaluationReason::prerequisite_failed(@key))
117
- check_variation_range(flag, errors_out, @variation, "prerequisite")
118
127
  end
119
128
 
120
129
  # @return [Hash]
@@ -173,6 +182,19 @@ module LaunchDarkly
173
182
  attr_reader :variation_or_rollout
174
183
  end
175
184
 
185
+
186
+ class MigrationSettings
187
+ #
188
+ # @param check_ratio [Int, nil]
189
+ #
190
+ def initialize(check_ratio)
191
+ @check_ratio = check_ratio
192
+ end
193
+
194
+ # @return [Integer, nil]
195
+ attr_reader :check_ratio
196
+ end
197
+
176
198
  class VariationOrRollout
177
199
  def initialize(variation, rollout_data, flag = nil, errors_out = nil, description = nil)
178
200
  @variation = variation
@@ -16,9 +16,8 @@ module LaunchDarkly
16
16
 
17
17
  def start
18
18
  @worker = Thread.new do
19
- if @start_delay
20
- sleep(@start_delay)
21
- end
19
+ sleep(@start_delay) unless @start_delay.nil? || @start_delay == 0
20
+
22
21
  until @stopped.value do
23
22
  started_at = Time.now
24
23
  begin