launchdarkly-server-sdk 6.3.0 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -4
  3. data/lib/ldclient-rb/config.rb +112 -62
  4. data/lib/ldclient-rb/context.rb +444 -0
  5. data/lib/ldclient-rb/evaluation_detail.rb +26 -22
  6. data/lib/ldclient-rb/events.rb +256 -146
  7. data/lib/ldclient-rb/flags_state.rb +26 -15
  8. data/lib/ldclient-rb/impl/big_segments.rb +18 -18
  9. data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
  10. data/lib/ldclient-rb/impl/context.rb +96 -0
  11. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  12. data/lib/ldclient-rb/impl/data_source.rb +188 -0
  13. data/lib/ldclient-rb/impl/data_store.rb +59 -0
  14. data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
  15. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  16. data/lib/ldclient-rb/impl/evaluator.rb +386 -142
  17. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  18. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  19. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  20. data/lib/ldclient-rb/impl/event_sender.rb +7 -6
  21. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  22. data/lib/ldclient-rb/impl/event_types.rb +136 -0
  23. data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
  24. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +19 -7
  25. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
  26. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
  27. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
  28. data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
  29. data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
  30. data/lib/ldclient-rb/impl/model/clause.rb +45 -0
  31. data/lib/ldclient-rb/impl/model/feature_flag.rb +255 -0
  32. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  33. data/lib/ldclient-rb/impl/model/segment.rb +132 -0
  34. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  35. data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
  36. data/lib/ldclient-rb/impl/sampler.rb +25 -0
  37. data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
  38. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  39. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  40. data/lib/ldclient-rb/impl/util.rb +59 -1
  41. data/lib/ldclient-rb/in_memory_store.rb +9 -2
  42. data/lib/ldclient-rb/integrations/consul.rb +2 -2
  43. data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
  44. data/lib/ldclient-rb/integrations/file_data.rb +4 -4
  45. data/lib/ldclient-rb/integrations/redis.rb +5 -5
  46. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
  47. data/lib/ldclient-rb/integrations/test_data.rb +18 -14
  48. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
  49. data/lib/ldclient-rb/interfaces.rb +600 -14
  50. data/lib/ldclient-rb/ldclient.rb +314 -134
  51. data/lib/ldclient-rb/memoized_value.rb +1 -1
  52. data/lib/ldclient-rb/migrations.rb +230 -0
  53. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  54. data/lib/ldclient-rb/polling.rb +52 -6
  55. data/lib/ldclient-rb/reference.rb +274 -0
  56. data/lib/ldclient-rb/requestor.rb +9 -11
  57. data/lib/ldclient-rb/stream.rb +96 -34
  58. data/lib/ldclient-rb/util.rb +97 -14
  59. data/lib/ldclient-rb/version.rb +1 -1
  60. data/lib/ldclient-rb.rb +3 -4
  61. metadata +65 -23
  62. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  63. data/lib/ldclient-rb/file_data_source.rb +0 -23
  64. data/lib/ldclient-rb/impl/event_factory.rb +0 -126
  65. data/lib/ldclient-rb/newrelic.rb +0 -17
  66. data/lib/ldclient-rb/redis_store.rb +0 -88
  67. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -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
@@ -0,0 +1,45 @@
1
+ require "ldclient-rb/reference"
2
+
3
+
4
+ # See serialization.rb for implementation notes on the data model classes.
5
+
6
+ module LaunchDarkly
7
+ module Impl
8
+ module Model
9
+ class Clause
10
+ def initialize(data, errors_out = nil)
11
+ @data = data
12
+ @context_kind = data[:contextKind]
13
+ @op = data[:op].to_sym
14
+ if @op == :segmentMatch
15
+ @attribute = nil
16
+ else
17
+ @attribute = (@context_kind.nil? || @context_kind.empty?) ? Reference.create_literal(data[:attribute]) : Reference.create(data[:attribute])
18
+ unless errors_out.nil? || @attribute.error.nil?
19
+ errors_out << "clause has invalid attribute: #{@attribute.error}"
20
+ end
21
+ end
22
+ @values = data[:values] || []
23
+ @negate = !!data[:negate]
24
+ end
25
+
26
+ # @return [Hash]
27
+ attr_reader :data
28
+ # @return [String|nil]
29
+ attr_reader :context_kind
30
+ # @return [LaunchDarkly::Reference]
31
+ attr_reader :attribute
32
+ # @return [Symbol]
33
+ attr_reader :op
34
+ # @return [Array]
35
+ attr_reader :values
36
+ # @return [Boolean]
37
+ attr_reader :negate
38
+
39
+ def as_json
40
+ @data
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end