launchdarkly-server-sdk 6.2.3 → 6.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/lib/ldclient-rb/config.rb +81 -4
  4. data/lib/ldclient-rb/evaluation_detail.rb +67 -8
  5. data/lib/ldclient-rb/file_data_source.rb +9 -300
  6. data/lib/ldclient-rb/impl/big_segments.rb +117 -0
  7. data/lib/ldclient-rb/impl/evaluator.rb +80 -28
  8. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +82 -18
  9. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
  10. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +84 -31
  11. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
  12. data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
  13. data/lib/ldclient-rb/impl/util.rb +4 -1
  14. data/lib/ldclient-rb/integrations/consul.rb +7 -0
  15. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -2
  16. data/lib/ldclient-rb/integrations/file_data.rb +108 -0
  17. data/lib/ldclient-rb/integrations/redis.rb +41 -1
  18. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +438 -0
  19. data/lib/ldclient-rb/integrations/test_data.rb +209 -0
  20. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +5 -0
  21. data/lib/ldclient-rb/integrations.rb +2 -51
  22. data/lib/ldclient-rb/interfaces.rb +152 -2
  23. data/lib/ldclient-rb/ldclient.rb +21 -7
  24. data/lib/ldclient-rb/polling.rb +22 -41
  25. data/lib/ldclient-rb/util.rb +1 -1
  26. data/lib/ldclient-rb/version.rb +1 -1
  27. metadata +31 -132
  28. data/.circleci/config.yml +0 -40
  29. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -37
  30. data/.github/ISSUE_TEMPLATE/config.yml +0 -5
  31. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
  32. data/.github/pull_request_template.md +0 -21
  33. data/.gitignore +0 -16
  34. data/.hound.yml +0 -2
  35. data/.ldrelease/build-docs.sh +0 -18
  36. data/.ldrelease/circleci/linux/execute.sh +0 -18
  37. data/.ldrelease/circleci/mac/execute.sh +0 -18
  38. data/.ldrelease/circleci/template/build.sh +0 -29
  39. data/.ldrelease/circleci/template/publish.sh +0 -23
  40. data/.ldrelease/circleci/template/set-gem-home.sh +0 -7
  41. data/.ldrelease/circleci/template/test.sh +0 -10
  42. data/.ldrelease/circleci/template/update-version.sh +0 -8
  43. data/.ldrelease/circleci/windows/execute.ps1 +0 -19
  44. data/.ldrelease/config.yml +0 -29
  45. data/.rspec +0 -2
  46. data/.rubocop.yml +0 -600
  47. data/.simplecov +0 -4
  48. data/CHANGELOG.md +0 -367
  49. data/CODEOWNERS +0 -1
  50. data/CONTRIBUTING.md +0 -37
  51. data/Gemfile +0 -3
  52. data/azure-pipelines.yml +0 -51
  53. data/docs/Makefile +0 -26
  54. data/docs/index.md +0 -9
  55. data/launchdarkly-server-sdk.gemspec +0 -45
  56. data/spec/config_spec.rb +0 -63
  57. data/spec/diagnostic_events_spec.rb +0 -165
  58. data/spec/evaluation_detail_spec.rb +0 -135
  59. data/spec/event_sender_spec.rb +0 -197
  60. data/spec/event_summarizer_spec.rb +0 -63
  61. data/spec/events_spec.rb +0 -607
  62. data/spec/expiring_cache_spec.rb +0 -76
  63. data/spec/feature_store_spec_base.rb +0 -213
  64. data/spec/file_data_source_spec.rb +0 -283
  65. data/spec/fixtures/feature.json +0 -37
  66. data/spec/fixtures/feature1.json +0 -36
  67. data/spec/fixtures/user.json +0 -9
  68. data/spec/flags_state_spec.rb +0 -81
  69. data/spec/http_util.rb +0 -132
  70. data/spec/impl/evaluator_bucketing_spec.rb +0 -216
  71. data/spec/impl/evaluator_clause_spec.rb +0 -55
  72. data/spec/impl/evaluator_operators_spec.rb +0 -141
  73. data/spec/impl/evaluator_rule_spec.rb +0 -128
  74. data/spec/impl/evaluator_segment_spec.rb +0 -125
  75. data/spec/impl/evaluator_spec.rb +0 -349
  76. data/spec/impl/evaluator_spec_base.rb +0 -75
  77. data/spec/impl/event_factory_spec.rb +0 -108
  78. data/spec/impl/model/serialization_spec.rb +0 -41
  79. data/spec/in_memory_feature_store_spec.rb +0 -12
  80. data/spec/integrations/consul_feature_store_spec.rb +0 -40
  81. data/spec/integrations/dynamodb_feature_store_spec.rb +0 -103
  82. data/spec/integrations/store_wrapper_spec.rb +0 -276
  83. data/spec/launchdarkly-server-sdk_spec.rb +0 -13
  84. data/spec/launchdarkly-server-sdk_spec_autoloadtest.rb +0 -9
  85. data/spec/ldclient_end_to_end_spec.rb +0 -157
  86. data/spec/ldclient_spec.rb +0 -635
  87. data/spec/newrelic_spec.rb +0 -5
  88. data/spec/polling_spec.rb +0 -120
  89. data/spec/redis_feature_store_spec.rb +0 -121
  90. data/spec/requestor_spec.rb +0 -209
  91. data/spec/segment_store_spec_base.rb +0 -95
  92. data/spec/simple_lru_cache_spec.rb +0 -24
  93. data/spec/spec_helper.rb +0 -9
  94. data/spec/store_spec.rb +0 -10
  95. data/spec/stream_spec.rb +0 -45
  96. data/spec/user_filter_spec.rb +0 -91
  97. data/spec/util_spec.rb +0 -17
  98. data/spec/version_spec.rb +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c4646e511419b3e7883147b5040b136c84e333286518eca3171105c93304453
