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.
Files changed (59) 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 +34 -19
  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/polling.rb +108 -0
  10. data/lib/ldclient-rb/impl/data_source/requestor.rb +106 -0
  11. data/lib/ldclient-rb/impl/data_source/status_provider.rb +78 -0
  12. data/lib/ldclient-rb/impl/data_source/stream.rb +198 -0
  13. data/lib/ldclient-rb/impl/data_source.rb +3 -3
  14. data/lib/ldclient-rb/impl/data_store/data_kind.rb +108 -0
  15. data/lib/ldclient-rb/impl/data_store/feature_store_client_wrapper.rb +187 -0
  16. data/lib/ldclient-rb/impl/data_store/in_memory_feature_store.rb +130 -0
  17. data/lib/ldclient-rb/impl/data_store/status_provider.rb +82 -0
  18. data/lib/ldclient-rb/impl/data_store/store.rb +371 -0
  19. data/lib/ldclient-rb/impl/data_store.rb +11 -97
  20. data/lib/ldclient-rb/impl/data_system/fdv1.rb +20 -7
  21. data/lib/ldclient-rb/impl/data_system/fdv2.rb +471 -0
  22. data/lib/ldclient-rb/impl/data_system/polling.rb +601 -0
  23. data/lib/ldclient-rb/impl/data_system/protocolv2.rb +264 -0
  24. data/lib/ldclient-rb/impl/dependency_tracker.rb +21 -9
  25. data/lib/ldclient-rb/impl/evaluator.rb +3 -2
  26. data/lib/ldclient-rb/impl/event_sender.rb +4 -3
  27. data/lib/ldclient-rb/impl/expiring_cache.rb +79 -0
  28. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +8 -8
  29. data/lib/ldclient-rb/impl/integrations/test_data/test_data_source_v2.rb +288 -0
  30. data/lib/ldclient-rb/impl/memoized_value.rb +34 -0
  31. data/lib/ldclient-rb/impl/migrations/migrator.rb +2 -1
  32. data/lib/ldclient-rb/impl/migrations/tracker.rb +2 -1
  33. data/lib/ldclient-rb/impl/model/serialization.rb +6 -6
  34. data/lib/ldclient-rb/impl/non_blocking_thread_pool.rb +48 -0
  35. data/lib/ldclient-rb/impl/repeating_task.rb +2 -2
  36. data/lib/ldclient-rb/impl/simple_lru_cache.rb +27 -0
  37. data/lib/ldclient-rb/impl/util.rb +65 -0
  38. data/lib/ldclient-rb/impl.rb +1 -2
  39. data/lib/ldclient-rb/in_memory_store.rb +1 -18
  40. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +9 -9
  41. data/lib/ldclient-rb/integrations/test_data.rb +11 -11
  42. data/lib/ldclient-rb/integrations/test_data_v2/flag_builder_v2.rb +582 -0
  43. data/lib/ldclient-rb/integrations/test_data_v2.rb +248 -0
  44. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +3 -2
  45. data/lib/ldclient-rb/interfaces/data_system.rb +755 -0
  46. data/lib/ldclient-rb/interfaces/feature_store.rb +3 -0
  47. data/lib/ldclient-rb/ldclient.rb +55 -131
  48. data/lib/ldclient-rb/util.rb +11 -70
  49. data/lib/ldclient-rb/version.rb +1 -1
  50. data/lib/ldclient-rb.rb +8 -17
  51. metadata +35 -17
  52. data/lib/ldclient-rb/cache_store.rb +0 -45
  53. data/lib/ldclient-rb/expiring_cache.rb +0 -77
  54. data/lib/ldclient-rb/memoized_value.rb +0 -32
  55. data/lib/ldclient-rb/non_blocking_thread_pool.rb +0 -46
  56. data/lib/ldclient-rb/polling.rb +0 -102
  57. data/lib/ldclient-rb/requestor.rb +0 -102
  58. data/lib/ldclient-rb/simple_lru_cache.rb +0 -25
  59. data/lib/ldclient-rb/stream.rb +0 -197
