openfeature-sdk 0.7.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b8b6e8a5fd01205278fa44c809eb3c1ef578b56e5542aa94793a090e605458d
4
- data.tar.gz: f85c7d0e8b804fb651e02cc3c59d14c21458f9fdb419d58df6d8fbb714a35b9b
3
+ metadata.gz: bf1041c34e962f832b25f1be3e9eeab7fee3fd4272dc9e1e0e8dabe85b6d21e8
4
+ data.tar.gz: f45667a6a898d3521ea87c31aea62491ace0682c72ef2cfa2a16d5f8a0f3f0b5
5
5
  SHA512:
6
- metadata.gz: a7fef199498fb02863ed74cf76a6fbff8acf65df23e2e5d4baad90b6ed3da589b6ca98da1540443a7367456a527819eb2380979aa4f1a1ef8780c33ccf425d32
7
- data.tar.gz: 71db2db0d85592861ebd5f117d7599e9d54374deb9a6eae5060716044abddb27a4bd10db7ca07314540a36adeb7c366afa1a8c21fb4f19824e05e15fd59a1a27
6
+ metadata.gz: 393adb8b637763e5b397f6ee702017705bf355c8785ceb1280aa4f95b585a061f8da6e4a6d67ed9e8ab20de1f341b36d4dc3febb38dbbcc7a41a68cfb8002b8c
7
+ data.tar.gz: d145cc98fb609aa75ad06485d4486cc6aec19ea953cd627f9f71b33e991fafd3a57fa139fbe07f878e5bc105885ce35a74c0ab12485e50649d63b79a100eec98
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.7.0"
2
+ ".": "0.7.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## [0.7.0](https://github.com/open-feature/ruby-sdk/compare/v0.6.7...v0.7.0) (2026-10-23)
4
16
 
