Skip to content

Commit d92eabc

Browse files
feat: Add setProviderAndWait method for blocking provider initialization (#200)
Signed-off-by: Leo Romanovsky <[email protected]>
1 parent 2f1e327 commit d92eabc

File tree

7 files changed

+443
-2
lines changed

7 files changed

+443
-2
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,43 @@ OpenFeature::SDK.configure do |config|
130130
end
131131
```
132132

133+
#### Blocking Provider Registration
134+
135+
If you need to ensure that a provider is fully initialized before continuing, you can use `set_provider_and_wait`:
136+
137+
```ruby
138+
# Using the SDK directly
139+
begin
140+
OpenFeature::SDK.set_provider_and_wait(my_provider)
141+
puts "Provider is ready!"
142+
rescue OpenFeature::SDK::ProviderInitializationError => e
143+
puts "Provider failed to initialize: #{e.message}"
144+
puts "Error code: #{e.error_code}"
145+
puts "Original error: #{e.original_error}"
146+
end
147+
148+
# With custom timeout (default is 30 seconds)
149+
OpenFeature::SDK.set_provider_and_wait(my_provider, timeout: 60)
150+
151+
# Domain-specific provider
152+
OpenFeature::SDK.set_provider_and_wait(my_provider, domain: "feature-flags")
153+
154+
# Via configuration block
155+
OpenFeature::SDK.configure do |config|
156+
begin
157+
config.set_provider_and_wait(my_provider)
158+
rescue OpenFeature::SDK::ProviderInitializationError => e
159+
# Handle initialization failure
160+
end
161+
end
162+
```
163+
164+
The `set_provider_and_wait` method:
165+
- Waits for the provider's `init` method to complete successfully
166+
- Raises `ProviderInitializationError` with `PROVIDER_FATAL` error code if initialization fails or times out
167+
- Provides access to the original error, provider instance, and error code for debugging
168+
- Uses the same thread-safe provider switching as `set_provider`
169+
133170
In some situations, it may be beneficial to register multiple providers in the same application.
134171
This is possible using [domains](#domains), which is covered in more detail below.
135172

lib/open_feature/sdk/api.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class API
3232
include Singleton # Satisfies Flag Evaluation API Requirement 1.1.1
3333
extend Forwardable
3434

35-
def_delegators :configuration, :provider, :set_provider, :hooks, :evaluation_context
35+
def_delegators :configuration, :provider, :set_provider, :set_provider_and_wait, :hooks, :evaluation_context
3636

3737
def configuration
3838
@configuration ||= Configuration.new

lib/open_feature/sdk/configuration.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# frozen_string_literal: true
22

3+
require "timeout"
34
require_relative "api"
5+
require_relative "provider_initialization_error"
46

57
module OpenFeature
68
module SDK
@@ -36,6 +38,52 @@ def set_provider(provider, domain: nil)
3638
@providers = new_providers
3739
end
3840
end
41+
42+
# Sets a provider and waits for the initialization to complete or fail.
43+
# This method ensures the provider is ready (or in error state) before returning.
44+
#
45+
# @param provider [Object] the provider to set
46+
# @param domain [String, nil] the domain for the provider (optional)
47+
# @param timeout [Integer] maximum time to wait for initialization in seconds (default: 30)
48+
# @raise [ProviderInitializationError] if the provider fails to initialize or times out
49+
def set_provider_and_wait(provider, domain: nil, timeout: 30)
50+
@provider_mutex.synchronize do
51+
old_provider = @providers[domain]
52+
53+
# Shutdown old provider (ignore errors)
54+
begin
55+
old_provider.shutdown if old_provider.respond_to?(:shutdown)
56+
rescue
57+
# Ignore shutdown errors and continue with provider initialization
58+
end
59+
60+
begin
61+
# Initialize new provider with timeout
62+
if provider.respond_to?(:init)
63+
Timeout.timeout(timeout) do
64+
provider.init
65+
end
66+
end
67+
68+
# Set the new provider
69+
new_providers = @providers.dup
70+
new_providers[domain] = provider
71+
@providers = new_providers
72+
rescue Timeout::Error => e
73+
raise ProviderInitializationError.new(
74+
"Provider initialization timed out after #{timeout} seconds",
75+
provider:,
76+
original_error: e
77+
)
78+
rescue => e
79+
raise ProviderInitializationError.new(
80+
"Provider initialization failed: #{e.message}",
81+
provider:,
82+
original_error: e
83+
)
84+
end
85+
end
86+
end
3987
end
4088
end
4189
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "provider/error_code"
4+
5+
module OpenFeature
6+
module SDK
7+
# Exception raised when a provider fails to initialize during setProviderAndWait
8+
#
9+
# This exception provides access to both the original error that caused the
10+
# initialization failure and the provider instance that failed to initialize.
11+
class ProviderInitializationError < StandardError
12+
# @return [Object] the provider that failed to initialize
13+
attr_reader :provider
14+
15+
# @return [Exception] the original error that caused the initialization failure
16+
attr_reader :original_error
17+
18+
# @return [String] the OpenFeature error code
19+
attr_reader :error_code
20+
21+
# @param message [String] the error message
22+
# @param provider [Object] the provider that failed to initialize
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)
26+
super(message)
27+
@provider = provider
28+
@original_error = original_error
29+
@error_code = error_code
30+
end
31+
end
32+
end
33+
end

spec/open_feature/sdk/configuration_spec.rb

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,173 @@
4747
providers[0, 2].each { |provider| expect(provider).to receive(:shutdown) }
4848
configuration.set_provider(providers[0])
4949

50-
allow(providers[0]).to receive(:shutdown).once { sleep 0.5 }
50+
allow(providers[0]).to(receive(:shutdown).once { sleep 0.5 })
5151
background { configuration.set_provider(providers[1]) }
5252
background { configuration.set_provider(providers[2]) }
5353
yield_to_background
5454
expect(configuration.provider).to be(providers[2])
5555
end
5656
end
5757
end
58+
59+
describe "#set_provider_and_wait" do
60+
context "when provider has a successful init method" do
61+
let(:provider) { OpenFeature::SDK::Provider::InMemoryProvider.new }
62+
63+
it "waits for init to complete and sets the provider" do
64+
expect(provider).to receive(:init).once
65+
66+
configuration.set_provider_and_wait(provider)
67+
68+
expect(configuration.provider).to be(provider)
69+
end
70+
71+
it "supports custom timeout" do
72+
expect(provider).to receive(:init).once
73+
74+
configuration.set_provider_and_wait(provider, timeout: 60)
75+
76+
expect(configuration.provider).to be(provider)
77+
end
78+
end
79+
80+
context "when provider does not have an init method" do
81+
it "sets the provider without waiting" do
82+
provider = OpenFeature::SDK::Provider::NoOpProvider.new
83+
84+
configuration.set_provider_and_wait(provider)
85+
86+
expect(configuration.provider).to be(provider)
87+
end
88+
end
89+
90+
context "when domain is given" do
91+
it "binds the provider to that domain" do
92+
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
93+
expect(provider).to receive(:init).once
94+
95+
configuration.set_provider_and_wait(provider, domain: "testing")
96+
97+
expect(configuration.provider(domain: "testing")).to be(provider)
98+
end
99+
end
100+
101+
context "when provider init raises an exception" do
102+
let(:provider) { OpenFeature::SDK::Provider::InMemoryProvider.new }
103+
let(:error_message) { "Database connection failed" }
104+
105+
before do
106+
allow(provider).to receive(:init).and_raise(StandardError.new(error_message))
107+
end
108+
109+
it "raises ProviderInitializationError" do
110+
expect do
111+
configuration.set_provider_and_wait(provider)
112+
end.to raise_error(OpenFeature::SDK::ProviderInitializationError) do |error|
113+
expect(error.message).to include("Provider initialization failed")
114+
expect(error.message).to include(error_message)
115+
expect(error.provider).to be(provider)
116+
expect(error.original_error).to be_a(StandardError)
117+
expect(error.original_error.message).to eq(error_message)
118+
expect(error.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::PROVIDER_FATAL)
119+
end
120+
end
121+
122+
it "does not set the provider when init fails" do
123+
old_provider = configuration.provider
124+
125+
expect do
126+
configuration.set_provider_and_wait(provider)
127+
end.to raise_error(OpenFeature::SDK::ProviderInitializationError)
128+
129+
expect(configuration.provider).to be(old_provider)
130+
end
131+
end
132+
133+
context "when provider init times out" do
134+
let(:provider) { OpenFeature::SDK::Provider::InMemoryProvider.new }
135+
136+
before do
137+
allow(provider).to receive(:init) do
138+
sleep 2 # Simulate slow initialization
139+
end
140+
end
141+
142+
it "raises ProviderInitializationError after timeout" do
143+
expect do
144+
configuration.set_provider_and_wait(provider, timeout: 0.1)
145+
end.to raise_error(OpenFeature::SDK::ProviderInitializationError) do |error|
146+
expect(error.message).to include("Provider initialization timed out after 0.1 seconds")
147+
expect(error.provider).to be(provider)
148+
expect(error.original_error).to be_a(Timeout::Error)
149+
expect(error.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::PROVIDER_FATAL)
150+
end
151+
end
152+
153+
it "does not set the provider when init times out" do
154+
old_provider = configuration.provider
155+
156+
expect do
157+
configuration.set_provider_and_wait(provider, timeout: 0.1)
158+
end.to raise_error(OpenFeature::SDK::ProviderInitializationError)
159+
160+
expect(configuration.provider).to be(old_provider)
161+
end
162+
end
163+
164+
context "when shutting down the old provider fails" do
165+
let(:old_provider) { OpenFeature::SDK::Provider::InMemoryProvider.new }
166+
let(:new_provider) { OpenFeature::SDK::Provider::InMemoryProvider.new }
167+
168+
before do
169+
# Set up initial provider
170+
configuration.set_provider(old_provider)
171+
allow(old_provider).to receive(:shutdown).and_raise(StandardError.new("Shutdown failed"))
172+
allow(new_provider).to receive(:init)
173+
end
174+
175+
it "continues with setting the new provider" do
176+
# Should not raise an exception even if shutdown fails
177+
configuration.set_provider_and_wait(new_provider)
178+
179+
expect(configuration.provider).to be(new_provider)
180+
end
181+
end
182+
183+
context "when the provider is set concurrently" do
184+
let(:providers) { (0..2).map { OpenFeature::SDK::Provider::InMemoryProvider.new } }
185+
186+
it "handles concurrent calls safely" do
187+
providers.each { |provider| expect(provider).to receive(:init).once }
188+
# First two providers should be shut down
189+
expect(providers[0]).to receive(:shutdown).once
190+
expect(providers[1]).to receive(:shutdown).once
191+
192+
configuration.set_provider_and_wait(providers[0])
193+
194+
# Simulate slow initialization for concurrent testing
195+
allow(providers[0]).to receive(:shutdown) { sleep 0.1 }
196+
197+
background { configuration.set_provider_and_wait(providers[1]) }
198+
background { configuration.set_provider_and_wait(providers[2]) }
199+
yield_to_background
200+
201+
# The last provider should be set
202+
expect(configuration.provider).to be(providers[2])
203+
end
204+
end
205+
206+
context "when handling complex initialization scenarios" do
207+
let(:provider) { OpenFeature::SDK::Provider::InMemoryProvider.new }
208+
209+
it "handles provider that responds_to init but init is nil" do
210+
allow(provider).to receive(:respond_to?).with(:init).and_return(true)
211+
allow(provider).to receive(:init).and_return(nil)
212+
213+
configuration.set_provider_and_wait(provider)
214+
215+
expect(configuration.provider).to be(provider)
216+
end
217+
end
218+
end
58219
end

0 commit comments

Comments
 (0)