launchdarkly-server-sdk 8.11.1 → 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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ldclient-rb/config.rb +66 -3
  3. data/lib/ldclient-rb/context.rb +1 -1
  4. data/lib/ldclient-rb/data_system.rb +243 -0
  5. data/lib/ldclient-rb/events.rb +35 -20
  6. data/lib/ldclient-rb/flags_state.rb +1 -1
  7. data/lib/ldclient-rb/impl/big_segments.rb +4 -4
  8. data/lib/ldclient-rb/impl/cache_store.rb +44 -0
  9. data/lib/ldclient-rb/impl/data_source/null_processor.rb +52 -0
  10. data/lib/ldclient-rb/impl/data_source/polling.rb +108 -0
  11. data/lib/ldclient-rb/impl/data_source/requestor.rb +106 -0
  12. data/lib/ldclient-rb/impl/data_source/status_provider.rb +78 -0
  13. data/lib/ldclient-rb/impl/data_source/stream.rb +198 -0
  14. data/lib/ldclient-rb/impl/data_source.rb +3 -3
  15. data/lib/ldclient-rb/impl/data_store/data_kind.rb +108 -0
  16. data/lib/ldclient-rb/impl/data_store/feature_store_client_wrapper.rb +187 -0
  17. data/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb +130 -0
  18. data/lib/ldclient-rb/impl/data_store/status_provider.rb +82 -0
  19. data/lib/ldclient-rb/impl/data_store/store.rb +371 -0
  20. data/lib/ldclient-rb/impl/data_store.rb +11 -97
  21. data/lib/ldclient-rb/impl/data_system/fdv1.rb +178 -0
  22. data/lib/ldclient-rb/impl/data_system/fdv2.rb +471 -0
  23. data/lib/ldclient-rb/impl/data_system/polling.rb +601 -0
  24. data/lib/ldclient-rb/impl/data_system/protocolv2.rb +264 -0
  25. data/lib/ldclient-rb/impl/data_system.rb +298 -0
  26. data/lib/ldclient-rb/impl/dependency_tracker.rb +21 -9
  27. data/lib/ldclient-rb/impl/evaluator.rb +3 -2
  28. data/lib/ldclient-rb/impl/event_sender.rb +4 -3
  29. data/lib/ldclient-rb/impl/expiring_cache.rb +79 -0
  30. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +9 -9
  31. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +0 -1
  32. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +288 -0
  33. data/lib/ldclient-rb/impl/memoized_value.rb +34 -0
  34. data/lib/ldclient-rb/impl/migrations/migrator.rb +2 -1
  35. data/lib/ldclient-rb/impl/migrations/tracker.rb +2 -1
  36. data/lib/ldclient-rb/impl/model/serialization.rb +6 -6
  37. data/lib/ldclient-rb/impl/non_blocking_thread_pool.rb +48 -0
  38. data/lib/ldclient-rb/impl/repeating_task.rb +2 -2
  39. data/lib/ldclient-rb/impl/simple_lru_cache.rb +27 -0
  40. data/lib/ldclient-rb/impl/util.rb +65 -0
  41. data/lib/ldclient-rb/impl.rb +1 -2
  42. data/lib/ldclient-rb/in_memory_store.rb +1 -18
  43. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +9 -9
  44. data/lib/ldclient-rb/integrations/test_data.rb +11 -11
  45. data/lib/ldclient-rb/integrations/test_data_v2/flag_builder_v2.rb +582 -0
  46. data/lib/ldclient-rb/integrations/test_data_v2.rb +248 -0
  47. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +3 -2
  48. data/lib/ldclient-rb/interfaces/data_system.rb +755 -0
  49. data/lib/ldclient-rb/interfaces/feature_store.rb +3 -0
  50. data/lib/ldclient-rb/ldclient.rb +55 -149
  51. data/lib/ldclient-rb/util.rb +11 -70
  52. data/lib/ldclient-rb/version.rb +1 -1
  53. data/lib/ldclient-rb.rb +8 -17
  54. metadata +52 -17
  55. data/lib/ldclient-rb/cache_store.rb +0 -45
  56. data/lib/ldclient-rb/expiring_cache.rb +0 -77
  57. data/lib/ldclient-rb/memoized_value.rb +0 -32
  58. data/lib/ldclient-rb/non_blocking_thread_pool.rb +0 -46
  59. data/lib/ldclient-rb/polling.rb +0 -102
  60. data/lib/ldclient-rb/requestor.rb +0 -102
  61. data/lib/ldclient-rb/simple_lru_cache.rb +0 -25
  62. data/lib/ldclient-rb/stream.rb +0 -196
