launchdarkly-server-sdk 6.3.4 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ldclient-rb/config.rb +121 -55
  3. data/lib/ldclient-rb/context.rb +487 -0
  4. data/lib/ldclient-rb/evaluation_detail.rb +20 -20
  5. data/lib/ldclient-rb/events.rb +77 -132
  6. data/lib/ldclient-rb/flags_state.rb +4 -4
  7. data/lib/ldclient-rb/impl/big_segments.rb +17 -17
  8. data/lib/ldclient-rb/impl/context.rb +96 -0
  9. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  10. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  11. data/lib/ldclient-rb/impl/evaluator.rb +378 -139
  12. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  13. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  14. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  15. data/lib/ldclient-rb/impl/event_sender.rb +6 -6
  16. data/lib/ldclient-rb/impl/event_summarizer.rb +12 -7
  17. data/lib/ldclient-rb/impl/event_types.rb +18 -30
  18. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +7 -7
  19. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +29 -29
  20. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
  21. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +92 -12
  22. data/lib/ldclient-rb/impl/model/clause.rb +39 -0
  23. data/lib/ldclient-rb/impl/model/feature_flag.rb +213 -0
  24. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  25. data/lib/ldclient-rb/impl/model/segment.rb +126 -0
  26. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  27. data/lib/ldclient-rb/impl/repeating_task.rb +1 -1
  28. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  29. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  30. data/lib/ldclient-rb/impl/util.rb +59 -1
  31. data/lib/ldclient-rb/in_memory_store.rb +2 -2
  32. data/lib/ldclient-rb/integrations/consul.rb +1 -1
  33. data/lib/ldclient-rb/integrations/dynamodb.rb +1 -1
  34. data/lib/ldclient-rb/integrations/file_data.rb +3 -3
  35. data/lib/ldclient-rb/integrations/redis.rb +4 -4
  36. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +218 -62
  37. data/lib/ldclient-rb/integrations/test_data.rb +16 -12
  38. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +9 -9
  39. data/lib/ldclient-rb/interfaces.rb +14 -14
  40. data/lib/ldclient-rb/ldclient.rb +94 -144
  41. data/lib/ldclient-rb/memoized_value.rb +1 -1
  42. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  43. data/lib/ldclient-rb/polling.rb +2 -2
  44. data/lib/ldclient-rb/reference.rb +274 -0
  45. data/lib/ldclient-rb/requestor.rb +7 -7
  46. data/lib/ldclient-rb/stream.rb +8 -9
  47. data/lib/ldclient-rb/util.rb +4 -19
  48. data/lib/ldclient-rb/version.rb +1 -1
  49. data/lib/ldclient-rb.rb +2 -3
  50. metadata +36 -17
  51. data/lib/ldclient-rb/file_data_source.rb +0 -23
  52. data/lib/ldclient-rb/newrelic.rb +0 -17
  53. data/lib/ldclient-rb/redis_store.rb +0 -88
  54. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -0,0 +1,126 @@
