launchdarkly-server-sdk 6.2.5 → 6.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,6 +4,11 @@ require "ldclient-rb/expiring_cache"
4
4
 
5
5
  module LaunchDarkly
6
6
  module Integrations
7
+ #
8
+ # Support code that may be helpful in creating integrations.
9
+ #
10
+ # @since 5.5.0
11
+ #
7
12
  module Util
8
13
  #
9
14
  # CachingStoreWrapper is a partial implementation of the {LaunchDarkly::Interfaces::FeatureStore}
@@ -1,55 +1,6 @@
1
1
  require "ldclient-rb/integrations/consul"
2
2
  require "ldclient-rb/integrations/dynamodb"
3
+ require "ldclient-rb/integrations/file_data"
3
4
  require "ldclient-rb/integrations/redis"
5
+ require "ldclient-rb/integrations/test_data"
4
6
  require "ldclient-rb/integrations/util/store_wrapper"
5
-
6
- module LaunchDarkly
7
- #
8
- # Tools for connecting the LaunchDarkly client to other software.
9
- #
10
- module Integrations
11
- #
12
- # Integration with [Consul](https://www.consul.io/).
13
- #
14
- # Note that in order to use this integration, you must first install the gem `diplomat`.
15
- #
16
- # @since 5.5.0
17
- #
18
- module Consul
19
- # code is in ldclient-rb/impl/integrations/consul_impl
20
- end
21
-
22
- #
23
- # Integration with [DynamoDB](https://aws.amazon.com/dynamodb/).
24
- #
25
- # Note that in order to use this integration, you must first install one of the AWS SDK gems: either
26
- # `aws-sdk-dynamodb`, or the full `aws-sdk`.
27
- #
28
- # @since 5.5.0
29
- #
30
- module DynamoDB
31
- # code is in ldclient-rb/impl/integrations/dynamodb_impl
32
- end
33
-
34
- #
35
- # Integration with [Redis](https://redis.io/).
36
- #
37
- # Note that in order to use this integration, you must first install the `redis` and `connection-pool`
38
- # gems.
39
- #
40
- # @since 5.5.0
41
- #
42
- module Redis
43
- # code is in ldclient-rb/impl/integrations/redis_impl
44
- end
45
-
46
- #
47
- # Support code that may be helpful in creating integrations.
48
- #
49
- # @since 5.5.0
50
- #
51
- module Util
52
- # code is in ldclient-rb/integrations/util/
53
- end
54
- end
55
- end
@@ -1,3 +1,4 @@
1
+ require "observer"
1
2
 
2
3
  module LaunchDarkly
3
4
  #
@@ -120,7 +121,8 @@ module LaunchDarkly
120
121
  #
121
122
  # The client has its own standard implementation, which uses either a streaming connection or
122
123
  # polling depending on your configuration. Normally you will not need to use another one
123
- # except for testing purposes. {FileDataSource} provides one such test fixture.
124
+ # except for testing purposes. Two such test fixtures are {LaunchDarkly::Integrations::FileData}
125
+ # and {LaunchDarkly::Integrations::TestData}.
124
126
  #
125
127
  module DataSource
126
128
  #
@@ -149,5 +151,153 @@ module LaunchDarkly
149
151
  def stop
150
152
  end
151
153
  end
