launchdarkly-server-sdk 6.3.0 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -4
  3. data/lib/ldclient-rb/config.rb +112 -62
  4. data/lib/ldclient-rb/context.rb +444 -0
  5. data/lib/ldclient-rb/evaluation_detail.rb +26 -22
  6. data/lib/ldclient-rb/events.rb +256 -146
  7. data/lib/ldclient-rb/flags_state.rb +26 -15
  8. data/lib/ldclient-rb/impl/big_segments.rb +18 -18
  9. data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
  10. data/lib/ldclient-rb/impl/context.rb +96 -0
  11. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  12. data/lib/ldclient-rb/impl/data_source.rb +188 -0
  13. data/lib/ldclient-rb/impl/data_store.rb +59 -0
  14. data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
  15. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  16. data/lib/ldclient-rb/impl/evaluator.rb +386 -142
  17. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  18. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  19. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  20. data/lib/ldclient-rb/impl/event_sender.rb +7 -6
  21. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  22. data/lib/ldclient-rb/impl/event_types.rb +136 -0
  23. data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
  24. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +19 -7
  25. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
  26. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
  27. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
  28. data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
  29. data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
  30. data/lib/ldclient-rb/impl/model/clause.rb +45 -0
  31. data/lib/ldclient-rb/impl/model/feature_flag.rb +255 -0
  32. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  33. data/lib/ldclient-rb/impl/model/segment.rb +132 -0
  34. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  35. data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
  36. data/lib/ldclient-rb/impl/sampler.rb +25 -0
  37. data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
  38. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  39. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  40. data/lib/ldclient-rb/impl/util.rb +59 -1
  41. data/lib/ldclient-rb/in_memory_store.rb +9 -2
  42. data/lib/ldclient-rb/integrations/consul.rb +2 -2
  43. data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
  44. data/lib/ldclient-rb/integrations/file_data.rb +4 -4
  45. data/lib/ldclient-rb/integrations/redis.rb +5 -5
  46. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
  47. data/lib/ldclient-rb/integrations/test_data.rb +18 -14
  48. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
  49. data/lib/ldclient-rb/interfaces.rb +600 -14
  50. data/lib/ldclient-rb/ldclient.rb +314 -134
  51. data/lib/ldclient-rb/memoized_value.rb +1 -1
  52. data/lib/ldclient-rb/migrations.rb +230 -0
  53. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  54. data/lib/ldclient-rb/polling.rb +52 -6
  55. data/lib/ldclient-rb/reference.rb +274 -0
  56. data/lib/ldclient-rb/requestor.rb +9 -11
  57. data/lib/ldclient-rb/stream.rb +96 -34
  58. data/lib/ldclient-rb/util.rb +97 -14
  59. data/lib/ldclient-rb/version.rb +1 -1
  60. data/lib/ldclient-rb.rb +3 -4
  61. metadata +65 -23
  62. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  63. data/lib/ldclient-rb/file_data_source.rb +0 -23
  64. data/lib/ldclient-rb/impl/event_factory.rb +0 -126
  65. data/lib/ldclient-rb/newrelic.rb +0 -17
  66. data/lib/ldclient-rb/redis_store.rb +0 -88
  67. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -1,3 +1,4 @@
1
+ require "concurrent"
1
2
  require "ldclient-rb/interfaces"
2
3
  require "ldclient-rb/impl/store_data_set_sorter"
3
4
 
@@ -5,34 +6,45 @@ module LaunchDarkly
5
6
  module Impl
6
7
  #
7
8
  # Provides additional behavior that the client requires before or after feature store operations.
8
- # Currently this just means sorting the data set for init(). In the future we may also use this
9
- # to provide an update listener capability.
9
+ # This just means sorting the data set for init() and dealing with data store status listeners.
10
10
  #
11
11
  class FeatureStoreClientWrapper
12
12
  include Interfaces::FeatureStore
13
13
 
14
- def initialize(store)
14
+ def initialize(store, store_update_sink, logger)
15
+ # @type [LaunchDarkly::Interfaces::FeatureStore]
15
16
  @store = store
17
+
18
+ @monitoring_enabled = does_store_support_monitoring?
19
+
20
+ # @type [LaunchDarkly::Impl::DataStore::UpdateSink]
21
+ @store_update_sink = store_update_sink
22
+ @logger = logger
23
+
24
+ @mutex = Mutex.new # Covers the following variables
25
+ @last_available = true
26
+ # @type [LaunchDarkly::Impl::RepeatingTask, nil]
27
+ @poller = nil
16
28
  end
17
29
 
18
30
  def init(all_data)