@@ -0,0 +1,79 @@
1
+
2
+ module LaunchDarkly
3
+ module Impl
4
+ # A thread-safe cache with maximum number of entries and TTL.
5
+ # Adapted from https://github.com/SamSaffron/lru_redux/blob/master/lib/lru_redux/ttl/cache.rb
6
+ # under MIT license with the following changes:
7
+ # * made thread-safe
8
+ # * removed many unused methods
9
+ # * reading a key does not reset its expiration time, only writing
10
+ class ExpiringCache
11
+ def initialize(max_size, ttl)
12
+ @max_size = max_size
13
+ @ttl = ttl
14
+ @data_lru = {}
15
+ @data_ttl = {}
16
+ @lock = Mutex.new
17
+ end
18
+
19
+ def [](key)
20
+ @lock.synchronize do
21
+ ttl_evict
22
+ @data_lru[key]
23
+ end
24
+ end
25
+
26
+ def []=(key, val)
27
+ @lock.synchronize do
28
+ ttl_evict
29
+
30
+ @data_lru.delete(key)
31
+ @data_ttl.delete(key)
32
+
33
+ @data_lru[key] = val
34
+ @data_ttl[key] = Time.now.to_f
35
+
36
+ if @data_lru.size > @max_size
37
+ key, _ = @data_lru.first # hashes have a FIFO ordering in Ruby
38
+
39
+ @data_ttl.delete(key)
40
+ @data_lru.delete(key)
41
+ end
42
+
43
+ val
44
+ end
45
+ end
46
+
47
+ def delete(key)
48
+ @lock.synchronize do
49
+ ttl_evict
50
+
51
+ @data_lru.delete(key)
52
+ @data_ttl.delete(key)
53
+ end
54
+ end
55
+
56
+ def clear
57
+ @lock.synchronize do
58
+ @data_lru.clear
59
+ @data_ttl.clear
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def ttl_evict
66
+ ttl_horizon = Time.now.to_f - @ttl
67
+ key, time = @data_ttl.first
68
+
69
+ until time.nil? || time > ttl_horizon
70
+ @data_ttl.delete(key)
71
+ @data_lru.delete(key)
72
+
73
+ key, time = @data_ttl.first
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+
@@ -1,5 +1,5 @@
1
1
  require 'ldclient-rb/in_memory_store'
2
- require 'ldclient-rb/util'
2
+ require 'ldclient-rb/impl/util'
3
3
 
4
4
  require 'concurrent/atomics'
5
5
  require 'json'
@@ -75,14 +75,14 @@ module LaunchDarkly
75
75
 
76
76
  def load_all
77
77
  all_data = {
78
- FEATURES => {},
79
- SEGMENTS => {},
78
+ Impl::DataStore::FEATURES => {},
79
+ Impl::DataStore::SEGMENTS => {},
80
80
  }
81
81
  @paths.each do |path|
82
82
  begin
83
83
  load_file(path, all_data)
84
84
  rescue => exn
85
- LaunchDarkly::Util.log_exception(@logger, "Unable to load flag data from \"#{path}\"", exn)
85
+ Impl::Util.log_exception(@logger, "Unable to load flag data from \"#{path}\"", exn)
86
86
  @data_source_update_sink&.update_status(
87
87
  LaunchDarkly::Interfaces::DataSource::Status::INTERRUPTED,
88
88
  LaunchDarkly::Interfaces::DataSource::ErrorInfo.new(LaunchDarkly::Interfaces::DataSource::ErrorInfo::INVALID_DATA, 0, exn.to_s, Time.now)
@@ -102,17 +102,17 @@ module LaunchDarkly
102
102
  @last_version += 1
103
103
  }
104
104
 
105
- parsed = parse_content(IO.read(path))
105
+ parsed = parse_content(File.read(path))
106
106
  (parsed[:flags] || {}).each do |key, flag|
107
107
  flag[:version] = version
108
- add_item(all_data, FEATURES, flag)
108
+ add_item(all_data, Impl::DataStore::FEATURES, flag)
109
109
  end
110
110
  (parsed[:flagValues] || {}).each do |key, value|
111
- add_item(all_data, FEATURES, make_flag_with_value(key.to_s, value, version))
111
+ add_item(all_data, Impl::DataStore::FEATURES, make_flag_with_value(key.to_s, value, version))
112
112
  end
113
113
  (parsed[:segments] || {}).each do |key, segment|
114
114
  segment[:version] = version
115
- add_item(all_data, SEGMENTS, segment)
115
+ add_item(all_data, Impl::DataStore::SEGMENTS, segment)
116
116
  end
117
117
  end
118
118
 
@@ -212,7 +212,7 @@ module LaunchDarkly
212
212
  end
213
213
  reloader.call if changed
214
214
  rescue => exn
215
- LaunchDarkly::Util.log_exception(logger, "Unexpected exception in FileDataSourcePoller", exn)
215
+ Impl::Util.log_exception(logger, "Unexpected exception in FileDataSourcePoller", exn)
216
216
  end
217
217
  end
218
218
  end
@@ -5,7 +5,6 @@ module LaunchDarkly
5
5
  module Impl
6
6
  module Integrations
7
7
  module TestData
8
- # @private
9
8
  class TestDataSource
10
9
  include LaunchDarkly::Interfaces::DataSource
11
10
 
@@ -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
- LaunchDarkly::Util.log_exception(@logger, "Unexpected error running method for '#{origin}' origin", e)
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
- LaunchDarkly::Util.log_exception(@logger, "Exception raised during consistency check; failed to record measurement", e)
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 if !input.is_a?(String) && !input.is_a?(Hash)
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
- LaunchDarkly::Util.log_exception(@logger, "Uncaught exception from repeating task", e)
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
+