openfeature-sdk 0.4.1 → 0.5.0
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/.release-please-manifest.json +1 -1
- data/.ruby-version +1 -1
- data/.tool-versions +1 -1
- data/CHANGELOG.md +11 -0
- data/Gemfile.lock +4 -1
- data/LICENSE +1 -1
- data/README.md +44 -10
- data/lib/open_feature/sdk/api.rb +16 -0
- data/lib/open_feature/sdk/client.rb +10 -0
- data/lib/open_feature/sdk/configuration.rb +205 -41
- data/lib/open_feature/sdk/event_dispatcher.rb +80 -0
- data/lib/open_feature/sdk/provider/event_emitter.rb +37 -0
- data/lib/open_feature/sdk/provider/in_memory_provider.rb +1 -1
- data/lib/open_feature/sdk/provider.rb +11 -0
- data/lib/open_feature/sdk/provider_event.rb +25 -0
- data/lib/open_feature/sdk/provider_state.rb +27 -0
- data/lib/open_feature/sdk/provider_state_registry.rb +99 -0
- data/lib/open_feature/sdk/version.rb +1 -1
- metadata +21 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c91c19e8a0f7ef628c048224287a276713d704914142d4a2771bd74bf01ea68e
|
|
4
|
+
data.tar.gz: 565d980320e40d5a573c4710d6313f038c0a58350f6d5eb1564dcd5c85148853
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7d5dd24ccd3b4e61de24d7a46d02169ad7373d74776664fc0686db4d51dde98c8851c81e392043130f08a4dfd6cdb877a31c3a5ecb2d42c7bd2f5c92b1008fef
|
|
7
|
+
data.tar.gz: f8ed5ee65b6bee9dd1e36e1373248b12f3729a5ce5a22857c4212957246f7c3b150d48d5cea6e59a7e164d06e69bb53a5d90fc883924c85aaa4f6d64b15e4764
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.4.
|
|
1
|
+
3.4.8
|
data/.tool-versions
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby 3.4.
|
|
1
|
+
ruby 3.4.8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0](https://github.com/open-feature/ruby-sdk/compare/v0.4.1...v0.5.0) (2026-01-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* add provider eventing, remove setProvider timeout ([#207](https://github.com/open-feature/ruby-sdk/issues/207))
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* add provider eventing, remove setProvider timeout ([#207](https://github.com/open-feature/ruby-sdk/issues/207)) ([fffd615](https://github.com/open-feature/ruby-sdk/commit/fffd6155ff308c75220185de030c38eb6eeac7e8))
|
|
13
|
+
|
|
3
14
|
## [0.4.1](https://github.com/open-feature/ruby-sdk/compare/v0.4.0...v0.4.1) (2025-11-03)
|
|
4
15
|
|
|
5
16
|
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
openfeature-sdk (0.
|
|
4
|
+
openfeature-sdk (0.5.0)
|
|
5
5
|
|
|
6
6
|
GEM
|
|
7
7
|
remote: https://rubygems.org/
|
|
@@ -89,12 +89,14 @@ GEM
|
|
|
89
89
|
rubocop-performance (~> 1.20.2)
|
|
90
90
|
stringio (3.1.0)
|
|
91
91
|
strscan (3.1.0)
|
|
92
|
+
timecop (0.9.10)
|
|
92
93
|
unicode-display_width (2.5.0)
|
|
93
94
|
|
|
94
95
|
PLATFORMS
|
|
95
96
|
arm64-darwin-21
|
|
96
97
|
arm64-darwin-22
|
|
97
98
|
arm64-darwin-23
|
|
99
|
+
arm64-darwin-24
|
|
98
100
|
x64-mingw-ucrt
|
|
99
101
|
x64-mingw32
|
|
100
102
|
x86_64-darwin-19
|
|
@@ -112,6 +114,7 @@ DEPENDENCIES
|
|
|
112
114
|
simplecov-cobertura (~> 2.1.0)
|
|
113
115
|
standard
|
|
114
116
|
standard-performance
|
|
117
|
+
timecop (~> 0.9.10)
|
|
115
118
|
|
|
116
119
|
BUNDLED WITH
|
|
117
120
|
2.3.25
|
data/LICENSE
CHANGED
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
|
187
187
|
identification within third-party archives.
|
|
188
188
|
|
|
189
|
-
Copyright
|
|
189
|
+
Copyright OpenFeature Maintainers
|
|
190
190
|
|
|
191
191
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
192
|
you may not use this file except in compliance with the License.
|
data/README.md
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
</a>
|
|
18
18
|
<!-- x-release-please-start-version -->
|
|
19
19
|
|
|
20
|
-
<a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.
|
|
21
|
-
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.
|
|
20
|
+
<a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.5.0">
|
|
21
|
+
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.5.0&color=blue&style=for-the-badge" />
|
|
22
22
|
</a>
|
|
23
23
|
|
|
24
24
|
<!-- x-release-please-end -->
|
|
@@ -104,7 +104,7 @@ object = client.fetch_object_value(flag_key: 'object_value', default_value: { na
|
|
|
104
104
|
| ⚠️ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
|
|
105
105
|
| ❌ | [Logging](#logging) | Integrate with popular logging packages. |
|
|
106
106
|
| ✅ | [Domains](#domains) | Logically bind clients with providers. |
|
|
107
|
-
|
|
|
107
|
+
| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
|
|
108
108
|
| ⚠️ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
|
|
109
109
|
| ❌ | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread) |
|
|
110
110
|
| ⚠️ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
|
|
@@ -142,6 +142,7 @@ begin
|
|
|
142
142
|
rescue OpenFeature::SDK::ProviderInitializationError => e
|
|
143
143
|
puts "Provider failed to initialize: #{e.message}"
|
|
144
144
|
puts "Error code: #{e.error_code}"
|
|
145
|
+
# original_error contains the underlying exception that caused the initialization failure
|
|
145
146
|
puts "Original error: #{e.original_error}"
|
|
146
147
|
end
|
|
147
148
|
|
|
@@ -163,8 +164,9 @@ end
|
|
|
163
164
|
|
|
164
165
|
The `set_provider_and_wait` method:
|
|
165
166
|
- Waits for the provider's `init` method to complete successfully
|
|
166
|
-
- Raises `ProviderInitializationError`
|
|
167
|
-
- Provides access to the
|
|
167
|
+
- Raises `ProviderInitializationError` if initialization fails or times out
|
|
168
|
+
- Provides access to the provider instance, error code, and original exception for debugging
|
|
169
|
+
- The `original_error` field contains the underlying exception that caused the initialization failure
|
|
168
170
|
- Uses the same thread-safe provider switching as `set_provider`
|
|
169
171
|
|
|
170
172
|
In some situations, it may be beneficial to register multiple providers in the same application.
|
|
@@ -231,15 +233,47 @@ legacy_flag_client = OpenFeature::SDK.build_client(domain: "legacy_flags")
|
|
|
231
233
|
|
|
232
234
|
### Eventing
|
|
233
235
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
<!-- Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.
|
|
236
|
+
Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.
|
|
237
237
|
Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider.
|
|
238
238
|
Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`.
|
|
239
239
|
|
|
240
|
-
Please refer to the documentation of the provider you're using to see what events are supported.
|
|
240
|
+
Please refer to the documentation of the provider you're using to see what events are supported.
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
# Register event handlers at the API (global) level
|
|
244
|
+
ready_handler = ->(event_details) do
|
|
245
|
+
puts "Provider #{event_details[:provider].metadata.name} is ready!"
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
OpenFeature::SDK.add_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_READY, ready_handler)
|
|
249
|
+
|
|
250
|
+
# The SDK automatically emits lifecycle events. Providers can emit additional spontaneous events
|
|
251
|
+
# using the EventEmitter mixin to signal internal state changes like configuration updates.
|
|
252
|
+
class MyEventAwareProvider
|
|
253
|
+
include OpenFeature::SDK::Provider::EventEmitter
|
|
241
254
|
|
|
242
|
-
|
|
255
|
+
def init(evaluation_context)
|
|
256
|
+
# Start background process to monitor for configuration changes
|
|
257
|
+
# Note: SDK automatically emits PROVIDER_READY when init completes successfully
|
|
258
|
+
start_background_process
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def start_background_process
|
|
262
|
+
Thread.new do
|
|
263
|
+
# Monitor for configuration changes and emit events when they occur
|
|
264
|
+
if configuration_changed?
|
|
265
|
+
emit_event(
|
|
266
|
+
OpenFeature::SDK::ProviderEvent::PROVIDER_CONFIGURATION_CHANGED,
|
|
267
|
+
message: "Flag configuration updated"
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Remove specific handlers when no longer needed
|
|
275
|
+
OpenFeature::SDK.remove_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_READY, ready_handler)
|
|
276
|
+
```
|
|
243
277
|
|
|
244
278
|
### Shutdown
|
|
245
279
|
|
data/lib/open_feature/sdk/api.rb
CHANGED
|
@@ -51,6 +51,22 @@ module OpenFeature
|
|
|
51
51
|
rescue
|
|
52
52
|
Client.new(provider: Provider::NoOpProvider.new, evaluation_context:)
|
|
53
53
|
end
|
|
54
|
+
|
|
55
|
+
def add_handler(event_type, handler)
|
|
56
|
+
configuration.add_handler(event_type, handler)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def remove_handler(event_type, handler)
|
|
60
|
+
configuration.remove_handler(event_type, handler)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def logger
|
|
64
|
+
configuration.logger
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def logger=(new_logger)
|
|
68
|
+
configuration.logger = new_logger
|
|
69
|
+
end
|
|
54
70
|
end
|
|
55
71
|
end
|
|
56
72
|
end
|
|
@@ -27,6 +27,16 @@ module OpenFeature
|
|
|
27
27
|
@hooks = []
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
def add_handler(event_type, handler = nil, &block)
|
|
31
|
+
actual_handler = handler || block
|
|
32
|
+
OpenFeature::SDK.configuration.add_client_handler(self, event_type, actual_handler)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def remove_handler(event_type, handler = nil, &block)
|
|
36
|
+
actual_handler = handler || block
|
|
37
|
+
OpenFeature::SDK.configuration.remove_client_handler(self, event_type, actual_handler)
|
|
38
|
+
end
|
|
39
|
+
|
|
30
40
|
RESULT_TYPE.each do |result_type|
|
|
31
41
|
SUFFIXES.each do |suffix|
|
|
32
42
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
require "timeout"
|
|
4
4
|
require_relative "api"
|
|
5
5
|
require_relative "provider_initialization_error"
|
|
6
|
+
require_relative "event_dispatcher"
|
|
7
|
+
require_relative "provider_event"
|
|
8
|
+
require_relative "provider_state_registry"
|
|
9
|
+
require_relative "provider/event_emitter"
|
|
6
10
|
|
|
7
11
|
module OpenFeature
|
|
8
12
|
module SDK
|
|
@@ -14,73 +18,233 @@ module OpenFeature
|
|
|
14
18
|
extend Forwardable
|
|
15
19
|
|
|
16
20
|
attr_accessor :evaluation_context, :hooks
|
|
21
|
+
attr_reader :logger
|
|
17
22
|
|
|
18
23
|
def initialize
|
|
19
24
|
@hooks = []
|
|
20
25
|
@providers = {}
|
|
21
26
|
@provider_mutex = Mutex.new
|
|
27
|
+
@logger = nil
|
|
28
|
+
@event_dispatcher = EventDispatcher.new(@logger)
|
|
29
|
+
@provider_state_registry = ProviderStateRegistry.new
|
|
30
|
+
@client_handlers = {}
|
|
31
|
+
@client_handlers_mutex = Mutex.new
|
|
22
32
|
end
|
|
23
33
|
|
|
24
34
|
def provider(domain: nil)
|
|
25
35
|
@providers[domain] || @providers[nil]
|
|
26
36
|
end
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
def logger=(new_logger)
|
|
39
|
+
@logger = new_logger
|
|
40
|
+
@event_dispatcher.logger = new_logger if @event_dispatcher
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def add_handler(event_type, handler)
|
|
44
|
+
@event_dispatcher.add_handler(event_type, handler)
|
|
45
|
+
run_immediate_handler(event_type, handler, nil)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def remove_handler(event_type, handler)
|
|
49
|
+
@event_dispatcher.remove_handler(event_type, handler)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @api private
|
|
53
|
+
def add_client_handler(client, event_type, handler)
|
|
54
|
+
@client_handlers_mutex.synchronize do
|
|
55
|
+
@client_handlers[client] ||= Hash.new { |h, k| h[k] = [] }
|
|
56
|
+
@client_handlers[client][event_type] << handler
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
run_immediate_handler(event_type, handler, client)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @api private
|
|
63
|
+
def remove_client_handler(client, event_type, handler)
|
|
64
|
+
@client_handlers_mutex.synchronize do
|
|
65
|
+
return unless @client_handlers[client]
|
|
66
|
+
handlers = @client_handlers[client][event_type]
|
|
67
|
+
handlers&.delete(handler)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
32
71
|
def set_provider(provider, domain: nil)
|
|
72
|
+
set_provider_internal(provider, domain: domain, wait_for_init: false)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def set_provider_and_wait(provider, domain: nil)
|
|
76
|
+
set_provider_internal(provider, domain: domain, wait_for_init: true)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def reset
|
|
82
|
+
@event_dispatcher.clear_all_handlers
|
|
83
|
+
@client_handlers_mutex.synchronize do
|
|
84
|
+
@client_handlers.clear
|
|
85
|
+
end
|
|
86
|
+
@provider_state_registry.clear
|
|
33
87
|
@provider_mutex.synchronize do
|
|
34
|
-
@providers
|
|
35
|
-
provider.init if provider.respond_to?(:init)
|
|
36
|
-
new_providers = @providers.dup
|
|
37
|
-
new_providers[domain] = provider
|
|
38
|
-
@providers = new_providers
|
|
88
|
+
@providers.clear
|
|
39
89
|
end
|
|
40
90
|
end
|
|
41
91
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# @raise [ProviderInitializationError] if the provider fails to initialize or times out
|
|
49
|
-
def set_provider_and_wait(provider, domain: nil, timeout: 30)
|
|
92
|
+
def set_provider_internal(provider, domain:, wait_for_init:)
|
|
93
|
+
# Capture evaluation context before acquiring mutex to prevent race conditions
|
|
94
|
+
context_for_init = @evaluation_context
|
|
95
|
+
|
|
96
|
+
old_provider, provider_to_init = nil
|
|
97
|
+
|
|
50
98
|
@provider_mutex.synchronize do
|
|
51
99
|
old_provider = @providers[domain]
|
|
52
100
|
|
|
53
|
-
#
|
|
101
|
+
# Remove old provider state to prevent memory leaks
|
|
102
|
+
@provider_state_registry.remove_provider(old_provider)
|
|
103
|
+
|
|
104
|
+
new_providers = @providers.dup
|
|
105
|
+
new_providers[domain] = provider
|
|
106
|
+
@providers = new_providers
|
|
107
|
+
|
|
108
|
+
@provider_state_registry.set_initial_state(provider)
|
|
109
|
+
|
|
110
|
+
provider.send(:attach, self) if provider.is_a?(Provider::EventEmitter)
|
|
111
|
+
|
|
112
|
+
provider_to_init = provider
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Shutdown old provider outside mutex to avoid blocking other operations
|
|
116
|
+
# Only shutdown if it's a different provider to prevent race condition
|
|
117
|
+
if old_provider && old_provider != provider
|
|
54
118
|
begin
|
|
55
119
|
old_provider.shutdown if old_provider.respond_to?(:shutdown)
|
|
56
|
-
rescue
|
|
57
|
-
|
|
120
|
+
rescue => e
|
|
121
|
+
@logger&.warn("Error shutting down previous provider #{old_provider&.class&.name || "unknown"}: #{e.message}")
|
|
58
122
|
end
|
|
123
|
+
end
|
|
59
124
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
125
|
+
# Initialize provider outside the mutex to avoid blocking other operations
|
|
126
|
+
if wait_for_init
|
|
127
|
+
init_provider(provider_to_init, context_for_init, raise_on_error: true)
|
|
128
|
+
else
|
|
129
|
+
Thread.new do
|
|
130
|
+
init_provider(provider_to_init, context_for_init, raise_on_error: false)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def init_provider(provider, context, raise_on_error: false)
|
|
136
|
+
if provider.respond_to?(:init)
|
|
137
|
+
init_method = provider.method(:init)
|
|
138
|
+
if init_method.parameters.empty?
|
|
139
|
+
provider.init
|
|
140
|
+
else
|
|
141
|
+
provider.init(context)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
|
|
146
|
+
rescue => e
|
|
147
|
+
dispatch_provider_event(provider, ProviderEvent::PROVIDER_ERROR,
|
|
148
|
+
error_code: Provider::ErrorCode::GENERAL,
|
|
149
|
+
message: e.message)
|
|
150
|
+
|
|
151
|
+
if raise_on_error
|
|
152
|
+
# Re-raise as ProviderInitializationError for synchronous callers
|
|
153
|
+
raise ProviderInitializationError.new(
|
|
154
|
+
"Provider #{provider.class.name} initialization failed: #{e.message}",
|
|
155
|
+
provider:,
|
|
156
|
+
error_code: Provider::ErrorCode::GENERAL,
|
|
157
|
+
original_error: e
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def dispatch_provider_event(provider, event_type, details = {})
|
|
163
|
+
@provider_state_registry.update_state_from_event(provider, event_type, details)
|
|
164
|
+
|
|
165
|
+
provider_name = extract_provider_name(provider)
|
|
166
|
+
|
|
167
|
+
event_details = {
|
|
168
|
+
provider_name: provider_name
|
|
169
|
+
}.merge(details)
|
|
170
|
+
|
|
171
|
+
run_handlers_for_provider(provider, event_type, event_details)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def provider_state(provider)
|
|
175
|
+
@provider_state_registry.get_state(provider)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def extract_provider_name(provider)
|
|
181
|
+
provider.respond_to?(:metadata) ? provider.metadata.name : provider.class.name
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def run_handlers_for_provider(provider, event_type, event_details)
|
|
185
|
+
# Run global handlers (API-level, no domain filtering)
|
|
186
|
+
@event_dispatcher.trigger_event(event_type, event_details)
|
|
187
|
+
|
|
188
|
+
# Run client handlers (domain-scoped)
|
|
189
|
+
@client_handlers_mutex.synchronize do
|
|
190
|
+
@client_handlers.each do |client, handlers_by_event|
|
|
191
|
+
# Check if this client should receive events from this provider
|
|
192
|
+
client_provider = provider(domain: client.metadata.domain)
|
|
193
|
+
next unless client_provider&.equal?(provider)
|
|
194
|
+
|
|
195
|
+
# Trigger handlers for this client
|
|
196
|
+
handlers = handlers_by_event[event_type]
|
|
197
|
+
handlers.each do |handler|
|
|
198
|
+
handler.call(event_details)
|
|
199
|
+
rescue => e
|
|
200
|
+
@logger&.error("Client event handler failed: #{e.message}")
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def run_immediate_handler(event_type, handler, client)
|
|
207
|
+
status_to_event = {
|
|
208
|
+
ProviderState::READY => ProviderEvent::PROVIDER_READY,
|
|
209
|
+
ProviderState::ERROR => ProviderEvent::PROVIDER_ERROR,
|
|
210
|
+
ProviderState::FATAL => ProviderEvent::PROVIDER_ERROR,
|
|
211
|
+
ProviderState::STALE => ProviderEvent::PROVIDER_STALE
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if client.nil?
|
|
215
|
+
# API-level handler: check all providers
|
|
216
|
+
@providers.each do |domain, provider|
|
|
217
|
+
next unless provider
|
|
218
|
+
|
|
219
|
+
provider_state = @provider_state_registry.get_state(provider)
|
|
220
|
+
|
|
221
|
+
if event_type == status_to_event[provider_state]
|
|
222
|
+
provider_name = extract_provider_name(provider)
|
|
223
|
+
event_details = {provider_name: provider_name}
|
|
224
|
+
|
|
225
|
+
begin
|
|
226
|
+
handler.call(event_details)
|
|
227
|
+
rescue => e
|
|
228
|
+
@logger&.error("Immediate API event handler failed: #{e.message}")
|
|
65
229
|
end
|
|
66
230
|
end
|
|
231
|
+
end
|
|
232
|
+
else
|
|
233
|
+
# Client-level handler: check specific provider only
|
|
234
|
+
client_provider = provider(domain: client.metadata.domain)
|
|
235
|
+
return unless client_provider
|
|
67
236
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
raise ProviderInitializationError.new(
|
|
80
|
-
"Provider initialization failed: #{e.message}",
|
|
81
|
-
provider:,
|
|
82
|
-
original_error: e
|
|
83
|
-
)
|
|
237
|
+
provider_state = @provider_state_registry.get_state(client_provider)
|
|
238
|
+
|
|
239
|
+
if event_type == status_to_event[provider_state]
|
|
240
|
+
provider_name = extract_provider_name(client_provider)
|
|
241
|
+
event_details = {provider_name: provider_name}
|
|
242
|
+
|
|
243
|
+
begin
|
|
244
|
+
handler.call(event_details)
|
|
245
|
+
rescue => e
|
|
246
|
+
@logger&.error("Immediate client event handler failed: #{e.message}")
|
|
247
|
+
end
|
|
84
248
|
end
|
|
85
249
|
end
|
|
86
250
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "provider_event"
|
|
4
|
+
|
|
5
|
+
module OpenFeature
|
|
6
|
+
module SDK
|
|
7
|
+
# Thread-safe pub-sub for provider events
|
|
8
|
+
class EventDispatcher
|
|
9
|
+
attr_writer :logger
|
|
10
|
+
|
|
11
|
+
def initialize(logger = nil)
|
|
12
|
+
@handlers = {}
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
@logger = logger
|
|
15
|
+
ProviderEvent::ALL_EVENTS.each { |event| @handlers[event] = [] }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def add_handler(event_type, handler)
|
|
19
|
+
raise ArgumentError, "Invalid event type: #{event_type}" unless valid_event?(event_type)
|
|
20
|
+
raise ArgumentError, "Handler must respond to call" unless handler.respond_to?(:call)
|
|
21
|
+
|
|
22
|
+
@mutex.synchronize do
|
|
23
|
+
@handlers[event_type] << handler
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def remove_handler(event_type, handler)
|
|
28
|
+
return unless valid_event?(event_type)
|
|
29
|
+
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
@handlers[event_type].delete(handler)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def remove_all_handlers(event_type)
|
|
36
|
+
return unless valid_event?(event_type)
|
|
37
|
+
|
|
38
|
+
@mutex.synchronize do
|
|
39
|
+
@handlers[event_type].clear
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def trigger_event(event_type, event_details = {})
|
|
44
|
+
return unless valid_event?(event_type)
|
|
45
|
+
|
|
46
|
+
handlers_to_call = nil
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
handlers_to_call = @handlers[event_type].dup
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Call handlers outside of mutex to avoid deadlocks
|
|
52
|
+
handlers_to_call.each do |handler|
|
|
53
|
+
handler.call(event_details)
|
|
54
|
+
rescue => e
|
|
55
|
+
@logger&.warn "Event handler failed for #{event_type}: #{e.message}\n#{e.backtrace.join("\n")}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def handler_count(event_type)
|
|
60
|
+
return 0 unless valid_event?(event_type)
|
|
61
|
+
|
|
62
|
+
@mutex.synchronize do
|
|
63
|
+
@handlers[event_type].size
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def clear_all_handlers
|
|
68
|
+
@mutex.synchronize do
|
|
69
|
+
@handlers.each_value(&:clear)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def valid_event?(event_type)
|
|
76
|
+
ProviderEvent::ALL_EVENTS.include?(event_type)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../provider_event"
|
|
4
|
+
|
|
5
|
+
module OpenFeature
|
|
6
|
+
module SDK
|
|
7
|
+
module Provider
|
|
8
|
+
# Mixin for providers that emit lifecycle events
|
|
9
|
+
module EventEmitter
|
|
10
|
+
def emit_event(event_type, details = {})
|
|
11
|
+
config = @configuration
|
|
12
|
+
return unless config
|
|
13
|
+
|
|
14
|
+
unless ::OpenFeature::SDK::ProviderEvent::ALL_EVENTS.include?(event_type)
|
|
15
|
+
raise ArgumentError, "Invalid event type: #{event_type}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
config.send(:dispatch_provider_event, self, event_type, details)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def configuration_attached?
|
|
22
|
+
!@configuration.nil?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def attach(configuration)
|
|
28
|
+
@configuration = configuration
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def detach
|
|
32
|
+
@configuration = nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -2,9 +2,20 @@ require_relative "provider/error_code"
|
|
|
2
2
|
require_relative "provider/reason"
|
|
3
3
|
require_relative "provider/resolution_details"
|
|
4
4
|
require_relative "provider/provider_metadata"
|
|
5
|
+
|
|
6
|
+
# Provider interfaces
|
|
7
|
+
require_relative "provider/event_emitter"
|
|
8
|
+
|
|
9
|
+
# Provider implementations
|
|
5
10
|
require_relative "provider/no_op_provider"
|
|
6
11
|
require_relative "provider/in_memory_provider"
|
|
7
12
|
|
|
13
|
+
# Event system components
|
|
14
|
+
require_relative "provider_event"
|
|
15
|
+
require_relative "provider_state"
|
|
16
|
+
require_relative "event_dispatcher"
|
|
17
|
+
require_relative "provider_state_registry"
|
|
18
|
+
|
|
8
19
|
module OpenFeature
|
|
9
20
|
module SDK
|
|
10
21
|
module Provider
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenFeature
|
|
4
|
+
module SDK
|
|
5
|
+
# Provider Event Types
|
|
6
|
+
#
|
|
7
|
+
# Defines the standard event types that providers can emit during their lifecycle.
|
|
8
|
+
# These events correspond to the OpenFeature specification events:
|
|
9
|
+
# https://openfeature.dev/specification/sections/events/
|
|
10
|
+
#
|
|
11
|
+
module ProviderEvent
|
|
12
|
+
PROVIDER_READY = "PROVIDER_READY"
|
|
13
|
+
PROVIDER_ERROR = "PROVIDER_ERROR"
|
|
14
|
+
PROVIDER_CONFIGURATION_CHANGED = "PROVIDER_CONFIGURATION_CHANGED"
|
|
15
|
+
PROVIDER_STALE = "PROVIDER_STALE"
|
|
16
|
+
|
|
17
|
+
ALL_EVENTS = [
|
|
18
|
+
PROVIDER_READY,
|
|
19
|
+
PROVIDER_ERROR,
|
|
20
|
+
PROVIDER_CONFIGURATION_CHANGED,
|
|
21
|
+
PROVIDER_STALE
|
|
22
|
+
].freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenFeature
|
|
4
|
+
module SDK
|
|
5
|
+
# Provider State Types
|
|
6
|
+
#
|
|
7
|
+
# Defines the standard states that providers can be in during their lifecycle.
|
|
8
|
+
# These states correspond to the OpenFeature specification provider states:
|
|
9
|
+
# https://openfeature.dev/specification/types#provider-status
|
|
10
|
+
#
|
|
11
|
+
module ProviderState
|
|
12
|
+
NOT_READY = "NOT_READY"
|
|
13
|
+
READY = "READY"
|
|
14
|
+
ERROR = "ERROR"
|
|
15
|
+
STALE = "STALE"
|
|
16
|
+
FATAL = "FATAL"
|
|
17
|
+
|
|
18
|
+
ALL_STATES = [
|
|
19
|
+
NOT_READY,
|
|
20
|
+
READY,
|
|
21
|
+
ERROR,
|
|
22
|
+
STALE,
|
|
23
|
+
FATAL
|
|
24
|
+
].freeze
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "provider_state"
|
|
4
|
+
require_relative "provider_event"
|
|
5
|
+
require_relative "provider/error_code"
|
|
6
|
+
|
|
7
|
+
module OpenFeature
|
|
8
|
+
module SDK
|
|
9
|
+
# Tracks provider states
|
|
10
|
+
class ProviderStateRegistry
|
|
11
|
+
def initialize
|
|
12
|
+
@states = {}
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def set_initial_state(provider, state = ProviderState::NOT_READY)
|
|
17
|
+
return unless provider
|
|
18
|
+
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
@states[provider.object_id] = state
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def update_state_from_event(provider, event_type, event_details = nil)
|
|
25
|
+
return ProviderState::NOT_READY unless provider
|
|
26
|
+
|
|
27
|
+
new_state = state_from_event(event_type, event_details)
|
|
28
|
+
|
|
29
|
+
# Only update state if the event should cause a state change
|
|
30
|
+
if new_state
|
|
31
|
+
@mutex.synchronize do
|
|
32
|
+
@states[provider.object_id] = new_state
|
|
33
|
+
end
|
|
34
|
+
new_state
|
|
35
|
+
else
|
|
36
|
+
# Return current state without changing it
|
|
37
|
+
get_state(provider)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def get_state(provider)
|
|
42
|
+
return ProviderState::NOT_READY unless provider
|
|
43
|
+
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
@states[provider.object_id] || ProviderState::NOT_READY
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def remove_provider(provider)
|
|
50
|
+
return unless provider
|
|
51
|
+
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
@states.delete(provider.object_id)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ready?(provider)
|
|
58
|
+
get_state(provider) == ProviderState::READY
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def error?(provider)
|
|
62
|
+
state = get_state(provider)
|
|
63
|
+
[ProviderState::ERROR, ProviderState::FATAL].include?(state)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def clear
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
@states.clear
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def state_from_event(event_type, event_details = nil)
|
|
75
|
+
case event_type
|
|
76
|
+
when ProviderEvent::PROVIDER_READY
|
|
77
|
+
ProviderState::READY
|
|
78
|
+
when ProviderEvent::PROVIDER_STALE
|
|
79
|
+
ProviderState::STALE
|
|
80
|
+
when ProviderEvent::PROVIDER_ERROR
|
|
81
|
+
state_from_error_event(event_details)
|
|
82
|
+
when ProviderEvent::PROVIDER_CONFIGURATION_CHANGED
|
|
83
|
+
nil # No state change per OpenFeature spec Requirement 5.3.5
|
|
84
|
+
else
|
|
85
|
+
nil # No state change for unknown events - conservative default
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def state_from_error_event(event_details)
|
|
90
|
+
error_code = event_details&.dig(:error_code)
|
|
91
|
+
if error_code == Provider::ErrorCode::PROVIDER_FATAL
|
|
92
|
+
ProviderState::FATAL
|
|
93
|
+
else
|
|
94
|
+
ProviderState::ERROR
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openfeature-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- OpenFeature Authors
|
|
@@ -121,6 +121,20 @@ dependencies:
|
|
|
121
121
|
- - "~>"
|
|
122
122
|
- !ruby/object:Gem::Version
|
|
123
123
|
version: 2.1.0
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: timecop
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: 0.9.10
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: 0.9.10
|
|
124
138
|
description: Ruby SDK for an the specifications for the open standard of feature flag
|
|
125
139
|
management
|
|
126
140
|
email:
|
|
@@ -152,21 +166,26 @@ files:
|
|
|
152
166
|
- lib/open_feature/sdk/evaluation_context.rb
|
|
153
167
|
- lib/open_feature/sdk/evaluation_context_builder.rb
|
|
154
168
|
- lib/open_feature/sdk/evaluation_details.rb
|
|
169
|
+
- lib/open_feature/sdk/event_dispatcher.rb
|
|
155
170
|
- lib/open_feature/sdk/hooks/hints.rb
|
|
156
171
|
- lib/open_feature/sdk/provider.rb
|
|
157
172
|
- lib/open_feature/sdk/provider/error_code.rb
|
|
173
|
+
- lib/open_feature/sdk/provider/event_emitter.rb
|
|
158
174
|
- lib/open_feature/sdk/provider/in_memory_provider.rb
|
|
159
175
|
- lib/open_feature/sdk/provider/no_op_provider.rb
|
|
160
176
|
- lib/open_feature/sdk/provider/provider_metadata.rb
|
|
161
177
|
- lib/open_feature/sdk/provider/reason.rb
|
|
162
178
|
- lib/open_feature/sdk/provider/resolution_details.rb
|
|
179
|
+
- lib/open_feature/sdk/provider_event.rb
|
|
163
180
|
- lib/open_feature/sdk/provider_initialization_error.rb
|
|
181
|
+
- lib/open_feature/sdk/provider_state.rb
|
|
182
|
+
- lib/open_feature/sdk/provider_state_registry.rb
|
|
164
183
|
- lib/open_feature/sdk/version.rb
|
|
165
184
|
- release-please-config.json
|
|
166
185
|
- renovate.json
|
|
167
186
|
homepage: https://github.com/open-feature/openfeature-ruby
|
|
168
187
|
licenses:
|
|
169
|
-
- Apache-2.0
|
|
188
|
+
- Apache-2.0
|
|
170
189
|
metadata:
|
|
171
190
|
homepage_uri: https://github.com/open-feature/openfeature-ruby
|
|
172
191
|
source_code_uri: https://github.com/open-feature/openfeature-ruby
|