ff-ruby-server-sdk 1.4.3 → 1.4.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21ede6e2de94f3ed2aec871c40b8dbee7a9feec6adbdec14726e5237caedbb0b
4
- data.tar.gz: e89c0b57ae84666c87dccf6b25a67f0d0ca77990220ee014c9abe1a1225993c9
3
+ metadata.gz: c9ee87181e0d3822c070ba3c889bf4b339b1c4e7853b046885c98372d25f7a5e
4
+ data.tar.gz: 3692d620f62ff96a59b17a4bcbd939f245e0ae1befcb9b477bb7389d5beca7e7
5
5
  SHA512:
6
- metadata.gz: 943c5b49c1bf887dd6ff24edce3e03cd2bff1badde8dd0802fdc8bda042e2e260b2cd7929c18189ec9dfb39f8b8189bec5b49e32ab021c722a17da71268bcccb
7
- data.tar.gz: 5f32b626e8c49fd3074401db1ef6810acfb79b4cdba6f03e7f55e31a2a6e921b73ce273d71780f30dc90f18e74f691e4a203da6503bb0a473a60e49b0b56d37b
6
+ metadata.gz: b9c6768454e988de4a4829b9c34d3e168ad43ad5a6d5100ca9671bac272714074b5245fe688aaa0bac81da820f1c172e4cc3fb6ceb577e444b22e833d4578980
7
+ data.tar.gz: 404694820857c6e8959ac6fdc711bf4119779c0b52f8e48ee56c38e27814a6d7bbe024d7423be9d6e90549c087b68da0add84f7a3911b9a820102da621641d27
@@ -91,6 +91,20 @@ class Evaluator < Evaluation
91
91
 
92
92
  flag = @repository.get_flag(identifier)
93
93
 
94
+ # Check if flag exists
95
+ if flag.nil?
96
+ # Log a warning if the flag is not found
97
+ @logger.warn "Flag not found for identifier '#{identifier}'. Serving default variation."
98
+ return nil
99
+ end
100
+
101
+ # Check if the flag's kind matches the expected type
102
+ unless flag.kind == expected
103
+ @logger.warn "Flag kind mismatch: expected '#{expected}', but got '#{flag.kind}' for identifier '#{identifier}'. Serving default variation."
104
+ return nil
105
+ end
106
+
107
+ # Proceed with prerequisite check if flag type is as expected
94
108
  if flag != nil && flag.kind == expected
95
109
  unless flag.prerequisites.empty?
96
110
  pre_req = check_pre_requisite(flag, target)
@@ -109,6 +123,7 @@ class Evaluator < Evaluation
109
123
  end
110
124
  end
111
125
 
126
+ # Returning nil will indicate to callers to serve the default variation
112
127
  nil
113
128
  end
114
129
 
@@ -50,6 +50,7 @@ class InnerClient < ClientCallback
50
50
 
51
51
  @connector = connector
52
52
  end
53
+ @condition = ConditionVariable.new
53
54
 
54
55
  @closing = false
55
56
  @failure = false
@@ -66,22 +67,34 @@ class InnerClient < ClientCallback
66
67
  end
67
68
 
68
69
  def bool_variation(identifier, target, default_value)
69
-
70
+ unless @initialized
71
+ log_sdk_not_initialized_warning(identifier, default_value)
72
+ return default_value
73
+ end
70
74
  @evaluator.bool_variation(identifier, target, default_value, @evaluator_callback)
71
75
  end
72
76
 
73
77
  def string_variation(identifier, target, default_value)
74
-
78
+ unless @initialized
79
+ log_sdk_not_initialized_warning(identifier, default_value)
80
+ return default_value
81
+ end
75
82
  @evaluator.string_variation(identifier, target, default_value, @evaluator_callback)
76
83
  end
77
84
 
78
85
  def number_variation(identifier, target, default_value)
79
-
86
+ unless @initialized
87
+ log_sdk_not_initialized_warning(identifier, default_value)
88
+ return default_value
89
+ end
80
90
  @evaluator.number_variation(identifier, target, default_value, @evaluator_callback)
81
91
  end
82
92
 
83
93
  def json_variation(identifier, target, default_value)
84
-
94
+ unless @initialized
95
+ log_sdk_not_initialized_warning(identifier, default_value)
96
+ return default_value
97
+ end
85
98
  @evaluator.json_variation(identifier, target, default_value, @evaluator_callback)
86
99
  end
87
100
 
@@ -109,6 +122,7 @@ class InnerClient < ClientCallback
109
122
  def on_auth_failed
110
123
  SdkCodes::warn_auth_failed_srv_defaults @config.logger
