ff-ruby-server-sdk 1.4.0 → 1.4.2

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: 6411beee7ad512c4e70efc7d9fbd45c71835b18eff4bf41b8e056333074dc083
4
- data.tar.gz: 1f13b504ffda1c95f4956813ec8dc887935340a26dcba09689864250dbf4589b
3
+ metadata.gz: d40a21b8332b96f4c3f507f2adece4bc12a9475fb87518323c3b79f959d2782a
4
+ data.tar.gz: 4be776d630a6c97cc563e801b74b8c25e6250ea3611fa83b6a1b26274ead4d06
5
5
  SHA512:
6
- metadata.gz: f3cc5c07fe09718e260ca6ab19bb720d0db276a60f6ff9b9229a4374bdfeebce0b519e6edbbf82d202183382f55c447d3ed2293b614035ab4f8e2ff7880b4d56
7
- data.tar.gz: 7771ae2a6823afffa73dbda7e49beceab7ad973e8ef4cb8dfdd1781ca0de7ee3a10822d19c58a749a90ffe492c72e5dbcaf0b8a05a04159c510e38d675ff25c9
6
+ metadata.gz: d11cd5ae4daddce986eae1faa3ca10d16e68a0c708e4e15131d0ee74883e67bac88af39cedfa1ee5497f7ac916bfd270bd8067bcbf71c3e90de242520ff49cd1
7
+ data.tar.gz: bfc9bc6662bc500c25281c36bb9d8352ba16bdf8ad9f43c06b44074aa4ff791e2ddb8e1e84b1796bd4fb10bfe819190d6eb33bde7ac315eff02e918784653fcb
@@ -46,7 +46,7 @@ The synchronous method is useful for environments where feature flag decisions a
46
46
 
47
47
  You can use the `wait_for_initialization` method, optionally providing a timeout in milliseconds to prevent waiting indefinitely in case of unrecoverable isues, e.g. incorrect API key used.
48
48
 
49
- **Usage with a timeout**
49
+ **Usage without a timeout**
50
50
 
51
51
  ```ruby
52
52
  client = CfClient.instance
@@ -57,13 +57,15 @@ client.wait_for_initialization
57
57
  result = client.bool_variation("bool_flag", target, false)
58
58
  ```
59
59
 
60
- **Usage without a timeout**
60
+ **Usage with a timeout**
61
61
 
62
62
  ```ruby
63
63
  client = CfClient.instance
64
64
  client.init(api_key, config)
65
65
 
66
- client.wait_for_initialization(timeout_ms: 3000)
66
+ # Only wait for 30 seconds, after which if the SDK has not initialized the call will
67
+ # unblock and the SDK will then serve defaults
68
+ client.wait_for_initialization(timeout_ms: 30000)
67
69
 
68
70
  result = client.bool_variation("bool_flag", target, false)
69
71
  ```
@@ -1,56 +1,23 @@
1
-
1
+ require 'singleton'
2
2
  require_relative "../../generated/lib/openapi_client"
3
3
  require_relative "../common/closeable"
4
4
  require_relative "inner_client"
5
5
 
6
6
  class CfClient < Closeable
7
-
8
- # Static:
9
- class << self
10
-
11
- @@instance = CfClient.new
12
-
13
- def instance
14
-
15
- @@instance
7
+ include Singleton
8
+
9
+ def init(api_key, config, connector = nil)
10
+ # Only initialize if @client is nil to avoid reinitialization
11
+ unless @client
12
+ @config = config || ConfigBuilder.new.build
13
+ @client = InnerClient.new(api_key, @config, connector)
14
+ @config.logger.debug "Client initialized with API key: #{api_key}"
16
15
  end
16
+ @client
17
17
  end
18
18
 
19
- # Static - End
20
-
21
- def initialize(api_key = nil, config = nil, connector = nil)
22
-
23
- if config == nil
24
-
25
- @config = ConfigBuilder.new.build
26
- else
27
-
28
- @config = config
29
- end
30
-
31
- @client = InnerClient.new(api_key, config, connector)
32
-
33
- @config.logger.debug "Client (1): " + @client.to_s
34
- end
35
-
36
- def init(api_key = nil, config = nil, connector = nil)
37
-
38
- if @client == nil
39
-
40
- @config = config
41
-
42
- @client = InnerClient.new(
43
-
44
- api_key = api_key,
45
- config = config,
46
- connector = connector
47
- )
48
-
49
- @config.logger.debug "Client (2): " + @client.to_s
50
- end
51
- end
52
19
 
53
- def wait_for_initialization(timeout_ms: nil)
20
+ def wait_for_initialization(timeout_ms: nil)
54
21
  if @client != nil
