launchdarkly-server-sdk 6.3.0 → 8.0.0

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