111
124
  @initialized = true
125
+ @condition.signal
112
126
  end
113
127
 
114
128
  def close
@@ -195,67 +209,74 @@ class InnerClient < ClientCallback
195
209
  end
196
210
 
197
211
  def on_processor_ready(processor)
212
+ @my_mutex.synchronize do
198
213
 
199
- if @closing
214
+ if @closing
200
215
 
201
- return
202
- end
216
+ return
217
+ end
203
218
 
204
- if processor == @poll_processor
219
+ if processor == @poll_processor
205
220
 
206
- @poller_ready = true
207
- @config.logger.debug "PollingProcessor ready"
208
- end
221
+ @poller_ready = true
222
+ @config.logger.debug "PollingProcessor ready"
223
+ end
209
224
 
210
- if processor == @update_processor
225
+ if processor == @update_processor
211
226
 
212
- @stream_ready = true
213
- @config.logger.debug "Updater ready"
214
- end
227
+ @stream_ready = true
228
+ @config.logger.debug "Updater ready"
229
+ end
215
230
 
216
- if processor == @metrics_processor
231
+ if processor == @metrics_processor
217
232
 
218
- @metrics_ready = true
219
- @config.logger.debug "Metrics ready"
220
- end
233
+ @metrics_ready = true
234
+ @config.logger.debug "Metrics ready"
235
+ end
221
236
 
222
- if (@config.stream_enabled && !@stream_ready) ||
223
- (@config.analytics_enabled && !@metrics_ready) ||
224
- !@poller_ready
237
+ if (@config.stream_enabled && !@stream_ready) ||
238
+ (@config.analytics_enabled && !@metrics_ready) ||
239
+ !@poller_ready
225
240
 
226
- return
227
- end
241
+ return
242
+ end
228
243
 
229
- SdkCodes.info_sdk_init_ok @config.logger
244
+ SdkCodes.info_sdk_init_ok @config.logger
230
245
 
231
- @initialized = true
246
+ @condition.signal
247
+ @initialized = true
248
+ end
232
249
  end
233
250
 
234
251
  def wait_for_initialization(timeout: nil)
235
- synchronize do
236
- SdkCodes::info_sdk_waiting_to_initialize(@config.logger, timeout)
252
+ SdkCodes::info_sdk_waiting_to_initialize(@config.logger, timeout)
253
+ return if @initialized
237
254
 
255
+ @my_mutex.synchronize do
238
256
  start_time = Time.now
257
+ remaining = timeout ? timeout / 1000.0 : nil # Convert timeout to seconds
239
258
 
240
259
  until @initialized
241
- # Check if a timeout is specified and has been exceeded
242
- if timeout && (Time.now - start_time) > (timeout / 1000.0)
243
- @config.logger.warn "The SDK has timed out waiting to initialize with supplied timeout #{timeout} ms"
244
- handle_initialization_failure
245
- end
246
260
 
247
- sleep(1)
248
- end
261
+ # Break if timeout has elapsed
262
+ if remaining && remaining <= 0
263
+ @config.logger.warn "The SDK has timed out waiting to initialize with supplied timeout #{timeout} ms. The SDK will continue to initialize in the background. Default variations will be served until the SDK initializes."
264
+ break
265
+ end
266
+ # Wait for the signal or timeout
267
+ @condition.wait(@my_mutex, remaining)
249
268
 
250
- if @failure
251
- raise "Initialization failed"
269
+ # Recalculate the remaining time after the wait
270
+ if timeout
271
+ elapsed = Time.now - start_time
272
+ remaining = (timeout / 1000.0) - elapsed
273
+ end
252
274
  end
253
275
 
254
- @config.logger.debug "Waiting for initialization has completed"
276
+ @config.logger.debug "Waiting for initialization has completed" if @initialized
255
277
  end
256
278
  end
257
279
 
258
-
259
280
  protected
260
281
 
261
282
  def handle_initialization_failure
@@ -316,9 +337,8 @@ class InnerClient < ClientCallback
316
337
 
317
338
  private
318
339
 
319
- def synchronize(&block)
320
-
321
- @my_mutex.synchronize(&block)
340
+ def log_sdk_not_initialized_warning(identifier, default_value)
341
+ @config.logger.warn "SDKCODE:6001: SDK is not initialized; serving default variation for bool variation: identifier=#{identifier}, default=#{default_value}"
322
342
  end
323
343
 
324
344
  end
@@ -21,7 +21,11 @@ class MetricsEvent
21
21
  # project sizes. Issue being tracked in FFM-12192, and once resolved, can feasibly remove
22
22
  # these checks in a future release.
