launchdarkly-server-sdk 8.11.2 → 8.11.3
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/lib/ldclient-rb/config.rb +66 -3
- data/lib/ldclient-rb/context.rb +1 -1
- data/lib/ldclient-rb/data_system.rb +243 -0
- data/lib/ldclient-rb/events.rb +34 -19
- data/lib/ldclient-rb/flags_state.rb +1 -1
- data/lib/ldclient-rb/impl/big_segments.rb +4 -4
- data/lib/ldclient-rb/impl/cache_store.rb +44 -0
- data/lib/ldclient-rb/impl/data_source/polling.rb +108 -0
- data/lib/ldclient-rb/impl/data_source/requestor.rb +106 -0
- data/lib/ldclient-rb/impl/data_source/status_provider.rb +78 -0
- data/lib/ldclient-rb/impl/data_source/stream.rb +198 -0
- data/lib/ldclient-rb/impl/data_source.rb +3 -3
- data/lib/ldclient-rb/impl/data_store/data_kind.rb +108 -0
- data/lib/ldclient-rb/impl/data_store/feature_store_client_wrapper.rb +187 -0
- data/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb +130 -0
- data/lib/ldclient-rb/impl/data_store/status_provider.rb +82 -0
- data/lib/ldclient-rb/impl/data_store/store.rb +371 -0
- data/lib/ldclient-rb/impl/data_store.rb +11 -97
- data/lib/ldclient-rb/impl/data_system/fdv1.rb +20 -7
- data/lib/ldclient-rb/impl/data_system/fdv2.rb +471 -0
- data/lib/ldclient-rb/impl/data_system/polling.rb +601 -0
- data/lib/ldclient-rb/impl/data_system/protocolv2.rb +264 -0
- data/lib/ldclient-rb/impl/dependency_tracker.rb +21 -9
- data/lib/ldclient-rb/impl/evaluator.rb +3 -2
- data/lib/ldclient-rb/impl/event_sender.rb +4 -3
- data/lib/ldclient-rb/impl/expiring_cache.rb +79 -0
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +288 -0
- data/lib/ldclient-rb/impl/memoized_value.rb +34 -0
- data/lib/ldclient-rb/impl/migrations/migrator.rb +2 -1
- data/lib/ldclient-rb/impl/migrations/tracker.rb +2 -1
- data/lib/ldclient-rb/impl/model/serialization.rb +6 -6
- data/lib/ldclient-rb/impl/non_blocking_thread_pool.rb +48 -0
- data/lib/ldclient-rb/impl/repeating_task.rb +2 -2
- data/lib/ldclient-rb/impl/simple_lru_cache.rb +27 -0
- data/lib/ldclient-rb/impl/util.rb +65 -0
- data/lib/ldclient-rb/impl.rb +1 -2
- data/lib/ldclient-rb/in_memory_store.rb +1 -18
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +9 -9
- data/lib/ldclient-rb/integrations/test_data.rb +11 -11
- data/lib/ldclient-rb/integrations/test_data_v2/flag_builder_v2.rb +582 -0
- data/lib/ldclient-rb/integrations/test_data_v2.rb +248 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +3 -2
- data/lib/ldclient-rb/interfaces/data_system.rb +755 -0
- data/lib/ldclient-rb/interfaces/feature_store.rb +3 -0
- data/lib/ldclient-rb/ldclient.rb +55 -131
- data/lib/ldclient-rb/util.rb +11 -70
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +8 -17
- metadata +35 -17
- data/lib/ldclient-rb/cache_store.rb +0 -45
- data/lib/ldclient-rb/expiring_cache.rb +0 -77
- data/lib/ldclient-rb/memoized_value.rb +0 -32
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +0 -46
- data/lib/ldclient-rb/polling.rb +0 -102
- data/lib/ldclient-rb/requestor.rb +0 -102
- data/lib/ldclient-rb/simple_lru_cache.rb +0 -25
- data/lib/ldclient-rb/stream.rb +0 -197
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
require 'concurrent/atomics'
|
|
2
|
+
require 'ldclient-rb/impl/data_system'
|
|
3
|
+
require 'ldclient-rb/interfaces/data_system'
|
|
4
|
+
require 'ldclient-rb/util'
|
|
5
|
+
require 'thread'
|
|
6
|
+
|
|
7
|
+
module LaunchDarkly
|
|
8
|
+
module Impl
|
|
9
|
+
module Integrations
|
|
10
|
+
module TestData
|
|
11
|
+
#
|
|
12
|
+
# Internal implementation of both Initializer and Synchronizer protocols for TestDataV2.
|
|
13
|
+
#
|
|
14
|
+
# This component bridges the test data management in TestDataV2 with the FDv2 protocol
|
|
15
|
+
# interfaces. Each instance implements both Initializer and Synchronizer protocols
|
|
16
|
+
# and receives change notifications for dynamic updates.
|
|
17
|
+
#
|
|
18
|
+
class TestDataSourceV2
|
|
19
|
+
include LaunchDarkly::Interfaces::DataSystem::Initializer
|
|
20
|
+
include LaunchDarkly::Interfaces::DataSystem::Synchronizer
|
|
21
|
+
|
|
22
|
+
# @api private
|
|
23
|
+
#
|
|
24
|
+
# @param test_data [LaunchDarkly::Integrations::TestDataV2] the test data instance
|
|
25
|
+
#
|
|
26
|
+
def initialize(test_data)
|
|
27
|
+
@test_data = test_data
|
|
28
|
+
@closed = false
|
|
29
|
+
@update_queue = Queue.new
|
|
30
|
+
@lock = Mutex.new
|
|
31
|
+
|
|
32
|
+
# Always register for change notifications
|
|
33
|
+
@test_data.add_instance(self)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#
|
|
37
|
+
# Return the name of this data source.
|
|
38
|
+
#
|
|
39
|
+
# @return [String]
|
|
40
|
+
#
|
|
41
|
+
def name
|
|
42
|
+
'TestDataV2'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# Implementation of the Initializer.fetch method.
|
|
47
|
+
#
|
|
48
|
+
# Returns the current test data as a Basis for initial data loading.
|
|
49
|
+
#
|
|
50
|
+
# @param selector_store [LaunchDarkly::Interfaces::DataSystem::SelectorStore] Provides the Selector (unused for test data)
|
|
51
|
+
# @return [LaunchDarkly::Result] A Result containing either a Basis or an error message
|
|
52
|
+
#
|
|
53
|
+
def fetch(selector_store)
|
|
54
|
+
begin
|
|
55
|
+
@lock.synchronize do
|
|
56
|
+
if @closed
|
|
57
|
+
return LaunchDarkly::Result.fail('TestDataV2 source has been closed')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get all current flags and segments from test data
|
|
61
|
+
init_data = @test_data.make_init_data
|
|
62
|
+
version = @test_data.get_version
|
|
63
|
+
|
|
64
|
+
# Build a full transfer changeset
|
|
65
|
+
builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
|
|
66
|
+
builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL)
|
|
67
|
+
|
|
68
|
+
# Add all flags to the changeset
|
|
69
|
+
init_data[:flags].each do |key, flag_data|
|
|
70
|
+
builder.add_put(
|
|
71
|
+
LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG,
|
|
72
|
+
key,
|
|
73
|
+
flag_data[:version] || 1,
|
|
74
|
+
flag_data
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Add all segments to the changeset
|
|
79
|
+
init_data[:segments].each do |key, segment_data|
|
|
80
|
+
builder.add_put(
|
|
81
|
+
LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT,
|
|
82
|
+
key,
|
|
83
|
+
segment_data[:version] || 1,
|
|
84
|
+
segment_data
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Create selector for this version
|
|
89
|
+
selector = LaunchDarkly::Interfaces::DataSystem::Selector.new_selector(version.to_s, version)
|
|
90
|
+
change_set = builder.finish(selector)
|
|
91
|
+
|
|
92
|
+
basis = LaunchDarkly::Interfaces::DataSystem::Basis.new(change_set: change_set, persist: false, environment_id: nil)
|
|
93
|
+
|
|
94
|
+
LaunchDarkly::Result.success(basis)
|
|
95
|
+
end
|
|
96
|
+
rescue => e
|
|
97
|
+
LaunchDarkly::Result.fail("Error fetching test data: #{e.message}", e)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
#
|
|
102
|
+
# Implementation of the Synchronizer.sync method.
|
|
103
|
+
#
|
|
104
|
+
# Yields updates as test data changes occur.
|
|
105
|
+
#
|
|
106
|
+
# @param selector_store [LaunchDarkly::Interfaces::DataSystem::SelectorStore] Provides the Selector (unused for test data)
|
|
107
|
+
# @yield [LaunchDarkly::Interfaces::DataSystem::Update] Yields Update objects as synchronization progresses
|
|
108
|
+
# @return [void]
|
|
109
|
+
#
|
|
110
|
+
def sync(selector_store)
|
|
111
|
+
# First yield initial data
|
|
112
|
+
initial_result = fetch(selector_store)
|
|
113
|
+
unless initial_result.success?
|
|
114
|
+
yield LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
115
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
|
|
116
|
+
error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
117
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::STORE_ERROR,
|
|
118
|
+
0,
|
|
119
|
+
initial_result.error,
|
|
120
|
+
Time.now
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Yield the initial successful state
|
|
127
|
+
yield LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
128
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
|
|
129
|
+
change_set: initial_result.value.change_set
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Continue yielding updates as they arrive
|
|
133
|
+
until @closed
|
|
134
|
+
begin
|
|
135
|
+
# stop() will push nil to the queue to wake us up when shutting down
|
|
136
|
+
update = @update_queue.pop
|
|
137
|
+
|
|
138
|
+
# Handle nil sentinel for shutdown
|
|
139
|
+
break if update.nil?
|
|
140
|
+
|
|
141
|
+
# Yield the actual update
|
|
142
|
+
yield update
|
|
143
|
+
rescue => e
|
|
144
|
+
yield LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
145
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
|
|
146
|
+
error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
147
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::UNKNOWN,
|
|
148
|
+
0,
|
|
149
|
+
"Error in test data synchronizer: #{e.message}",
|
|
150
|
+
Time.now
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
break
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
#
|
|
159
|
+
# Stop the data source and clean up resources
|
|
160
|
+
#
|
|
161
|
+
# @return [void]
|
|
162
|
+
#
|
|
163
|
+
def stop
|
|
164
|
+
@lock.synchronize do
|
|
165
|
+
return if @closed
|
|
166
|
+
@closed = true
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
@test_data.closed_instance(self)
|
|
170
|
+
# Signal shutdown to sync generator
|
|
171
|
+
@update_queue.push(nil)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
#
|
|
175
|
+
# Called by TestDataV2 when a flag is updated.
|
|
176
|
+
#
|
|
177
|
+
# This method converts the flag update into an FDv2 changeset and
|
|
178
|
+
# queues it for delivery through the sync() generator.
|
|
179
|
+
#
|
|
180
|
+
# @param flag_data [Hash] the flag data
|
|
181
|
+
# @return [void]
|
|
182
|
+
#
|
|
183
|
+
def upsert_flag(flag_data)
|
|
184
|
+
@lock.synchronize do
|
|
185
|
+
return if @closed
|
|
186
|
+
|
|
187
|
+
begin
|
|
188
|
+
version = @test_data.get_version
|
|
189
|
+
|
|
190
|
+
# Build a changes transfer changeset
|
|
191
|
+
builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
|
|
192
|
+
builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_CHANGES)
|
|
193
|
+
|
|
194
|
+
# Add the updated flag
|
|
195
|
+
builder.add_put(
|
|
196
|
+
LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG,
|
|
197
|
+
flag_data[:key],
|
|
198
|
+
flag_data[:version] || 1,
|
|
199
|
+
flag_data
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Create selector for this version
|
|
203
|
+
selector = LaunchDarkly::Interfaces::DataSystem::Selector.new_selector(version.to_s, version)
|
|
204
|
+
change_set = builder.finish(selector)
|
|
205
|
+
|
|
206
|
+
# Queue the update
|
|
207
|
+
update = LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
208
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
|
|
209
|
+
change_set: change_set
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
@update_queue.push(update)
|
|
213
|
+
rescue => e
|
|
214
|
+
# Queue an error update
|
|
215
|
+
error_update = LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
216
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
|
|
217
|
+
error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
218
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::STORE_ERROR,
|
|
219
|
+
0,
|
|
220
|
+
"Error processing flag update: #{e.message}",
|
|
221
|
+
Time.now
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
@update_queue.push(error_update)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
#
|
|
230
|
+
# Called by TestDataV2 when a segment is updated.
|
|
231
|
+
#
|
|
232
|
+
# This method converts the segment update into an FDv2 changeset and
|
|
233
|
+
# queues it for delivery through the sync() generator.
|
|
234
|
+
#
|
|
235
|
+
# @param segment_data [Hash] the segment data
|
|
236
|
+
# @return [void]
|
|
237
|
+
#
|
|
238
|
+
def upsert_segment(segment_data)
|
|
239
|
+
@lock.synchronize do
|
|
240
|
+
return if @closed
|
|
241
|
+
|
|
242
|
+
begin
|
|
243
|
+
version = @test_data.get_version
|
|
244
|
+
|
|
245
|
+
# Build a changes transfer changeset
|
|
246
|
+
builder = LaunchDarkly::Interfaces::DataSystem::ChangeSetBuilder.new
|
|
247
|
+
builder.start(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_CHANGES)
|
|
248
|
+
|
|
249
|
+
# Add the updated segment
|
|
250
|
+
builder.add_put(
|
|
251
|
+
LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT,
|
|
252
|
+
segment_data[:key],
|
|
253
|
+
segment_data[:version] || 1,
|
|
254
|
+
segment_data
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Create selector for this version
|
|
258
|
+
selector = LaunchDarkly::Interfaces::DataSystem::Selector.new_selector(version.to_s, version)
|
|
259
|
+
change_set = builder.finish(selector)
|
|
260
|
+
|
|
261
|
+
# Queue the update
|
|
262
|
+
update = LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
263
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::VALID,
|
|
264
|
+
change_set: change_set
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
@update_queue.push(update)
|
|
268
|
+
rescue => e
|
|
269
|
+
# Queue an error update
|
|
270
|
+
error_update = LaunchDarkly::Interfaces::DataSystem::Update.new(
|
|
271
|
+
state: LaunchDarkly::Interfaces::DataSource::Status::OFF,
|
|
272
|
+
error: LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(
|
|
273
|
+
LaunchDarkly::Interfaces::DataSource::ErrorInfo::STORE_ERROR,
|
|
274
|
+
0,
|
|
275
|
+
"Error processing segment update: #{e.message}",
|
|
276
|
+
Time.now
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
@update_queue.push(error_update)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
module LaunchDarkly
|
|
3
|
+
module Impl
|
|
4
|
+
# Simple implementation of a thread-safe memoized value whose generator function will never be
|
|
5
|
+
# run more than once, and whose value can be overridden by explicit assignment.
|
|
6
|
+
# Note that we no longer use this class and it will be removed in a future version.
|
|
7
|
+
class MemoizedValue
|
|
8
|
+
def initialize(&generator)
|
|
9
|
+
@generator = generator
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
@inited = false
|
|
12
|
+
@value = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get
|
|
16
|
+
@mutex.synchronize do
|
|
17
|
+
unless @inited
|
|
18
|
+
@value = @generator.call
|
|
19
|
+
@inited = true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
@value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def set(value)
|
|
26
|
+
@mutex.synchronize do
|
|
27
|
+
@value = value
|
|
28
|
+
@inited = true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'thread'
|
|
2
|
+
require 'ldclient-rb/impl/util'
|
|
2
3
|
|
|
3
4
|
module LaunchDarkly
|
|
4
5
|
module Impl
|
|
@@ -274,7 +275,7 @@ module LaunchDarkly
|
|
|
274
275
|
begin
|
|
275
276
|
result = @fn.call(@payload)
|
|
276
277
|
rescue => e
|
|
277
|
-
|
|
278
|
+
Impl::Util.log_exception(@logger, "Unexpected error running method for '#{origin}' origin", e)
|
|
278
279
|
result = LaunchDarkly::Result.fail("'#{origin}' operation raised an exception", e)
|
|
279
280
|
end
|
|
280
281
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "set"
|
|
2
2
|
require "ldclient-rb/impl/sampler"
|
|
3
|
+
require "ldclient-rb/impl/util"
|
|
3
4
|
require "logger"
|
|
4
5
|
|
|
5
6
|
module LaunchDarkly
|
|
@@ -67,7 +68,7 @@ module LaunchDarkly
|
|
|
67
68
|
begin
|
|
68
69
|
@consistent = is_consistent.call
|
|
69
70
|
rescue => e
|
|
70
|
-
|
|
71
|
+
Impl::Util.log_exception(@logger, "Exception raised during consistency check; failed to record measurement", e)
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
74
|
end
|
|
@@ -34,19 +34,19 @@ module LaunchDarkly
|
|
|
34
34
|
# SDK code outside of Impl::Model should use this method instead of calling the model class
|
|
35
35
|
# constructors directly, so as not to rely on implementation details.
|
|
36
36
|
#
|
|
37
|
-
# @param kind [Hash] normally either FEATURES or SEGMENTS
|
|
37
|
+
# @param kind [Hash] normally either Impl::DataStore::FEATURES or Impl::DataStore::SEGMENTS
|
|
38
38
|
# @param input [object] a JSON string or a parsed hash (or a data model object, in which case
|
|
39
39
|
# we'll just return the original object)
|
|
40
40
|
# @param logger [Logger|nil] logs errors if there are any data validation problems
|
|
41
41
|
# @return [Object] the flag or segment (or, for an unknown data kind, the data as a hash)
|
|
42
42
|
def self.deserialize(kind, input, logger = nil)
|
|
43
43
|
return nil if input.nil?
|
|
44
|
-
return input
|
|
44
|
+
return input unless input.is_a?(String) || input.is_a?(Hash)
|
|
45
45
|
data = input.is_a?(Hash) ? input : JSON.parse(input, symbolize_names: true)
|
|
46
46
|
case kind
|
|
47
|
-
when FEATURES
|
|
47
|
+
when Impl::DataStore::FEATURES
|
|
48
48
|
FeatureFlag.new(data, logger)
|
|
49
|
-
when SEGMENTS
|
|
49
|
+
when Impl::DataStore::SEGMENTS
|
|
50
50
|
Segment.new(data, logger)
|
|
51
51
|
else
|
|
52
52
|
data
|
|
@@ -63,8 +63,8 @@ module LaunchDarkly
|
|
|
63
63
|
# Translates a { flags: ..., segments: ... } object received from LaunchDarkly to the data store format.
|
|
64
64
|
def self.make_all_store_data(received_data, logger = nil)
|
|
65
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) },
|
|
66
|
+
Impl::DataStore::FEATURES => (received_data[:flags] || {}).transform_values { |data| FeatureFlag.new(data, logger) },
|
|
67
|
+
Impl::DataStore::SEGMENTS => (received_data[:segments] || {}).transform_values { |data| Segment.new(data, logger) },
|
|
68
68
|
}
|
|
69
69
|
end
|
|
70
70
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require "concurrent"
|
|
2
|
+
require "concurrent/atomics"
|
|
3
|
+
require "concurrent/executors"
|
|
4
|
+
require "thread"
|
|
5
|
+
|
|
6
|
+
module LaunchDarkly
|
|
7
|
+
module Impl
|
|
8
|
+
# Simple wrapper for a FixedThreadPool that rejects new jobs if all the threads are busy, rather
|
|
9
|
+
# than blocking. Also provides a way to wait for all jobs to finish without shutting down.
|
|
10
|
+
class NonBlockingThreadPool
|
|
11
|
+
def initialize(capacity, name = 'LD/NonBlockingThreadPool')
|
|
12
|
+
@capacity = capacity
|
|
13
|
+
@pool = Concurrent::FixedThreadPool.new(capacity, name: name)
|
|
14
|
+
@semaphore = Concurrent::Semaphore.new(capacity)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Attempts to submit a job, but only if a worker is available. Unlike the regular post method,
|
|
18
|
+
# this returns a value: true if the job was submitted, false if all workers are busy.
|
|
19
|
+
def post
|
|
20
|
+
unless @semaphore.try_acquire(1)
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
@pool.post do
|
|
24
|
+
begin
|
|
25
|
+
yield
|
|
26
|
+
ensure
|
|
27
|
+
@semaphore.release(1)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Waits until no jobs are executing, without shutting down the pool.
|
|
33
|
+
def wait_all
|
|
34
|
+
@semaphore.acquire(@capacity)
|
|
35
|
+
@semaphore.release(@capacity)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def shutdown
|
|
39
|
+
@pool.shutdown
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def wait_for_termination
|
|
43
|
+
@pool.wait_for_termination
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require "ldclient-rb/util"
|
|
1
|
+
require "ldclient-rb/impl/util"
|
|
2
2
|
|
|
3
3
|
require "concurrent/atomics"
|
|
4
4
|
|
|
@@ -26,7 +26,7 @@ module LaunchDarkly
|
|
|
26
26
|
begin
|
|
27
27
|
@task.call
|
|
28
28
|
rescue => e
|
|
29
|
-
|
|
29
|
+
Impl::Util.log_exception(@logger, "Uncaught exception from repeating task", e)
|
|
30
30
|
end
|
|
31
31
|
delta = @interval - (Time.now - started_at)
|
|
32
32
|
if delta > 0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
module LaunchDarkly
|
|
3
|
+
module Impl
|
|
4
|
+
# A non-thread-safe implementation of a LRU cache set with only add and reset methods.
|
|
5
|
+
# Based on https://github.com/SamSaffron/lru_redux/blob/master/lib/lru_redux/cache.rb
|
|
6
|
+
class SimpleLRUCacheSet
|
|
7
|
+
def initialize(capacity)
|
|
8
|
+
@values = {}
|
|
9
|
+
@capacity = capacity
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Adds a value to the cache or marks it recent if it was already there. Returns true if already there.
|
|
13
|
+
def add(value)
|
|
14
|
+
found = true
|
|
15
|
+
@values.delete(value) { found = false }
|
|
16
|
+
@values[value] = true
|
|
17
|
+
@values.shift if @values.length > @capacity
|
|
18
|
+
found
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def clear
|
|
22
|
+
@values = {}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
require "http"
|
|
3
|
+
|
|
1
4
|
module LaunchDarkly
|
|
2
5
|
module Impl
|
|
3
6
|
module Util
|
|
@@ -93,6 +96,68 @@ module LaunchDarkly
|
|
|
93
96
|
}
|
|
94
97
|
nil
|
|
95
98
|
end
|
|
99
|
+
|
|
100
|
+
#
|
|
101
|
+
# Append the payload filter key query parameter to the provided URI.
|
|
102
|
+
#
|
|
103
|
+
# @param uri [String]
|
|
104
|
+
# @param config [Config]
|
|
105
|
+
# @return [String]
|
|
106
|
+
#
|
|
107
|
+
def self.add_payload_filter_key(uri, config)
|
|
108
|
+
return uri if config.payload_filter_key.nil?
|
|
109
|
+
|
|
110
|
+
begin
|
|
111
|
+
parsed = URI.parse(uri)
|
|
112
|
+
new_query_params = URI.decode_www_form(String(parsed.query)) << ["filter", config.payload_filter_key]
|
|
113
|
+
parsed.query = URI.encode_www_form(new_query_params)
|
|
114
|
+
parsed.to_s
|
|
115
|
+
rescue URI::InvalidURIError
|
|
116
|
+
config.logger.warn { "[LDClient] URI could not be parsed. No filtering will be applied." }
|
|
117
|
+
uri
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.new_http_client(uri_s, config)
|
|
122
|
+
http_client_options = {}
|
|
123
|
+
if config.socket_factory
|
|
124
|
+
http_client_options["socket_class"] = config.socket_factory
|
|
125
|
+
end
|
|
126
|
+
proxy = URI.parse(uri_s).find_proxy
|
|
127
|
+
unless proxy.nil?
|
|
128
|
+
http_client_options["proxy"] = {
|
|
129
|
+
proxy_address: proxy.host,
|
|
130
|
+
proxy_port: proxy.port,
|
|
131
|
+
proxy_username: proxy.user,
|
|
132
|
+
proxy_password: proxy.password,
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
HTTP::Client.new(http_client_options)
|
|
136
|
+
.timeout({
|
|
137
|
+
read: config.read_timeout,
|
|
138
|
+
connect: config.connect_timeout,
|
|
139
|
+
})
|
|
140
|
+
.persistent(uri_s)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.log_exception(logger, message, exc)
|
|
144
|
+
logger.error { "[LDClient] #{message}: #{exc.inspect}" }
|
|
145
|
+
logger.debug { "[LDClient] Exception trace: #{exc.backtrace}" }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def self.http_error_recoverable?(status)
|
|
149
|
+
if status >= 400 && status < 500
|
|
150
|
+
status == 400 || status == 408 || status == 429
|
|
151
|
+
else
|
|
152
|
+
true
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.http_error_message(status, context, recoverable_message)
|
|
157
|
+
desc = (status == 401 || status == 403) ? " (invalid SDK key)" : ""
|
|
158
|
+
message = http_error_recoverable?(status) ? recoverable_message : "giving up permanently"
|
|
159
|
+
"HTTP error #{status}#{desc} for #{context} - #{message}"
|
|
160
|
+
end
|
|
96
161
|
end
|
|
97
162
|
end
|
|
98
163
|
end
|
data/lib/ldclient-rb/impl.rb
CHANGED
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
require "concurrent/atomics"
|
|
2
|
+
require "ldclient-rb/impl/data_store"
|
|
2
3
|
|
|
3
4
|
module LaunchDarkly
|
|
4
|
-
|
|
5
|
-
# These constants denote the types of data that can be stored in the feature store. If
|
|
6
|
-
# we add another storable data type in the future, as long as it follows the same pattern
|
|
7
|
-
# (having "key", "version", and "deleted" properties), we only need to add a corresponding
|
|
8
|
-
# constant here and the existing store should be able to handle it.
|
|
9
|
-
#
|
|
10
|
-
# The :priority and :get_dependency_keys properties are used by FeatureStoreDataSetSorter
|
|
11
|
-
# to ensure data consistency during non-atomic updates.
|
|
12
|
-
|
|
13
|
-
# @private
|
|
14
|
-
FEATURES = Impl::DataStore::DataKind.new(namespace: "features", priority: 1).freeze
|
|
15
|
-
|
|
16
|
-
# @private
|
|
17
|
-
SEGMENTS = Impl::DataStore::DataKind.new(namespace: "segments", priority: 0).freeze
|
|
18
|
-
|
|
19
|
-
# @private
|
|
20
|
-
ALL_KINDS = [FEATURES, SEGMENTS].freeze
|
|
21
|
-
|
|
22
5
|
#
|
|
23
6
|
# Default implementation of the LaunchDarkly client's feature store, using an in-memory
|
|
24
7
|
# cache. This object holds feature flags and related data received from LaunchDarkly.
|
|
@@ -12,14 +12,14 @@ module LaunchDarkly
|
|
|
12
12
|
class FlagBuilder
|
|
13
13
|
attr_reader :key
|
|
14
14
|
|
|
15
|
-
# @private
|
|
15
|
+
# @api private
|
|
16
16
|
def initialize(key)
|
|
17
17
|
@key = key
|
|
18
18
|
@on = true
|
|
19
19
|
@variations = []
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
# @private
|
|
22
|
+
# @api private
|
|
23
23
|
def initialize_copy(other)
|
|
24
24
|
super(other)
|
|
25
25
|
@variations = @variations.clone
|
|
@@ -357,7 +357,7 @@ module LaunchDarkly
|
|
|
357
357
|
self
|
|
358
358
|
end
|
|
359
359
|
|
|
360
|
-
# @private
|
|
360
|
+
# @api private
|
|
361
361
|
def add_rule(rule)
|
|
362
362
|
if @rules.nil?
|
|
363
363
|
@rules = Array.new
|
|
@@ -386,7 +386,7 @@ module LaunchDarkly
|
|
|
386
386
|
end
|
|
387
387
|
end
|
|
388
388
|
|
|
389
|
-
# @private
|
|
389
|
+
# @api private
|
|
390
390
|
def build(version)
|
|
391
391
|
res = { key: @key,
|
|
392
392
|
version: version,
|
|
@@ -486,16 +486,16 @@ module LaunchDarkly
|
|
|
486
486
|
# Finally, call {#then_return} to finish defining the rule.
|
|
487
487
|
#
|
|
488
488
|
class FlagRuleBuilder
|
|
489
|
-
# @private
|
|
489
|
+
# @api private
|
|
490
490
|
FlagRuleClause = Struct.new(:contextKind, :attribute, :op, :values, :negate, keyword_init: true) # rubocop:disable Naming/MethodName:
|
|
491
491
|
|
|
492
|
-
# @private
|
|
492
|
+
# @api private
|
|
493
493
|
def initialize(flag_builder)
|
|
494
494
|
@flag_builder = flag_builder
|
|
495
495
|
@clauses = Array.new
|
|
496
496
|
end
|
|
497
497
|
|
|
498
|
-
# @private
|
|
498
|
+
# @api private
|
|
499
499
|
def intialize_copy(other)
|
|
500
500
|
super(other)
|
|
501
501
|
@clauses = @clauses.clone
|
|
@@ -612,7 +612,7 @@ module LaunchDarkly
|
|
|
612
612
|
end
|
|
613
613
|
end
|
|
614
614
|
|
|
615
|
-
# @private
|
|
615
|
+
# @api private
|
|
616
616
|
def build(ri)
|
|
617
617
|
{
|
|
618
618
|
id: 'rule' + ri.to_s,
|
|
@@ -622,7 +622,7 @@ module LaunchDarkly
|
|
|
622
622
|
end
|
|
623
623
|
end
|
|
624
624
|
|
|
625
|
-
# @private
|
|
625
|
+
# @api private
|
|
626
626
|
def variation_for_boolean(variation)
|
|
627
627
|
variation ? TRUE_VARIATION_INDEX : FALSE_VARIATION_INDEX
|
|
628
628
|
end
|