154
+
155
+ module BigSegmentStore
156
+ #
157
+ # Returns information about the overall state of the store. This method will be called only
158
+ # when the SDK needs the latest state, so it should not be cached.
159
+ #
160
+ # @return [BigSegmentStoreMetadata]
161
+ #
162
+ def get_metadata
163
+ end
164
+
165
+ #
166
+ # Queries the store for a snapshot of the current segment state for a specific user.
167
+ #
168
+ # The user_hash is a base64-encoded string produced by hashing the user key as defined by
169
+ # the Big Segments specification; the store implementation does not need to know the details
170
+ # of how this is done, because it deals only with already-hashed keys, but the string can be
171
+ # assumed to only contain characters that are valid in base64.
172
+ #
173
+ # The return value should be either a Hash, or nil if the user is not referenced in any big
174
+ # segments. Each key in the Hash is a "segment reference", which is how segments are
175
+ # identified in Big Segment data. This string is not identical to the segment key-- the SDK
176
+ # will add other information. The store implementation should not be concerned with the
177
+ # format of the string. Each value in the Hash is true if the user is explicitly included in
178
+ # the segment, false if the user is explicitly excluded from the segment-- and is not also
179
+ # explicitly included (that is, if both an include and an exclude existed in the data, the
180
+ # include would take precedence). If the user's status in a particular segment is undefined,
181
+ # there should be no key or value for that segment.
182
+ #
183
+ # This Hash may be cached by the SDK, so it should not be modified after it is created. It
184
+ # is a snapshot of the segment membership state at one point in time.
185
+ #
186
+ # @param user_hash [String]
187
+ # @return [Hash] true/false values for Big Segments that reference this user
188
+ #
189
+ def get_membership(user_hash)
190
+ end
191
+
192
+ #
193
+ # Performs any necessary cleanup to shut down the store when the client is being shut down.
194
+ #
195
+ # @return [void]
196
+ #
197
+ def stop
198
+ end
199
+ end
200
+
201
+ #
202
+ # Values returned by {BigSegmentStore#get_metadata}.
203
+ #
204
+ class BigSegmentStoreMetadata
205
+ def initialize(last_up_to_date)
206
+ @last_up_to_date = last_up_to_date
207
+ end
208
+
209
+ # The Unix epoch millisecond timestamp of the last update to the {BigSegmentStore}. It is
210
+ # nil if the store has never been updated.
211
+ #
212
+ # @return [Integer|nil]
213
+ attr_reader :last_up_to_date
214
+ end
215
+
216
+ #
217
+ # Information about the status of a Big Segment store, provided by {BigSegmentStoreStatusProvider}.
218
+ #
219
+ # Big Segments are a specific type of user segments. For more information, read the LaunchDarkly
220
+ # documentation: https://docs.launchdarkly.com/home/users/big-segments
221
+ #
222
+ class BigSegmentStoreStatus
223
+ def initialize(available, stale)
224
+ @available = available
225
+ @stale = stale
226
+ end
227
+
228
+ # True if the Big Segment store is able to respond to queries, so that the SDK can evaluate
229
+ # whether a user is in a segment or not.
230
+ #
231
+ # If this property is false, the store is not able to make queries (for instance, it may not have
232
+ # a valid database connection). In this case, the SDK will treat any reference to a Big Segment
233
+ # as if no users are included in that segment. Also, the {EvaluationReason} associated with
234
+ # with any flag evaluation that references a Big Segment when the store is not available will
235
+ # have a `big_segments_status` of `STORE_ERROR`.
236
+ #
237
+ # @return [Boolean]
238
+ attr_reader :available
239
+
240
+ # True if the Big Segment store is available, but has not been updated within the amount of time
241
+ # specified by {BigSegmentsConfig#stale_after}.
242
+ #
243
+ # This may indicate that the LaunchDarkly Relay Proxy, which populates the store, has stopped
244
+ # running or has become unable to receive fresh data from LaunchDarkly. Any feature flag
245
+ # evaluations that reference a Big Segment will be using the last known data, which may be out
246
+ # of date. Also, the {EvaluationReason} associated with those evaluations will have a
247
+ # `big_segments_status` of `STALE`.
248
+ #
249
+ # @return [Boolean]
250
+ attr_reader :stale
251
+
252
+ def ==(other)
253
+ self.available == other.available && self.stale == other.stale
254
+ end
255
+ end
256
+
257
+ #
258
+ # An interface for querying the status of a Big Segment store.
259
+ #
260
+ # The Big Segment store is the component that receives information about Big Segments, normally
261
+ # from a database populated by the LaunchDarkly Relay Proxy. Big Segments are a specific type
262
+ # of user segments. For more information, read the LaunchDarkly documentation:
263
+ # https://docs.launchdarkly.com/home/users/big-segments
264
+ #
265
+ # An implementation of this interface is returned by {LDClient#big_segment_store_status_provider}.
266
+ # Application code never needs to implement this interface.
267
+ #
268
+ # There are two ways to interact with the status. One is to simply get the current status; if its
269
+ # `available` property is true, then the SDK is able to evaluate user membership in Big Segments,
270
+ # and the `stale`` property indicates whether the data might be out of date.
271
+ #
272
+ # The other way is to subscribe to status change notifications. Applications may wish to know if
273
+ # there is an outage in the Big Segment store, or if it has become stale (the Relay Proxy has
274
+ # stopped updating it with new data), since then flag evaluations that reference a Big Segment
275
+ # might return incorrect values. To allow finding out about status changes as soon as possible,
276
+ # `BigSegmentStoreStatusProvider` mixes in Ruby's
277
+ # [Observable](https://docs.ruby-lang.org/en/2.5.0/Observable.html) module to provide standard
278
+ # methods such as `add_observer`. Observers will be called with a new {BigSegmentStoreStatus}
279
+ # value whenever the status changes.
280
+ #
281
+ # @example Getting the current status
282
+ # status = client.big_segment_store_status_provider.status
283
+ #
284
+ # @example Subscribing to status notifications
285
+ # client.big_segment_store_status_provider.add_observer(self, :big_segments_status_changed)
286
+ #
287
+ # def big_segments_status_changed(new_status)
288
+ # puts "Big segment store status is now: #{new_status}"
289
+ # end
290
+ #
291
+ module BigSegmentStoreStatusProvider
292
+ include Observable
293
+ #
294
+ # Gets the current status of the store, if known.
295
+ #
296
+ # @return [BigSegmentStoreStatus] the status, or nil if the SDK has not yet queried the Big
297
+ # Segment store status
298
+ #
299
+ def status
300
+ end
301
+ end
152
302
  end