23
23
  unless other.is_a?(MetricsEvent)
24
- @logger.warn("Warning: Attempted to compare MetricsEvent with #{other.class.name}" )
24
+ # We should always have a logger available except when we've deep cloned this class. We don't do any
25
+ # equality check on clones in metrics code anyway, so this is just a safety check.
26
+ if @logger
27
+ @logger.warn("Warning: Attempted to compare MetricsEvent with #{other.class.name}")
28
+ end
25
29
  return false
26
30
  end
27
31
 
@@ -35,4 +39,15 @@ class MetricsEvent
35
39
  end
36
40
 
37
41
 
42
+ # Exclude logger from serialization
43
+ def marshal_dump
44
+ [@feature_config, @target, @variation]
45
+ end
46
+
47
+ def marshal_load(array)
48
+ @feature_config, @target, @variation = array
49
+ @logger = nil
50
+ end
51
+
52
+
38
53
  end
@@ -1,5 +1,6 @@
1
1
  require "time"
2
- require "concurrent-ruby"
2
+ require 'thread'
3
+ require "set"
3
4
 
4
5
  require_relative "../dto/target"
5
6
  require_relative "../../sdk/version"
@@ -11,40 +12,6 @@ require_relative "../api/summary_metrics"
11
12
  class MetricsProcessor < Closeable
12
13
  GLOBAL_TARGET = Target.new(identifier: "__global__cf_target", name: "Global Target").freeze
13
14
 
14
- class FrequencyMap < Concurrent::Map
15
- def initialize(options = nil, &block)
16
- super
17
- end
18
-
19
- def increment(key)
20
- compute(key) do |old_value|
21
- if old_value == nil;
22
- 1
23
- else
24
- old_value + 1
25
- end
26
- end
27
- end
28
-
29
- def get(key)
30
- self[key]
31
- end
32
-
33
- # TODO Will be removed in V2 in favour of simplified clearing. Currently not used outside of tests.
34
- def drain_to_map
35
- result = {}
36
- each_key do |key|
37
- result[key] = 0
38
- end
39
- result.each_key do |key|
40
- value = get_and_set(key, 0)
41
- result[key] = value
42
- delete_pair(key, 0)
43
- end
44
- result
45
- end
46
- end
47
-
48
15
  def init(connector, config, callback)
49
16
 
50
17
  unless connector.kind_of?(Connector)
@@ -76,15 +43,20 @@ class MetricsProcessor < Closeable
76
43
  @feature_name_attribute = "featureName"
77
44
  @variation_identifier_attribute = "variationIdentifier"
78
45
 
79
- @executor = Concurrent::FixedThreadPool.new(10)
80
-
81
- # Used for locking the evalution and target metrics maps before we clone them
46
+ # Evaluation and target metrics
82
47
  @metric_maps_mutex = Mutex.new
83
- @evaluation_metrics = FrequencyMap.new
84
- @target_metrics = Concurrent::Map.new
48
+ @evaluation_metrics = {}
49
+ @target_metrics = {}
85
50
 
86
- # Keep track of targets that have already been sent to avoid sending them again
87
- @seen_targets = Concurrent::Map.new
51
+ # Keep track of targets that have already been sent to avoid sending them again. We track a max 500K targets
52
+ # to prevent unbounded growth.
53
+ @seen_targets_mutex = Mutex.new
54
+ @seen_targets = Set.new
55
+ @max_seen_targets = 500000
56
+ @seen_targets_full = false
57
+
58
+ # Mutex to protect aggregation and sending metrics at the end of an interval
59
+ @send_data_mutex = Mutex.new
88
60
 
89
61
  @callback.on_metrics_ready
90
62
  end
@@ -118,9 +90,9 @@ class MetricsProcessor < Closeable
118
90
 
119
91
  def register_evaluation_metric(feature_config, variation)
120
92
  # Guard clause to ensure feature_config, @global_target, and variation are valid.
121
- # While they should be, this adds protection for an edge case we are seeing with very large
122
- # project sizes. Issue being tracked in FFM-12192, and once resolved, can feasibly remove
123
- # these checks in a future release.
93
+ # While they should be, this adds protection for an edge case we are seeing where the the ConcurrentMap (now replaced with our own thread safe hash)
94
+ # seemed to be accessing invalid areas of memory and seg faulting.
95
+ # Issue being tracked in FFM-12192, and once resolved, can remove these checks in a future release && once the issue is resolved.
124
96
  if feature_config.nil? || !feature_config.respond_to?(:feature) || feature_config.feature.nil?