19
- @store.init(FeatureStoreDataSetSorter.sort_all_collections(all_data))
31
+ wrapper { @store.init(FeatureStoreDataSetSorter.sort_all_collections(all_data)) }
20
32
  end
21
33
 
22
34
  def get(kind, key)
23
- @store.get(kind, key)
35
+ wrapper { @store.get(kind, key) }
24
36
  end
25
37
 
26
38
  def all(kind)
27
- @store.all(kind)
39
+ wrapper { @store.all(kind) }
28
40
  end
29
41
 
30
42
  def upsert(kind, item)
31
- @store.upsert(kind, item)
43
+ wrapper { @store.upsert(kind, item) }
32
44
  end
33
45
 
34
46
  def delete(kind, key, version)
35
- @store.delete(kind, key, version)
47
+ wrapper { @store.delete(kind, key, version) }
36
48
  end
37
49
 
38
50
  def initialized?
@@ -41,6 +53,88 @@ module LaunchDarkly
41
53
 
42
54
  def stop
43
55
  @store.stop
56
+ @mutex.synchronize do
57
+ return if @poller.nil?
58
+
59
+ @poller.stop
60
+ @poller = nil
61
+ end
62
+ end
63
+
64
+ def monitoring_enabled?
65
+ @monitoring_enabled
66
+ end
67
+
68
+ private def wrapper()
69
+ begin
70
+ yield
71
+ rescue => e
72
+ update_availability(false) if @monitoring_enabled
73
+ raise
74
+ end
75
+ end
76
+
77
+ private def update_availability(available)
78
+ @mutex.synchronize do
79
+ return if available == @last_available
80
+ @last_available = available
81
+ end
82
+
83
+ status = LaunchDarkly::Interfaces::DataStore::Status.new(available, false)
84
+
85
+ @logger.warn("Persistent store is available again") if available
86
+
87
+ @store_update_sink.update_status(status)
88
+
89
+ if available
90
+ @mutex.synchronize do
91
+ return if @poller.nil?
92
+
93
+ @poller.stop
94
+ @poller = nil
95
+ end
96
+
97
+ return
98
+ end
99
+
100
+ @logger.warn("Detected persistent store unavailability; updates will be cached until it recovers.")
101
+
102
+ task = Impl::RepeatingTask.new(0.5, 0, -> { self.check_availability }, @logger)
103
+
104
+ @mutex.synchronize do
105
+ @poller = task
106
+ @poller.start
107
+ end
108
+ end
109
+
110
+ private def check_availability
111
+ begin
112
+ update_availability(true) if @store.available?
113
+ rescue => e
114
+ @logger.error("Unexpected error from data store status function: #{e}")
115
+ end
116
+ end
117
+
118
+ # This methods determines whether the wrapped store can support enabling monitoring.
119
+ #
120
+ # The wrapped store must provide a monitoring_enabled method, which must
121
+ # be true. But this alone is not sufficient.
122
+ #
123
+ # Because this class wraps all interactions with a provided store, it can
124
+ # technically "monitor" any store. However, monitoring also requires that
125
+ # we notify listeners when the store is available again.
126
+ #
127
+ # We determine this by checking the store's `available?` method, so this
128
+ # is also a requirement for monitoring support.
129
+ #
130
+ # These extra checks won't be necessary once `available` becomes a part
131
+ # of the core interface requirements and this class no longer wraps every
132
+ # feature store.
133
+ private def does_store_support_monitoring?
134
+ return false unless @store.respond_to? :monitoring_enabled?
135
+ return false unless @store.respond_to? :available?
136
+
137
+ @store.monitoring_enabled?
44
138
  end
45
139
  end
46
140
  end
@@ -33,7 +33,7 @@ module LaunchDarkly
33
33
  return input if dependency_fn.nil? || input.empty?
34
34
  remaining_items = input.clone
35
35
  items_out = {}
36
- while !remaining_items.empty?
36
+ until remaining_items.empty?
37
37
  # pick a random item that hasn't been updated yet
38
38
  key, item = remaining_items.first
39
39
  self.add_with_dependencies_first(item, dependency_fn, remaining_items, items_out)
@@ -46,7 +46,7 @@ module LaunchDarkly
46
46
  remaining_items.delete(item_key) # we won't need to visit this item again
47
47
  dependency_fn.call(item).each do |dep_key|
48
48
  dep_item = remaining_items[dep_key.to_sym]
49
- self.add_with_dependencies_first(dep_item, dependency_fn, remaining_items, items_out) if !dep_item.nil?
49
+ self.add_with_dependencies_first(dep_item, dependency_fn, remaining_items, items_out) unless dep_item.nil?
50
50
  end