55
22
  @client.wait_for_initialization(timeout: timeout_ms)
56
23
  end
@@ -23,7 +23,7 @@ class InnerClientFlagEvaluateCallback < FlagEvaluateCallback
23
23
 
24
24
  def process_evaluation(feature_config, target, variation)
25
25
 
26
- @logger.debug "Processing evaluation: " + feature_config.feature.to_s + ", " + target.identifier.to_s
26
+ @logger.debug "Processing evaluation: #{feature_config&.feature || 'nil feature'}, #{target&.identifier || 'nil target'}"
27
27
 
28
28
  @metrics_processor.register_evaluation(target, feature_config, variation)
29
29
  end
@@ -2,11 +2,12 @@ class MetricsEvent
2
2
 
3
3
  attr_accessor :feature_config, :target, :variation
4
4
 
5
- def initialize(feature_config, target, variation)
5
+ def initialize(feature_config, target, variation, logger)
6
6
 
7
7
  @target = target
8
8
  @variation = variation
9
9
  @feature_config = feature_config
10
+ @logger = logger
10
11
  freeze
11
12
  end
12
13
 
@@ -15,6 +16,15 @@ class MetricsEvent
15
16
  end
16
17
 
17
18
  def eql?(other)
19
+ # Guard clause other is the same type.
20
+ # While it should be, this adds protection for an edge case we are seeing with very large
21
+ # project sizes. Issue being tracked in FFM-12192, and once resolved, can feasibly remove
22
+ # these checks in a future release.
23
+ unless other.is_a?(MetricsEvent)
24
+ @logger.warn("Warning: Attempted to compare MetricsEvent with #{other.class.name}" )
25
+ return false
26
+ end
27
+
18
28
  feature_config.feature == other.feature_config.feature and
19
29
  variation.identifier == other.variation.identifier and
20
30
  target.identifier == other.target.identifier
@@ -9,6 +9,7 @@ require_relative "../api/metrics_event"
9
9
  require_relative "../api/summary_metrics"
10
10
 
11
11
  class MetricsProcessor < Closeable
12
+ GLOBAL_TARGET = Target.new(identifier: "__global__cf_target", name: "Global Target").freeze
12
13
 
13
14
  class FrequencyMap < Concurrent::Map
14
15
  def initialize(options = nil, &block)
@@ -29,6 +30,7 @@ class MetricsProcessor < Closeable
29
30
  self[key]
30
31
  end
31
32
 
33
+ # TODO Will be removed in V2 in favour of simplified clearing. Currently not used outside of tests.
32
34
  def drain_to_map
33
35
  result = {}
34
36
  each_key do |key|
@@ -65,7 +67,6 @@ class MetricsProcessor < Closeable
65
67
  @target_attribute = "target"
66
68
  @global_target_identifier = "__global__cf_target" # <--- This target identifier is used to aggregate and send data for all
67
69
  # targets as a summary
68
- @global_target = Target.new("RubySDK1", identifier = @global_target_identifier, name = @global_target_name)
69
70
  @ready = false
70
71
  @jar_version = Ff::Ruby::Server::Sdk::VERSION
71
72
  @server = "server"
@@ -77,6 +78,8 @@ class MetricsProcessor < Closeable
77
78
 
78
79
  @executor = Concurrent::FixedThreadPool.new(10)
79
80
 
81
+ # Used for locking the evalution and target metrics maps before we clone them
82
+ @metric_maps_mutex = Mutex.new
80
83
  @evaluation_metrics = FrequencyMap.new
81
84
  @target_metrics = Concurrent::Map.new
82
85
 
@@ -111,12 +114,34 @@ class MetricsProcessor < Closeable
111
114
 
112
115
  def register_evaluation(target, feature_config, variation)
113
116
  register_evaluation_metric(feature_config, variation)
114
- register_target_metric(target)
117
+ if target
118
+ register_target_metric(target)
119
+ end
115
120
  end
116
121
 
117
122
  private
118
123
 
119
124
  def register_evaluation_metric(feature_config, variation)
125
+ # Guard clause to ensure feature_config, @global_target, and variation are valid.
126
+ # While they should be, this adds protection for an edge case we are seeing with very large
127
+ # project sizes. Issue being tracked in FFM-12192, and once resolved, can feasibly remove
128
+ # these checks in a future release.
129
+ if feature_config.nil? || !feature_config.respond_to?(:feature) || feature_config.feature.nil?
130
+ @config.logger.warn("Skipping invalid MetricsEvent: feature_config is missing or incomplete. feature_config=#{feature_config.inspect}")
131
+ return
132
+ end
133
+
134
+ if GLOBAL_TARGET.nil? || !GLOBAL_TARGET.respond_to?(:identifier) || GLOBAL_TARGET.identifier.nil?
135
+ @config.logger.warn("Skipping invalid MetricsEvent: global_target is missing or incomplete. global_target=#{GLOBAL_TARGET.inspect}")
136
+ return
137
+ end
138
+
139
+ if variation.nil? || !variation.respond_to?(:identifier) || variation.identifier.nil?
140
+ @config.logger.warn("Skipping iInvalid MetricsEvent: variation is missing or incomplete. variation=#{variation.inspect}")
141
+ return
142
+ end
143
+
144
+
120
145
  if @evaluation_metrics.size > @max_buffer_size