153
303
  end
@@ -1,3 +1,4 @@
1
+ require "ldclient-rb/impl/big_segments"
1
2
  require "ldclient-rb/impl/diagnostic_events"
2
3
  require "ldclient-rb/impl/evaluator"
3
4
  require "ldclient-rb/impl/event_factory"
@@ -57,9 +58,13 @@ module LaunchDarkly
57
58
  updated_config.instance_variable_set(:@feature_store, @store)
58
59
  @config = updated_config
59
60
 
61
+ @big_segment_store_manager = Impl::BigSegmentStoreManager.new(config.big_segments, @config.logger)
62
+ @big_segment_store_status_provider = @big_segment_store_manager.status_provider
63
+
60
64
  get_flag = lambda { |key| @store.get(FEATURES, key) }
61
65
  get_segment = lambda { |key| @store.get(SEGMENTS, key) }
62
- @evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, @config.logger)
66
+ get_big_segments_membership = lambda { |key| @big_segment_store_manager.get_user_membership(key) }
67
+ @evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, get_big_segments_membership, @config.logger)
63
68
 
64
69
  if !@config.offline? && @config.send_events && !@config.diagnostic_opt_out?
65
70
  diagnostic_accumulator = Impl::DiagnosticAccumulator.new(Impl::DiagnosticAccumulator.create_diagnostic_id(sdk_key))
@@ -173,7 +178,7 @@ module LaunchDarkly
173
178
  # Other supported user attributes include IP address, country code, and an arbitrary hash of
174
179
  # custom attributes. For more about the supported user properties and how they work in
175
180
  # LaunchDarkly, see [Targeting users](https://docs.launchdarkly.com/home/flags/targeting-users).
176
- #
181
+ #
177
182
  # The optional `:privateAttributeNames` user property allows you to specify a list of
178
183
  # attribute names that should not be sent back to LaunchDarkly.