125
97
  @config.logger.warn("Skipping invalid MetricsEvent: feature_config is missing or incomplete. feature_config=#{feature_config.inspect}")
126
98
  return
@@ -138,72 +110,81 @@ class MetricsProcessor < Closeable
138
110
 
139
111
  event = MetricsEvent.new(feature_config, GLOBAL_TARGET, variation, @config.logger)
140
112
  @metric_maps_mutex.synchronize do
141
- @evaluation_metrics.increment event
113
+ @evaluation_metrics[event] = (@evaluation_metrics[event] || 0) + 1
142
114
  end
143
115
  end
144
116
 
145
117
  def register_target_metric(target)
146
- if target.is_private
147
- return
148
- end
149
-
150
- already_seen = @seen_targets.put_if_absent(target.identifier, true)
151
-
152
- if already_seen
153
- return
118
+ return if target.is_private
119
+
120
+ add_to_target_metrics = @seen_targets_mutex.synchronize do
121
+ # If the set is full, directly allow adding to target_metrics
122
+ if @seen_targets_full
123
+ true
124
+ elsif @seen_targets.include?(target.identifier)
125
+ false
126
+ else
127
+ @seen_targets.add(target.identifier)
128
+ @seen_targets_full = @seen_targets.size >= @max_seen_targets
129
+ true
130
+ end
154
131
  end
155
132
 
133
+ # Add to target_metrics if marked for inclusion
156
134
  @metric_maps_mutex.synchronize do
157
- @target_metrics.put(target.identifier, target)
158
- end
135
+ @target_metrics[target.identifier] = target
136
+ end if add_to_target_metrics
159
137
  end
160
138
 
139
+
161
140
  def run_one_iteration
162
141
  send_data_and_reset_cache(@evaluation_metrics, @target_metrics)
163
142
  end
164
143
 
165
144
  def send_data_and_reset_cache(evaluation_metrics_map, target_metrics_map)
166
- # A single lock is used to synchronise access to both the evaluation and target metrics maps.
167
- # While separate locks could be applied to each map individually, we want an interval's eval/target
168
- # metrics to be processed in an atomic unit.
169
- evaluation_metrics_map_clone, target_metrics_map_clone = @metric_maps_mutex.synchronize do
170
-
171
- clone_evaluations = Concurrent::Map.new
172
- clone_targets = Concurrent::Map.new
173
- # Clone and clear evaluation metrics map
174
- evaluation_metrics_map.each_pair do |key, value|
175
- clone_evaluations[key] = value
176
- end
145
+ @send_data_mutex.synchronize do
146
+ begin
177
147
 
178
- evaluation_metrics_map.clear
179
-
180
- target_metrics_map.each_pair do |key, value|
181
- clone_targets[key] = value
182
- end
148
+ evaluation_metrics_map_clone, target_metrics_map_clone = @metric_maps_mutex.synchronize do
149
+ # Check if we have metrics to send; if not, skip sending metrics
150
+ if evaluation_metrics_map.empty? && target_metrics_map.empty?
151
+ @config.logger.debug "No metrics to send. Skipping sending metrics this interval"
152
+ return
153
+ end
183
154
 
184
- target_metrics_map.clear
155
+ # Deep clone the evaluation metrics
156
+ cloned_evaluations = Marshal.load(Marshal.dump(evaluation_metrics_map)).freeze
157
+ evaluation_metrics_map.clear
185
158
 
186
- [clone_evaluations, clone_targets]
159
+ # Deep clone the target metrics
160
+ cloned_targets = Marshal.load(Marshal.dump(target_metrics_map)).freeze
161
+ target_metrics_map.clear
162
+ [cloned_evaluations, cloned_targets]
187
163
 
188
- end
164
+ end
189
165
 
190
- metrics = prepare_summary_metrics_body(evaluation_metrics_map_clone, target_metrics_map_clone)
166
+ metrics = prepare_summary_metrics_body(evaluation_metrics_map_clone, target_metrics_map_clone)
191
167
 
192
- unless metrics.metrics_data.empty?
193
- start_time = (Time.now.to_f * 1000).to_i
194
- @connector.post_metrics(metrics)
195
- end_time = (Time.now.to_f * 1000).to_i
196
- if end_time - start_time > @config.metrics_service_acceptable_duration
197
- @config.logger.debug "Metrics service API duration=[" + (end_time - start_time).to_s + "]"
168
+ unless metrics.metrics_data.empty?
169
+ start_time = (Time.now.to_f * 1000).to_i
170
+ @connector.post_metrics(metrics)
171
+ end_time = (Time.now.to_f * 1000).to_i
172
+ if end_time - start_time > @config.metrics_service_acceptable_duration
173
+ @config.logger.debug "Metrics service API duration=[" + (end_time - start_time).to_s + "]"
174
+ end
175
+ end
176
+ rescue => e
177
+ @config.logger.warn "Error when preparing and sending metrics: #{e.message}"
178
+ @config.logger.warn e.backtrace&.join("\n") || "No backtrace available"
198
179
  end