@@ -0,0 +1,264 @@
1
+ require 'json'
2
+
3
+ module LaunchDarkly
4
+ module Impl
5
+ module DataSystem
6
+ module ProtocolV2
7
+ #
8
+ # This module contains the protocol definitions and data types for the
9
+ # LaunchDarkly data system version 2 (FDv2).
10
+ #
11
+
12
+ #
13
+ # DeleteObject specifies the deletion of a particular object.
14
+ #
15
+ # This type is not stable, and not subject to any backwards
16
+ # compatibility guarantees or semantic versioning. It is not suitable for production usage.
17
+ #
18
+ class DeleteObject
19
+ # @return [Integer] The version
20
+ attr_reader :version
21
+
22
+ # @return [String] The object kind ({LaunchDarkly::Interfaces::DataSystem::ObjectKind})
23
+ attr_reader :kind
24
+
25
+ # @return [String] The key
26
+ attr_reader :key
27
+
28
+ #
29
+ # @param version [Integer] The version
30
+ # @param kind [String] The object kind ({LaunchDarkly::Interfaces::DataSystem::ObjectKind})
31
+ # @param key [String] The key
32
+ #
33
+ def initialize(version:, kind:, key:)
34
+ @version = version
35
+ @kind = kind
36
+ @key = key
37
+ end
38
+
39
+ #
40
+ # Returns the event name.
41
+ #
42
+ # @return [String]
43
+ #
44
+ def name
45
+ LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT
46
+ end
47
+
48
+ #
49
+ # Serializes the DeleteObject to a JSON-compatible hash.
50
+ #
51
+ # @return [Hash]
52
+ #
53
+ def to_h
54
+ {
55
+ version: @version,
56
+ kind: @kind,
57
+ key: @key,
58
+ }
59
+ end
60
+
61
+ #
62
+ # Deserializes a DeleteObject from a JSON-compatible hash.
63
+ #
64
+ # @param data [Hash] The hash representation
65
+ # @return [DeleteObject]
66
+ # @raise [ArgumentError] if required fields are missing
67
+ #
68
+ def self.from_h(data)
69
+ version = data[:version]
70
+ kind = data[:kind]
71
+ key = data[:key]
72
+
73
+ raise ArgumentError, "Missing required fields in DeleteObject" if version.nil? || kind.nil? || key.nil?
74
+
75
+ new(version: version, kind: kind, key: key)
76
+ end
77
+ end
78
+
79
+ #
80
+ # PutObject specifies the addition of a particular object with upsert semantics.
81
+ #
82
+ # This type is not stable, and not subject to any backwards
83
+ # compatibility guarantees or semantic versioning. It is not suitable for production usage.
84
+ #
85
+ class PutObject
86
+ # @return [Integer] The version
87
+ attr_reader :version
88
+
89
+ # @return [String] The object kind ({LaunchDarkly::Interfaces::DataSystem::ObjectKind})
90
+ attr_reader :kind
91
+
92
+ # @return [String] The key
93
+ attr_reader :key
94
+
95
+ # @return [Hash] The object data
96
+ attr_reader :object
97
+
98
+ #
99
+ # @param version [Integer] The version
100
+ # @param kind [String] The object kind ({LaunchDarkly::Interfaces::DataSystem::ObjectKind})
101
+ # @param key [String] The key
102
+ # @param object [Hash] The object data
103
+ #
104
+ def initialize(version:, kind:, key:, object:)
105
+ @version = version
106
+ @kind = kind
107
+ @key = key
108
+ @object = object
109
+ end
110
+
111
+ #
112
+ # Returns the event name.
113
+ #
114
+ # @return [String]
115
+ #
116
+ def name
117
+ LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT
118
+ end
119
+
120
+ #
121
+ # Serializes the PutObject to a JSON-compatible hash.
122
+ #
123
+ # @return [Hash]
124
+ #
125
+ def to_h
126
+ {
127
+ version: @version,
128
+ kind: @kind,
129
+ key: @key,
130
+ object: @object,
131
+ }
132
+ end
133
+
134
+ #
135
+ # Deserializes a PutObject from a JSON-compatible hash.
136
+ #
137
+ # @param data [Hash] The hash representation
138
+ # @return [PutObject]
139
+ # @raise [ArgumentError] if required fields are missing
140
+ #
141
+ def self.from_h(data)
142
+ version = data[:version]
143
+ kind = data[:kind]
144
+ key = data[:key]
145
+ object_data = data[:object]
146
+
147
+ raise ArgumentError, "Missing required fields in PutObject" if version.nil? || kind.nil? || key.nil? || object_data.nil?
148
+
149
+ new(version: version, kind: kind, key: key, object: object_data)
150
+ end
151
+ end
152
+
153
+ #
154
+ # Goodbye represents a goodbye event.
155
+ #
156
+ # This type is not stable, and not subject to any backwards
157
+ # compatibility guarantees or semantic versioning. It is not suitable for production usage.
158
+ #
159
+ class Goodbye
160
+ # @return [String] The reason for goodbye
161
+ attr_reader :reason
162
+
163
+ # @return [Boolean] Whether the goodbye is silent
164
+ attr_reader :silent
165
+
166
+ # @return [Boolean] Whether this represents a catastrophic failure
167
+ attr_reader :catastrophe
168
+
169
+ #
170
+ # @param reason [String] The reason for goodbye
171
+ # @param silent [Boolean] Whether the goodbye is silent
172
+ # @param catastrophe [Boolean] Whether this represents a catastrophic failure
173
+ #
174
+ def initialize(reason:, silent:, catastrophe:)
175
+ @reason = reason
176
+ @silent = silent
177
+ @catastrophe = catastrophe
178
+ end
179
+
180
+ #
181
+ # Serializes the Goodbye to a JSON-compatible hash.
182
+ #
183
+ # @return [Hash]
184
+ #
185
+ def to_h
186
+ {
187
+ reason: @reason,
188
+ silent: @silent,
189
+ catastrophe: @catastrophe,
190
+ }
191
+ end
192
+
193
+ #
194
+ # Deserializes a Goodbye event from a JSON-compatible hash.
195
+ #
196
+ # @param data [Hash] The hash representation
197
+ # @return [Goodbye]
198
+ # @raise [ArgumentError] if required fields are missing
199
+ #
200
+ def self.from_h(data)
201
+ reason = data[:reason]
202
+ silent = data[:silent]
203
+ catastrophe = data[:catastrophe]
204
+
205
+ raise ArgumentError, "Missing required fields in Goodbye" if reason.nil? || silent.nil? || catastrophe.nil?
206
+
207
+ new(reason: reason, silent: silent, catastrophe: catastrophe)
208
+ end
209
+ end
210
+
211
+ #
212
+ # Error represents an error event.
213
+ #
214
+ # This type is not stable, and not subject to any backwards
215
+ # compatibility guarantees or semantic versioning. It is not suitable for production usage.
216
+ #
217
+ class Error
218
+ # @return [String] The payload ID
219
+ attr_reader :payload_id
220
+
221
+ # @return [String] The reason for the error
222
+ attr_reader :reason
223
+
224
+ #
225
+ # @param payload_id [String] The payload ID
226
+ # @param reason [String] The reason for the error
227
+ #
228
+ def initialize(payload_id:, reason:)
229
+ @payload_id = payload_id
230
+ @reason = reason
231
+ end
232
+
233
+ #
234
+ # Serializes the Error to a JSON-compatible hash.
235
+ #
236
+ # @return [Hash]
237
+ #
238
+ def to_h
239
+ {
240
+ payloadId: @payload_id,
241
+ reason: @reason,
242
+ }
243
+ end
244
+
245
+ #
246
+ # Deserializes an Error from a JSON-compatible hash.
247
+ #
248
+ # @param data [Hash] The hash representation
249
+ # @return [Error]
250
+ # @raise [ArgumentError] if required fields are missing
251
+ #
252
+ def self.from_h(data)
253
+ payload_id = data[:payloadId]
254
+ reason = data[:reason]
255
+
256
+ raise ArgumentError, "Missing required fields in Error" if payload_id.nil? || reason.nil?
257
+
258
+ new(payload_id: payload_id, reason: reason)
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
@@ -1,9 +1,12 @@
1
+ require "ldclient-rb/impl/model/serialization"
2
+
1
3
  module LaunchDarkly