179
184
  # [Private attributes](https://docs.launchdarkly.com/home/users/attributes#creating-private-user-attributes)
@@ -243,8 +248,8 @@ module LaunchDarkly
243
248
  # @return [void]
244
249
  #
245
250
  def identify(user)
246
- if !user || user[:key].nil?
247
- @config.logger.warn("Identify called with nil user or nil user key!")
251
+ if !user || user[:key].nil? || user[:key].empty?
252
+ @config.logger.warn("Identify called with nil user or empty user key!")
248
253
  return
249
254
  end
250
255
  sanitize_user(user)
@@ -333,6 +338,15 @@ module LaunchDarkly
333
338
  def all_flags_state(user, options={})
334
339
  return FeatureFlagsState.new(false) if @config.offline?
335
340
 
341
+ if !initialized?
342
+ if @store.initialized?
343
+ @config.logger.warn { "Called all_flags_state before client initialization; using last known values from data store" }
344
+ else
345
+ @config.logger.warn { "Called all_flags_state before client initialization. Data store not available; returning empty state" }
346
+ return FeatureFlagsState.new(false)
347
+ end
348
+ end
349
+
336
350
  unless user && !user[:key].nil?
337
351
  @config.logger.error { "[LDClient] User and user key must be specified in all_flags_state" }
338
352
  return FeatureFlagsState.new(false)
@@ -354,14 +368,25 @@ module LaunchDarkly
354
368
  next
355
369
  end
356
370
  begin
357
- result = @evaluator.evaluate(f, user, @event_factory_default)
358
- state.add_flag(f, result.detail.value, result.detail.variation_index, with_reasons ? result.detail.reason : nil,
359
- details_only_if_tracked)
371
+ detail = @evaluator.evaluate(f, user, @event_factory_default).detail
360
372
  rescue => exn
373
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION))
361
374
  Util.log_exception(@config.logger, "Error evaluating flag \"#{k}\" in all_flags_state", exn)
362
- state.add_flag(f, nil, nil, with_reasons ? EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION) : nil,
363
- details_only_if_tracked)
364
375
  end
376
+
377
+ requires_experiment_data = EventFactory.is_experiment(f, detail.reason)
378
+ flag_state = {
379
+ key: f[:key],
380
+ value: detail.value,
381
+ variation: detail.variation_index,
382
+ reason: detail.reason,
383
+ version: f[:version],
384
+ trackEvents: f[:trackEvents] || requires_experiment_data,
385
+ trackReason: requires_experiment_data,
386
+ debugEventsUntilDate: f[:debugEventsUntilDate],
387
+ }
388
+
389
+ state.add_flag(flag_state, with_reasons, details_only_if_tracked)
365
390
  end
366
391
 
367
392
  state
@@ -375,9 +400,18 @@ module LaunchDarkly
375
400
  @config.logger.info { "[LDClient] Closing LaunchDarkly client..." }
376
401
  @data_source.stop
377
402
  @event_processor.stop
403
+ @big_segment_store_manager.stop
378
404
  @store.stop
379
405
  end
380
406
 
407
+ #
408
+ # Returns an interface for tracking the status of a Big Segment store.
409
+ #
410
+ # The {BigSegmentStoreStatusProvider} has methods for checking whether the Big Segment store
411
+ # is (as far as the SDK knows) currently operational and tracking changes in this status.
412
+ #
413
+ attr_reader :big_segment_store_status_provider
414
+
381
415
  private
382
416
 
383
417
  def create_default_data_source(sdk_key, config, diagnostic_accumulator)
@@ -1,3 +1,5 @@
1
+ require "ldclient-rb/impl/repeating_task"
2
+
1
3
  require "concurrent/atomics"
2
4
  require "thread"
3
5
 
@@ -9,8 +11,8 @@ module LaunchDarkly
9
11
  @requestor = requestor
10
12
  @initialized = Concurrent::AtomicBoolean.new(false)
11
13
  @started = Concurrent::AtomicBoolean.new(false)