51
51
  items_out[item_key] = item
52
52
  end
@@ -25,7 +25,7 @@ module LaunchDarkly
25
25
 
26
26
  def dispose_all
27
27
  @lock.synchronize {
28
- @pool.map { |instance| @instance_destructor.call(instance) } if !@instance_destructor.nil?
28
+ @pool.map { |instance| @instance_destructor.call(instance) } unless @instance_destructor.nil?
29
29
  @pool.clear()
30
30
  }
31
31
  end
@@ -1,7 +1,7 @@
1
1
  module LaunchDarkly
2
2
  module Impl
3
3
  module Util
4
- def self.is_bool(aObject)
4
+ def self.bool?(aObject)
5
5
  [true,false].include? aObject
6
6
  end
7
7
 
@@ -15,8 +15,66 @@ module LaunchDarkly
15
15
  ret["X-LaunchDarkly-Wrapper"] = config.wrapper_name +
16
16
  (config.wrapper_version ? "/" + config.wrapper_version : "")
17
17
  end
18
+
19
+ app_value = application_header_value config.application
20
+ ret["X-LaunchDarkly-Tags"] = app_value unless app_value.nil? || app_value.empty?
21
+
18
22
  ret
19
23
  end
24
+
25
+ #
26
+ # Generate an HTTP Header value containing the application meta information (@see #application).
27
+ #
28
+ # @return [String]
29
+ #
30
+ def self.application_header_value(application)
31
+ parts = []
32
+ unless application[:id].empty?
33
+ parts << "application-id/#{application[:id]}"
34
+ end
35
+
36
+ unless application[:version].empty?
37
+ parts << "application-version/#{application[:version]}"
38
+ end
39
+
40
+ parts.join(" ")
41
+ end
42
+
43
+ #
44
+ # @param value [String]
45
+ # @param name [Symbol]
46
+ # @param logger [Logger]
47
+ # @return [String]
48
+ #
49
+ def self.validate_application_value(value, name, logger)
50
+ value = value.to_s
51
+
52
+ return "" if value.empty?
53
+
54
+ if value.length > 64
55
+ logger.warn { "Value of application[#{name}] was longer than 64 characters and was discarded" }
56
+ return ""
57
+ end
58
+
59
+ if /[^a-zA-Z0-9._-]/.match?(value)
60
+ logger.warn { "Value of application[#{name}] contained invalid characters and was discarded" }
61
+ return ""
62
+ end
63
+
64
+ value
65
+ end
66
+
67
+ #
68
+ # @param app [Hash]
69
+ # @param logger [Logger]
70
+ # @return [Hash]
71
+ #
72
+ def self.validate_application_info(app, logger)
73
+ {
74
+ id: validate_application_value(app[:id], :id, logger),
75
+ version: validate_application_value(app[:version], :version, logger),
76
+ }
77
+ end
20
78
  end
21
79
  end
22
80
  end
@@ -14,15 +14,18 @@ module LaunchDarkly
14
14
  FEATURES = {
15
15
  namespace: "features",
16
16
  priority: 1, # that is, features should be stored after segments
17
- get_dependency_keys: lambda { |flag| (flag[:prerequisites] || []).map { |p| p[:key] } }
17
+ get_dependency_keys: lambda { |flag| (flag[:prerequisites] || []).map { |p| p[:key] } },
18
18
  }.freeze
19
19
 
20
20
  # @private
21
21
  SEGMENTS = {
22
22
  namespace: "segments",
23
- priority: 0
23
+ priority: 0,
24
24
  }.freeze
25
25
 
26
+ # @private
27
+ ALL_KINDS = [FEATURES, SEGMENTS].freeze
28
+
26
29
  #
27
30
  # Default implementation of the LaunchDarkly client's feature store, using an in-memory
28
31
  # cache. This object holds feature flags and related data received from LaunchDarkly.
@@ -37,6 +40,10 @@ module LaunchDarkly
37
40
  @initialized = Concurrent::AtomicBoolean.new(false)
38
41
  end
39
42
 
43
+ def monitoring_enabled?
44
+ false
45
+ end
46
+
40
47
  def get(kind, key)
41
48
  @lock.with_read_lock do
42
49
  coll = @items[kind]
@@ -36,9 +36,9 @@ module LaunchDarkly
36
36
  # @option opts [Integer] :capacity (1000) maximum number of items in the cache
37
37
  # @return [LaunchDarkly::Interfaces::FeatureStore] a feature store object
38
38
  #
