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 +4 -4
- data/docs/further_reading.md +5 -3
- data/lib/ff/ruby/server/sdk/api/cf_client.rb +11 -44
- 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 +63 -8
- 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: d40a21b8332b96f4c3f507f2adece4bc12a9475fb87518323c3b79f959d2782a
|
4
|
+
data.tar.gz: 4be776d630a6c97cc563e801b74b8c25e6250ea3611fa83b6a1b26274ead4d06
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d11cd5ae4daddce986eae1faa3ca10d16e68a0c708e4e15131d0ee74883e67bac88af39cedfa1ee5497f7ac916bfd270bd8067bcbf71c3e90de242520ff49cd1
|
7
|
+
data.tar.gz: bfc9bc6662bc500c25281c36bb9d8352ba16bdf8ad9f43c06b44074aa4ff791e2ddb8e1e84b1796bd4fb10bfe819190d6eb33bde7ac315eff02e918784653fcb
|
data/docs/further_reading.md
CHANGED
@@ -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
|
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
|
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
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
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:
|
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
|
-
|
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,
|
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
|
-
|
166
|
-
|
167
|
-
|
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
|
-
|
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
|
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.
|
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-
|
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
|