12
- @stopped = Concurrent::AtomicBoolean.new(false)
13
14
  @ready = Concurrent::Event.new
15
+ @task = Impl::RepeatingTask.new(@config.poll_interval, 0, -> { self.poll }, @config.logger)
14
16
  end
15
17
 
16
18
  def initialized?
@@ -20,56 +22,35 @@ module LaunchDarkly
20
22
  def start
21
23
  return @ready unless @started.make_true
22
24
  @config.logger.info { "[LDClient] Initializing polling connection" }
23
- create_worker
25
+ @task.start
24
26
  @ready
25
27
  end
26
28
 
27
29
  def stop
28
- if @stopped.make_true
29
- if @worker && @worker.alive? && @worker != Thread.current
30
- @worker.run # causes the thread to wake up if it's currently in a sleep
31
- @worker.join
32
- end
33
- @config.logger.info { "[LDClient] Polling connection stopped" }
34
- end
30
+ @task.stop
31
+ @config.logger.info { "[LDClient] Polling connection stopped" }
35
32
  end
36
33
 
37
34
  def poll
38
- all_data = @requestor.request_all_data
39
- if all_data
40
- @config.feature_store.init(all_data)
41
- if @initialized.make_true
42
- @config.logger.info { "[LDClient] Polling connection initialized" }
43
- @ready.set
44
- end
45
- end
46
- end
47
-
48
- def create_worker
49
- @worker = Thread.new do
50
- @config.logger.debug { "[LDClient] Starting polling worker" }
51
- while !@stopped.value do
52
- started_at = Time.now
53
- begin
54
- poll
55
- rescue UnexpectedResponseError => e
56
- message = Util.http_error_message(e.status, "polling request", "will retry")
57
- @config.logger.error { "[LDClient] #{message}" };
58
- if !Util.http_error_recoverable?(e.status)
59
- @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
60
- stop
61
- end
62
- rescue StandardError => exn
63
- Util.log_exception(@config.logger, "Exception while polling", exn)
64
- end
65
- delta = @config.poll_interval - (Time.now - started_at)
66
- if delta > 0
67
- sleep(delta)
35
+ begin
36
+ all_data = @requestor.request_all_data
37
+ if all_data
38
+ @config.feature_store.init(all_data)
39
+ if @initialized.make_true
40
+ @config.logger.info { "[LDClient] Polling connection initialized" }
41
+ @ready.set
68
42
  end
69
43
  end
44
+ rescue UnexpectedResponseError => e
45
+ message = Util.http_error_message(e.status, "polling request", "will retry")
46
+ @config.logger.error { "[LDClient] #{message}" };
47
+ if !Util.http_error_recoverable?(e.status)
48
+ @ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
49
+ stop
50
+ end
51
+ rescue StandardError => e
52
+ Util.log_exception(@config.logger, "Exception while polling", e)
70
53
  end
71
54
  end
72
-
73
- private :poll, :create_worker
74
55
  end
75
56
  end
@@ -47,7 +47,8 @@ module LaunchDarkly
47
47
  headers: headers,
48
48
  read_timeout: READ_TIMEOUT_SECONDS,
49
49
  logger: @config.logger,
50
- socket_factory: @config.socket_factory
50
+ socket_factory: @config.socket_factory,
51
+ reconnect_time: @config.initial_reconnect_delay
51
52
  }
52
53
  log_connection_started
53
54
  @es = SSE::Client.new(@config.stream_uri + "/all", **opts) do |conn|
@@ -18,12 +18,21 @@ module LaunchDarkly
18
18
  end
19
19
  ret
20
20
  end
21
-
21
+
22
22
  def self.new_http_client(uri_s, config)
23
23
  http_client_options = {}
24
24
  if config.socket_factory
25
25
  http_client_options["socket_class"] = config.socket_factory
26
26
  end