1
+ require "ldclient-rb/impl/model/clause"
2
+ require "ldclient-rb/impl/model/preprocessed_data"
3
+ require "set"
4
+
5
+ # See serialization.rb for implementation notes on the data model classes.
6
+
7
+ module LaunchDarkly
8
+ module Impl
9
+ module Model
10
+ class Segment
11
+ # @param data [Hash]
12
+ # @param logger [Logger|nil]
13
+ def initialize(data, logger = nil)
14
+ raise ArgumentError, "expected hash but got #{data.class}" unless data.is_a?(Hash)
15
+ @data = data
16
+ @key = data[:key]
17
+ @version = data[:version]
18
+ @deleted = !!data[:deleted]
19
+ return if @deleted
20
+ @included = data[:included] || []
21
+ @excluded = data[:excluded] || []
22
+ @included_contexts = (data[:includedContexts] || []).map do |target_data|
23
+ SegmentTarget.new(target_data)
24
+ end
25
+ @excluded_contexts = (data[:excludedContexts] || []).map do |target_data|
26
+ SegmentTarget.new(target_data)
27
+ end
28
+ @rules = (data[:rules] || []).map do |rule_data|
29
+ SegmentRule.new(rule_data, logger)
30
+ end
31
+ @unbounded = !!data[:unbounded]
32
+ @unbounded_context_kind = data[:unboundedContextKind] || LDContext::KIND_DEFAULT
33
+ @generation = data[:generation]
34
+ @salt = data[:salt]
35
+ end
36
+
37
+ # @return [Hash]
38
+ attr_reader :data
39
+ # @return [String]
40
+ attr_reader :key
41
+ # @return [Integer]
42
+ attr_reader :version
43
+ # @return [Boolean]
44
+ attr_reader :deleted
45
+ # @return [Array<String>]
46
+ attr_reader :included
47
+ # @return [Array<String>]
48
+ attr_reader :excluded
49
+ # @return [Array<LaunchDarkly::Impl::Model::SegmentTarget>]
50
+ attr_reader :included_contexts
51
+ # @return [Array<LaunchDarkly::Impl::Model::SegmentTarget>]
52
+ attr_reader :excluded_contexts
53
+ # @return [Array<SegmentRule>]
54
+ attr_reader :rules
55
+ # @return [Boolean]
56
+ attr_reader :unbounded
57
+ # @return [String]
58
+ attr_reader :unbounded_context_kind
59
+ # @return [Integer|nil]
60
+ attr_reader :generation
61
+ # @return [String]
62
+ attr_reader :salt
63
+
64
+ # This method allows us to read properties of the object as if it's just a hash. Currently this is
65
+ # necessary because some data store logic is still written to expect hashes; we can remove it once
66
+ # we migrate entirely to using attributes of the class.
67
+ def [](key)
68
+ @data[key]
69
+ end
70
+
71
+ def ==(other)
72
+ other.is_a?(Segment) && other.data == self.data
73
+ end
74
+
75
+ def as_json(*) # parameter is unused, but may be passed if we're using the json gem
76
+ @data
77
+ end
78
+
79
+ # Same as as_json, but converts the JSON structure into a string.
80
+ def to_json(*a)
81
+ as_json.to_json(a)
82
+ end
83
+ end
84
+
85
+ class SegmentTarget
86
+ def initialize(data)
87
+ @data = data
88
+ @context_kind = data[:contextKind]
89
+ @values = Set.new(data[:values] || [])
90
+ end
91
+
92
+ # @return [Hash]
93
+ attr_reader :data
94
+ # @return [String]
95
+ attr_reader :context_kind
96
+ # @return [Set]
97
+ attr_reader :values
98
+ end
99
+
100
+ class SegmentRule
101
+ def initialize(data, logger)
102
+ @data = data
103
+ @clauses = (data[:clauses] || []).map do |clause_data|
104
+ Clause.new(clause_data, logger)
105
+ end
106
+ @weight = data[:weight]
107
+ @bucket_by = data[:bucketBy]
108
+ @rollout_context_kind = data[:rolloutContextKind]
109
+ end
110
+
111
+ # @return [Hash]
112
+ attr_reader :data
113
+ # @return [Array<LaunchDarkly::Impl::Model::Clause>]
114
+ attr_reader :clauses
115
+ # @return [Integer|nil]
116
+ attr_reader :weight
117
+ # @return [String|nil]
118
+ attr_reader :bucket_by
119
+ # @return [String|nil]
120
+ attr_reader :rollout_context_kind
121
+ end
122
+
123
+ # Clause is defined in its own file because clauses are used by both flags and segments
124
+ end
125
+ end
126
+ end
@@ -1,61 +1,71 @@
1
+ require "ldclient-rb/impl/model/feature_flag"
2
+ require "ldclient-rb/impl/model/preprocessed_data"
3
+ require "ldclient-rb/impl/model/segment"
4
+
5
+ # General implementation notes about the data model classes in LaunchDarkly::Impl::Model--
6
+ #
7
+ # As soon as we receive flag/segment JSON data from LaunchDarkly (or, read it from a database), we
8
+ # transform it into the model classes FeatureFlag, Segment, etc. The constructor of each of these
9
+ # classes takes a hash (the parsed JSON), and transforms it into an internal representation that
10
+ # is more efficient for evaluations.
11
+ #
12
+ # Validation works as follows:
13
+ # - A property value that is of the correct type, but is invalid for other reasons (for example,
14
+ # if a flag rule refers to variation index 5, but there are only 2 variations in the flag), does
15
+ # not prevent the flag from being parsed and stored. It does cause a warning to be logged, if a
16
+ # logger was passed to the constructor.
17
+ # - If a value is completely invalid for the schema, the constructor may throw an
18
+ # exception, causing the whole data set to be rejected. This is consistent with the behavior of
19
+ # the strongly-typed SDKs.
20
+ #
21
+ # Currently, the model classes also retain the original hash of the parsed JSON. This is because
22
+ # we may need to re-serialize them to JSON, and building the JSON on the fly would be very
23
+ # inefficient, so each model class has a to_json method that just returns the same Hash. If we
24
+ # are able in the future to either use a custom streaming serializer, or pass the JSON data
25
+ # straight through from LaunchDarkly to a database instead of re-serializing, we could stop
26
+ # retaining this data.
1
27
 
2
28
  module LaunchDarkly
3
29
  module Impl
4
30
  module Model
5
31
  # Abstraction of deserializing a feature flag or segment that was read from a data store or
6
32
  # received from LaunchDarkly.
