Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ jobs:
run: bundle exec rspec
- name: Upload coverage to Codecov
if: ${{ strategy.job-index == 0 }} # only run codecov on first run
uses: codecov/codecov-action@v6.0.1
uses: codecov/codecov-action@8cad3ba95e5920c42f44492e54bc9639cba47959 # v6.0.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
verbose: true
file: coverage/coverage.xml
files: coverage/coverage.xml
standard:
name: Standard
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ GEM
base64 (0.3.0)
bigdecimal (4.1.1)
builder (3.3.0)
concurrent-ruby (1.3.6)
concurrent-ruby (1.3.7)
csv (3.3.5)
cucumber (11.0.0)
base64 (~> 0.2)
Expand Down Expand Up @@ -43,7 +43,7 @@ GEM
reline (>= 0.3.8)
diff-lcs (1.6.2)
docile (1.4.1)
erb (6.0.2)
erb (6.0.4)
ffi (1.17.4)
fileutils (1.8.0)
io-console (0.8.2)
Expand Down
101 changes: 88 additions & 13 deletions lib/open_feature/sdk/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,20 @@ def set_provider_internal(provider, domain:, wait_for_init:)
needs_shutdown = false

@provider_mutex.synchronize do
validate_domain_scoped_binding!(provider, domain)

old_provider = @providers[domain]

new_providers = @providers.dup
new_providers[domain] = provider
@providers = new_providers

# Spec 1.1.2.2: Only initialize if the provider is not already active
# (i.e., not already bound to another domain)
already_active = @providers.any? { |d, p| d != domain && p.equal?(provider) && @provider_state_registry.tracked?(p) }
was_bound_elsewhere = @providers.any? do |d, p|
d != domain && p.equal?(provider) && @provider_state_registry.tracked?(p)
end
was_bound_here = old_provider&.equal?(provider) && @provider_state_registry.tracked?(provider)
already_active = was_bound_elsewhere || was_bound_here
needs_init = !already_active

if needs_init
Expand Down Expand Up @@ -163,10 +168,10 @@ def set_provider_internal(provider, domain:, wait_for_init:)
# Initialize provider outside the mutex to avoid blocking other operations
if needs_init
if wait_for_init
init_provider(provider, context_for_init, raise_on_error: true)
init_provider(provider, context_for_init, domain: domain, raise_on_error: true)
else
Thread.new do
init_provider(provider, context_for_init, raise_on_error: false)
init_provider(provider, context_for_init, domain: domain, raise_on_error: false)
end
end
elsif wait_for_init
Expand All @@ -175,15 +180,8 @@ def set_provider_internal(provider, domain:, wait_for_init:)
end
end

def init_provider(provider, context, raise_on_error: false)
if provider.respond_to?(:init)
init_method = provider.method(:init)
if init_method.parameters.empty?
provider.init
else
provider.init(context)
end
end
def init_provider(provider, context, domain:, raise_on_error: false)
call_init(provider, context, domain)

dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
rescue => e
Expand Down Expand Up @@ -226,6 +224,83 @@ def extract_provider_name(provider)
provider.respond_to?(:metadata) ? provider.metadata.name : provider.class.name
end

def domain_scoped?(provider)
return false unless declares_domain_scoped?(provider)

provider.domain_scoped?
end

def declares_domain_scoped?(provider)
class_declares_method?(provider.class, :domain_scoped?)
end

def provider_bindings(provider)
@providers.select { |_, p| p.equal?(provider) }.keys
end

def validate_domain_scoped_binding!(provider, domain)
return unless domain_scoped?(provider)

bindings = provider_bindings(provider)
return if bindings.empty?
return if bindings.include?(domain)

raise ArgumentError, "Cannot bind domain-scoped provider to more than one domain"
end

def call_init(provider, context, domain)
return unless provider.respond_to?(:init)

if init_accepts_domain?(provider)
if init_accepts_evaluation_context?(provider)
provider.init(context, domain: domain)
else
provider.init(domain: domain)
end
elsif init_accepts_evaluation_context?(provider)
provider.init(context)
else
provider.init
end
end

def init_accepts_domain?(provider)
init_parameters(provider).any? do |kind, name|
name == :domain || kind == :keyrest
end
end

def init_accepts_evaluation_context?(provider)
init_parameters(provider).any? do |kind, name|
next false if name == :domain

kind == :opt || kind == :req
end
end

def init_parameters(provider)
method = instance_method_on_class(provider.class, :init)
method&.parameters || []
end

def instance_method_on_class(klass, method_name)
while klass && klass != Object
if klass.method_defined?(method_name, false)
return klass.instance_method(method_name)
end
klass = klass.superclass
end
nil
end

def class_declares_method?(klass, method_name)
while klass && klass != Object
return true if klass.method_defined?(method_name, false)
klass = klass.superclass
end
false
end

def run_handlers_for_provider(provider, event_type, event_details)
# Run global handlers (API-level, no domain filtering)
@event_dispatcher.trigger_event(event_type, event_details)
Expand Down
2 changes: 1 addition & 1 deletion sig/open_feature/sdk/configuration.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module OpenFeature

def reset: () -> void
def set_provider_internal: (untyped provider, domain: String?, wait_for_init: bool) -> void
def init_provider: (untyped provider, EvaluationContext? context, ?raise_on_error: bool) -> void
def init_provider: (untyped provider, EvaluationContext? context, domain: String?, ?raise_on_error: bool) -> void
def dispatch_provider_event: (untyped provider, String event_type, ?Hash[Symbol, untyped] details) -> void
def extract_provider_name: (untyped provider) -> String
def run_handlers_for_provider: (untyped provider, String event_type, Hash[Symbol, untyped] event_details) -> void
Expand Down
3 changes: 2 additions & 1 deletion sig/open_feature/sdk/provider.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ module OpenFeature
end

interface _LifecycleProvider
def init: (?EvaluationContext? evaluation_context) -> void
def init: (?EvaluationContext? evaluation_context, ?domain: String?) -> void
def domain_scoped?: () -> bool
def shutdown: () -> void
def metadata: () -> ProviderMetadata
end
Expand Down
58 changes: 58 additions & 0 deletions spec/open_feature/sdk/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,64 @@ def metadata

expect(provider.init_called).to be true
end

it "passes domain to init when init accepts evaluation context and domain" do
provider = Class.new do
attr_reader :init_domain, :init_context

def init(evaluation_context = nil, domain: nil)
@init_context = evaluation_context
@init_domain = domain
end

def metadata
OpenFeature::SDK::Provider::ProviderMetadata.new(name: "DomainAwareProvider")
end
end.new

context = OpenFeature::SDK::EvaluationContext.new(targeting_key: "user")
configuration.evaluation_context = context
configuration.set_provider_and_wait(provider, domain: "billing")

expect(provider.init_context).to eq(context)
expect(provider.init_domain).to eq("billing")
end

it "passes domain to init when init only accepts domain" do
provider = Class.new do
attr_reader :init_domain

def init(domain: nil)
@init_domain = domain
end

def metadata
OpenFeature::SDK::Provider::ProviderMetadata.new(name: "DomainOnlyProvider")
end
end.new

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

expect(provider.init_domain).to eq("billing")
end

it "passes domain to init when init accepts keyword rest" do
provider = Class.new do
attr_reader :init_kwargs

def init(**kwargs)
@init_kwargs = kwargs
end

def metadata
OpenFeature::SDK::Provider::ProviderMetadata.new(name: "KwargsProvider")
end
end.new

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

expect(provider.init_kwargs[:domain]).to eq("billing")
end
end

describe "event handler error logging" do
Expand Down
Loading
Loading