39
- def self.new_feature_store(opts, &block)
39
+ def self.new_feature_store(opts = {})
40
40
  core = LaunchDarkly::Impl::Integrations::Consul::ConsulFeatureStoreCore.new(opts)
41
- return LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
41
+ LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
42
42
  end
43
43
  end
44
44
  end
@@ -46,7 +46,7 @@ module LaunchDarkly
46
46
  # @option opts [Integer] :capacity (1000) maximum number of items in the cache
47
47
  # @return [LaunchDarkly::Interfaces::FeatureStore] a feature store object
48
48
  #
49
- def self.new_feature_store(table_name, opts)
49
+ def self.new_feature_store(table_name, opts = {})
50
50
  core = LaunchDarkly::Impl::Integrations::DynamoDB::DynamoDBFeatureStoreCore.new(table_name, opts)
51
51
  LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
52
52
  end
@@ -54,7 +54,7 @@ module LaunchDarkly
54
54
  #
55
55
  # Creates a DynamoDB-backed Big Segment store.
56
56
  #
57
- # Big Segments are a specific type of user segments. For more information, read the LaunchDarkly
57
+ # Big Segments are a specific type of segments. For more information, read the LaunchDarkly
58
58
  # documentation: https://docs.launchdarkly.com/home/users/big-segments
59
59
  #
60
60
  # To use this method, you must first install one of the AWS SDK gems: either `aws-sdk-dynamodb`, or
@@ -25,7 +25,7 @@ module LaunchDarkly
25
25
  #
26
26
  # - `flags`: Feature flag definitions.
27
27
  # - `flagValues`: Simplified feature flags that contain only a value.
28
- # - `segments`: User segment definitions.
28
+ # - `segments`: Context segment definitions.
29
29
  #
30
30
  # The format of the data in `flags` and `segments` is defined by the LaunchDarkly application
31
31
  # and is subject to change. Rather than trying to construct these objects yourself, it is simpler
@@ -78,7 +78,7 @@ module LaunchDarkly
78
78
  # same flag key or segment key more than once, either in a single file or across multiple files.
79
79
  #
80
80
  # If the data source encounters any error in any file-- malformed content, a missing file, or a
81
- # duplicate key-- it will not load flags from any of the files.
81
+ # duplicate key-- it will not load flags from any of the files.
82
82
  #
83
83
  module FileData
84
84
  #
@@ -100,8 +100,8 @@ module LaunchDarkly
100
100
  # @return an object that can be stored in {Config#data_source}
101
101
  #
102
102
  def self.data_source(options={})
103
- return lambda { |sdk_key, config|
104
- Impl::Integrations::FileDataSourceImpl.new(config.feature_store, config.logger, options) }
103
+ lambda { |sdk_key, config|
104
+ Impl::Integrations::FileDataSourceImpl.new(config.feature_store, config.data_source_update_sink, config.logger, options) }
105
105
  end
106
106
  end
107
107
  end
@@ -1,4 +1,4 @@
1
- require "ldclient-rb/redis_store" # eventually we will just refer to impl/integrations/redis_impl directly
1
+ require "ldclient-rb/impl/integrations/redis_impl"
2
2
 
3
3
  module LaunchDarkly
4
4
  module Integrations
@@ -58,14 +58,14 @@ module LaunchDarkly
58
58
  # lifecycle to be independent of the SDK client
59
59
  # @return [LaunchDarkly::Interfaces::FeatureStore] a feature store object
60
60
  #
61
- def self.new_feature_store(opts)
62
- return RedisFeatureStore.new(opts)
61
+ def self.new_feature_store(opts = {})
62
+ LaunchDarkly::Impl::Integrations::Redis::RedisFeatureStore.new(opts)
63
63
  end
64
64
 
65
65
  #
66
66
  # Creates a Redis-backed Big Segment store.
67
67
  #
68
- # Big Segments are a specific type of user segments. For more information, read the LaunchDarkly
68
+ # Big Segments are a specific type of segments. For more information, read the LaunchDarkly
69
69
  # documentation: https://docs.launchdarkly.com/home/users/big-segments
70
70
  #
71
71
  # To use this method, you must first have the `redis` and `connection-pool` gems installed. Then,
@@ -91,7 +91,7 @@ module LaunchDarkly
91
91
  # @return [LaunchDarkly::Interfaces::BigSegmentStore] a Big Segment store object
92
92
  #
93
93
  def self.new_big_segment_store(opts)
94
- return LaunchDarkly::Impl::Integrations::Redis::RedisBigSegmentStore.new(opts)
94
+ LaunchDarkly::Impl::Integrations::Redis::RedisBigSegmentStore.new(opts)
95
95
  end
96
96
  end
97
97
  end