2
4
  module Impl
3
5
  class DependencyTracker
4
- def initialize
6
+ def initialize(logger = nil)
5
7
  @from = {}
6
8
  @to = {}
9
+ @logger = logger
7
10
  end
8
11
 
9
12
  #
@@ -11,11 +14,11 @@ module LaunchDarkly
11
14
  #
12
15
  # @param from_kind [Object] the changed item's kind
13
16
  # @param from_key [String] the changed item's key
14
- # @param from_item [Object] the changed item
17
+ # @param from_item [Object] the changed item (can be a Hash, model object, or nil)
15
18
  #
16
19
  def update_dependencies_from(from_kind, from_key, from_item)
17
20
  from_what = { kind: from_kind, key: from_key }
18
- updated_dependencies = DependencyTracker.compute_dependencies_from(from_kind, from_item)
21
+ updated_dependencies = DependencyTracker.compute_dependencies_from(from_kind, from_item, @logger)
19
22
 
20
23
  old_dependency_set = @from[from_what]
21
24
  unless old_dependency_set.nil?
@@ -39,7 +42,7 @@ module LaunchDarkly
39
42
  def self.segment_keys_from_clauses(clauses)
40
43
  clauses.flat_map do |clause|
41
44
  if clause.op == :segmentMatch
42
- clause.values.map { |value| {kind: LaunchDarkly::SEGMENTS, key: value }}
45
+ clause.values.map { |value| {kind: DataStore::SEGMENTS, key: value }}
43
46
  else
44
47
  []
45
48
  end
@@ -48,19 +51,28 @@ module LaunchDarkly
48
51
 
49
52
  #
50
53
  # @param from_kind [String]
51
- # @param from_item [LaunchDarkly::Impl::Model::FeatureFlag, LaunchDarkly::Impl::Model::Segment]
54
+ # @param from_item [Hash, LaunchDarkly::Impl::Model::FeatureFlag, LaunchDarkly::Impl::Model::Segment, nil] the item (can be a hash, model object, or nil)
55
+ # @param logger [Logger, nil] optional logger for deserialization
52
56
  # @return [Set]
