Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,42 @@ OpenFeature::SDK.configure do |config|
end
```

#### Blocking Provider Registration

If you need to ensure that a provider is fully initialized before continuing, you can use `set_provider_and_wait`:

```ruby
# Using the SDK directly
begin
OpenFeature::SDK.set_provider_and_wait(my_provider)
puts "Provider is ready!"
rescue OpenFeature::SDK::ProviderInitializationError => e
puts "Provider failed to initialize: #{e.message}"
puts "Original error: #{e.original_error}"
end

# With custom timeout (default is 30 seconds)
OpenFeature::SDK.set_provider_and_wait(my_provider, timeout: 60)

# Domain-specific provider
OpenFeature::SDK.set_provider_and_wait(my_provider, domain: "feature-flags")

# Via configuration block
OpenFeature::SDK.configure do |config|
begin
config.set_provider_and_wait(my_provider)
rescue OpenFeature::SDK::ProviderInitializationError => e
# Handle initialization failure
end
end
```

The `set_provider_and_wait` method:
- Waits for the provider's `init` method to complete successfully
- Raises `ProviderInitializationError` if initialization fails or times out
- Provides access to the original error and provider instance for debugging
- Uses the same thread-safe provider switching as `set_provider`

In some situations, it may be beneficial to register multiple providers in the same application.
This is possible using [domains](#domains), which is covered in more detail below.

Expand Down
2 changes: 1 addition & 1 deletion lib/open_feature/sdk/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class API
include Singleton # Satisfies Flag Evaluation API Requirement 1.1.1
extend Forwardable

def_delegators :configuration, :provider, :set_provider, :hooks, :evaluation_context
def_delegators :configuration, :provider, :set_provider, :set_provider_and_wait, :hooks, :evaluation_context

def configuration
@configuration ||= Configuration.new
Expand Down
48 changes: 48 additions & 0 deletions lib/open_feature/sdk/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

require "timeout"
require_relative "api"
require_relative "provider_initialization_error"

module OpenFeature
module SDK
Expand Down Expand Up @@ -36,6 +38,52 @@ def set_provider(provider, domain: nil)
@providers = new_providers
end
end

# Sets a provider and waits for the initialization to complete or fail.
# This method ensures the provider is ready (or in error state) before returning.
#
# @param provider [Object] the provider to set
# @param domain [String, nil] the domain for the provider (optional)
# @param timeout [Integer] maximum time to wait for initialization in seconds (default: 30)
# @raise [ProviderInitializationError] if the provider fails to initialize or times out
def set_provider_and_wait(provider, domain: nil, timeout: 30)
@provider_mutex.synchronize do
old_provider = @providers[domain]

# Shutdown old provider (ignore errors)
begin
old_provider.shutdown if old_provider.respond_to?(:shutdown)
rescue
# Ignore shutdown errors and continue with provider initialization
end

begin
# Initialize new provider with timeout
if provider.respond_to?(:init)
Timeout.timeout(timeout) do
provider.init
end
end

# Set the new provider
new_providers = @providers.dup
new_providers[domain] = provider
@providers = new_providers
rescue Timeout::Error => e
raise ProviderInitializationError.new(
"Provider initialization timed out after #{timeout} seconds",
provider:,
original_error: e
)
rescue => e
raise ProviderInitializationError.new(
"Provider initialization failed: #{e.message}",
provider:,
original_error: e
)
end
end
end
end
end
end
26 changes: 26 additions & 0 deletions lib/open_feature/sdk/provider_initialization_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module OpenFeature
module SDK
# Exception raised when a provider fails to initialize during setProviderAndWait
#
# This exception provides access to both the original error that caused the
# initialization failure and the provider instance that failed to initialize.
class ProviderInitializationError < StandardError
# @return [Object] the provider that failed to initialize
attr_reader :provider

# @return [Exception] the original error that caused the initialization failure
attr_reader :original_error

# @param message [String] the error message
# @param provider [Object] the provider that failed to initialize
# @param original_error [Exception] the original error that caused the failure
def initialize(message, provider: nil, original_error: nil)
super(message)
@provider = provider
@original_error = original_error
end
end
end
end
161 changes: 160 additions & 1 deletion spec/open_feature/sdk/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,171 @@
providers[0, 2].each { |provider| expect(provider).to receive(:shutdown) }
configuration.set_provider(providers[0])

allow(providers[0]).to receive(:shutdown).once { sleep 0.5 }
allow(providers[0]).to(receive(:shutdown).once { sleep 0.5 })
background { configuration.set_provider(providers[1]) }
background { configuration.set_provider(providers[2]) }
yield_to_background
expect(configuration.provider).to be(providers[2])
end
end
end

describe "#set_provider_and_wait" do
context "when provider has a successful init method" do
let(:provider) { OpenFeature::SDK::Provider::InMemoryProvider.new }

it "waits for init to complete and sets the provider" do
expect(provider).to receive(:init).once

configuration.set_provider_and_wait(provider)

expect(configuration.provider).to be(provider)
end

it "supports custom timeout" do
expect(provider).to receive(:init).once

configuration.set_provider_and_wait(provider, timeout: 60)

expect(configuration.provider).to be(provider)
end
end

context "when provider does not have an init method" do
it "sets the provider without waiting" do
provider = OpenFeature::SDK::Provider::NoOpProvider.new

configuration.set_provider_and_wait(provider)

expect(configuration.provider).to be(provider)
end
end

context "when domain is given" do
it "binds the provider to that domain" do
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
expect(provider).to receive(:init).once

configuration.set_provider_and_wait(provider, domain: "testing")

expect(configuration.provider(domain: "testing")).to be(provider)
end
end

context "when provider init raises an exception" do
let(:provider) { OpenFeature::SDK::Provider::InMemoryProvider.new }
let(:error_message) { "Database connection failed" }

before do
allow(provider).to receive(:init).and_raise(StandardError.new(error_message))
end

it "raises ProviderInitializationError" do
expect do
configuration.set_provider_and_wait(provider)
end.to raise_error(OpenFeature::SDK::ProviderInitializationError) do |error|
expect(error.message).to include("Provider initialization failed")
expect(error.message).to include(error_message)
expect(error.provider).to be(provider)
expect(error.original_error).to be_a(StandardError)
expect(error.original_error.message).to eq(error_message)
end
end

it "does not set the provider when init fails" do
old_provider = configuration.provider

expect do
configuration.set_provider_and_wait(provider)
end.to raise_error(OpenFeature::SDK::ProviderInitializationError)

expect(configuration.provider).to be(old_provider)
end
end

context "when provider init times out" do
let(:provider) { OpenFeature::SDK::Provider::InMemoryProvider.new }

before do
allow(provider).to receive(:init) do
sleep 2 # Simulate slow initialization
end
end

it "raises ProviderInitializationError after timeout" do
expect do
configuration.set_provider_and_wait(provider, timeout: 0.1)
end.to raise_error(OpenFeature::SDK::ProviderInitializationError) do |error|
expect(error.message).to include("Provider initialization timed out after 0.1 seconds")
expect(error.provider).to be(provider)
expect(error.original_error).to be_a(Timeout::Error)
end
end

it "does not set the provider when init times out" do
old_provider = configuration.provider

expect do
configuration.set_provider_and_wait(provider, timeout: 0.1)
end.to raise_error(OpenFeature::SDK::ProviderInitializationError)

expect(configuration.provider).to be(old_provider)
end
end

context "when shutting down the old provider fails" do
let(:old_provider) { OpenFeature::SDK::Provider::InMemoryProvider.new }
let(:new_provider) { OpenFeature::SDK::Provider::InMemoryProvider.new }

before do
# Set up initial provider
configuration.set_provider(old_provider)
allow(old_provider).to receive(:shutdown).and_raise(StandardError.new("Shutdown failed"))
allow(new_provider).to receive(:init)
end

it "continues with setting the new provider" do
# Should not raise an exception even if shutdown fails
configuration.set_provider_and_wait(new_provider)

expect(configuration.provider).to be(new_provider)
end
end

context "when the provider is set concurrently" do
let(:providers) { (0..2).map { OpenFeature::SDK::Provider::InMemoryProvider.new } }

it "handles concurrent calls safely" do
providers.each { |provider| expect(provider).to receive(:init).once }
# First two providers should be shut down
expect(providers[0]).to receive(:shutdown).once
expect(providers[1]).to receive(:shutdown).once

configuration.set_provider_and_wait(providers[0])

# Simulate slow initialization for concurrent testing
allow(providers[0]).to receive(:shutdown) { sleep 0.1 }

background { configuration.set_provider_and_wait(providers[1]) }
background { configuration.set_provider_and_wait(providers[2]) }
yield_to_background

# The last provider should be set
expect(configuration.provider).to be(providers[2])
end
end

context "when handling complex initialization scenarios" do
let(:provider) { OpenFeature::SDK::Provider::InMemoryProvider.new }

it "handles provider that responds_to init but init is nil" do
allow(provider).to receive(:respond_to?).with(:init).and_return(true)
allow(provider).to receive(:init).and_return(nil)

configuration.set_provider_and_wait(provider)

expect(configuration.provider).to be(provider)
end
end
end
end
Loading
Loading