27
+ proxy = URI.parse(uri_s).find_proxy
28
+ if !proxy.nil?
29
+ http_client_options["proxy"] = {
30
+ proxy_address: proxy.host,
31
+ proxy_port: proxy.port,
32
+ proxy_username: proxy.user,
33
+ proxy_password: proxy.password
34
+ }
35
+ end
27
36
  return HTTP::Client.new(http_client_options)
28
37
  .timeout({
29
38
  read: config.read_timeout,
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "6.2.5"
2
+ VERSION = "6.3.2"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: launchdarkly-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.2.5
4
+ version: 6.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-12 00:00:00.000000000 Z
11
+ date: 2022-03-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-dynamodb
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - '='
32
32
  - !ruby/object:Gem::Version
33
- version: 2.2.10
33
+ version: 2.2.33
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - '='
39
39
  - !ruby/object:Gem::Version
40
- version: 2.2.10
40
+ version: 2.2.33
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -198,14 +198,14 @@ dependencies:
198
198
  requirements:
199
199
  - - '='
200
200
  - !ruby/object:Gem::Version
201
- version: 2.1.1
201
+ version: 2.2.0
202
202
  type: :runtime
203
203
  prerelease: false
204
204
  version_requirements: !ruby/object:Gem::Requirement
205
205
  requirements:
206
206
  - - '='
207
207
  - !ruby/object:Gem::Version
208
- version: 2.1.1
208
+ version: 2.2.0
209
209
  - !ruby/object:Gem::Dependency
210
210
  name: json
211
211
  requirement: !ruby/object:Gem::Requirement
@@ -260,6 +260,7 @@ files:
260
260
  - lib/ldclient-rb/file_data_source.rb
261
261
  - lib/ldclient-rb/flags_state.rb
262
262
  - lib/ldclient-rb/impl.rb
263
+ - lib/ldclient-rb/impl/big_segments.rb
263
264
  - lib/ldclient-rb/impl/diagnostic_events.rb
264
265
  - lib/ldclient-rb/impl/evaluator.rb
265
266
  - lib/ldclient-rb/impl/evaluator_bucketing.rb
@@ -268,8 +269,11 @@ files:
268
269
  - lib/ldclient-rb/impl/event_sender.rb
269
270
  - lib/ldclient-rb/impl/integrations/consul_impl.rb
270
271
  - lib/ldclient-rb/impl/integrations/dynamodb_impl.rb
272
+ - lib/ldclient-rb/impl/integrations/file_data_source.rb
271
273
  - lib/ldclient-rb/impl/integrations/redis_impl.rb
274
+ - lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb
272
275
  - lib/ldclient-rb/impl/model/serialization.rb
276
+ - lib/ldclient-rb/impl/repeating_task.rb
273
277
  - lib/ldclient-rb/impl/store_client_wrapper.rb
274
278
  - lib/ldclient-rb/impl/store_data_set_sorter.rb
275
279
  - lib/ldclient-rb/impl/unbounded_pool.rb
@@ -278,7 +282,10 @@ files:
278
282
  - lib/ldclient-rb/integrations.rb
279
283
  - lib/ldclient-rb/integrations/consul.rb
280
284
  - lib/ldclient-rb/integrations/dynamodb.rb
285
+ - lib/ldclient-rb/integrations/file_data.rb
281
286
  - lib/ldclient-rb/integrations/redis.rb
287
+ - lib/ldclient-rb/integrations/test_data.rb
288
+ - lib/ldclient-rb/integrations/test_data/flag_builder.rb
282
289
  - lib/ldclient-rb/integrations/util/store_wrapper.rb
283
290
  - lib/ldclient-rb/interfaces.rb
284
291
  - lib/ldclient-rb/ldclient.rb
@@ -312,7 +319,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
312
319
  - !ruby/object:Gem::Version
313
320
  version: '0'
314
321
  requirements: []
315
- rubygems_version: 3.2.29
322
+ rubygems_version: 3.3.9
316
323
  signing_key:
317
324
  specification_version: 4
318
325
  summary: LaunchDarkly SDK for Ruby