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.
- checksums.yaml +4 -4
- data/README.md +3 -4
- data/lib/ldclient-rb/config.rb +112 -62
- data/lib/ldclient-rb/context.rb +444 -0
- data/lib/ldclient-rb/evaluation_detail.rb +26 -22
- data/lib/ldclient-rb/events.rb +256 -146
- data/lib/ldclient-rb/flags_state.rb +26 -15
- data/lib/ldclient-rb/impl/big_segments.rb +18 -18
- data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
- data/lib/ldclient-rb/impl/context.rb +96 -0
- data/lib/ldclient-rb/impl/context_filter.rb +145 -0
- data/lib/ldclient-rb/impl/data_source.rb +188 -0
- data/lib/ldclient-rb/impl/data_store.rb +59 -0
- data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
- data/lib/ldclient-rb/impl/evaluator.rb +386 -142
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
- data/lib/ldclient-rb/impl/event_sender.rb +7 -6
- data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
- data/lib/ldclient-rb/impl/event_types.rb +136 -0
- data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +19 -7
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
- data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
- data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
- data/lib/ldclient-rb/impl/model/clause.rb +45 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +255 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
- data/lib/ldclient-rb/impl/model/segment.rb +132 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
- data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
- data/lib/ldclient-rb/impl/sampler.rb +25 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
- data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
- data/lib/ldclient-rb/impl/util.rb +59 -1
- data/lib/ldclient-rb/in_memory_store.rb +9 -2
- data/lib/ldclient-rb/integrations/consul.rb +2 -2
- data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
- data/lib/ldclient-rb/integrations/file_data.rb +4 -4
- data/lib/ldclient-rb/integrations/redis.rb +5 -5
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
- data/lib/ldclient-rb/integrations/test_data.rb +18 -14
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
- data/lib/ldclient-rb/interfaces.rb +600 -14
- data/lib/ldclient-rb/ldclient.rb +314 -134
- data/lib/ldclient-rb/memoized_value.rb +1 -1
- data/lib/ldclient-rb/migrations.rb +230 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
- data/lib/ldclient-rb/polling.rb +52 -6
- data/lib/ldclient-rb/reference.rb +274 -0
- data/lib/ldclient-rb/requestor.rb +9 -11
- data/lib/ldclient-rb/stream.rb +96 -34
- data/lib/ldclient-rb/util.rb +97 -14
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +3 -4
- metadata +65 -23
- data/lib/ldclient-rb/event_summarizer.rb +0 -55
- data/lib/ldclient-rb/file_data_source.rb +0 -23
- data/lib/ldclient-rb/impl/event_factory.rb +0 -126
- data/lib/ldclient-rb/newrelic.rb +0 -17
- data/lib/ldclient-rb/redis_store.rb +0 -88
- 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
|
-
#
|
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
|
-
|
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)
|
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) }
|
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.
|
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
|
39
|
+
def self.new_feature_store(opts = {})
|
40
40
|
core = LaunchDarkly::Impl::Integrations::Consul::ConsulFeatureStoreCore.new(opts)
|
41
|
-
|
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
|
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`:
|
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
|
-
|
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/
|
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
|
-
|
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
|
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
|
-
|
94
|
+
LaunchDarkly::Impl::Integrations::Redis::RedisBigSegmentStore.new(opts)
|
95
95
|
end
|
96
96
|
end
|
97
97
|
end
|