openfeature-sdk 0.6.7 → 0.7.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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +31 -0
- data/Gemfile.lock +1 -1
- data/README.md +47 -7
- data/lib/open_feature/sdk/api.rb +6 -8
- data/lib/open_feature/sdk/client.rb +32 -22
- data/lib/open_feature/sdk/client_metadata.rb +5 -0
- data/lib/open_feature/sdk/configuration.rb +131 -52
- data/lib/open_feature/sdk/event_dispatcher.rb +6 -2
- data/lib/open_feature/sdk/hooks/hook_executor.rb +1 -1
- data/lib/open_feature/sdk/isolated.rb +15 -0
- data/lib/open_feature/sdk/provider/event_emitter.rb +9 -4
- data/lib/open_feature/sdk/provider/in_memory_provider.rb +0 -4
- data/lib/open_feature/sdk/provider/no_op_provider.rb +1 -1
- data/lib/open_feature/sdk/provider_initialization_error.rb +2 -2
- data/lib/open_feature/sdk/provider_state_registry.rb +7 -7
- data/lib/open_feature/sdk/thread_local_transaction_context_propagator.rb +7 -2
- data/lib/open_feature/sdk/version.rb +1 -1
- data/sig/open_feature/sdk/api.rbs +5 -2
- data/sig/open_feature/sdk/client.rbs +4 -3
- data/sig/open_feature/sdk/configuration.rbs +10 -2
- data/sig/open_feature/sdk/event_dispatcher.rbs +1 -0
- data/sig/open_feature/sdk/isolated.rbs +7 -0
- data/sig/open_feature/sdk/provider/in_memory_provider.rbs +0 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf1041c34e962f832b25f1be3e9eeab7fee3fd4272dc9e1e0e8dabe85b6d21e8
|
|
4
|
+
data.tar.gz: f45667a6a898d3521ea87c31aea62491ace0682c72ef2cfa2a16d5f8a0f3f0b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 393adb8b637763e5b397f6ee702017705bf355c8785ceb1280aa4f95b585a061f8da6e4a6d67ed9e8ab20de1f341b36d4dc3febb38dbbcc7a41a68cfb8002b8c
|
|
7
|
+
data.tar.gz: d145cc98fb609aa75ad06485d4486cc6aec19ea953cd627f9f71b33e991fafd3a57fa139fbe07f878e5bc105885ce35a74c0ab12485e50649d63b79a100eec98
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.1](https://github.com/open-feature/ruby-sdk/compare/v0.7.0...v0.7.1) (2026-10-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* isolated API instances ([#354](https://github.com/open-feature/ruby-sdk/issues/354)) ([1c92b4a](https://github.com/open-feature/ruby-sdk/commit/1c92b4ad7550a6b345e7471f84cd5a7ff445591e))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* key provider state by identity, default the NoOp reason, freeze client metadata ([#355](https://github.com/open-feature/ruby-sdk/issues/355)) ([d15bd84](https://github.com/open-feature/ruby-sdk/commit/d15bd84e1456b6200e29c12c3463c4f8ddfe693f))
|
|
14
|
+
|
|
15
|
+
## [0.7.0](https://github.com/open-feature/ruby-sdk/compare/v0.6.7...v0.7.0) (2026-10-23)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### ⚠ BREAKING CHANGES
|
|
19
|
+
|
|
20
|
+
* resolve a client's provider by domain on every call ([#352](https://github.com/open-feature/ruby-sdk/issues/352))
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* let providers own their lifecycle events ([#353](https://github.com/open-feature/ruby-sdk/issues/353)) ([15faf98](https://github.com/open-feature/ruby-sdk/commit/15faf98d1c900118da362f6b8d76ccdec1b3b99e))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* catch NotImplementedError in track and the hook lifecycle ([#349](https://github.com/open-feature/ruby-sdk/issues/349)) ([62759f4](https://github.com/open-feature/ruby-sdk/commit/62759f443fa8cb2f7a1b67720397240063623da1))
|
|
30
|
+
* deliver provider events in order, outside handler locks ([#351](https://github.com/open-feature/ruby-sdk/issues/351)) ([16bf89c](https://github.com/open-feature/ruby-sdk/commit/16bf89cf72541c22de9565f0dcdbfcf9dfe097ad))
|
|
31
|
+
* release init waiters on replacement and same-thread re-entry ([#348](https://github.com/open-feature/ruby-sdk/issues/348)) ([9e82b15](https://github.com/open-feature/ruby-sdk/commit/9e82b15e9730f0c53f2186aeb39cc53275d5f929))
|
|
32
|
+
* resolve a client's provider by domain on every call ([#352](https://github.com/open-feature/ruby-sdk/issues/352)) ([fa0076f](https://github.com/open-feature/ruby-sdk/commit/fa0076fb8a159a180af408ee0a335e9f8d43afc9))
|
|
33
|
+
|
|
3
34
|
## [0.6.7](https://github.com/open-feature/ruby-sdk/compare/v0.6.6...v0.6.7) (2026-10-23)
|
|
4
35
|
|
|
5
36
|
|
data/Gemfile.lock
CHANGED
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.7.1">
|
|
21
|
+
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.7.1&color=blue&style=for-the-badge" />
|
|
22
22
|
</a>
|
|
23
23
|
|
|
24
24
|
<!-- x-release-please-end -->
|
|
@@ -287,6 +287,7 @@ Log output uses a structured key=value format:
|
|
|
287
287
|
|
|
288
288
|
Clients can be assigned to a domain. A domain is a logical identifier which can be used to associate clients with a particular provider.
|
|
289
289
|
If a domain has no associated provider, the default provider is used.
|
|
290
|
+
Clients look up their domain's provider on every call, so a client created before `set_provider` follows the new binding.
|
|
290
291
|
|
|
291
292
|
```ruby
|
|
292
293
|
OpenFeature::SDK.configure do |config|
|
|
@@ -300,7 +301,11 @@ legacy_flag_client = OpenFeature::SDK.build_client(domain: "legacy_flags")
|
|
|
300
301
|
### Eventing
|
|
301
302
|
|
|
302
303
|
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.
|
|
303
|
-
|
|
304
|
+
Providers emit initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) from `init`.
|
|
305
|
+
A provider without `init` is `READY` as soon as it's registered, and its `PROVIDER_READY` handlers run before `set_provider` returns.
|
|
306
|
+
For providers that define `init` but not `emits_lifecycle_events?` returning `true`, the SDK emits `PROVIDER_READY` after `init` returns and `PROVIDER_ERROR` if it raises.
|
|
307
|
+
That path is deprecated (spec Appendix E), and the SDK logs a deprecation warning once per provider class to its configured logger.
|
|
308
|
+
Handlers for events a provider emits from `init` run while `init` is still in progress, so they can't call `set_provider_and_wait` for that same provider.
|
|
304
309
|
Some providers emit additional events: `PROVIDER_CONFIGURATION_CHANGED`, `PROVIDER_STALE`, `PROVIDER_RECONCILING` and `PROVIDER_CONTEXT_CHANGED`.
|
|
305
310
|
Handlers receive a hash with the `provider_name` and any details the provider sent, such as `message` or `error_code`.
|
|
306
311
|
|
|
@@ -314,15 +319,17 @@ end
|
|
|
314
319
|
|
|
315
320
|
OpenFeature::SDK.add_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_READY, ready_handler)
|
|
316
321
|
|
|
317
|
-
#
|
|
318
|
-
#
|
|
322
|
+
# Providers emit events using the EventEmitter mixin: lifecycle events from init, and spontaneous
|
|
323
|
+
# events to signal internal state changes like configuration updates.
|
|
319
324
|
class MyEventAwareProvider
|
|
320
325
|
include OpenFeature::SDK::Provider::EventEmitter
|
|
321
326
|
|
|
327
|
+
def emits_lifecycle_events? = true
|
|
328
|
+
|
|
322
329
|
def init(evaluation_context)
|
|
323
330
|
# Start background process to monitor for configuration changes
|
|
324
|
-
# Note: SDK automatically emits PROVIDER_READY when init completes successfully
|
|
325
331
|
start_background_process
|
|
332
|
+
emit_event(OpenFeature::SDK::ProviderEvent::PROVIDER_READY)
|
|
326
333
|
end
|
|
327
334
|
|
|
328
335
|
def start_background_process
|
|
@@ -436,6 +443,29 @@ class MyRequestScopedPropagator
|
|
|
436
443
|
end
|
|
437
444
|
```
|
|
438
445
|
|
|
446
|
+
### Isolated API instances
|
|
447
|
+
|
|
448
|
+
`OpenFeature::SDK` is a global singleton. Some applications need more than one independent API in a process, for example separately developed modules that each need their own provider, or tests that run in parallel. Require the factory explicitly to create one:
|
|
449
|
+
|
|
450
|
+
```ruby
|
|
451
|
+
require "open_feature/sdk/isolated"
|
|
452
|
+
|
|
453
|
+
api = OpenFeature::SDK::Isolated.create_api
|
|
454
|
+
api.set_provider_and_wait(MyProvider.new)
|
|
455
|
+
client = api.build_client
|
|
456
|
+
client.fetch_boolean_value(flag_key: "my-flag", default_value: false)
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Each instance has its own providers, hooks, evaluation context, event handlers, transaction context propagator and `shutdown`, and shares none of them with `OpenFeature::SDK` or other instances.
|
|
460
|
+
Build its clients with `api.build_client`; `OpenFeature::SDK::Client.new` binds to the global API.
|
|
461
|
+
Prefer the global API unless you need this.
|
|
462
|
+
|
|
463
|
+
A provider instance should be registered with only one API at a time.
|
|
464
|
+
Registering a provider that includes `OpenFeature::SDK::Provider::EventEmitter` with a second API raises `ArgumentError`; other providers can't be detected.
|
|
465
|
+
Once the first API replaces the provider or shuts down, it can be registered with another.
|
|
466
|
+
|
|
467
|
+
A `LoggingHook` created without `logger:` logs to the global API's logger, even when added to an isolated API. Pass `logger:` to use another.
|
|
468
|
+
|
|
439
469
|
## Extending
|
|
440
470
|
|
|
441
471
|
### Develop a provider
|
|
@@ -446,13 +476,23 @@ You’ll then need to write the provider by implementing the `Provider` duck.
|
|
|
446
476
|
|
|
447
477
|
```ruby
|
|
448
478
|
class MyProvider
|
|
479
|
+
include OpenFeature::SDK::Provider::EventEmitter
|
|
480
|
+
|
|
481
|
+
# Tells the SDK that init emits PROVIDER_READY/PROVIDER_ERROR itself.
|
|
482
|
+
def emits_lifecycle_events? = true
|
|
483
|
+
|
|
484
|
+
# Optional: a provider without init is READY as soon as it's registered.
|
|
449
485
|
# Both arguments are optional; declare only the ones you need.
|
|
450
486
|
# `domain` is the domain the provider was bound to, or nil for the default provider.
|
|
451
487
|
def init(evaluation_context = nil, domain: nil)
|
|
452
|
-
# Perform any initialization steps with flag management system here
|
|
488
|
+
# Perform any initialization steps with flag management system here, then emit PROVIDER_READY.
|
|
453
489
|
# Return value is ignored
|
|
454
490
|
# **Note** The OpenFeature spec defines a lifecycle method called `initialize` to be called when a new provider is set.
|
|
455
491
|
# To avoid conflicting with the Ruby `initialize` method, this method should be named `init` when creating a provider.
|
|
492
|
+
emit_event(OpenFeature::SDK::ProviderEvent::PROVIDER_READY)
|
|
493
|
+
rescue => e
|
|
494
|
+
emit_event(OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR, error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, message: e.message)
|
|
495
|
+
raise
|
|
456
496
|
end
|
|
457
497
|
|
|
458
498
|
# Optional: return true if this provider keeps state for a single domain.
|
data/lib/open_feature/sdk/api.rb
CHANGED
|
@@ -32,13 +32,15 @@ module OpenFeature
|
|
|
32
32
|
#
|
|
33
33
|
# client = OpenFeature::SDK::API.instance.build_client(name: 'my-open-feature-client')
|
|
34
34
|
class API
|
|
35
|
-
include Singleton # Satisfies Flag Evaluation API Requirement 1.1.1
|
|
35
|
+
include Singleton # Satisfies Flag Evaluation API Requirement 1.1.1; Isolated.create_api builds others (1.8.1)
|
|
36
36
|
extend Forwardable
|
|
37
37
|
|
|
38
38
|
def_delegators :configuration, :provider, :set_provider, :set_provider_and_wait, :hooks, :add_hooks, :evaluation_context
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
attr_reader :configuration
|
|
41
|
+
|
|
42
|
+
def initialize
|
|
43
|
+
@configuration = Configuration.new
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def configure(&block)
|
|
@@ -48,11 +50,7 @@ module OpenFeature
|
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
def build_client(domain: nil, evaluation_context: nil)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
Client.new(provider: active_provider, domain:, evaluation_context:)
|
|
54
|
-
rescue
|
|
55
|
-
Client.new(provider: Provider::NoOpProvider.new, evaluation_context:)
|
|
53
|
+
Client.new(domain:, evaluation_context:, configuration:)
|
|
56
54
|
end
|
|
57
55
|
|
|
58
56
|
def provider_metadata(domain: nil)
|
|
@@ -17,12 +17,13 @@ module OpenFeature
|
|
|
17
17
|
SUFFIXES = %i[value details].freeze
|
|
18
18
|
EMPTY_HINTS = Hooks::Hints.new.freeze
|
|
19
19
|
|
|
20
|
-
attr_reader :metadata
|
|
20
|
+
attr_reader :metadata
|
|
21
21
|
|
|
22
|
-
attr_accessor :hooks
|
|
22
|
+
attr_accessor :hooks, :evaluation_context
|
|
23
23
|
|
|
24
|
-
def initialize(provider
|
|
24
|
+
def initialize(provider: nil, domain: nil, evaluation_context: nil, configuration: API.instance.configuration)
|
|
25
25
|
@provider = provider
|
|
26
|
+
@configuration = configuration
|
|
26
27
|
@metadata = ClientMetadata.new(domain:)
|
|
27
28
|
@evaluation_context = evaluation_context
|
|
28
29
|
@hooks = []
|
|
@@ -33,17 +34,17 @@ module OpenFeature
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def provider_status
|
|
36
|
-
|
|
37
|
+
@configuration.provider_state(resolve_provider)
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def add_handler(event_type, handler = nil, &block)
|
|
40
41
|
actual_handler = handler || block
|
|
41
|
-
|
|
42
|
+
@configuration.add_client_handler(self, event_type, actual_handler)
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
def remove_handler(event_type, handler = nil, &block)
|
|
45
46
|
actual_handler = handler || block
|
|
46
|
-
|
|
47
|
+
@configuration.remove_client_handler(self, event_type, actual_handler)
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
# Tracking API (spec 6.1.1.1) — dynamic-context paradigm
|
|
@@ -51,12 +52,13 @@ module OpenFeature
|
|
|
51
52
|
# Records a tracking event. If the provider does not implement
|
|
52
53
|
# tracking, this is a no-op (spec 6.1.4).
|
|
53
54
|
def track(tracking_event_name, evaluation_context: nil, tracking_event_details: nil)
|
|
54
|
-
|
|
55
|
+
provider = resolve_provider
|
|
56
|
+
return unless provider.respond_to?(:track)
|
|
55
57
|
|
|
56
58
|
built_context = build_evaluation_context(evaluation_context)
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
rescue
|
|
60
|
+
provider.track(tracking_event_name, evaluation_context: built_context, tracking_event_details: tracking_event_details)
|
|
61
|
+
rescue StandardError, NotImplementedError
|
|
60
62
|
nil
|
|
61
63
|
end
|
|
62
64
|
|
|
@@ -75,16 +77,18 @@ module OpenFeature
|
|
|
75
77
|
private
|
|
76
78
|
|
|
77
79
|
def fetch_details(type:, flag_key:, default_value:, evaluation_context: nil, invocation_hooks: [], hook_hints: nil)
|
|
80
|
+
provider = resolve_provider
|
|
78
81
|
built_context = build_evaluation_context(evaluation_context)
|
|
79
82
|
|
|
80
83
|
# Assemble ordered hooks: API → Client → Invocation → Provider (spec 4.4.2)
|
|
81
|
-
provider_hooks =
|
|
82
|
-
ordered_hooks = [
|
|
84
|
+
provider_hooks = provider.respond_to?(:hooks) ? Array(provider.hooks) : []
|
|
85
|
+
ordered_hooks = [*@configuration.hooks, *@hooks, *invocation_hooks, *provider_hooks]
|
|
83
86
|
|
|
84
|
-
# Short-circuit NOT_READY/FATAL providers (required by evaluation_v2.feature)
|
|
87
|
+
# Short-circuit NOT_READY/FATAL providers (required by evaluation_v2.feature). A provider resolved from the
|
|
88
|
+
# configuration is tracked while bound, so one that isn't was replaced mid-call and reads NOT_READY.
|
|
85
89
|
short_circuit_code = nil
|
|
86
|
-
if
|
|
87
|
-
short_circuit_code = short_circuit_error_code(
|
|
90
|
+
if @provider ? @configuration.provider_tracked?(provider) : !provider.equal?(@fallback_provider)
|
|
91
|
+
short_circuit_code = short_circuit_error_code(@configuration.provider_state(provider))
|
|
88
92
|
end
|
|
89
93
|
|
|
90
94
|
# Fast path: skip hook ceremony when no hooks are registered
|
|
@@ -98,7 +102,7 @@ module OpenFeature
|
|
|
98
102
|
return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
|
|
99
103
|
end
|
|
100
104
|
|
|
101
|
-
return evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: built_context)
|
|
105
|
+
return evaluate_flag(provider: provider, type: type, flag_key: flag_key, default_value: default_value, evaluation_context: built_context)
|
|
102
106
|
end
|
|
103
107
|
|
|
104
108
|
hook_context = Hooks::HookContext.new(
|
|
@@ -107,7 +111,7 @@ module OpenFeature
|
|
|
107
111
|
default_value: default_value,
|
|
108
112
|
evaluation_context: built_context,
|
|
109
113
|
client_metadata: @metadata,
|
|
110
|
-
provider_metadata:
|
|
114
|
+
provider_metadata: provider.respond_to?(:metadata) ? provider.metadata : nil
|
|
111
115
|
)
|
|
112
116
|
|
|
113
117
|
hints = if hook_hints.is_a?(Hooks::Hints)
|
|
@@ -118,7 +122,7 @@ module OpenFeature
|
|
|
118
122
|
EMPTY_HINTS
|
|
119
123
|
end
|
|
120
124
|
|
|
121
|
-
executor = Hooks::HookExecutor.new(logger:
|
|
125
|
+
executor = Hooks::HookExecutor.new(logger: @configuration.logger)
|
|
122
126
|
|
|
123
127
|
if short_circuit_code
|
|
124
128
|
resolution = Provider::ResolutionDetails.new(
|
|
@@ -131,7 +135,7 @@ module OpenFeature
|
|
|
131
135
|
evaluation_details
|
|
132
136
|
else
|
|
133
137
|
executor.execute(ordered_hooks: ordered_hooks, hook_context: hook_context, hints: hints) do |hctx|
|
|
134
|
-
evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: hctx.evaluation_context)
|
|
138
|
+
evaluate_flag(provider: provider, type: type, flag_key: flag_key, default_value: default_value, evaluation_context: hctx.evaluation_context)
|
|
135
139
|
end
|
|
136
140
|
end
|
|
137
141
|
rescue StandardError, NotImplementedError => e
|
|
@@ -145,8 +149,8 @@ module OpenFeature
|
|
|
145
149
|
EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
|
|
146
150
|
end
|
|
147
151
|
|
|
148
|
-
def evaluate_flag(type:, flag_key:, default_value:, evaluation_context:)
|
|
149
|
-
resolution_details =
|
|
152
|
+
def evaluate_flag(provider:, type:, flag_key:, default_value:, evaluation_context:)
|
|
153
|
+
resolution_details = provider.send(
|
|
150
154
|
:"fetch_#{type}_value",
|
|
151
155
|
flag_key: flag_key,
|
|
152
156
|
default_value: default_value,
|
|
@@ -163,6 +167,12 @@ module OpenFeature
|
|
|
163
167
|
EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution_details)
|
|
164
168
|
end
|
|
165
169
|
|
|
170
|
+
# Callers resolve once per operation, so a rebinding mid-evaluation can't mix two providers.
|
|
171
|
+
# The fallback NoOp is never registered: untracked, so not short-circuited, and its status reads NOT_READY.
|
|
172
|
+
def resolve_provider
|
|
173
|
+
@provider || @configuration.provider(domain: @metadata.domain) || (@fallback_provider ||= Provider::NoOpProvider.new)
|
|
174
|
+
end
|
|
175
|
+
|
|
166
176
|
def short_circuit_error_code(state)
|
|
167
177
|
case state
|
|
168
178
|
when ProviderState::NOT_READY then Provider::ErrorCode::PROVIDER_NOT_READY
|
|
@@ -171,11 +181,11 @@ module OpenFeature
|
|
|
171
181
|
end
|
|
172
182
|
|
|
173
183
|
def build_evaluation_context(invocation_context)
|
|
174
|
-
propagator =
|
|
184
|
+
propagator = @configuration.transaction_context_propagator
|
|
175
185
|
transaction_context = propagator&.get_transaction_context
|
|
176
186
|
|
|
177
187
|
EvaluationContextBuilder.new.call(
|
|
178
|
-
api_context:
|
|
188
|
+
api_context: @configuration.evaluation_context,
|
|
179
189
|
transaction_context: transaction_context,
|
|
180
190
|
client_context: evaluation_context,
|
|
181
191
|
invocation_context: invocation_context
|
|
@@ -21,19 +21,21 @@ module OpenFeature
|
|
|
21
21
|
attr_reader :logger
|
|
22
22
|
|
|
23
23
|
# One per registration; +done+ is closed once init finishes, so waiters can pop on it
|
|
24
|
-
InitAttempt = Struct.new(:state, :error, :done)
|
|
24
|
+
InitAttempt = Struct.new(:state, :error, :done, :thread)
|
|
25
25
|
private_constant :InitAttempt
|
|
26
26
|
|
|
27
27
|
def initialize
|
|
28
28
|
@hooks = []
|
|
29
29
|
@providers = {}
|
|
30
30
|
@init_attempts = {}.compare_by_identity
|
|
31
|
+
@legacy_warned = Set.new.compare_by_identity
|
|
31
32
|
@provider_mutex = Mutex.new
|
|
32
33
|
@logger = nil
|
|
33
34
|
@event_dispatcher = EventDispatcher.new(@logger)
|
|
34
35
|
@provider_state_registry = ProviderStateRegistry.new
|
|
35
36
|
@client_handlers = {}
|
|
36
37
|
@client_handlers_mutex = Mutex.new
|
|
38
|
+
@dispatch_key = :"openfeature_dispatch_#{object_id}"
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
def add_hooks(*new_hooks)
|
|
@@ -60,6 +62,8 @@ module OpenFeature
|
|
|
60
62
|
|
|
61
63
|
# @api private
|
|
62
64
|
def add_client_handler(client, event_type, handler)
|
|
65
|
+
@event_dispatcher.validate_handler!(event_type, handler)
|
|
66
|
+
|
|
63
67
|
@client_handlers_mutex.synchronize do
|
|
64
68
|
@client_handlers[client] ||= Hash.new { |h, k| h[k] = [] }
|
|
65
69
|
@client_handlers[client][event_type] << handler
|
|
@@ -123,9 +127,9 @@ module OpenFeature
|
|
|
123
127
|
@provider_state_registry.clear
|
|
124
128
|
@provider_mutex.synchronize do
|
|
125
129
|
@providers.clear
|
|
126
|
-
|
|
127
|
-
@init_attempts.each_value { |attempt| attempt.done.close }
|
|
130
|
+
@init_attempts.each_value { |attempt| abandon_init_attempt(attempt) }
|
|
128
131
|
@init_attempts.clear
|
|
132
|
+
@legacy_warned.clear
|
|
129
133
|
end
|
|
130
134
|
@hooks.clear
|
|
131
135
|
@evaluation_context = nil
|
|
@@ -144,31 +148,36 @@ module OpenFeature
|
|
|
144
148
|
|
|
145
149
|
# Provider code runs before taking the lock: it may call back into the SDK
|
|
146
150
|
scoped = domain_scoped?(provider)
|
|
151
|
+
has_init = provider.respond_to?(:init)
|
|
152
|
+
legacy = has_init && !emits_lifecycle_events?(provider)
|
|
153
|
+
warn_legacy_provider(provider) if legacy
|
|
147
154
|
|
|
148
155
|
@provider_mutex.synchronize do
|
|
149
156
|
validate_domain_scoped_binding!(provider, domain, scoped)
|
|
150
157
|
|
|
151
158
|
old_provider = @providers[domain]
|
|
152
159
|
|
|
153
|
-
new_providers = @providers.dup
|
|
154
|
-
new_providers[domain] = provider
|
|
155
|
-
@providers = new_providers
|
|
156
|
-
|
|
157
160
|
# Spec 1.1.2.2: initialize once per registration; a failed init is retried on the next set
|
|
158
161
|
attempt = @init_attempts[provider]
|
|
159
162
|
needs_init = attempt.nil? || attempt.state == :failed
|
|
160
163
|
|
|
161
164
|
if needs_init
|
|
165
|
+
# First, so a provider registered with another API (1.8.4) leaves no state behind
|
|
166
|
+
provider.send(:attach, self) if provider.is_a?(Provider::EventEmitter)
|
|
162
167
|
attempt = @init_attempts[provider] = InitAttempt.new(:running, nil, Thread::Queue.new)
|
|
163
168
|
@provider_state_registry.set_initial_state(provider)
|
|
164
|
-
provider.send(:attach, self) if provider.is_a?(Provider::EventEmitter)
|
|
165
169
|
end
|
|
166
170
|
|
|
171
|
+
# Publish last: clients read @providers without the lock, and must not see an untracked provider
|
|
172
|
+
new_providers = @providers.dup
|
|
173
|
+
new_providers[domain] = provider
|
|
174
|
+
@providers = new_providers
|
|
175
|
+
|
|
167
176
|
# Spec 1.1.2.3: Only shutdown old provider if it's no longer bound to any domain
|
|
168
177
|
if old_provider && !old_provider.equal?(provider)
|
|
169
178
|
needs_shutdown = !provider_bound?(old_provider)
|
|
170
179
|
if needs_shutdown
|
|
171
|
-
@init_attempts.delete(old_provider)
|
|
180
|
+
abandon_init_attempt(@init_attempts.delete(old_provider))
|
|
172
181
|
@provider_state_registry.remove_provider(old_provider)
|
|
173
182
|
old_provider.send(:detach) if old_provider.is_a?(Provider::EventEmitter)
|
|
174
183
|
end
|
|
@@ -186,37 +195,43 @@ module OpenFeature
|
|
|
186
195
|
|
|
187
196
|
# Initialize provider outside the mutex to avoid blocking other operations
|
|
188
197
|
if needs_init
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
# Spec 2.8.5.1: a provider without init is READY, and its handlers have run, when set_provider returns
|
|
199
|
+
if wait_for_init || !has_init
|
|
200
|
+
init_provider(provider, context_for_init, domain:, attempt:, raise_on_error: wait_for_init)
|
|
201
|
+
# Replaced or shut down while init ran: fail like the waiters on this attempt did
|
|
202
|
+
raise incomplete_init_error(provider, "initialization did not complete") if wait_for_init && attempt.state != :succeeded
|
|
191
203
|
else
|
|
192
204
|
init_thread = Thread.new do
|
|
193
205
|
init_provider(provider, context_for_init, domain:, attempt:, raise_on_error: false)
|
|
194
206
|
end
|
|
195
207
|
end
|
|
208
|
+
elsif !has_init
|
|
209
|
+
# Spec 2.8.5.1: each new binding of a provider without init runs its domain's READY handlers
|
|
210
|
+
dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY, token: attempt) unless old_provider.equal?(provider)
|
|
196
211
|
elsif wait_for_init
|
|
197
|
-
# Already registered: wait for an init still in flight, then report its outcome
|
|
212
|
+
# Already registered: wait for an init still in flight, then report its outcome.
|
|
213
|
+
# From inside that init (e.g. a handler of an event it emits) the wait could never end.
|
|
214
|
+
raise incomplete_init_error(provider, "is still initializing on this thread") if running_on_this_thread?(attempt)
|
|
215
|
+
|
|
198
216
|
attempt.done.pop
|
|
199
|
-
unless attempt.state == :succeeded
|
|
200
|
-
raise attempt.error || ProviderInitializationError.new(
|
|
201
|
-
"Provider #{provider.class.name} initialization did not complete",
|
|
202
|
-
provider:,
|
|
203
|
-
error_code: Provider::ErrorCode::GENERAL
|
|
204
|
-
)
|
|
205
|
-
end
|
|
217
|
+
raise attempt.error || incomplete_init_error(provider, "initialization did not complete") unless attempt.state == :succeeded
|
|
206
218
|
|
|
207
|
-
dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
|
|
219
|
+
dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY, token: attempt) if legacy
|
|
208
220
|
end
|
|
209
221
|
ensure
|
|
210
222
|
# Fail an attempt no init took over (e.g. a Timeout hit first), or it would stay running forever
|
|
211
|
-
finish_init_attempt(attempt, :failed) if needs_init && !init_thread
|
|
223
|
+
finish_init_attempt(attempt, :failed) if needs_init && attempt && !init_thread
|
|
212
224
|
end
|
|
213
225
|
|
|
214
226
|
def init_provider(provider, context, domain:, attempt:, raise_on_error: false)
|
|
227
|
+
emits_lifecycle_events = emits_lifecycle_events?(provider)
|
|
228
|
+
attempt.thread = Thread.current
|
|
215
229
|
call_init(provider, context, domain)
|
|
216
230
|
|
|
217
|
-
# Settle the attempt before handlers run, so a handler can register this provider again
|
|
218
|
-
|
|
219
|
-
|
|
231
|
+
# Settle the attempt before handlers run, so a handler can register this provider again.
|
|
232
|
+
# An abandoned attempt (provider replaced or shut down meanwhile) reports no READY.
|
|
233
|
+
# Spec 2.8.2/2.8.3: a provider that emits its own lifecycle events gets no synthetic READY or ERROR
|
|
234
|
+
dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY, token: attempt) if finish_init_attempt(attempt, :succeeded) && !emits_lifecycle_events
|
|
220
235
|
rescue => e
|
|
221
236
|
# Spec 1.7.5: PROVIDER_FATAL from init makes the provider FATAL
|
|
222
237
|
error_code = EvaluationError.code_for(e)
|
|
@@ -228,9 +243,10 @@ module OpenFeature
|
|
|
228
243
|
)
|
|
229
244
|
finish_init_attempt(attempt, :failed, error)
|
|
230
245
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
246
|
+
unless emits_lifecycle_events
|
|
247
|
+
dispatch_provider_event(provider, ProviderEvent::PROVIDER_ERROR,
|
|
248
|
+
{error_code: error_code, message: e.message}, token: attempt)
|
|
249
|
+
end
|
|
234
250
|
|
|
235
251
|
raise error if raise_on_error
|
|
236
252
|
ensure
|
|
@@ -238,35 +254,80 @@ module OpenFeature
|
|
|
238
254
|
finish_init_attempt(attempt, :failed)
|
|
239
255
|
end
|
|
240
256
|
|
|
257
|
+
# Caller holds @provider_mutex: the provider was unbound or the API reset before its init settled
|
|
258
|
+
def abandon_init_attempt(attempt)
|
|
259
|
+
return unless attempt
|
|
260
|
+
|
|
261
|
+
attempt.state = :failed if attempt.state == :running
|
|
262
|
+
attempt.done.close
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def incomplete_init_error(provider, reason)
|
|
266
|
+
ProviderInitializationError.new("Provider #{provider.class.name} #{reason}", provider:, error_code: Provider::ErrorCode::GENERAL)
|
|
267
|
+
end
|
|
268
|
+
|
|
241
269
|
def finish_init_attempt(attempt, state, error = nil)
|
|
242
270
|
@provider_mutex.synchronize do
|
|
243
|
-
return unless attempt.state == :running
|
|
271
|
+
return false unless attempt.state == :running
|
|
244
272
|
|
|
245
273
|
attempt.state = state
|
|
246
274
|
attempt.error = error
|
|
247
275
|
end
|
|
248
276
|
attempt.done.close
|
|
277
|
+
true
|
|
249
278
|
end
|
|
250
279
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
280
|
+
# An event for a provider already dispatching on this call stack is queued behind the current one
|
|
281
|
+
def dispatch_provider_event(provider, event_type, details = {}, token: nil)
|
|
282
|
+
# Lifecycle events (the SDK's own, or the provider's from its init thread) record status when sent, so
|
|
283
|
+
# set_provider_and_wait returns with it even from the provider's own handler (1.1.2.4); others record it when delivered (5.3.5)
|
|
284
|
+
token ||= running_init_attempt_on_this_thread(provider)
|
|
285
|
+
return if token && !accept_provider_event(provider, event_type, details, token)
|
|
286
|
+
|
|
287
|
+
queues = (Thread.current[@dispatch_key] ||= {}.compare_by_identity)
|
|
288
|
+
if (queue = queues[provider])
|
|
289
|
+
queue << [event_type, details.dup, token]
|
|
290
|
+
return
|
|
257
291
|
end
|
|
258
292
|
|
|
259
|
-
|
|
293
|
+
queue = queues[provider] = [[event_type, details, token]]
|
|
294
|
+
begin
|
|
295
|
+
deliver_provider_event(provider, *queue.shift)
|
|
296
|
+
until queue.empty?
|
|
297
|
+
begin
|
|
298
|
+
deliver_provider_event(provider, *queue.shift)
|
|
299
|
+
rescue => e
|
|
300
|
+
# Its own emitter already returned, so log rather than fail the event that was dispatching
|
|
301
|
+
@logger&.error("Queued provider event failed: #{e.message}")
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
ensure
|
|
305
|
+
queues.delete(provider)
|
|
306
|
+
Thread.current[@dispatch_key] = nil if queues.empty?
|
|
307
|
+
end
|
|
308
|
+
end
|
|
260
309
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}.merge(details)
|
|
310
|
+
def deliver_provider_event(provider, event_type, details, token)
|
|
311
|
+
return unless accept_provider_event(provider, event_type, details, token, record_status: token.nil?)
|
|
264
312
|
|
|
313
|
+
event_details = {provider_name: extract_provider_name(provider)}.merge(details)
|
|
265
314
|
run_handlers_for_provider(provider, event_type, event_details)
|
|
266
315
|
end
|
|
267
316
|
|
|
268
317
|
private
|
|
269
318
|
|
|
319
|
+
# Under the replacement lock, so a late event can't re-track a replaced or shut-down provider
|
|
320
|
+
def accept_provider_event(provider, event_type, details, token, record_status: true)
|
|
321
|
+
@provider_mutex.synchronize do
|
|
322
|
+
return false unless provider_bound?(provider)
|
|
323
|
+
# An SDK READY/ERROR from an earlier registration of this provider is stale
|
|
324
|
+
return false unless token.nil? || @init_attempts[provider].equal?(token)
|
|
325
|
+
|
|
326
|
+
@provider_state_registry.update_state_from_event(provider, event_type, details) if record_status
|
|
327
|
+
true
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
270
331
|
def provider_bound?(provider)
|
|
271
332
|
@providers.each_value.any? { |p| p.equal?(provider) }
|
|
272
333
|
end
|
|
@@ -279,6 +340,26 @@ module OpenFeature
|
|
|
279
340
|
provider.respond_to?(:domain_scoped?) && provider.domain_scoped?
|
|
280
341
|
end
|
|
281
342
|
|
|
343
|
+
# Spec 2.8.5.1: without init there's nothing to report, so the marker only counts alongside init
|
|
344
|
+
def emits_lifecycle_events?(provider)
|
|
345
|
+
provider.respond_to?(:init) && provider.respond_to?(:emits_lifecycle_events?) && provider.emits_lifecycle_events?
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def running_init_attempt_on_this_thread(provider)
|
|
349
|
+
attempt = @provider_mutex.synchronize { @init_attempts[provider] }
|
|
350
|
+
attempt if attempt && running_on_this_thread?(attempt)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def running_on_this_thread?(attempt)
|
|
354
|
+
attempt.state == :running && attempt.thread.equal?(Thread.current)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def warn_legacy_provider(provider)
|
|
358
|
+
return if @logger.nil? || !@provider_mutex.synchronize { @legacy_warned.add?(provider.class) }
|
|
359
|
+
@logger.warn("Provider #{extract_provider_name(provider)} relies on SDK-emitted lifecycle events, which are deprecated; " \
|
|
360
|
+
"emit PROVIDER_READY/PROVIDER_ERROR from init and define emits_lifecycle_events? returning true (spec Appendix E)")
|
|
361
|
+
end
|
|
362
|
+
|
|
282
363
|
def validate_domain_scoped_binding!(provider, domain, scoped)
|
|
283
364
|
return unless scoped && provider_bound?(provider) && !@providers[domain].equal?(provider)
|
|
284
365
|
|
|
@@ -313,22 +394,20 @@ module OpenFeature
|
|
|
313
394
|
# Run global handlers (API-level, no domain filtering)
|
|
314
395
|
@event_dispatcher.trigger_event(event_type, event_details)
|
|
315
396
|
|
|
316
|
-
# Run client handlers (domain-scoped)
|
|
317
|
-
@client_handlers_mutex.synchronize do
|
|
318
|
-
@client_handlers.
|
|
397
|
+
# Run client handlers (domain-scoped) outside the lock, so they can add or remove handlers
|
|
398
|
+
handlers = @client_handlers_mutex.synchronize do
|
|
399
|
+
@client_handlers.flat_map do |client, handlers_by_event|
|
|
319
400
|
# Check if this client should receive events from this provider
|
|
320
401
|
client_provider = provider(domain: client.metadata.domain)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
# Trigger handlers for this client
|
|
324
|
-
handlers = handlers_by_event[event_type]
|
|
325
|
-
handlers.each do |handler|
|
|
326
|
-
handler.call(event_details)
|
|
327
|
-
rescue => e
|
|
328
|
-
@logger&.error("Client event handler failed: #{e.message}")
|
|
329
|
-
end
|
|
402
|
+
client_provider&.equal?(provider) ? handlers_by_event[event_type] : []
|
|
330
403
|
end
|
|
331
404
|
end
|
|
405
|
+
|
|
406
|
+
handlers.each do |handler|
|
|
407
|
+
handler.call(event_details)
|
|
408
|
+
rescue => e
|
|
409
|
+
@logger&.error("Client event handler failed: #{e.message}")
|
|
410
|
+
end
|
|
332
411
|
end
|
|
333
412
|
|
|
334
413
|
def run_immediate_handler(event_type, handler, client)
|
|
@@ -340,8 +419,8 @@ module OpenFeature
|
|
|
340
419
|
}
|
|
341
420
|
|
|
342
421
|
if client.nil?
|
|
343
|
-
# API-level handler: check
|
|
344
|
-
@providers.each do |
|
|
422
|
+
# API-level handler: check each bound provider once, however many domains it's bound to
|
|
423
|
+
@providers.values.uniq(&:object_id).each do |provider|
|
|
345
424
|
next unless provider
|
|
346
425
|
|
|
347
426
|
provider_state = @provider_state_registry.get_state(provider)
|
|
@@ -16,14 +16,18 @@ module OpenFeature
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def add_handler(event_type, handler)
|
|
19
|
-
|
|
20
|
-
raise ArgumentError, "Handler must respond to call" unless handler.respond_to?(:call)
|
|
19
|
+
validate_handler!(event_type, handler)
|
|
21
20
|
|
|
22
21
|
@mutex.synchronize do
|
|
23
22
|
@handlers[event_type] << handler
|
|
24
23
|
end
|
|
25
24
|
end
|
|
26
25
|
|
|
26
|
+
def validate_handler!(event_type, handler)
|
|
27
|
+
raise ArgumentError, "Invalid event type: #{event_type}" unless valid_event?(event_type)
|
|
28
|
+
raise ArgumentError, "Handler must respond to call" unless handler.respond_to?(:call)
|
|
29
|
+
end
|
|
30
|
+
|
|
27
31
|
def remove_handler(event_type, handler)
|
|
28
32
|
return unless valid_event?(event_type)
|
|
29
33
|
|
|
@@ -54,7 +54,7 @@ module OpenFeature
|
|
|
54
54
|
else
|
|
55
55
|
run_after_hooks(ordered_hooks, hook_context, evaluation_details, hints)
|
|
56
56
|
end
|
|
57
|
-
rescue => e
|
|
57
|
+
rescue StandardError, NotImplementedError => e
|
|
58
58
|
run_error_hooks(ordered_hooks, hook_context, e, hints)
|
|
59
59
|
|
|
60
60
|
evaluation_details = EvaluationDetails.new(
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../sdk"
|
|
4
|
+
|
|
5
|
+
module OpenFeature
|
|
6
|
+
module SDK
|
|
7
|
+
# Spec 1.8.3: a separate require, so isolated instances are an explicit opt-in
|
|
8
|
+
module Isolated
|
|
9
|
+
# Spec 1.8.1: a new API that shares no state with the global one or other isolated APIs
|
|
10
|
+
def self.create_api
|
|
11
|
+
API.send(:new)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -8,7 +8,7 @@ module OpenFeature
|
|
|
8
8
|
# Mixin for providers that emit lifecycle events
|
|
9
9
|
module EventEmitter
|
|
10
10
|
def emit_event(event_type, details = {})
|
|
11
|
-
config = @
|
|
11
|
+
config = @openfeature_configuration
|
|
12
12
|
return unless config
|
|
13
13
|
|
|
14
14
|
unless ::OpenFeature::SDK::ProviderEvent::ALL_EVENTS.include?(event_type)
|
|
@@ -19,17 +19,22 @@ module OpenFeature
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def configuration_attached?
|
|
22
|
-
!@
|
|
22
|
+
!@openfeature_configuration.nil?
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
27
|
def attach(configuration)
|
|
28
|
-
|
|
28
|
+
# Spec 1.8.4: only providers using this mixin know which API they're registered with
|
|
29
|
+
if @openfeature_configuration && !@openfeature_configuration.equal?(configuration)
|
|
30
|
+
raise ArgumentError, "Provider is already registered with another OpenFeature API instance"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@openfeature_configuration = configuration
|
|
29
34
|
end
|
|
30
35
|
|
|
31
36
|
def detach
|
|
32
|
-
@
|
|
37
|
+
@openfeature_configuration = nil
|
|
33
38
|
end
|
|
34
39
|
end
|
|
35
40
|
end
|
|
@@ -21,8 +21,8 @@ module OpenFeature
|
|
|
21
21
|
# @param message [String] the error message
|
|
22
22
|
# @param provider [Object] the provider that failed to initialize
|
|
23
23
|
# @param original_error [Exception] the original error that caused the failure
|
|
24
|
-
# @param error_code [String] the OpenFeature error code (defaults to
|
|
25
|
-
def initialize(message, provider: nil, original_error: nil, error_code: Provider::ErrorCode::
|
|
24
|
+
# @param error_code [String] the OpenFeature error code (defaults to GENERAL)
|
|
25
|
+
def initialize(message, provider: nil, original_error: nil, error_code: Provider::ErrorCode::GENERAL)
|
|
26
26
|
super(message)
|
|
27
27
|
@provider = provider
|
|
28
28
|
@original_error = original_error
|
|
@@ -9,7 +9,7 @@ module OpenFeature
|
|
|
9
9
|
# Tracks provider states
|
|
10
10
|
class ProviderStateRegistry
|
|
11
11
|
def initialize
|
|
12
|
-
@states = {}
|
|
12
|
+
@states = {}.compare_by_identity
|
|
13
13
|
@mutex = Mutex.new
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -17,7 +17,7 @@ module OpenFeature
|
|
|
17
17
|
return unless provider
|
|
18
18
|
|
|
19
19
|
@mutex.synchronize do
|
|
20
|
-
@states[provider
|
|
20
|
+
@states[provider] = {state: state, details: {}}
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
@@ -29,7 +29,7 @@ module OpenFeature
|
|
|
29
29
|
# Only update state if the event should cause a state change
|
|
30
30
|
if new_state
|
|
31
31
|
@mutex.synchronize do
|
|
32
|
-
@states[provider
|
|
32
|
+
@states[provider] = {state: new_state, details: event_details || {}}
|
|
33
33
|
end
|
|
34
34
|
new_state
|
|
35
35
|
else
|
|
@@ -42,7 +42,7 @@ module OpenFeature
|
|
|
42
42
|
return ProviderState::NOT_READY unless provider
|
|
43
43
|
|
|
44
44
|
@mutex.synchronize do
|
|
45
|
-
entry = @states[provider
|
|
45
|
+
entry = @states[provider]
|
|
46
46
|
entry ? entry[:state] : ProviderState::NOT_READY
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -51,7 +51,7 @@ module OpenFeature
|
|
|
51
51
|
return {} unless provider
|
|
52
52
|
|
|
53
53
|
@mutex.synchronize do
|
|
54
|
-
entry = @states[provider
|
|
54
|
+
entry = @states[provider]
|
|
55
55
|
entry ? entry[:details] : {}
|
|
56
56
|
end
|
|
57
57
|
end
|
|
@@ -60,7 +60,7 @@ module OpenFeature
|
|
|
60
60
|
return unless provider
|
|
61
61
|
|
|
62
62
|
@mutex.synchronize do
|
|
63
|
-
@states.delete(provider
|
|
63
|
+
@states.delete(provider)
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
@@ -68,7 +68,7 @@ module OpenFeature
|
|
|
68
68
|
return false unless provider
|
|
69
69
|
|
|
70
70
|
@mutex.synchronize do
|
|
71
|
-
@states.key?(provider
|
|
71
|
+
@states.key?(provider)
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
@@ -5,14 +5,19 @@ module OpenFeature
|
|
|
5
5
|
class ThreadLocalTransactionContextPropagator
|
|
6
6
|
include TransactionContextPropagator
|
|
7
7
|
|
|
8
|
+
# One context per propagator under this key, so separate APIs never share one (spec 1.8.2)
|
|
9
|
+
# while resetting the key still clears them all. Weak keys let discarded propagators be collected.
|
|
8
10
|
THREAD_KEY = :openfeature_transaction_context
|
|
9
11
|
|
|
10
12
|
def set_transaction_context(evaluation_context)
|
|
11
|
-
Thread.current[THREAD_KEY]
|
|
13
|
+
contexts = Thread.current[THREAD_KEY]
|
|
14
|
+
contexts = Thread.current[THREAD_KEY] = ObjectSpace::WeakKeyMap.new unless contexts.is_a?(ObjectSpace::WeakKeyMap)
|
|
15
|
+
evaluation_context.nil? ? contexts.delete(self) : contexts[self] = evaluation_context
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
def get_transaction_context
|
|
15
|
-
Thread.current[THREAD_KEY]
|
|
19
|
+
contexts = Thread.current[THREAD_KEY]
|
|
20
|
+
contexts[self] if contexts.is_a?(ObjectSpace::WeakKeyMap)
|
|
16
21
|
end
|
|
17
22
|
end
|
|
18
23
|
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
module OpenFeature
|
|
2
2
|
module SDK
|
|
3
3
|
class API
|
|
4
|
-
include Singleton
|
|
5
4
|
extend Forwardable
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
include Singleton
|
|
7
|
+
|
|
8
|
+
attr_reader configuration: Configuration
|
|
9
|
+
|
|
10
|
+
def initialize: () -> void
|
|
8
11
|
|
|
9
12
|
def configure: () { (Configuration) -> void } -> void
|
|
10
13
|
|
|
@@ -7,10 +7,10 @@ module OpenFeature
|
|
|
7
7
|
EMPTY_HINTS: Hooks::Hints
|
|
8
8
|
|
|
9
9
|
attr_reader metadata: ClientMetadata
|
|
10
|
-
|
|
10
|
+
attr_accessor evaluation_context: EvaluationContext?
|
|
11
11
|
attr_accessor hooks: Array[untyped]
|
|
12
12
|
|
|
13
|
-
def initialize: (provider: untyped, ?domain: String?, ?evaluation_context: EvaluationContext?) -> void
|
|
13
|
+
def initialize: (?provider: untyped, ?domain: String?, ?evaluation_context: EvaluationContext?, ?configuration: Configuration) -> void
|
|
14
14
|
|
|
15
15
|
def add_hooks: (*Array[untyped] new_hooks) -> void
|
|
16
16
|
|
|
@@ -40,7 +40,8 @@ module OpenFeature
|
|
|
40
40
|
private
|
|
41
41
|
|
|
42
42
|
def fetch_details: (type: Symbol, flag_key: String, default_value: untyped, ?evaluation_context: EvaluationContext?, ?invocation_hooks: Array[untyped], ?hook_hints: Hooks::Hints?) -> EvaluationDetails?
|
|
43
|
-
def evaluate_flag: (type: Symbol, flag_key: String, default_value: untyped, evaluation_context: EvaluationContext?) -> EvaluationDetails
|
|
43
|
+
def evaluate_flag: (provider: untyped, type: Symbol, flag_key: String, default_value: untyped, evaluation_context: EvaluationContext?) -> EvaluationDetails
|
|
44
|
+
def resolve_provider: () -> untyped
|
|
44
45
|
def short_circuit_error_code: (String state) -> String?
|
|
45
46
|
def build_evaluation_context: (EvaluationContext? invocation_context) -> EvaluationContext?
|
|
46
47
|
def validate_default_value_type: (Symbol type, untyped default_value) -> void
|
|
@@ -33,10 +33,18 @@ module OpenFeature
|
|
|
33
33
|
def reset: () -> void
|
|
34
34
|
def set_provider_internal: (untyped provider, domain: String?, wait_for_init: bool) -> void
|
|
35
35
|
def init_provider: (untyped provider, EvaluationContext? context, domain: String?, attempt: untyped, ?raise_on_error: bool) -> void
|
|
36
|
-
def finish_init_attempt: (untyped attempt, Symbol state, ?ProviderInitializationError? error) ->
|
|
37
|
-
def
|
|
36
|
+
def finish_init_attempt: (untyped attempt, Symbol state, ?ProviderInitializationError? error) -> bool
|
|
37
|
+
def abandon_init_attempt: (untyped attempt) -> void
|
|
38
|
+
def incomplete_init_error: (untyped provider, String reason) -> ProviderInitializationError
|
|
39
|
+
def dispatch_provider_event: (untyped provider, String event_type, ?Hash[Symbol, untyped] details, ?token: untyped) -> void
|
|
40
|
+
def deliver_provider_event: (untyped provider, String event_type, Hash[Symbol, untyped]? details, untyped token) -> void
|
|
41
|
+
def accept_provider_event: (untyped provider, String event_type, Hash[Symbol, untyped]? details, untyped token, ?record_status: bool) -> bool
|
|
38
42
|
def provider_bound?: (untyped provider) -> bool
|
|
39
43
|
def domain_scoped?: (untyped provider) -> bool
|
|
44
|
+
def emits_lifecycle_events?: (untyped provider) -> bool
|
|
45
|
+
def warn_legacy_provider: (untyped provider) -> void
|
|
46
|
+
def running_init_attempt_on_this_thread: (untyped provider) -> untyped
|
|
47
|
+
def running_on_this_thread?: (untyped attempt) -> bool
|
|
40
48
|
def validate_domain_scoped_binding!: (untyped provider, String? domain, bool scoped) -> void
|
|
41
49
|
def call_init: (untyped provider, EvaluationContext? context, String? domain) -> void
|
|
42
50
|
def init_accepts_domain?: (Array[[Symbol, Symbol?]] parameters) -> bool
|
|
@@ -6,6 +6,7 @@ module OpenFeature
|
|
|
6
6
|
def initialize: (?untyped? logger) -> void
|
|
7
7
|
|
|
8
8
|
def add_handler: (String event_type, _ToProc handler) -> void
|
|
9
|
+
def validate_handler!: (String event_type, untyped handler) -> void
|
|
9
10
|
def remove_handler: (String event_type, _ToProc handler) -> void
|
|
10
11
|
def remove_all_handlers: (String event_type) -> void
|
|
11
12
|
def trigger_event: (String event_type, ?Hash[Symbol, untyped] event_details) -> void
|
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.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- OpenFeature Authors
|
|
@@ -55,6 +55,7 @@ files:
|
|
|
55
55
|
- lib/open_feature/sdk/hooks/hook_context.rb
|
|
56
56
|
- lib/open_feature/sdk/hooks/hook_executor.rb
|
|
57
57
|
- lib/open_feature/sdk/hooks/logging_hook.rb
|
|
58
|
+
- lib/open_feature/sdk/isolated.rb
|
|
58
59
|
- lib/open_feature/sdk/provider.rb
|
|
59
60
|
- lib/open_feature/sdk/provider/error_code.rb
|
|
60
61
|
- lib/open_feature/sdk/provider/event_emitter.rb
|
|
@@ -92,6 +93,7 @@ files:
|
|
|
92
93
|
- sig/open_feature/sdk/hooks/hook_context.rbs
|
|
93
94
|
- sig/open_feature/sdk/hooks/hook_executor.rbs
|
|
94
95
|
- sig/open_feature/sdk/hooks/logging_hook.rbs
|
|
96
|
+
- sig/open_feature/sdk/isolated.rbs
|
|
95
97
|
- sig/open_feature/sdk/provider.rbs
|
|
96
98
|
- sig/open_feature/sdk/provider/error_code.rbs
|
|
97
99
|
- sig/open_feature/sdk/provider/event_emitter.rbs
|