launchdarkly-server-sdk 7.1.0 → 7.3.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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/lib/ldclient-rb/config.rb +16 -1
- data/lib/ldclient-rb/context.rb +4 -0
- data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
- data/lib/ldclient-rb/impl/context.rb +3 -3
- 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/event_sender.rb +1 -0
- data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +12 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +8 -0
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +16 -3
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +18 -1
- data/lib/ldclient-rb/impl/repeating_task.rb +2 -3
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
- data/lib/ldclient-rb/in_memory_store.rb +7 -0
- data/lib/ldclient-rb/integrations/file_data.rb +1 -1
- data/lib/ldclient-rb/integrations/test_data.rb +3 -3
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +11 -0
- data/lib/ldclient-rb/interfaces.rb +489 -0
- data/lib/ldclient-rb/ldclient.rb +65 -3
- data/lib/ldclient-rb/polling.rb +51 -5
- data/lib/ldclient-rb/stream.rb +88 -28
- data/lib/ldclient-rb/version.rb +1 -1
- metadata +22 -3
@@ -1,3 +1,4 @@
|
|
1
|
+
require "ldclient-rb/interfaces"
|
1
2
|
require "concurrent/atomics"
|
2
3
|
require "json"
|
3
4
|
|
@@ -42,6 +43,14 @@ module LaunchDarkly
|
|
42
43
|
@wrapper = LaunchDarkly::Integrations::Util::CachingStoreWrapper.new(core, opts)
|
43
44
|
end
|
44
45
|
|
46
|
+
def monitoring_enabled?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def available?
|
51
|
+
@wrapper.available?
|
52
|
+
end
|
53
|
+
|
45
54
|
#
|
46
55
|
# Default value for the `redis_url` constructor parameter; points to an instance of Redis
|
47
56
|
# running at `localhost` with its default port.
|
@@ -109,7 +118,7 @@ module LaunchDarkly
|
|
109
118
|
@logger = opts[:logger] || Config.default_logger
|
110
119
|
@test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
|
111
120
|
|
112
|
-
@stopped = Concurrent::AtomicBoolean.new
|
121
|
+
@stopped = Concurrent::AtomicBoolean.new
|
113
122
|
|
114
123
|
with_connection do |redis|
|
115
124
|
@logger.info("#{description}: using Redis instance at #{redis.connection[:host]}:#{redis.connection[:port]} and prefix: #{@prefix}")
|
@@ -154,6 +163,14 @@ module LaunchDarkly
|
|
154
163
|
@test_hook = opts[:test_hook] # used for unit tests, deliberately undocumented
|
155
164
|
end
|
156
165
|
|
166
|
+
def available?
|
167
|
+
# We don't care what the status is, only that we can connect
|
168
|
+
initialized_internal?
|
169
|
+
true
|
170
|
+
rescue
|
171
|
+
false
|
172
|
+
end
|
173
|
+
|
157
174
|
def description
|
158
175
|
"RedisFeatureStore"
|
159
176
|
end
|
@@ -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
|
@@ -23,6 +23,9 @@ module LaunchDarkly
|
|
23
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]
|
@@ -101,7 +101,7 @@ module LaunchDarkly
|
|
101
101
|
#
|
102
102
|
def self.data_source(options={})
|
103
103
|
lambda { |sdk_key, config|
|
104
|
-
Impl::Integrations::FileDataSourceImpl.new(config.feature_store, config.logger, options) }
|
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
|
@@ -90,7 +90,7 @@ module LaunchDarkly
|
|
90
90
|
#
|
91
91
|
def flag(key)
|
92
92
|
existing_builder = @lock.with_read_lock { @flag_builders[key] }
|
93
|
-
if existing_builder.nil?
|
93
|
+
if existing_builder.nil?
|
94
94
|
FlagBuilder.new(key).boolean_flag
|
95
95
|
else
|
96
96
|
existing_builder.clone
|
@@ -118,7 +118,7 @@ module LaunchDarkly
|
|
118
118
|
@flag_builders[flag_builder.key] = flag_builder
|
119
119
|
version = 0
|
120
120
|
flag_key = flag_builder.key.to_sym
|
121
|
-
if @current_flags[flag_key]
|
121
|
+
if @current_flags[flag_key]
|
122
122
|
version = @current_flags[flag_key][:version]
|
123
123
|
end
|
124
124
|
new_flag = Impl::Model.deserialize(FEATURES, flag_builder.build(version+1))
|
@@ -175,7 +175,7 @@ module LaunchDarkly
|
|
175
175
|
key = item.key.to_sym
|
176
176
|
@lock.with_write_lock do
|
177
177
|
old_item = current[key]
|
178
|
-
unless old_item.nil?
|
178
|
+
unless old_item.nil?
|
179
179
|
data = item.as_json
|
180
180
|
data[:version] = old_item.version + 1
|
181
181
|
item = Impl::Model.deserialize(kind, data)
|
@@ -43,6 +43,17 @@ module LaunchDarkly
|
|
43
43
|
end
|
44
44
|
|
45
45
|
@inited = Concurrent::AtomicBoolean.new(false)
|
46
|
+
@has_available_method = @core.respond_to? :available?
|
47
|
+
end
|
48
|
+
|
49
|
+
def monitoring_enabled?
|
50
|
+
@has_available_method
|
51
|
+
end
|
52
|
+
|
53
|
+
def available?
|
54
|
+
return false unless @has_available_method
|
55
|
+
|
56
|
+
@core.available?
|
46
57
|
end
|
47
58
|
|
48
59
|
def init(all_data)
|