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,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
@@ -0,0 +1,298 @@
1
+ module LaunchDarkly
2
+ module Impl
3
+ #
4
+ # Mixin that defines the required methods of a data system implementation. The data system
5
+ # is responsible for managing the SDK's data model, including storage, retrieval, and change
6
+ # detection for feature flag configurations.
7
+ #
8
+ # This module also contains supporting classes and additional mixins for data system
9
+ # implementations, such as DataAvailability, Update, and protocol-specific mixins.
10
+ #
11
+ # For operations that can fail, use {LaunchDarkly::Result} from util.rb.
12
+ #
13
+ # Application code should not need to implement this directly; it is used internally by the
14
+ # SDK's data system implementations.
15
+ #
16
+ # @private
17
+ #
18
+ module DataSystem
19
+ #
20
+ # Starts the data system.
21
+ #
22
+ # This method will return immediately. The returned event will be set when the system
23
+ # has reached an initial state (either permanently failed, e.g. due to bad auth, or succeeded).
24
+ #
25
+ # If called multiple times, returns the same event as the first call.
26
+ #
27
+ # @return [Concurrent::Event] Event that will be set when initialization is complete
28
+ #
29
+ def start
30
+ raise NotImplementedError, "#{self.class} must implement #start"
31
+ end
32
+
33
+ #
34
+ # Halts the data system. Should be called when the client is closed to stop any long running
35
+ # operations. Makes the data system no longer usable.
36
+ #
37
+ # @return [void]
38
+ #
39
+ def stop
40
+ raise NotImplementedError, "#{self.class} must implement #stop"
41
+ end
42
+
43
+ #
44
+ # Returns an interface for tracking the status of the data source.
45
+ #
46
+ # The data source is the mechanism that the SDK uses to get feature flag configurations, such
47
+ # as a streaming connection (the default) or poll requests.
48
+ #
49
+ # @return [LaunchDarkly::Interfaces::DataSource::StatusProvider]
50
+ #
51
+ def data_source_status_provider
52
+ raise NotImplementedError, "#{self.class} must implement #data_source_status_provider"
53
+ end
54
+
55
+ #
56
+ # Returns an interface for tracking the status of a persistent data store.
57
+ #
58
+ # The provider has methods for checking whether the data store is (as far
59
+ # as the SDK knows) currently operational, tracking changes in this
60
+ # status, and getting cache statistics. These are only relevant for a
61
+ # persistent data store; if you are using an in-memory data store, then
62
+ # this method will return a stub object that provides no information.
63
+ #
64
+ # @return [LaunchDarkly::Interfaces::DataStore::StatusProvider]
65
+ #
66
+ def data_store_status_provider
67
+ raise NotImplementedError, "#{self.class} must implement #data_store_status_provider"
68
+ end
69
+
70
+ #
71
+ # Returns the broadcaster for flag change notifications.
72
+ #
73
+ # Consumers can use this broadcaster to build their own flag tracker
74
+ # or listen for flag changes directly.
75
+ #
76
+ # @return [LaunchDarkly::Impl::Broadcaster]
77
+ #
78
+ def flag_change_broadcaster
79
+ raise NotImplementedError, "#{self.class} must implement #flag_change_broadcaster"
80
+ end
81
+
82
+ #
83
+ # Indicates what form of data is currently available.
84
+ #
85
+ # This is calculated dynamically based on current system state.
86
+ #
87
+ # @return [Symbol] one of the {DataAvailability} constants
88
+ #
89
+ def data_availability
90
+ raise NotImplementedError, "#{self.class} must implement #data_availability"
91
+ end
92
+
93
+ #
94
+ # Indicates the ideal form of data attainable given the current configuration.
95
+ #
96
+ # @return [Symbol] one of the {#DataAvailability} constants
97
+ #
98
+ def target_availability
99
+ raise NotImplementedError, "#{self.class} must implement #target_availability"
100
+ end
101
+
102
+ #
103
+ # Returns the data store used by the data system.
104
+ #
105
+ # @return [Object] The read-only store
106
+ #
107
+ def store
108
+ raise NotImplementedError, "#{self.class} must implement #store"
109
+ end
110
+
111
+ #
112
+ # Sets the diagnostic accumulator for streaming initialization metrics.
113
+ # This should be called before start() to ensure metrics are collected.
114
+ #
115
+ # @param diagnostic_accumulator [DiagnosticAccumulator] The diagnostic accumulator
116
+ # @return [void]
117
+ #
118
+ def set_diagnostic_accumulator(diagnostic_accumulator)
119
+ raise NotImplementedError, "#{self.class} must implement #set_diagnostic_accumulator"
120
+ end
121
+
122
+ #
123
+ # Represents the availability of data in the SDK.
124
+ #
125
+ class DataAvailability
126
+ # The SDK has no data and will evaluate flags using the application-provided default values.
127
+ DEFAULTS = :defaults
128
+
129
+ # The SDK has data, not necessarily the latest, which will be used to evaluate flags.
130
+ CACHED = :cached
131
+
132
+ # The SDK has obtained, at least once, the latest known data from LaunchDarkly.
133
+ REFRESHED = :refreshed
134
+
135
+ ALL = [DEFAULTS, CACHED, REFRESHED].freeze
136
+
137
+ #
138
+ # Returns whether this availability level is **at least** as good as the other.
139
+ #
140
+ # @param [Symbol] self_level The current availability level
141
+ # @param [Symbol] other The other availability level to compare against
142
+ # @return [Boolean] true if this availability level is at least as good as the other
143
+ #
144
+ def self.at_least?(self_level, other)
145
+ return true if self_level == other
146
+ return true if self_level == REFRESHED
147
+ return true if self_level == CACHED && other == DEFAULTS
148
+
149
+ false
150
+ end
151
+ end
152
+
153
+ #
154
+ # Mixin that defines the required methods of a diagnostic accumulator implementation.
155
+ # The diagnostic accumulator is used for collecting and reporting diagnostic events
156
+ # to LaunchDarkly for monitoring SDK performance and behavior.
157
+ #
158
+ # Application code should not need to implement this directly; it is used internally by the SDK.
159
+ #
160
+ module DiagnosticAccumulator
161
+ #
162
+ # Record a stream initialization.
163
+ #
164
+ # @param timestamp [Float] The timestamp
165
+ # @param duration [Float] The duration
166
+ # @param failed [Boolean] Whether it failed
167
+ # @return [void]
168
+ #
169
+ def record_stream_init(timestamp, duration, failed)
170
+ raise NotImplementedError, "#{self.class} must implement #record_stream_init"
171
+ end
172
+
173
+ #
174
+ # Record events in a batch.
175
+ #
176
+ # @param events_in_batch [Integer] The number of events
177
+ # @return [void]
178
+ #
179
+ def record_events_in_batch(events_in_batch)
180
+ raise NotImplementedError, "#{self.class} must implement #record_events_in_batch"
181
+ end
182
+
183
+ #
184
+ # Create an event and reset the accumulator.
185
+ #
186
+ # @param dropped_events [Integer] The number of dropped events
187
+ # @param deduplicated_users [Integer] The number of deduplicated users
188
+ # @return [Object] The diagnostic event
189
+ #
190
+ def create_event_and_reset(dropped_events, deduplicated_users)
191
+ raise NotImplementedError, "#{self.class} must implement #create_event_and_reset"
192
+ end
193
+ end
194
+
195
+ #
196
+ # Mixin that defines the required methods for components that can receive a diagnostic accumulator.
197
+ # Components that include this mixin can report diagnostic information to LaunchDarkly for
198
+ # monitoring SDK performance and behavior.
199
+ #
200
+ # Application code should not need to implement this directly; it is used internally by the SDK.
201
+ #
202
+ module DiagnosticSource
203
+ #
204
+ # Set the diagnostic_accumulator to be used for reporting diagnostic events.
205
+ #
206
+ # @param diagnostic_accumulator [DiagnosticAccumulator] The accumulator
207
+ # @return [void]
208
+ #
209
+ def set_diagnostic_accumulator(diagnostic_accumulator)
210
+ raise NotImplementedError, "#{self.class} must implement #set_diagnostic_accumulator"
211
+ end
212
+ end
213
+
214
+ #
215
+ # Mixin that defines the required methods of an initializer implementation. An initializer
216
+ # is a component capable of retrieving a single data result, such as from the LaunchDarkly
217
+ # polling API.
218
+ #
219
+ # The intent of initializers is to quickly fetch an initial set of data, which may be stale
220
+ # but is fast to retrieve. This initial data serves as a foundation for a Synchronizer to
221
+ # build upon, enabling it to provide updates as new changes occur.
222
+ #
223
+ # Application code should not need to implement this directly; it is used internally by the SDK.
224
+ #
225
+ module Initializer
226
+ #
227
+ # Fetch should retrieve the initial data set for the data source, returning
228
+ # a Basis object on success, or an error message on failure.
229
+ #
230
+ # @return [LaunchDarkly::Result] A Result containing either a Basis or an error message
231
+ #
232
+ def fetch
233
+ raise NotImplementedError, "#{self.class} must implement #fetch"
234
+ end
235
+ end
236
+
237
+ #
238
+ # Update represents the results of a synchronizer's ongoing sync method.
239
+ #
240
+ class Update
241
+ # @return [Symbol] The state of the data source
242
+ attr_reader :state
243
+
244
+ # @return [ChangeSet, nil] The change set if available
245
+ attr_reader :change_set
246
+
247
+ # @return [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] Error information if applicable
248
+ attr_reader :error
249
+
250
+ # @return [Boolean] Whether to revert to FDv1
251
+ attr_reader :revert_to_fdv1
252
+
253
+ # @return [String, nil] The environment ID if available
254
+ attr_reader :environment_id
255
+
256
+ #
257
+ # @param state [Symbol] The state of the data source
258
+ # @param change_set [ChangeSet, nil] The change set if available
259
+ # @param error [LaunchDarkly::Interfaces::DataSource::ErrorInfo, nil] Error information if applicable
260
+ # @param revert_to_fdv1 [Boolean] Whether to revert to FDv1
261
+ # @param environment_id [String, nil] The environment ID if available
262
+ #
263
+ def initialize(state:, change_set: nil, error: nil, revert_to_fdv1: false, environment_id: nil)
264
+ @state = state
265
+ @change_set = change_set
266
+ @error = error
267
+ @revert_to_fdv1 = revert_to_fdv1
268
+ @environment_id = environment_id
269
+ end
270
+ end
271
+
272
+ #
273
+ # Mixin that defines the required methods of a synchronizer implementation. A synchronizer
274
+ # is a component capable of synchronizing data from an external data source, such as a
275
+ # streaming or polling API.
276
+ #
277
+ # It is responsible for yielding Update objects that represent the current state of the
278
+ # data source, including any changes that have occurred since the last synchronization.
279
+ #
280
+ # Application code should not need to implement this directly; it is used internally by the SDK.
281
+ #
282
+ module Synchronizer
283
+ #
284
+ # Sync should begin the synchronization process for the data source, yielding
285
+ # Update objects until the connection is closed or an unrecoverable error
286
+ # occurs.
287
+ #
288
+ # @yield [Update] Yields Update objects as synchronization progresses
289
+ # @return [void]
290
+ #
291
+ def sync
292
+ raise NotImplementedError, "#{self.class} must implement #sync"
293
+ end
294
+ end
295
+ end
296
+ end
297
+ end
298
+
@@ -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)