ff-ruby-server-sdk 1.3.2 → 1.4.1

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: 2e06f7cec3eb38bff792b8406d0ff11fb487b8662c78ee9bdba3fdad46526e5f
4
- data.tar.gz: 7bd8b2c5d1cb6955bdcb9ca6363a6c8afa89cc8b8dbda7769dc0bf60eca62ba0
3
+ metadata.gz: e2bcc6b6312ee2462bbf6a2a689ddee47c6000bc010db561eb72b5204a7c8d8f
4
+ data.tar.gz: 7058cd57f6e72cb2c649b51071eaebed405ff3aac691f4aa186a096da70692b9
5
5
  SHA512:
6
- metadata.gz: 8eb61b5fa760cc59968d2ef51ddfe3263c3b75e9ec254304e9037a656a69318f4a2a7b6538c8ac5583394b5f0701af47c1ea2dbc1c94acb7dc830b4297c1d339
7
- data.tar.gz: 55af3f78ef20bc1cc0e667ef70fa60a5a7b41e79578db544038b0afc165470560cfd7566acf61f3902042b56d82f6264ef0d47ac121c149b6b77d31bfcf97b82
6
+ metadata.gz: 85ff6d9cda7f5579a7eeedce0f7ea293ccf95ff426b413f53aa09c22a0caa4bc1890607c63d67a0b3c58295d537f830a85e81ae45a2a6afafdd4d0521daa60df
7
+ data.tar.gz: '089aeb5abb536d43e118bade66a80fde7b70e1b74ce1a96fa32d53d7a4a61f08ec7b4c5d35a987e60f11fc4a8791b7233bad01c742bfc4ebc5545a738a13038d'
@@ -24,6 +24,52 @@ client.init(apiKey, ConfigBuilder
24
24
  | enableStream | analytics_enabled(false) | Enable streaming mode. | true |
25
25
  | enableAnalytics | stream_enabled(true) | Enable analytics. Metrics data is posted every 60s | true |
26
26
 
27
+ ## Client Initialization Options
28
+ The Harness Feature Flags SDK for Ruby provides flexible initialization strategies to accommodate various application requirements. You can choose between an asynchronous (non-blocking) or synchronous (blocking) approach to initialize the SDK.
29
+
30
+ ### Asynchronous (Non-Blocking) Initialization
31
+ The SDK can be initialized asynchronously without blocking the main thread or requiring a callback. In this case, defaults will be served until the SDK completes the initialization process.
32
+
33
+ ```ruby
34
+ client = CfClient.instance
35
+ client.init(api_key, config)
36
+
37
+ # Will serve default until the SDK completes initialization
38
+ result = client.bool_variation("bool_flag", target, false)
39
+ ```
40
+
41
+ ### Synchronous (Blocking) Initialization
42
+
43
+ In cases where it's critical to ensure the SDK is initialized before evaluating flags, the SDK offers a synchronous initialization method. This approach blocks the current thread until the SDK is fully initialized or the optional specified timeout (in milliseconds) period elapses.
44
+
45
+ The synchronous method is useful for environments where feature flag decisions are needed before continuing, such as during application startup.
46
+
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
+
49
+ **Usage without a timeout**
50
+
51
+ ```ruby
52
+ client = CfClient.instance
53
+ client.init(api_key, config)
54
+
55
+ client.wait_for_initialization
56
+
57
+ result = client.bool_variation("bool_flag", target, false)
58
+ ```
59
+
60
+ **Usage with a timeout**
61
+
62
+ ```ruby
63
+ client = CfClient.instance
64
+ client.init(api_key, config)
65
+
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)
69
+
70
+ result = client.bool_variation("bool_flag", target, false)
71
+ ```
72
+
27
73
  ## Logging Configuration
28
74
  You can provide your own logger to the SDK i.e. using the moneta logger we can do this
29
75
 
@@ -89,8 +89,9 @@ class AuthService < Closeable
89
89
  # 503 service unavailable
90
90
  # 504 gateway timeout
91
91
  # -1 OpenAPI error (timeout etc)
92
+ # 0 Un-categorised typhoeus error
92
93
  case code
93
- when 408,425,429,500,502,503,504,-1
94
+ when 408,425,429,500,502,503,504,-1, 0
94
95
  return true
95
96
  else
96
97
  return false
@@ -50,11 +50,9 @@ class CfClient < Closeable
50
50
  end
51
51
  end
52
52
 
53
- def wait_for_initialization
54
-
53
+ def wait_for_initialization(timeout_ms: nil)
55
54
  if @client != nil
56
-
57
- @client.wait_for_initialization
55
+ @client.wait_for_initialization(timeout: timeout_ms)
58
56
  end
59
57
  end
60
58
 
@@ -231,19 +231,23 @@ class InnerClient < ClientCallback
231
231
  @initialized = true
232
232
  end
233
233
 
234
- def wait_for_initialization
235
-
234
+ def wait_for_initialization(timeout: nil)
236
235
  synchronize do
236
+ SdkCodes::info_sdk_waiting_to_initialize(@config.logger, timeout)
237
237
 
238
- @config.logger.debug "Waiting for initialization to finish"
238
+ start_time = Time.now
239
239
 
240
240
  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
241
246
 
242
247
  sleep(1)
243
248
  end
244
249
 
245
250
  if @failure
246
-
247
251
  raise "Initialization failed"
248
252
  end
249
253
 
@@ -251,8 +255,17 @@ class InnerClient < ClientCallback
251
255
  end
252
256
  end
253
257
 
258
+
254
259
  protected
255
260
 
261
+ def handle_initialization_failure
262
+ @auth_service.close
263
+ @poll_processor.stop
264
+ @update_processor.stop
265
+ @metrics_processor.stop
266
+ on_auth_failed
267
+ end
268
+
256
269
  def setup
257
270
 
258
271
  @repository = StorageRepository.new(@config.cache, @repository_callback, @config.store, @config.logger)
@@ -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"
@@ -111,12 +112,34 @@ class MetricsProcessor < Closeable
111
112
 
112
113
  def register_evaluation(target, feature_config, variation)
113
114
  register_evaluation_metric(feature_config, variation)
114
- register_target_metric(target)
115
+ if target
116
+ register_target_metric(target)
117
+ end
115
118
  end
116
119
 
117
120
  private
118
121
 
119
122
  def register_evaluation_metric(feature_config, variation)
123
+ # Guard clause to ensure feature_config, @global_target, and variation are valid.
124
+ # While they should be, this adds protection for an edge case we are seeing with very large
125
+ # project sizes. Issue being tracked in FFM-12192, and once resolved, can feasibly remove
126
+ # these checks in a future release.
127
+ if feature_config.nil? || !feature_config.respond_to?(:feature) || feature_config.feature.nil?
128
+ @config.logger.warn("Skipping invalid MetricsEvent: feature_config is missing or incomplete. feature_config=#{feature_config.inspect}")
129
+ return
130
+ end
131
+
132
+ if GLOBAL_TARGET.nil? || !GLOBAL_TARGET.respond_to?(:identifier) || GLOBAL_TARGET.identifier.nil?
133
+ @config.logger.warn("Skipping invalid MetricsEvent: global_target is missing or incomplete. global_target=#{GLOBAL_TARGET.inspect}")
134
+ return
135
+ end
136
+
137
+ if variation.nil? || !variation.respond_to?(:identifier) || variation.identifier.nil?
138
+ @config.logger.warn("Skipping iInvalid MetricsEvent: variation is missing or incomplete. variation=#{variation.inspect}")
139
+ return
140
+ end
141
+
142
+
120
143
  if @evaluation_metrics.size > @max_buffer_size
121
144
  unless @evaluation_warning_issued.true?
122
145
  SdkCodes.warn_metrics_evaluations_max_size_exceeded(@config.logger)
@@ -125,7 +148,7 @@ class MetricsProcessor < Closeable
125
148
  return
126
149
  end
127
150
 
128
- event = MetricsEvent.new(feature_config, @global_target, variation)
151
+ event = MetricsEvent.new(feature_config, GLOBAL_TARGET, variation, @config.logger)
129
152
  @evaluation_metrics.increment event
130
153
  end
131
154
 
@@ -158,8 +181,14 @@ class MetricsProcessor < Closeable
158
181
  end
159
182
 
160
183
  def send_data_and_reset_cache(evaluation_metrics_map, target_metrics_map)
161
- evaluation_metrics_map_clone = evaluation_metrics_map.drain_to_map
184
+ # Clone and clear evaluation metrics map
185
+ evaluation_metrics_map_clone = Concurrent::Map.new
162
186
 
187
+ evaluation_metrics_map.each_pair do |key, value|
188
+ evaluation_metrics_map_clone[key] = value
189
+ end
190
+
191
+ evaluation_metrics_map.clear
163
192
  target_metrics_map_clone = Concurrent::Map.new
164
193
 
165
194
  target_metrics_map.each_pair do |key, value|
@@ -188,6 +217,24 @@ class MetricsProcessor < Closeable
188
217
 
189
218
  total_count = 0
190
219
  evaluation_metrics_map.each do |key, value|
220
+ # While Components should not be missing, this adds protection for an edge case we are seeing with very large
221
+ # project sizes. Issue being tracked in FFM-12192, and once resolved, can feasibly remove
222
+ # these checks in a future release.
223
+ # Initialize an array to collect missing components
224
+ missing_components = []
225
+
226
+ # Check each required component and add to missing_components if absent
227
+ missing_components << 'feature_config' unless key.respond_to?(:feature_config) && key.feature_config
228
+ missing_components << 'variation' unless key.respond_to?(:variation) && key.variation
229
+ missing_components << 'target' unless key.respond_to?(:target) && key.target
230
+ missing_components << 'count' if value.nil?
231
+
232
+ # If any components are missing, log a detailed warning and skip processing
233
+ unless missing_components.empty?
234
+ @config.logger.warn "Skipping invalid metrics event: missing #{missing_components.join(', ')} in key: #{key.inspect}, full details: #{key.inspect}"
235
+ next
236
+ end
237
+
191
238
  total_count += value
192
239
  metrics_data = OpenapiClient::MetricsData.new({ :attributes => [] })
193
240
  metrics_data.timestamp = (Time.now.to_f * 1000).to_i
@@ -14,6 +14,17 @@ class SdkCodes
14
14
  logger.info SdkCodes.sdk_err_msg(1000)
15
15
  end
16
16
 
17
+ def self.info_sdk_waiting_to_initialize(logger, timeout)
18
+ if timeout
19
+ message = "with timeout: #{timeout} ms"
20
+ else
21
+
22
+ message = "with no timeout"
23
+
24
+ end
25
+ logger.info SdkCodes.sdk_err_msg(1003, message)
26
+ end
27
+
17
28
  def self.info_sdk_auth_ok(logger)
18
29
  logger.info SdkCodes.sdk_err_msg(2000)
19
30
  end
@@ -76,6 +87,7 @@ class SdkCodes
76
87
  1000 => "The SDK has successfully initialized",
77
88
  1001 => "The SDK has failed to initialize due to the following authentication error:",
78
89
  1002 => "The SDK has failed to initialize due to a missing or empty API key",
90
+ 1003 => "The SDK is waiting for initialzation to complete",
79
91
  # SDK_AUTH_2xxx
80
92
  2000 => "Authenticated ok",
81
93
  2001 => "Authentication failed with a non-recoverable error - defaults will be served",
@@ -46,6 +46,11 @@ class HarnessConnector < Connector
46
46
  # determine if a timeout has occurred. This is fixed in 6.3.0 but requires Ruby version to be increased to 2.7
47
47
  # https://github.com/OpenAPITools/openapi-generator/releases/tag/v6.3.0
48
48
  @config.logger.warn "OpenapiClient::ApiError [\n\n#{e}\n]"
49
+
50
+ if e.code
51
+ return e.code
52
+ end
53
+
49
54
  return -1
50
55
  end
51
56
 
@@ -5,7 +5,7 @@ module Ff
5
5
  module Server
6
6
  module Sdk
7
7
 
8
- VERSION = "1.3.2"
8
+ VERSION = "1.4.1"
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.3.2"
4
+ export ff_ruby_sdk_version="1.4.1"
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.3.2
4
+ version: 1.4.1
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-09-09 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