7
- def self.deserialize(kind, json)
8
- return nil if json.nil?
9
- item = JSON.parse(json, symbolize_names: true)
10
- postprocess_item_after_deserializing!(kind, item)
11
- item
33
+ #
34
+ # SDK code outside of Impl::Model should use this method instead of calling the model class
35
+ # constructors directly, so as not to rely on implementation details.
36
+ #
37
+ # @param kind [Hash] normally either FEATURES or SEGMENTS
38
+ # @param input [object] a JSON string or a parsed hash (or a data model object, in which case
39
+ # we'll just return the original object)
40
+ # @param logger [Logger|nil] logs warnings if there are any data validation problems
41
+ # @return [Object] the flag or segment (or, for an unknown data kind, the data as a hash)
42
+ def self.deserialize(kind, input, logger = nil)
43
+ return nil if input.nil?
44
+ return input if !input.is_a?(String) && !input.is_a?(Hash)
45
+ data = input.is_a?(Hash) ? input : JSON.parse(input, symbolize_names: true)
46
+ case kind
47
+ when FEATURES
48
+ FeatureFlag.new(data, logger)
49
+ when SEGMENTS
50
+ Segment.new(data, logger)
51
+ else
52
+ data
53
+ end
12
54
  end
13
55
 
14
56
  # Abstraction of serializing a feature flag or segment that will be written to a data store.
15
- # Currently we just call to_json.
57
+ # Currently we just call to_json, but SDK code outside of Impl::Model should use this method
58
+ # instead of to_json, so as not to rely on implementation details.
16
59
  def self.serialize(kind, item)
17
60
  item.to_json
18
61
  end
19
62
 
20
63
  # Translates a { flags: ..., segments: ... } object received from LaunchDarkly to the data store format.
21
- def self.make_all_store_data(received_data)
22
- flags = received_data[:flags]
23
- postprocess_items_after_deserializing!(FEATURES, flags)
24
- segments = received_data[:segments]
25
- postprocess_items_after_deserializing!(SEGMENTS, segments)
26
- { FEATURES => flags, SEGMENTS => segments }
27
- end
28
-
29
- # Called after we have deserialized a model item from JSON (because we received it from LaunchDarkly,
30
- # or read it from a persistent data store). This allows us to precompute some derived attributes that
31
- # will never change during the lifetime of that item.
32
- def self.postprocess_item_after_deserializing!(kind, item)
33
- return if !item
34
- # Currently we are special-casing this for FEATURES; eventually it will be handled by delegating
35
- # to the "kind" object or the item class.
36
- if kind.eql? FEATURES
37
- # For feature flags, we precompute all possible parameterized EvaluationReason instances.
38
- prereqs = item[:prerequisites]
39
- if !prereqs.nil?
40
- prereqs.each do |prereq|
41
- prereq[:_reason] = EvaluationReason::prerequisite_failed(prereq[:key])
42
- end
43
- end
44
- rules = item[:rules]
45
- if !rules.nil?
46
- rules.each_index do |i|
47
- rule = rules[i]
48
- rule[:_reason] = EvaluationReason::rule_match(i, rule[:id])
49
- end
50
- end
51
- end
52
- end
53
-
54
- def self.postprocess_items_after_deserializing!(kind, items_map)
55
- return items_map if !items_map
56
- items_map.each do |key, item|
57
- postprocess_item_after_deserializing!(kind, item)
58
- end
64
+ def self.make_all_store_data(received_data, logger = nil)
65
+ {
66
+ FEATURES => (received_data[:flags] || {}).transform_values { |data| FeatureFlag.new(data, logger) },
67
+ SEGMENTS => (received_data[:segments] || {}).transform_values { |data| Segment.new(data, logger) },
68
+ }
59
69
  end
60
70
  end
61
71
  end
@@ -19,7 +19,7 @@ module LaunchDarkly
19
19
  if @start_delay
20
20
  sleep(@start_delay)
21
21
  end
22
- while !@stopped.value do
22
+ until @stopped.value do
23
23
  started_at = Time.now
24
24
  begin
25
25
  @task.call
@@ -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,13 +14,13 @@ 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
26
  #
@@ -38,7 +38,7 @@ module LaunchDarkly
38
38
  #
39
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
@@ -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,7 +100,7 @@ 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|
103
+ lambda { |sdk_key, config|
104
104
  Impl::Integrations::FileDataSourceImpl.new(config.feature_store, config.logger, options) }
105
105
  end
106
106
  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
@@ -59,13 +59,13 @@ module LaunchDarkly
59
59
  # @return [LaunchDarkly::Interfaces::FeatureStore] a feature store object
60
60
  #
61
61
  def self.new_feature_store(opts = {})
62
- return RedisFeatureStore.new(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