5
17
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk (0.7.0)
4
+ openfeature-sdk (0.7.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
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.7.0">
21
- <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.7.0&color=blue&style=for-the-badge" />
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 -->
@@ -443,6 +443,29 @@ class MyRequestScopedPropagator
443
443
  end
444
444
  ```
445
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
+
446
469
  ## Extending
447
470
 
448
471
  ### Develop a provider
@@ -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
- def configuration
41
- @configuration ||= Configuration.new
40
+ attr_reader :configuration
41
+
42
+ def initialize
43
+ @configuration = Configuration.new
42
44
  end
43
45
 
44
46
  def configure(&block)
@@ -48,7 +50,7 @@ module OpenFeature
48
50
  end
49
51
 
50
52
  def build_client(domain: nil, evaluation_context: nil)
51
- Client.new(domain:, evaluation_context:)
53
+ Client.new(domain:, evaluation_context:, configuration:)
52
54
  end
53
55
 
54
56
  def provider_metadata(domain: nil)
@@ -21,8 +21,9 @@ module OpenFeature
21
21
 
22
22
  attr_accessor :hooks, :evaluation_context
23
23
 
24
- def initialize(provider: nil, domain: nil, evaluation_context: nil)
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
- OpenFeature::SDK.configuration.provider_state(resolve_provider)
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
- OpenFeature::SDK.configuration.add_client_handler(self, event_type, actual_handler)
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
- OpenFeature::SDK.configuration.remove_client_handler(self, event_type, actual_handler)
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
@@ -81,13 +82,13 @@ module OpenFeature
81
82
 
82
83
  # Assemble ordered hooks: API → Client → Invocation → Provider (spec 4.4.2)
83
84
  provider_hooks = provider.respond_to?(:hooks) ? Array(provider.hooks) : []
84
- ordered_hooks = [*OpenFeature::SDK.hooks, *@hooks, *invocation_hooks, *provider_hooks]
85
+ ordered_hooks = [*@configuration.hooks, *@hooks, *invocation_hooks, *provider_hooks]
85
86
 
86
87
  # Short-circuit NOT_READY/FATAL providers (required by evaluation_v2.feature). A provider resolved from the
87
88
  # configuration is tracked while bound, so one that isn't was replaced mid-call and reads NOT_READY.
88
89
  short_circuit_code = nil
89
- if @provider ? OpenFeature::SDK.configuration.provider_tracked?(provider) : !provider.equal?(@fallback_provider)
90
- short_circuit_code = short_circuit_error_code(OpenFeature::SDK.configuration.provider_state(provider))
90
+ if @provider ? @configuration.provider_tracked?(provider) : !provider.equal?(@fallback_provider)
91
+ short_circuit_code = short_circuit_error_code(@configuration.provider_state(provider))
91
92
  end
92
93
 
93
94
  # Fast path: skip hook ceremony when no hooks are registered
@@ -121,7 +122,7 @@ module OpenFeature
121
122
  EMPTY_HINTS
122
123
  end
123
124
 
124
- executor = Hooks::HookExecutor.new(logger: OpenFeature::SDK.configuration.logger)
125
+ executor = Hooks::HookExecutor.new(logger: @configuration.logger)
125
126
 
126
127
  if short_circuit_code
127
128
  resolution = Provider::ResolutionDetails.new(
@@ -169,7 +170,7 @@ module OpenFeature
169
170
  # Callers resolve once per operation, so a rebinding mid-evaluation can't mix two providers.
170
171
  # The fallback NoOp is never registered: untracked, so not short-circuited, and its status reads NOT_READY.
171
172
  def resolve_provider
172
- @provider || OpenFeature::SDK.configuration.provider(domain: @metadata.domain) || (@fallback_provider ||= Provider::NoOpProvider.new)
173
+ @provider || @configuration.provider(domain: @metadata.domain) || (@fallback_provider ||= Provider::NoOpProvider.new)
173
174
  end
174
175
 
175
176
  def short_circuit_error_code(state)
@@ -180,11 +181,11 @@ module OpenFeature
180
181
  end
181
182
 
182
183
  def build_evaluation_context(invocation_context)
183
- propagator = OpenFeature::SDK.configuration.transaction_context_propagator
184
+ propagator = @configuration.transaction_context_propagator
184
185
  transaction_context = propagator&.get_transaction_context
185
186
 
186
187
  EvaluationContextBuilder.new.call(
187
- api_context: OpenFeature::SDK.evaluation_context,
188
+ api_context: @configuration.evaluation_context,
188
189
  transaction_context: transaction_context,
189
190
  client_context: evaluation_context,
190
191
  invocation_context: invocation_context
@@ -4,6 +4,11 @@ module OpenFeature
4
4
  module SDK
5
5
  ClientMetadata = Struct.new(:domain, keyword_init: true) do
6
6
  alias_method :name, :domain
7
+
8
+ def initialize(domain: nil)
9
+ super(domain: domain&.dup&.freeze)
10
+ freeze
11
+ end
7
12
  end
8
13
  end
9
14
  end
@@ -162,9 +162,10 @@ module OpenFeature
162
162
  needs_init = attempt.nil? || attempt.state == :failed
163
163
 
164
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)
165
167
  attempt = @init_attempts[provider] = InitAttempt.new(:running, nil, Thread::Queue.new)
166
168
  @provider_state_registry.set_initial_state(provider)
167
- provider.send(:attach, self) if provider.is_a?(Provider::EventEmitter)
168
169
  end
169
170
 
170
171
  # Publish last: clients read @providers without the lock, and must not see an untracked provider
@@ -219,7 +220,7 @@ module OpenFeature
219
220
  end
220
221
  ensure
221
222
  # Fail an attempt no init took over (e.g. a Timeout hit first), or it would stay running forever
222
- finish_init_attempt(attempt, :failed) if needs_init && !init_thread
223
+ finish_init_attempt(attempt, :failed) if needs_init && attempt && !init_thread
223
224
  end
224
225
 
225
226
  def init_provider(provider, context, domain:, attempt:, raise_on_error: false)
@@ -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 = @configuration
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
- !@configuration.nil?
22
+ !@openfeature_configuration.nil?
23
23
  end
24
24
 
25
25
  private
26
26
 
27
27
  def attach(configuration)
28
- @configuration = configuration
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
- @configuration = nil
37
+ @openfeature_configuration = nil
33
38
  end
34
39
  end
35
40
  end
@@ -23,7 +23,7 @@ module OpenFeature
23
23
  # * <tt>fetch_object_value</tt> - Retrieve feature flag object value
24
24
  #
25
25
  class NoOpProvider
26
- REASON_NO_OP = "No-op"
26
+ REASON_NO_OP = Reason::DEFAULT
27
27
  NAME = "No-op Provider"
28
28
 
29
29
  attr_reader :metadata
@@ -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 PROVIDER_FATAL)
25
- def initialize(message, provider: nil, original_error: nil, error_code: Provider::ErrorCode::PROVIDER_FATAL)
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.object_id] = {state: state, details: {}}
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.object_id] = {state: new_state, details: event_details || {}}
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.object_id]
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.object_id]
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.object_id)
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.object_id)
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] = evaluation_context
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenFeature
4
4
  module SDK
5
- VERSION = "0.7.0"
5
+ VERSION = "0.7.1"
6
6
  end
7
7
  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
- def configuration: () -> Configuration
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
 
@@ -10,7 +10,7 @@ module OpenFeature
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
 
@@ -0,0 +1,7 @@
1
+ module OpenFeature
2
+ module SDK
3
+ module Isolated
4
+ def self.create_api: () -> API
5
+ end
6
+ end
7
+ 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.7.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