121
146
  unless @evaluation_warning_issued.true?
122
147
  SdkCodes.warn_metrics_evaluations_max_size_exceeded(@config.logger)
@@ -125,7 +150,7 @@ class MetricsProcessor < Closeable
125
150
  return
126
151
  end
127
152
 
128
- event = MetricsEvent.new(feature_config, @global_target, variation)
153
+ event = MetricsEvent.new(feature_config, GLOBAL_TARGET, variation, @config.logger)
129
154
  @evaluation_metrics.increment event
130
155
  end
131
156
 
@@ -158,15 +183,27 @@ class MetricsProcessor < Closeable
158
183
  end
159
184
 
160
185
  def send_data_and_reset_cache(evaluation_metrics_map, target_metrics_map)
161
- evaluation_metrics_map_clone = evaluation_metrics_map.drain_to_map
162
186
 
187
+ evaluation_metrics_map_clone = Concurrent::Map.new
163
188
  target_metrics_map_clone = Concurrent::Map.new
164
189
 
165
- target_metrics_map.each_pair do |key, value|
166
- target_metrics_map_clone[key] = value
167
- end
190
+ # A single lock is used to synchronise access to both the evaluation and target metrics maps.
191
+ # While separate locks could be applied to each map individually, we want an interval's eval/target
192
+ # metrics to be processed in an atomic unit.
193
+ @metric_maps_mutex.synchronize do
194
+ # Clone and clear evaluation metrics map
195
+ evaluation_metrics_map.each_pair do |key, value|
196
+ evaluation_metrics_map_clone[key] = value
197
+ end
168
198
 
169
- target_metrics_map.clear
199
+ evaluation_metrics_map.clear
200
+
201
+ target_metrics_map.each_pair do |key, value|
202
+ target_metrics_map_clone[key] = value
203
+ end
204
+
205
+ target_metrics_map.clear
206
+ end
170
207
 
171
208
  @evaluation_warning_issued.make_false
172
209
  @target_warning_issued.make_false
@@ -188,6 +225,24 @@ class MetricsProcessor < Closeable
188
225
 
189
226
  total_count = 0
190
227
  evaluation_metrics_map.each do |key, value|
228
+ # While Components should not be missing, this adds protection for an edge case we are seeing with very large
229
+ # project sizes. Issue being tracked in FFM-12192, and once resolved, can feasibly remove
230
+ # these checks in a future release.
231
+ # Initialize an array to collect missing components
232
+ missing_components = []
233
+
234
+ # Check each required component and add to missing_components if absent
235
+ missing_components << 'feature_config' unless key.respond_to?(:feature_config) && key.feature_config
236
+ missing_components << 'variation' unless key.respond_to?(:variation) && key.variation
237
+ missing_components << 'target' unless key.respond_to?(:target) && key.target
238
+ missing_components << 'count' if value.nil?
239
+
240
+ # If any components are missing, log a detailed warning and skip processing
241
+ unless missing_components.empty?
242
+ @config.logger.warn "Skipping invalid metrics event: missing #{missing_components.join(', ')} in key: #{key.inspect}, full details: #{key.inspect}"
243
+ next
244
+ end
245
+
191
246
  total_count += value
192
247
  metrics_data = OpenapiClient::MetricsData.new({ :attributes => [] })
193
248
  metrics_data.timestamp = (Time.now.to_f * 1000).to_i
@@ -5,7 +5,7 @@ module Ff
5
5
  module Server
6
6
  module Sdk
7
7
 
8
- VERSION = "1.4.0"
8
+ VERSION = "1.4.2"
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.0"
4
+ export ff_ruby_sdk_version="1.4.2"
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.0
4
+ version: 1.4.2
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-10-02 00:00:00.000000000 Z
11
+ date: 2024-11-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -305,7 +305,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
305
305
  - !ruby/object:Gem::Version
306
306
  version: '0'
307
307
  requirements: []
308
- rubygems_version: 3.5.16
308
+ rubygems_version: 3.5.22
309
309
  signing_key:
310
310
  specification_version: 4
311
311
  summary: Harness is a feature management platform that helps teams to build better