199
180
  end
200
181
  end
201
182
 
202
- def prepare_summary_metrics_body(evaluation_metrics_map, target_metrics_map)
183
+ def prepare_summary_metrics_body(evaluation_metrics_clone, target_metrics_clone)
203
184
  metrics = OpenapiClient::Metrics.new({ :target_data => [], :metrics_data => [] })
204
185
 
205
186
  total_count = 0
206
- evaluation_metrics_map.each do |key, value|
187
+ evaluation_metrics_clone.each do |key, value|
207
188
  # While Components should not be missing, this adds protection for an edge case we are seeing with very large
208
189
  # project sizes. Issue being tracked in FFM-12192, and once resolved, can feasibly remove
209
190
  # these checks in a future release.
@@ -235,9 +216,9 @@ class MetricsProcessor < Closeable
235
216
  metrics_data.attributes.push(OpenapiClient::KeyValue.new({ :key => @sdk_version, :value => @jar_version }))
236
217
  metrics.metrics_data.push(metrics_data)
237
218
  end
238
- @config.logger.debug "Pushed #{total_count} metric evaluations to server. metrics_data count is #{evaluation_metrics_map.size}. target_data count is #{target_metrics_map.size}"
219
+ @config.logger.debug "Pushed #{total_count} metric evaluations to server. metrics_data count is #{evaluation_metrics_clone.size}. target_data count is #{target_metrics_clone.size}"
239
220
 
240
- target_metrics_map.each_pair do |_, value|
221
+ target_metrics_clone.each_pair do |_, value|
241
222
  add_target_data(metrics, value)
242
223
  end
243
224
 
@@ -276,18 +257,27 @@ class MetricsProcessor < Closeable
276
257
  @running = true
277
258
  @thread = Thread.new do
278
259
  @config.logger.debug "Async started: " + self.to_s
279
- while @ready do
260
+ mutex = Mutex.new
261
+ condition = ConditionVariable.new
262
+
263
+ while @ready
280
264
  unless @initialized
281
265
  @initialized = true
282
- SdkCodes::info_metrics_thread_started @config.logger
266
+ SdkCodes::info_metrics_thread_started(@config.logger)
283
267
  end
284
- sleep(@config.frequency)
285
- run_one_iteration
268
+
269
+ mutex.synchronize do
270
+ # Wait for the specified interval or until notified
271
+ condition.wait(mutex, @config.frequency)
272
+ end
273
+
274
+ # Re-check @ready before running the iteration
275
+ run_one_iteration if @ready
286
276
  end
287
277
  end
288
- @thread.run
289
278
  end
290
279
 
280
+
291
281
  def stop_async
292
282
  @ready = false
293
283
  @initialized = false
@@ -15,7 +15,7 @@ class UpdateProcessor < Closeable
15
15
  @connector = connector
16
16
  @repository = repository
17
17
  @updater = callback
18
- @executor = Concurrent::FixedThreadPool.new(100)
18
+ @executor = Concurrent::FixedThreadPool.new(20)
19
19
 
20
20
  if logger != nil
21
21
 
@@ -5,7 +5,7 @@ module Ff
5
5
  module Server
6
6
  module Sdk
7
7
 
8
- VERSION = "1.4.3"
8
+ VERSION = "1.4.4"
9
9
  end
10
10
  end
11
11
  end
data/scripts/sdk_specs.sh CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/bin/bash
2
2
 
3
3
  export ff_ruby_sdk="ff-ruby-server-sdk"
4
- export ff_ruby_sdk_version="1.4.3"
4
+ export ff_ruby_sdk_version="1.4.4"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ff-ruby-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.3
4
+ version: 1.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - 'Miloš Vasić, cyr.: Милош Васић'
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-13 00:00:00.000000000 Z
11
+ date: 2024-11-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -142,14 +142,14 @@ dependencies:
142
142
  requirements:
143
143
  - - '='
144
144
  - !ruby/object:Gem::Version
145
- version: 0.1.6
145
+ version: 0.1.7
146
146
  type: :runtime
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - '='
151
151
  - !ruby/object:Gem::Version
152
- version: 0.1.6
152
+ version: 0.1.7
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: typhoeus
155
155
  requirement: !ruby/object:Gem::Requirement