4
- data.tar.gz: b1d3f4fe3ee44591ec9f642abc8787ae5234bb72f794fce6f8acb46a53cd62b5
3
+ metadata.gz: d21f900f036d19f8b33271d825f3c00cda01d0ac44caf875774a196950ce2479
4
+ data.tar.gz: f409fa9e445a0387ef96256d6a8cc57320ebe354cc07cc9f1f78897eff5fc82f
5
5
  SHA512:
6
- metadata.gz: 3657e9b125c86998638479696fb97b1729ff62e0aa3538b9d9a74f446cc3816bd797a43279c3c2e7a2fa13afb28092de80720384f125e194ee6388a0d10c5c7f
7
- data.tar.gz: 001ce752eb3c8cc559d21ea5ca968c257a689345341324825fb6e76860062980406d8c5ee8476e2782c67bff29401d0b599ad40a896dc61ff79c719321c93365
6
+ metadata.gz: 21b358fa934cbe2d5c06e9f941ebbfeb785a3d93b2bbe562859fc59af3137b0b5673de8794d1d4a32c33f2e12d212c27451242ef85c9341ac2c0c59b80d5c5d5
7
+ data.tar.gz: fef2084a06846e16047d96a784b1d00351d45489baa657a48970a28190ebab67fdcc39a1716138c5b2c7f6d210be97d8289863d6f04c6a36a8bec2b5a8d49389
data/README.md CHANGED
@@ -10,7 +10,7 @@ LaunchDarkly Server-side SDK for Ruby
10
10
 
11
11
  LaunchDarkly overview
12
12
  -------------------------
