launchdarkly-server-sdk 6.2.5 → 6.3.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.
- checksums.yaml +4 -4
- data/README.md +0 -1
- data/lib/ldclient-rb/config.rb +95 -1
- data/lib/ldclient-rb/evaluation_detail.rb +67 -8
- data/lib/ldclient-rb/file_data_source.rb +9 -300
- data/lib/ldclient-rb/flags_state.rb +23 -12
- data/lib/ldclient-rb/impl/big_segments.rb +117 -0
- data/lib/ldclient-rb/impl/evaluator.rb +80 -28
- data/lib/ldclient-rb/impl/evaluator_operators.rb +1 -1
- data/lib/ldclient-rb/impl/event_factory.rb +9 -12
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +82 -18
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +84 -31
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
- data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
- data/lib/ldclient-rb/impl/util.rb +4 -1
- data/lib/ldclient-rb/integrations/consul.rb +8 -1
- data/lib/ldclient-rb/integrations/dynamodb.rb +47 -2
- data/lib/ldclient-rb/integrations/file_data.rb +108 -0
- data/lib/ldclient-rb/integrations/redis.rb +41 -1
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +438 -0
- data/lib/ldclient-rb/integrations/test_data.rb +209 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +5 -0
- data/lib/ldclient-rb/integrations.rb +2 -51
- data/lib/ldclient-rb/interfaces.rb +151 -1
- data/lib/ldclient-rb/ldclient.rb +43 -9
- data/lib/ldclient-rb/polling.rb +22 -41
- data/lib/ldclient-rb/stream.rb +2 -1
- data/lib/ldclient-rb/util.rb +10 -1
- data/lib/ldclient-rb/version.rb +1 -1
- metadata +14 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 70c2bfa2af852ab863bdd25cceedb4ac6eca57bfb4d6ccd9df70653b73a862e0
|
4
|
+
data.tar.gz: 38d1bef24f2ce6cced2342264e0b70dde44ee79dcdfb4199ff90b9b4319303eb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f44affa6d4f7beb1c21be04f91caa39acd708a44fc642768c5c42419e8a2295f59592531c1e07f5ed8e407bbb1a97817b42c6be1e54ef2a80e20b1e014b57836
|
7
|
+
data.tar.gz: fe229a2d10add2afbc5fe539ca494fe8e73ae9b586571436e9bae9a3275721735ac755b35b40b6e432b69fec91803191bfe2c3bff1b876f30f3ef9de950af3eb
|
data/README.md
CHANGED
@@ -4,7 +4,6 @@ LaunchDarkly Server-side SDK for Ruby
|
|
4
4
|
[](http://badge.fury.io/rb/launchdarkly-server-sdk)
|
5
5
|
|
6
6
|
[](https://circleci.com/gh/launchdarkly/ruby-server-sdk/tree/master)
|
7
|
-
[](https://hakiri.io/github/launchdarkly/ruby-server-sdk/master)
|
8
7
|
[](https://www.rubydoc.info/gems/launchdarkly-server-sdk)
|
9
8
|
[](https://launchdarkly.github.io/ruby-server-sdk)
|
10
9
|
|
data/lib/ldclient-rb/config.rb
CHANGED
@@ -21,6 +21,7 @@ module LaunchDarkly
|
|
21
21
|
# @option opts [Integer] :capacity (10000) See {#capacity}.
|
22
22
|
# @option opts [Float] :flush_interval (30) See {#flush_interval}.
|
23
23
|
# @option opts [Float] :read_timeout (10) See {#read_timeout}.
|
24
|
+
# @option opts [Float] :initial_reconnect_delay (1) See {#initial_reconnect_delay}.
|
24
25
|
# @option opts [Float] :connect_timeout (2) See {#connect_timeout}.
|
25
26
|
# @option opts [Object] :cache_store See {#cache_store}.
|
26
27
|
# @option opts [Object] :feature_store See {#feature_store}.
|
@@ -42,6 +43,7 @@ module LaunchDarkly
|
|
42
43
|
# @option opts [String] :wrapper_name See {#wrapper_name}.
|
43
44
|
# @option opts [String] :wrapper_version See {#wrapper_version}.
|
44
45
|
# @option opts [#open] :socket_factory See {#socket_factory}.
|
46
|
+
# @option opts [BigSegmentsConfig] :big_segments See {#big_segments}.
|
45
47
|
#
|
46
48
|
def initialize(opts = {})
|
47
49
|
@base_uri = (opts[:base_uri] || Config.default_base_uri).chomp("/")
|
@@ -53,6 +55,7 @@ module LaunchDarkly
|
|
53
55
|
@flush_interval = opts[:flush_interval] || Config.default_flush_interval
|
54
56
|
@connect_timeout = opts[:connect_timeout] || Config.default_connect_timeout
|
55
57
|
@read_timeout = opts[:read_timeout] || Config.default_read_timeout
|
58
|
+
@initial_reconnect_delay = opts[:initial_reconnect_delay] || Config.default_initial_reconnect_delay
|
56
59
|
@feature_store = opts[:feature_store] || Config.default_feature_store
|
57
60
|
@stream = opts.has_key?(:stream) ? opts[:stream] : Config.default_stream
|
58
61
|
@use_ldd = opts.has_key?(:use_ldd) ? opts[:use_ldd] : Config.default_use_ldd
|
@@ -73,6 +76,7 @@ module LaunchDarkly
|
|
73
76
|
@wrapper_name = opts[:wrapper_name]
|
74
77
|
@wrapper_version = opts[:wrapper_version]
|
75
78
|
@socket_factory = opts[:socket_factory]
|
79
|
+
@big_segments = opts[:big_segments] || BigSegmentsConfig.new(store: nil)
|
76
80
|
end
|
77
81
|
|
78
82
|
#
|
@@ -178,6 +182,13 @@ module LaunchDarkly
|
|
178
182
|
#
|
179
183
|
attr_reader :read_timeout
|
180
184
|
|
185
|
+
#
|
186
|
+
# The initial delay before reconnecting after an error in the SSE client.
|
187
|
+
# This only applies to the streaming connection.
|
188
|
+
# @return [Float]
|
189
|
+
#
|
190
|
+
attr_reader :initial_reconnect_delay
|
191
|
+
|
181
192
|
#
|
182
193
|
# The connect timeout for network connections in seconds.
|
183
194
|
# @return [Float]
|
@@ -258,10 +269,21 @@ module LaunchDarkly
|
|
258
269
|
# object.
|
259
270
|
#
|
260
271
|
# @return [LaunchDarkly::Interfaces::DataSource|lambda]
|
261
|
-
# @see
|
272
|
+
# @see LaunchDarkly::Integrations::FileData
|
273
|
+
# @see LaunchDarkly::Integrations::TestData
|
262
274
|
#
|
263
275
|
attr_reader :data_source
|
264
276
|
|
277
|
+
#
|
278
|
+
# Configuration options related to Big Segments.
|
279
|
+
#
|
280
|
+
# Big Segments are a specific type of user segments. For more information, read the LaunchDarkly
|
281
|
+
# documentation: https://docs.launchdarkly.com/home/users/big-segments
|
282
|
+
#
|
283
|
+
# @return [BigSegmentsConfig]
|
284
|
+
#
|
285
|
+
attr_reader :big_segments
|
286
|
+
|
265
287
|
# @deprecated This is replaced by {#data_source}.
|
266
288
|
attr_reader :update_processor
|
267
289
|
|
@@ -382,6 +404,14 @@ module LaunchDarkly
|
|
382
404
|
10
|
383
405
|
end
|
384
406
|
|
407
|
+
#
|
408
|
+
# The default value for {#initial_reconnect_delay}.
|
409
|
+
# @return [Float] 1
|
410
|
+
#
|
411
|
+
def self.default_initial_reconnect_delay
|
412
|
+
1
|
413
|
+
end
|
414
|
+
|
385
415
|
#
|
386
416
|
# The default value for {#connect_timeout}.
|
387
417
|
# @return [Float] 10
|
@@ -484,4 +514,68 @@ module LaunchDarkly
|
|
484
514
|
60
|
485
515
|
end
|
486
516
|
end
|
517
|
+
|
518
|
+
#
|
519
|
+
# Configuration options related to Big Segments.
|
520
|
+
#
|
521
|
+
# Big Segments are a specific type of user segments. For more information, read the LaunchDarkly
|
522
|
+
# documentation: https://docs.launchdarkly.com/home/users/big-segments
|
523
|
+
#
|
524
|
+
# If your application uses Big Segments, you will need to create a `BigSegmentsConfig` that at a
|
525
|
+
# minimum specifies what database integration to use, and then pass the `BigSegmentsConfig`
|
526
|
+
# object as the `big_segments` parameter when creating a {Config}.
|
527
|
+
#
|
528
|
+
# @example Configuring Big Segments with Redis
|
529
|
+
# store = LaunchDarkly::Integrations::Redis::new_big_segments_store(redis_url: "redis://my-server")
|
530
|
+
# config = LaunchDarkly::Config.new(big_segments:
|
531
|
+
# LaunchDarkly::BigSegmentsConfig.new(store: store))
|
532
|
+
# client = LaunchDarkly::LDClient.new(my_sdk_key, config)
|
533
|
+
#
|
534
|
+
class BigSegmentsConfig
|
535
|
+
DEFAULT_USER_CACHE_SIZE = 1000
|
536
|
+
DEFAULT_USER_CACHE_TIME = 5
|
537
|
+
DEFAULT_STATUS_POLL_INTERVAL = 5
|
538
|
+
DEFAULT_STALE_AFTER = 2 * 60
|
539
|
+
|
540
|
+
#
|
541
|
+
# Constructor for setting Big Segments options.
|
542
|
+
#
|
543
|
+
# @param store [LaunchDarkly::Interfaces::BigSegmentStore] the data store implementation
|
544
|
+
# @param user_cache_size [Integer] See {#user_cache_size}.
|
545
|
+
# @param user_cache_time [Float] See {#user_cache_time}.
|
546
|
+
# @param status_poll_interval [Float] See {#status_poll_interval}.
|
547
|
+
# @param stale_after [Float] See {#stale_after}.
|
548
|
+
#
|
549
|
+
def initialize(store:, user_cache_size: nil, user_cache_time: nil, status_poll_interval: nil, stale_after: nil)
|
550
|
+
@store = store
|
551
|
+
@user_cache_size = user_cache_size.nil? ? DEFAULT_USER_CACHE_SIZE : user_cache_size
|
552
|
+
@user_cache_time = user_cache_time.nil? ? DEFAULT_USER_CACHE_TIME : user_cache_time
|
553
|
+
@status_poll_interval = status_poll_interval.nil? ? DEFAULT_STATUS_POLL_INTERVAL : status_poll_interval
|
554
|
+
@stale_after = stale_after.nil? ? DEFAULT_STALE_AFTER : stale_after
|
555
|
+
end
|
556
|
+
|
557
|
+
# The implementation of {LaunchDarkly::Interfaces::BigSegmentStore} that will be used to
|
558
|
+
# query the Big Segments database.
|
559
|
+
# @return [LaunchDarkly::Interfaces::BigSegmentStore]
|
560
|
+
attr_reader :store
|
561
|
+
|
562
|
+
# The maximum number of users whose Big Segment state will be cached by the SDK at any given time.
|
563
|
+
# @return [Integer]
|
564
|
+
attr_reader :user_cache_size
|
565
|
+
|
566
|
+
# The maximum length of time (in seconds) that the Big Segment state for a user will be cached
|
567
|
+
# by the SDK.
|
568
|
+
# @return [Float]
|
569
|
+
attr_reader :user_cache_time
|
570
|
+
|
571
|
+
# The interval (in seconds) at which the SDK will poll the Big Segment store to make sure it is
|
572
|
+
# available and to determine how long ago it was updated.
|
573
|
+
# @return [Float]
|
574
|
+
attr_reader :status_poll_interval
|
575
|
+
|
576
|
+
# The maximum length of time between updates of the Big Segments data before the data is
|
577
|
+
# considered out of date.
|
578
|
+
# @return [Float]
|
579
|
+
attr_reader :stale_after
|
580
|
+
end
|
487
581
|
end
|
@@ -110,27 +110,42 @@ module LaunchDarkly
|
|
110
110
|
|
111
111
|
# Indicates the general category of the reason. Will always be one of the class constants such
|
112
112
|
# as {#OFF}.
|
113
|
+
# @return [Symbol]
|
113
114
|
attr_reader :kind
|
114
115
|
|
115
116
|
# The index of the rule that was matched (0 for the first rule in the feature flag). If
|
116
117
|
# {#kind} is not {#RULE_MATCH}, this will be `nil`.
|
118
|
+
# @return [Integer|nil]
|
117
119
|
attr_reader :rule_index
|
118
120
|
|
119
121
|
# A unique string identifier for the matched rule, which will not change if other rules are added
|
120
122
|
# or deleted. If {#kind} is not {#RULE_MATCH}, this will be `nil`.
|
123
|
+
# @return [String]
|
121
124
|
attr_reader :rule_id
|
122
125
|
|
123
126
|
# A boolean or nil value representing if the rule or fallthrough has an experiment rollout.
|
127
|
+
# @return [Boolean|nil]
|
124
128
|
attr_reader :in_experiment
|
125
129
|
|
126
130
|
# The key of the prerequisite flag that did not return the desired variation. If {#kind} is not
|
127
131
|
# {#PREREQUISITE_FAILED}, this will be `nil`.
|
132
|
+
# @return [String]
|
128
133
|
attr_reader :prerequisite_key
|
129
134
|
|
130
135
|
# A value indicating the general category of error. This should be one of the class constants such
|
131
136
|
# as {#ERROR_FLAG_NOT_FOUND}. If {#kind} is not {#ERROR}, it will be `nil`.
|
137
|
+
# @return [Symbol]
|
132
138
|
attr_reader :error_kind
|
133
139
|
|
140
|
+
# Describes the validity of Big Segment information, if and only if the flag evaluation required
|
141
|
+
# querying at least one Big Segment. Otherwise it returns `nil`. Possible values are defined by
|
142
|
+
# {BigSegmentsStatus}.
|
143
|
+
#
|
144
|
+
# Big Segments are a specific kind of user segments. For more information, read the LaunchDarkly
|
145
|
+
# documentation: https://docs.launchdarkly.com/home/users/big-segments
|
146
|
+
# @return [Symbol]
|
147
|
+
attr_reader :big_segments_status
|
148
|
+
|
134
149
|
# Returns an instance whose {#kind} is {#OFF}.
|
135
150
|
# @return [EvaluationReason]
|
136
151
|
def self.off
|
@@ -196,11 +211,13 @@ module LaunchDarkly
|
|
196
211
|
def ==(other)
|
197
212
|
if other.is_a? EvaluationReason
|
198
213
|
@kind == other.kind && @rule_index == other.rule_index && @rule_id == other.rule_id &&
|
199
|
-
@prerequisite_key == other.prerequisite_key && @error_kind == other.error_kind
|
214
|
+
@prerequisite_key == other.prerequisite_key && @error_kind == other.error_kind &&
|
215
|
+
@big_segments_status == other.big_segments_status
|
200
216
|
elsif other.is_a? Hash
|
201
217
|
@kind.to_s == other[:kind] && @rule_index == other[:ruleIndex] && @rule_id == other[:ruleId] &&
|
202
218
|
@prerequisite_key == other[:prerequisiteKey] &&
|
203
|
-
(other[:errorKind] == @error_kind.nil? ? nil : @error_kind.to_s)
|
219
|
+
(other[:errorKind] == @error_kind.nil? ? nil : @error_kind.to_s) &&
|
220
|
+
(other[:bigSegmentsStatus] == @big_segments_status.nil? ? nil : @big_segments_status.to_s)
|
204
221
|
end
|
205
222
|
end
|
206
223
|
|
@@ -242,7 +259,7 @@ module LaunchDarkly
|
|
242
259
|
# enabled for a flag and the application called variation_detail, or 2. experimentation is
|
243
260
|
# enabled for an evaluation. We can't reuse these hashes because an application could call
|
244
261
|
# as_json and then modify the result.
|
245
|
-
case @kind
|
262
|
+
ret = case @kind
|
246
263
|
when :RULE_MATCH
|
247
264
|
if @in_experiment
|
248
265
|
{ kind: @kind, ruleIndex: @rule_index, ruleId: @rule_id, inExperiment: @in_experiment }
|
@@ -262,6 +279,10 @@ module LaunchDarkly
|
|
262
279
|
else
|
263
280
|
{ kind: @kind }
|
264
281
|
end
|
282
|
+
if !@big_segments_status.nil?
|
283
|
+
ret[:bigSegmentsStatus] = @big_segments_status
|
284
|
+
end
|
285
|
+
ret
|
265
286
|
end
|
266
287
|
|
267
288
|
# Same as {#as_json}, but converts the JSON structure into a string.
|
@@ -285,14 +306,24 @@ module LaunchDarkly
|
|
285
306
|
@prerequisite_key
|
286
307
|
when :errorKind
|
287
308
|
@error_kind.nil? ? nil : @error_kind.to_s
|
309
|
+
when :bigSegmentsStatus
|
310
|
+
@big_segments_status.nil? ? nil : @big_segments_status.to_s
|
288
311
|
else
|
289
312
|
nil
|
290
313
|
end
|
291
314
|
end
|
292
315
|
|
293
|
-
|
316
|
+
def with_big_segments_status(big_segments_status)
|
317
|
+
return self if @big_segments_status == big_segments_status
|
318
|
+
EvaluationReason.new(@kind, @rule_index, @rule_id, @prerequisite_key, @error_kind, @in_experiment, big_segments_status)
|
319
|
+
end
|
294
320
|
|
295
|
-
|
321
|
+
#
|
322
|
+
# Constructor that sets all properties. Applications should not normally use this constructor,
|
323
|
+
# but should use class methods like {#off} to avoid creating unnecessary instances.
|
324
|
+
#
|
325
|
+
def initialize(kind, rule_index, rule_id, prerequisite_key, error_kind, in_experiment=nil,
|
326
|
+
big_segments_status = nil)
|
296
327
|
@kind = kind.to_sym
|
297
328
|
@rule_index = rule_index
|
298
329
|
@rule_id = rule_id
|
@@ -301,11 +332,10 @@ module LaunchDarkly
|
|
301
332
|
@prerequisite_key.freeze if !prerequisite_key.nil?
|
302
333
|
@error_kind = error_kind
|
303
334
|
@in_experiment = in_experiment
|
335
|
+
@big_segments_status = big_segments_status
|
304
336
|
end
|
305
337
|
|
306
|
-
private_class_method
|
307
|
-
|
308
|
-
def self.make_error(error_kind)
|
338
|
+
private_class_method def self.make_error(error_kind)
|
309
339
|
new(:ERROR, nil, nil, nil, error_kind)
|
310
340
|
end
|
311
341
|
|
@@ -321,4 +351,33 @@ module LaunchDarkly
|
|
321
351
|
ERROR_EXCEPTION => make_error(ERROR_EXCEPTION)
|
322
352
|
}
|
323
353
|
end
|
354
|
+
|
355
|
+
#
|
356
|
+
# Defines the possible values of {EvaluationReason#big_segments_status}.
|
357
|
+
#
|
358
|
+
module BigSegmentsStatus
|
359
|
+
#
|
360
|
+
# Indicates that the Big Segment query involved in the flag evaluation was successful, and
|
361
|
+
# that the segment state is considered up to date.
|
362
|
+
#
|
363
|
+
HEALTHY = :HEALTHY
|
364
|
+
|
365
|
+
#
|
366
|
+
# Indicates that the Big Segment query involved in the flag evaluation was successful, but
|
367
|
+
# that the segment state may not be up to date.
|
368
|
+
#
|
369
|
+
STALE = :STALE
|
370
|
+
|
371
|
+
#
|
372
|
+
# Indicates that Big Segments could not be queried for the flag evaluation because the SDK
|
373
|
+
# configuration did not include a Big Segment store.
|
374
|
+
#
|
375
|
+
NOT_CONFIGURED = :NOT_CONFIGURED
|
376
|
+
|
377
|
+
#
|
378
|
+
# Indicates that the Big Segment query involved in the flag evaluation failed, for instance
|
379
|
+
# due to a database error.
|
380
|
+
#
|
381
|
+
STORE_ERROR = :STORE_ERROR
|
382
|
+
end
|
324
383
|
end
|
@@ -1,314 +1,23 @@
|
|
1
|
-
require
|
2
|
-
require 'json'
|
3
|
-
require 'yaml'
|
4
|
-
require 'pathname'
|
1
|
+
require "ldclient-rb/integrations/file_data"
|
5
2
|
|
6
3
|
module LaunchDarkly
|
7
|
-
# To avoid pulling in 'listen' and its transitive dependencies for people who aren't using the
|
8
|
-
# file data source or who don't need auto-updating, we only enable auto-update if the 'listen'
|
9
|
-
# gem has been provided by the host app.
|
10
|
-
# @private
|
11
|
-
@@have_listen = false
|
12
|
-
begin
|
13
|
-
require 'listen'
|
14
|
-
@@have_listen = true
|
15
|
-
rescue LoadError
|
16
|
-
end
|
17
|
-
|
18
|
-
# @private
|
19
|
-
def self.have_listen?
|
20
|
-
@@have_listen
|
21
|
-
end
|
22
|
-
|
23
|
-
#
|
24
|
-
# Provides a way to use local files as a source of feature flag state. This allows using a
|
25
|
-
# predetermined feature flag state without an actual LaunchDarkly connection.
|
26
|
-
#
|
27
|
-
# Reading flags from a file is only intended for pre-production environments. Production
|
28
|
-
# environments should always be configured to receive flag updates from LaunchDarkly.
|
29
|
-
#
|
30
|
-
# To use this component, call {FileDataSource#factory}, and store its return value in the
|
31
|
-
# {Config#data_source} property of your LaunchDarkly client configuration. In the options
|
32
|
-
# to `factory`, set `paths` to the file path(s) of your data file(s):
|
33
|
-
#
|
34
|
-
# file_source = FileDataSource.factory(paths: [ myFilePath ])
|
35
|
-
# config = LaunchDarkly::Config.new(data_source: file_source)
|
36
|
-
#
|
37
|
-
# This will cause the client not to connect to LaunchDarkly to get feature flags. The
|
38
|
-
# client may still make network connections to send analytics events, unless you have disabled
|
39
|
-
# this with {Config#send_events} or {Config#offline?}.
|
40
|
-
#
|
41
|
-
# Flag data files can be either JSON or YAML. They contain an object with three possible
|
42
|
-
# properties:
|
43
|
-
#
|
44
|
-
# - `flags`: Feature flag definitions.
|
45
|
-
# - `flagValues`: Simplified feature flags that contain only a value.
|
46
|
-
# - `segments`: User segment definitions.
|
47
|
-
#
|
48
|
-
# The format of the data in `flags` and `segments` is defined by the LaunchDarkly application
|
49
|
-
# and is subject to change. Rather than trying to construct these objects yourself, it is simpler
|
50
|
-
# to request existing flags directly from the LaunchDarkly server in JSON format, and use this
|
51
|
-
# output as the starting point for your file. In Linux you would do this:
|
52
|
-
#
|
53
|
-
# ```
|
54
|
-
# curl -H "Authorization: YOUR_SDK_KEY" https://sdk.launchdarkly.com/sdk/latest-all
|
55
|
-
# ```
|
56
4
|
#
|
57
|
-
#
|
5
|
+
# Deprecated entry point for the file data source feature.
|
58
6
|
#
|
59
|
-
#
|
60
|
-
# "flags": {
|
61
|
-
# "flag-key-1": {
|
62
|
-
# "key": "flag-key-1",
|
63
|
-
# "on": true,
|
64
|
-
# "variations": [ "a", "b" ]
|
65
|
-
# }
|
66
|
-
# },
|
67
|
-
# "segments": {
|
68
|
-
# "segment-key-1": {
|
69
|
-
# "key": "segment-key-1",
|
70
|
-
# "includes": [ "user-key-1" ]
|
71
|
-
# }
|
72
|
-
# }
|
73
|
-
# }
|
7
|
+
# The new preferred usage is {LaunchDarkly::Integrations::FileData#data_source}.
|
74
8
|
#
|
75
|
-
#
|
76
|
-
# by LaunchDarkly. However, in many cases you will not need this complexity, but will just want to
|
77
|
-
# set specific flag keys to specific values. For that, you can use a much simpler format:
|
78
|
-
#
|
79
|
-
# {
|
80
|
-
# "flagValues": {
|
81
|
-
# "my-string-flag-key": "value-1",
|
82
|
-
# "my-boolean-flag-key": true,
|
83
|
-
# "my-integer-flag-key": 3
|
84
|
-
# }
|
85
|
-
# }
|
86
|
-
#
|
87
|
-
# Or, in YAML:
|
88
|
-
#
|
89
|
-
# flagValues:
|
90
|
-
# my-string-flag-key: "value-1"
|
91
|
-
# my-boolean-flag-key: true
|
92
|
-
# my-integer-flag-key: 1
|
93
|
-
#
|
94
|
-
# It is also possible to specify both "flags" and "flagValues", if you want some flags
|
95
|
-
# to have simple values and others to have complex behavior. However, it is an error to use the
|
96
|
-
# same flag key or segment key more than once, either in a single file or across multiple files.
|
97
|
-
#
|
98
|
-
# If the data source encounters any error in any file-- malformed content, a missing file, or a
|
99
|
-
# duplicate key-- it will not load flags from any of the files.
|
9
|
+
# @deprecated This is replaced by {LaunchDarkly::Integrations::FileData}.
|
100
10
|
#
|
101
11
|
class FileDataSource
|
102
12
|
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
# @param options [Hash] the configuration options
|
106
|
-
# @option options [Array] :paths The paths of the source files for loading flag data. These
|
107
|
-
# may be absolute paths or relative to the current working directory.
|
108
|
-
# @option options [Boolean] :auto_update True if the data source should watch for changes to
|
109
|
-
# the source file(s) and reload flags whenever there is a change. Auto-updating will only
|
110
|
-
# work if all of the files you specified have valid directory paths at startup time.
|
111
|
-
# Note that the default implementation of this feature is based on polling the filesystem,
|
112
|
-
# which may not perform well. If you install the 'listen' gem (not included by default, to
|
113
|
-
# avoid adding unwanted dependencies to the SDK), its native file watching mechanism will be
|
114
|
-
# used instead. However, 'listen' will not be used in JRuby 9.1 due to a known instability.
|
115
|
-
# @option options [Float] :poll_interval The minimum interval, in seconds, between checks for
|
116
|
-
# file modifications - used only if auto_update is true, and if the native file-watching
|
117
|
-
# mechanism from 'listen' is not being used. The default value is 1 second.
|
118
|
-
# @return an object that can be stored in {Config#data_source}
|
13
|
+
# Deprecated entry point for the file data source feature.
|
119
14
|
#
|
120
|
-
|
121
|
-
return lambda { |sdk_key, config| FileDataSourceImpl.new(config.feature_store, config.logger, options) }
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
# @private
|
126
|
-
class FileDataSourceImpl
|
127
|
-
def initialize(feature_store, logger, options={})
|
128
|
-
@feature_store = feature_store
|
129
|
-
@logger = logger
|
130
|
-
@paths = options[:paths] || []
|
131
|
-
if @paths.is_a? String
|
132
|
-
@paths = [ @paths ]
|
133
|
-
end
|
134
|
-
@auto_update = options[:auto_update]
|
135
|
-
if @auto_update && LaunchDarkly.have_listen? && !options[:force_polling] # force_polling is used only for tests
|
136
|
-
# We have seen unreliable behavior in the 'listen' gem in JRuby 9.1 (https://github.com/guard/listen/issues/449).
|
137
|
-
# Therefore, on that platform we'll fall back to file polling instead.
|
138
|
-
if defined?(JRUBY_VERSION) && JRUBY_VERSION.start_with?("9.1.")
|
139
|
-
@use_listen = false
|
140
|
-
else
|
141
|
-
@use_listen = true
|
142
|
-
end
|
143
|
-
end
|
144
|
-
@poll_interval = options[:poll_interval] || 1
|
145
|
-
@initialized = Concurrent::AtomicBoolean.new(false)
|
146
|
-
@ready = Concurrent::Event.new
|
147
|
-
end
|
148
|
-
|
149
|
-
def initialized?
|
150
|
-
@initialized.value
|
151
|
-
end
|
152
|
-
|
153
|
-
def start
|
154
|
-
ready = Concurrent::Event.new
|
155
|
-
|
156
|
-
# We will return immediately regardless of whether the file load succeeded or failed -
|
157
|
-
# the difference can be detected by checking "initialized?"
|
158
|
-
ready.set
|
159
|
-
|
160
|
-
load_all
|
161
|
-
|
162
|
-
if @auto_update
|
163
|
-
# If we're going to watch files, then the start event will be set the first time we get
|
164
|
-
# a successful load.
|
165
|
-
@listener = start_listener
|
166
|
-
end
|
167
|
-
|
168
|
-
ready
|
169
|
-
end
|
170
|
-
|
171
|
-
def stop
|
172
|
-
@listener.stop if !@listener.nil?
|
173
|
-
end
|
174
|
-
|
175
|
-
private
|
176
|
-
|
177
|
-
def load_all
|
178
|
-
all_data = {
|
179
|
-
FEATURES => {},
|
180
|
-
SEGMENTS => {}
|
181
|
-
}
|
182
|
-
@paths.each do |path|
|
183
|
-
begin
|
184
|
-
load_file(path, all_data)
|
185
|
-
rescue => exn
|
186
|
-
Util.log_exception(@logger, "Unable to load flag data from \"#{path}\"", exn)
|
187
|
-
return
|
188
|
-
end
|
189
|
-
end
|
190
|
-
@feature_store.init(all_data)
|
191
|
-
@initialized.make_true
|
192
|
-
end
|
193
|
-
|
194
|
-
def load_file(path, all_data)
|
195
|
-
parsed = parse_content(IO.read(path))
|
196
|
-
(parsed[:flags] || {}).each do |key, flag|
|
197
|
-
add_item(all_data, FEATURES, flag)
|
198
|
-
end
|
199
|
-
(parsed[:flagValues] || {}).each do |key, value|
|
200
|
-
add_item(all_data, FEATURES, make_flag_with_value(key.to_s, value))
|
201
|
-
end
|
202
|
-
(parsed[:segments] || {}).each do |key, segment|
|
203
|
-
add_item(all_data, SEGMENTS, segment)
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
def parse_content(content)
|
208
|
-
# We can use the Ruby YAML parser for both YAML and JSON (JSON is a subset of YAML and while
|
209
|
-
# not all YAML parsers handle it correctly, we have verified that the Ruby one does, at least
|
210
|
-
# for all the samples of actual flag data that we've tested).
|
211
|
-
symbolize_all_keys(YAML.safe_load(content))
|
212
|
-
end
|
213
|
-
|
214
|
-
def symbolize_all_keys(value)
|
215
|
-
# This is necessary because YAML.load doesn't have an option for parsing keys as symbols, and
|
216
|
-
# the SDK expects all objects to be formatted that way.
|
217
|
-
if value.is_a?(Hash)
|
218
|
-
value.map{ |k, v| [k.to_sym, symbolize_all_keys(v)] }.to_h
|
219
|
-
elsif value.is_a?(Array)
|
220
|
-
value.map{ |v| symbolize_all_keys(v) }
|
221
|
-
else
|
222
|
-
value
|
223
|
-
end
|
224
|
-
end
|
225
|
-
|
226
|
-
def add_item(all_data, kind, item)
|
227
|
-
items = all_data[kind]
|
228
|
-
raise ArgumentError, "Received unknown item kind #{kind} in add_data" if items.nil? # shouldn't be possible since we preinitialize the hash
|
229
|
-
key = item[:key].to_sym
|
230
|
-
if !items[key].nil?
|
231
|
-
raise ArgumentError, "#{kind[:namespace]} key \"#{item[:key]}\" was used more than once"
|
232
|
-
end
|
233
|
-
items[key] = item
|
234
|
-
end
|
235
|
-
|
236
|
-
def make_flag_with_value(key, value)
|
237
|
-
{
|
238
|
-
key: key,
|
239
|
-
on: true,
|
240
|
-
fallthrough: { variation: 0 },
|
241
|
-
variations: [ value ]
|
242
|
-
}
|
243
|
-
end
|
244
|
-
|
245
|
-
def start_listener
|
246
|
-
resolved_paths = @paths.map { |p| Pathname.new(File.absolute_path(p)).realpath.to_s }
|
247
|
-
if @use_listen
|
248
|
-
start_listener_with_listen_gem(resolved_paths)
|
249
|
-
else
|
250
|
-
FileDataSourcePoller.new(resolved_paths, @poll_interval, self.method(:load_all), @logger)
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
def start_listener_with_listen_gem(resolved_paths)
|
255
|
-
path_set = resolved_paths.to_set
|
256
|
-
dir_paths = resolved_paths.map{ |p| File.dirname(p) }.uniq
|
257
|
-
opts = { latency: @poll_interval }
|
258
|
-
l = Listen.to(*dir_paths, opts) do |modified, added, removed|
|
259
|
-
paths = modified + added + removed
|
260
|
-
if paths.any? { |p| path_set.include?(p) }
|
261
|
-
load_all
|
262
|
-
end
|
263
|
-
end
|
264
|
-
l.start
|
265
|
-
l
|
266
|
-
end
|
267
|
-
|
15
|
+
# The new preferred usage is {LaunchDarkly::Integrations::FileData#data_source}.
|
268
16
|
#
|
269
|
-
#
|
17
|
+
# @deprecated This is replaced by {LaunchDarkly::Integrations::FileData#data_source}.
|
270
18
|
#
|
271
|
-
|
272
|
-
|
273
|
-
@stopped = Concurrent::AtomicBoolean.new(false)
|
274
|
-
get_file_times = Proc.new do
|
275
|
-
ret = {}
|
276
|
-
resolved_paths.each do |path|
|
277
|
-
begin
|
278
|
-
ret[path] = File.mtime(path)
|
279
|
-
rescue Errno::ENOENT
|
280
|
-
ret[path] = nil
|
281
|
-
end
|
282
|
-
end
|
283
|
-
ret
|
284
|
-
end
|
285
|
-
last_times = get_file_times.call
|
286
|
-
@thread = Thread.new do
|
287
|
-
while true
|
288
|
-
sleep interval
|
289
|
-
break if @stopped.value
|
290
|
-
begin
|
291
|
-
new_times = get_file_times.call
|
292
|
-
changed = false
|
293
|
-
last_times.each do |path, old_time|
|
294
|
-
new_time = new_times[path]
|
295
|
-
if !new_time.nil? && new_time != old_time
|
296
|
-
changed = true
|
297
|
-
break
|
298
|
-
end
|
299
|
-
end
|
300
|
-
reloader.call if changed
|
301
|
-
rescue => exn
|
302
|
-
Util.log_exception(logger, "Unexpected exception in FileDataSourcePoller", exn)
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
306
|
-
end
|
307
|
-
|
308
|
-
def stop
|
309
|
-
@stopped.make_true
|
310
|
-
@thread.run # wakes it up if it's sleeping
|
311
|
-
end
|
19
|
+
def self.factory(options={})
|
20
|
+
LaunchDarkly::Integrations::FileData.data_source(options)
|
312
21
|
end
|
313
22
|
end
|
314
23
|
end
|