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 +4 -4
- data/docs/further_reading.md +46 -0
- data/lib/ff/ruby/server/sdk/api/auth_service.rb +2 -1
- data/lib/ff/ruby/server/sdk/api/cf_client.rb +2 -4
- data/lib/ff/ruby/server/sdk/api/inner_client.rb +17 -4
- data/lib/ff/ruby/server/sdk/api/inner_client_flag_evaluate_callback.rb +1 -1
- data/lib/ff/ruby/server/sdk/api/metrics_event.rb +11 -1
- data/lib/ff/ruby/server/sdk/api/metrics_processor.rb +51 -4
- data/lib/ff/ruby/server/sdk/common/sdk_codes.rb +12 -0
- data/lib/ff/ruby/server/sdk/connector/harness_connector.rb +5 -0
- data/lib/ff/ruby/server/sdk/version.rb +1 -1
- data/scripts/sdk_specs.sh +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e2bcc6b6312ee2462bbf6a2a689ddee47c6000bc010db561eb72b5204a7c8d8f
|
4
|
+
data.tar.gz: 7058cd57f6e72cb2c649b51071eaebed405ff3aac691f4aa186a096da70692b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85ff6d9cda7f5579a7eeedce0f7ea293ccf95ff426b413f53aa09c22a0caa4bc1890607c63d67a0b3c58295d537f830a85e81ae45a2a6afafdd4d0521daa60df
|
7
|
+
data.tar.gz: '089aeb5abb536d43e118bade66a80fde7b70e1b74ce1a96fa32d53d7a4a61f08ec7b4c5d35a987e60f11fc4a8791b7233bad01c742bfc4ebc5545a738a13038d'
|
data/docs/further_reading.md
CHANGED
@@ -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
|
-
|
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:
|
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
|
-
|
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,
|
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
|
-
|
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
|
|
data/scripts/sdk_specs.sh
CHANGED
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
|
+
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-
|
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.
|
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
|