13
- [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today!
13
+ [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today!
14
14
 
15
15
  [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly)
16
16
 
@@ -22,7 +22,7 @@ This version of the LaunchDarkly SDK has a minimum Ruby version of 2.5.0, or 9.2
22
22
  Getting started
23
23
  -----------
24
24
 
25
- Refer to the [SDK documentation](https://docs.launchdarkly.com/docs/ruby-sdk-reference#section-getting-started) for instructions on getting started with using the SDK.
25
+ Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/server-side/ruby#getting-started) for instructions on getting started with using the SDK.
26
26
 
27
27
  Learn more
28
28
  -----------
@@ -49,7 +49,7 @@ About LaunchDarkly
49
49
  * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
50
50
  * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
51
51
  * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline.
52
- * LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list.
52
+ * LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list.
53
53
  * Explore LaunchDarkly
54
54
  * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information
55
55
  * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides
@@ -42,6 +42,7 @@ module LaunchDarkly
42
42
  # @option opts [String] :wrapper_name See {#wrapper_name}.
43
43
  # @option opts [String] :wrapper_version See {#wrapper_version}.
44
44
  # @option opts [#open] :socket_factory See {#socket_factory}.
45
+ # @option opts [BigSegmentsConfig] :big_segments See {#big_segments}.
45
46
  #
46
47
  def initialize(opts = {})
47
48
  @base_uri = (opts[:base_uri] || Config.default_base_uri).chomp("/")
@@ -73,6 +74,7 @@ module LaunchDarkly
73
74
  @wrapper_name = opts[:wrapper_name]
74
75
  @wrapper_version = opts[:wrapper_version]
75
76
  @socket_factory = opts[:socket_factory]
77
+ @big_segments = opts[:big_segments] || BigSegmentsConfig.new(store: nil)
76
78
  end
77
79
 
78
80
  #
@@ -110,8 +112,8 @@ module LaunchDarkly
110
112
  # Whether to use the LaunchDarkly relay proxy in daemon mode. In this mode, the client does not
111
113
  # use polling or streaming to get feature flag updates from the server, but instead reads them
112
114
  # from the {#feature_store feature store}, which is assumed to be a database that is populated by
113
- # a LaunchDarkly relay proxy. For more information, see ["The relay proxy"](https://docs.launchdarkly.com/v2.0/docs/the-relay-proxy)
114
- # and ["Using a persistent feature store"](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store).
115
+ # a LaunchDarkly relay proxy. For more information, see ["The relay proxy"](https://docs.launchdarkly.com/home/relay-proxy)
116
+ # and ["Using a persistent data stores"](https://docs.launchdarkly.com/sdk/concepts/data-stores).
115
117
  #
116
118
  # All other properties related to streaming or polling are ignored if this option is set to true.
117
119
  #
@@ -189,7 +191,7 @@ module LaunchDarkly
189
191
  # from LaunchDarkly, and uses the last stored data when evaluating flags. Defaults to
190
192
  # {InMemoryFeatureStore}; for other implementations, see {LaunchDarkly::Integrations}.
191
193
  #
192
- # For more information, see ["Using a persistent feature store"](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store).
194
+ # For more information, see ["Persistent data stores"](https://docs.launchdarkly.com/sdk/concepts/data-stores).
193
195
  #
194
196
  # @return [LaunchDarkly::Interfaces::FeatureStore]
195
197
  #
@@ -258,10 +260,21 @@ module LaunchDarkly
258
260
  # object.
259
261
  #
260
262
  # @return [LaunchDarkly::Interfaces::DataSource|lambda]
261
- # @see FileDataSource
263
+ # @see LaunchDarkly::Integrations::FileData
264
+ # @see LaunchDarkly::Integrations::TestData
262
265
  #
263
266
  attr_reader :data_source
264
267
 
268
+ #
269
+ # Configuration options related to Big Segments.
270
+ #
271
+ # Big Segments are a specific type of user segments. For more information, read the LaunchDarkly
272
+ # documentation: https://docs.launchdarkly.com/home/users/big-segments
273
+ #
274
+ # @return [BigSegmentsConfig]
275
+ #
276
+ attr_reader :big_segments
277
+
265
278
  # @deprecated This is replaced by {#data_source}.
266
279
  attr_reader :update_processor
267
280
 
@@ -484,4 +497,68 @@ module LaunchDarkly
484
497
  60
485
498
  end
486
499
  end
500
+
501
+ #
502
+ # Configuration options related to Big Segments.
503
+ #
504
+ # Big Segments are a specific type of user segments. For more information, read the LaunchDarkly
505
+ # documentation: https://docs.launchdarkly.com/home/users/big-segments
506
+ #
507
+ # If your application uses Big Segments, you will need to create a `BigSegmentsConfig` that at a
508
+ # minimum specifies what database integration to use, and then pass the `BigSegmentsConfig`
509
+ # object as the `big_segments` parameter when creating a {Config}.
510
+ #
511
+ # @example Configuring Big Segments with Redis
512
+ # store = LaunchDarkly::Integrations::Redis::new_big_segments_store(redis_url: "redis://my-server")
513
+ # config = LaunchDarkly::Config.new(big_segments:
514
+ # LaunchDarkly::BigSegmentsConfig.new(store: store))
515
+ # client = LaunchDarkly::LDClient.new(my_sdk_key, config)
516
+ #
517
+ class BigSegmentsConfig
518
+ DEFAULT_USER_CACHE_SIZE = 1000
519
+ DEFAULT_USER_CACHE_TIME = 5
520
+ DEFAULT_STATUS_POLL_INTERVAL = 5
521
+ DEFAULT_STALE_AFTER = 2 * 60
522
+
523
+ #
524
+ # Constructor for setting Big Segments options.
525
+ #
526
+ # @param store [LaunchDarkly::Interfaces::BigSegmentStore] the data store implementation
527
+ # @param user_cache_size [Integer] See {#user_cache_size}.
528
+ # @param user_cache_time [Float] See {#user_cache_time}.
529
+ # @param status_poll_interval [Float] See {#status_poll_interval}.
530
+ # @param stale_after [Float] See {#stale_after}.
531
+ #
532
+ def initialize(store:, user_cache_size: nil, user_cache_time: nil, status_poll_interval: nil, stale_after: nil)
533
+ @store = store
534
+ @user_cache_size = user_cache_size.nil? ? DEFAULT_USER_CACHE_SIZE : user_cache_size
535
+ @user_cache_time = user_cache_time.nil? ? DEFAULT_USER_CACHE_TIME : user_cache_time
536
+ @status_poll_interval = status_poll_interval.nil? ? DEFAULT_STATUS_POLL_INTERVAL : status_poll_interval
537
+ @stale_after = stale_after.nil? ? DEFAULT_STALE_AFTER : stale_after
538
+ end
539
+
540
+ # The implementation of {LaunchDarkly::Interfaces::BigSegmentStore} that will be used to
541
+ # query the Big Segments database.
542
+ # @return [LaunchDarkly::Interfaces::BigSegmentStore]
543
+ attr_reader :store
544
+
545
+ # The maximum number of users whose Big Segment state will be cached by the SDK at any given time.
546
+ # @return [Integer]
547
+ attr_reader :user_cache_size
548
+
549
+ # The maximum length of time (in seconds) that the Big Segment state for a user will be cached
550
+ # by the SDK.
551
+ # @return [Float]
552
+ attr_reader :user_cache_time
553
+
554
+ # The interval (in seconds) at which the SDK will poll the Big Segment store to make sure it is
555
+ # available and to determine how long ago it was updated.
556
+ # @return [Float]
557
+ attr_reader :status_poll_interval
558
+
559
+ # The maximum length of time between updates of the Big Segments data before the data is
560
+ # considered out of date.
561
+ # @return [Float]
562
+ attr_reader :stale_after
563
+ end
487
564
  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
- private
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
- def initialize(kind, rule_index, rule_id, prerequisite_key, error_kind, in_experiment=nil)
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 :new
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 'concurrent/atomics'
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
- # The output will look something like this (but with many more properties):
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
- # Data in this format allows the SDK to exactly duplicate all the kinds of flag behavior supported
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
- # Returns a factory for the file data source component.
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
- def self.factory(options={})
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
- # Used internally by FileDataSource to track data file changes if the 'listen' gem is not available.
17
+ # @deprecated This is replaced by {LaunchDarkly::Integrations::FileData#data_source}.
270
18
  #
271
- class FileDataSourcePoller
272
- def initialize(resolved_paths, interval, reloader, logger)
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