53
57
  #
54
- def self.compute_dependencies_from(from_kind, from_item)
55
- return Set.new if from_item.nil?
58
+ def self.compute_dependencies_from(from_kind, from_item, logger = nil)
59
+ # Check for deleted items (matches Python: from_item.get('deleted', False))
60
+ return Set.new if from_item.nil? || (from_item.is_a?(Hash) && from_item[:deleted])
61
+
62
+ # Deserialize hash to model object if needed (matches Python: from_kind.decode(from_item) if isinstance(from_item, dict))
63
+ from_item = if from_item.is_a?(Hash)
64
+ LaunchDarkly::Impl::Model.deserialize(from_kind, from_item, logger)
65
+ else
66
+ from_item
67
+ end
56
68
 
57
- if from_kind == LaunchDarkly::FEATURES
69
+ if from_kind == DataStore::FEATURES && from_item.is_a?(LaunchDarkly::Impl::Model::FeatureFlag)
58
70
  prereq_keys = from_item.prerequisites.map { |prereq| {kind: from_kind, key: prereq.key} }
59
71
  segment_keys = from_item.rules.flat_map { |rule| DependencyTracker.segment_keys_from_clauses(rule.clauses) }
60
72
 
61
73
  results = Set.new(prereq_keys)
62
74
  results.merge(segment_keys)
63
- elsif from_kind == LaunchDarkly::SEGMENTS
75
+ elsif from_kind == DataStore::SEGMENTS && from_item.is_a?(LaunchDarkly::Impl::Model::Segment)
64
76
  kind_and_keys = from_item.rules.flat_map do |rule|
65
77
  DependencyTracker.segment_keys_from_clauses(rule.clauses)
66
78
  end
@@ -4,6 +4,7 @@ require "ldclient-rb/impl/evaluator_helpers"
4
4
  require "ldclient-rb/impl/evaluator_operators"
5
5
  require "ldclient-rb/impl/model/feature_flag"
6
6
  require "ldclient-rb/impl/model/segment"
7
+ require "ldclient-rb/impl/util"
7
8
 
8
9
  module LaunchDarkly
9
10
  module Impl
@@ -152,11 +153,11 @@ module LaunchDarkly
152
153
  begin
153
154
  detail = eval_internal(flag, context, result, state)
154
155
  rescue EvaluationException => exn
155
- LaunchDarkly::Util.log_exception(@logger, "Unexpected error when evaluating flag #{flag.key}", exn)
156
+ Impl::Util.log_exception(@logger, "Unexpected error when evaluating flag #{flag.key}", exn)
156
157
  result.detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(exn.error_kind))
157
158
  return result, state
158
159
  rescue => exn
159
- LaunchDarkly::Util.log_exception(@logger, "Unexpected error when evaluating flag #{flag.key}", exn)
160
+ Impl::Util.log_exception(@logger, "Unexpected error when evaluating flag #{flag.key}", exn)
160
161
  result.detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION))
161
162
  return result, state
162
163
  end
@@ -1,4 +1,5 @@
1
1
  require "ldclient-rb/impl/unbounded_pool"
2
+ require "ldclient-rb/impl/util"
2
3
 
3
4
  require "securerandom"
4
5
  require "http"
@@ -21,7 +22,7 @@ module LaunchDarkly
21
22
  @logger = config.logger
22
23
  @retry_interval = retry_interval
23
24
  @http_client_pool = UnboundedPool.new(
24
- lambda { LaunchDarkly::Util.new_http_client(@config.events_uri, @config) },
25
+ lambda { Impl::Util.new_http_client(@config.events_uri, @config) },
25
26
  lambda { |client| client.close })
26
27
  end
27
28
 
@@ -81,9 +82,9 @@ module LaunchDarkly
81
82
  end
82
83
  return EventSenderResult.new(true, false, res_time)
83
84
  end
84
- must_shutdown = !LaunchDarkly::Util.http_error_recoverable?(status)
85
+ must_shutdown = !Impl::Util.http_error_recoverable?(status)
85
86
  can_retry = !must_shutdown && attempt == 0
86
- message = LaunchDarkly::Util.http_error_message(status, "event delivery", can_retry ? "will retry" : "some events were dropped")
87
+ message = Impl::Util.http_error_message(status, "event delivery", can_retry ? "will retry" : "some events were dropped")
87
88
  @logger.error { "[LDClient] #{message}" }
88
89
  if must_shutdown
89
90
  return EventSenderResult.new(false, true, nil)
@@ -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)
@@ -105,14 +105,14 @@ module LaunchDarkly
105
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