launchdarkly-server-sdk 6.3.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -4
  3. data/lib/ldclient-rb/config.rb +112 -62
  4. data/lib/ldclient-rb/context.rb +444 -0
  5. data/lib/ldclient-rb/evaluation_detail.rb +26 -22
  6. data/lib/ldclient-rb/events.rb +256 -146
  7. data/lib/ldclient-rb/flags_state.rb +26 -15
  8. data/lib/ldclient-rb/impl/big_segments.rb +18 -18
  9. data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
  10. data/lib/ldclient-rb/impl/context.rb +96 -0
  11. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  12. data/lib/ldclient-rb/impl/data_source.rb +188 -0
  13. data/lib/ldclient-rb/impl/data_store.rb +59 -0
  14. data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
  15. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  16. data/lib/ldclient-rb/impl/evaluator.rb +386 -142
  17. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  18. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  19. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  20. data/lib/ldclient-rb/impl/event_sender.rb +7 -6
  21. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  22. data/lib/ldclient-rb/impl/event_types.rb +136 -0
  23. data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
  24. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +19 -7
  25. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
  26. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
  27. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
  28. data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
  29. data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
  30. data/lib/ldclient-rb/impl/model/clause.rb +45 -0
  31. data/lib/ldclient-rb/impl/model/feature_flag.rb +255 -0
  32. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  33. data/lib/ldclient-rb/impl/model/segment.rb +132 -0
  34. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  35. data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
  36. data/lib/ldclient-rb/impl/sampler.rb +25 -0
  37. data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
  38. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  39. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  40. data/lib/ldclient-rb/impl/util.rb +59 -1
  41. data/lib/ldclient-rb/in_memory_store.rb +9 -2
  42. data/lib/ldclient-rb/integrations/consul.rb +2 -2
  43. data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
  44. data/lib/ldclient-rb/integrations/file_data.rb +4 -4
  45. data/lib/ldclient-rb/integrations/redis.rb +5 -5
  46. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
  47. data/lib/ldclient-rb/integrations/test_data.rb +18 -14
  48. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
  49. data/lib/ldclient-rb/interfaces.rb +600 -14
  50. data/lib/ldclient-rb/ldclient.rb +314 -134
  51. data/lib/ldclient-rb/memoized_value.rb +1 -1
  52. data/lib/ldclient-rb/migrations.rb +230 -0
  53. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  54. data/lib/ldclient-rb/polling.rb +52 -6
  55. data/lib/ldclient-rb/reference.rb +274 -0
  56. data/lib/ldclient-rb/requestor.rb +9 -11
  57. data/lib/ldclient-rb/stream.rb +96 -34
  58. data/lib/ldclient-rb/util.rb +97 -14
  59. data/lib/ldclient-rb/version.rb +1 -1
  60. data/lib/ldclient-rb.rb +3 -4
  61. metadata +65 -23
  62. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  63. data/lib/ldclient-rb/file_data_source.rb +0 -23
  64. data/lib/ldclient-rb/impl/event_factory.rb +0 -126
  65. data/lib/ldclient-rb/newrelic.rb +0 -17
  66. data/lib/ldclient-rb/redis_store.rb +0 -88
  67. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -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