Skip to content
Merged
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: 4 additions & 0 deletions lib/open_feature/sdk/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ def logger
def logger=(new_logger)
configuration.logger = new_logger
end

def shutdown
configuration.shutdown
end
end
end
end
23 changes: 23 additions & 0 deletions lib/open_feature/sdk/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def initialize(provider:, domain: nil, evaluation_context: nil)
@hooks = []
end

def provider_status
OpenFeature::SDK.configuration.provider_state(@provider)
end

def add_handler(event_type, handler = nil, &block)
actual_handler = handler || block
OpenFeature::SDK.configuration.add_client_handler(self, event_type, actual_handler)
Expand All @@ -54,6 +58,18 @@ def fetch_#{result_type}_#{suffix}(flag_key:, default_value:, evaluation_context
def fetch_details(type:, flag_key:, default_value:, evaluation_context: nil, invocation_hooks: [], hook_hints: nil)
validate_default_value_type(type, default_value)

if OpenFeature::SDK.configuration.provider_tracked?(@provider)
error_code = short_circuit_error_code(provider_status)
if error_code
resolution = Provider::ResolutionDetails.new(
value: default_value,
error_code: error_code,
reason: Provider::Reason::ERROR
)
return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
end
end

built_context = EvaluationContextBuilder.new.call(
api_context: OpenFeature::SDK.evaluation_context,
client_context: self.evaluation_context,
Expand Down Expand Up @@ -109,6 +125,13 @@ def evaluate_flag(type:, flag_key:, default_value:, evaluation_context:)
EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution_details)
end

def short_circuit_error_code(state)
case state
when ProviderState::NOT_READY then Provider::ErrorCode::PROVIDER_NOT_READY
when ProviderState::FATAL then Provider::ErrorCode::PROVIDER_FATAL
end
end

def validate_default_value_type(type, default_value)
expected_classes = TYPE_CLASS_MAP[type]
unless expected_classes.any? { |klass| default_value.is_a?(klass) }
Expand Down
24 changes: 20 additions & 4 deletions lib/open_feature/sdk/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,26 @@ def set_provider_and_wait(provider, domain: nil)
set_provider_internal(provider, domain: domain, wait_for_init: true)
end

def provider_state(provider)
@provider_state_registry.get_state(provider)
end

def provider_tracked?(provider)
@provider_state_registry.tracked?(provider)
end

def shutdown
providers_to_shutdown = @provider_mutex.synchronize { @providers.values.uniq }

providers_to_shutdown.each do |prov|
prov.shutdown if prov.respond_to?(:shutdown)
rescue => e
@logger&.warn("Error shutting down provider #{prov&.class&.name || "unknown"}: #{e.message}")
end

reset
end

private

def reset
Expand Down Expand Up @@ -171,10 +191,6 @@ def dispatch_provider_event(provider, event_type, details = {})
run_handlers_for_provider(provider, event_type, event_details)
end

def provider_state(provider)
@provider_state_registry.get_state(provider)
end

private

def extract_provider_name(provider)
Expand Down
8 changes: 8 additions & 0 deletions lib/open_feature/sdk/provider_state_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ def remove_provider(provider)
end
end

def tracked?(provider)
return false unless provider

@mutex.synchronize do
@states.key?(provider.object_id)
end
end

def ready?(provider)
get_state(provider) == ProviderState::READY
end
Expand Down
77 changes: 77 additions & 0 deletions spec/specification/flag_evaluation_api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,83 @@
end
end

context "1.6 - Shutdown" do
context "Requirement 1.6.1" do
specify "The API MUST define a mechanism to propagate a shutdown request to registered providers." do
expect(OpenFeature::SDK).to respond_to(:shutdown)
end
end

context "Requirement 1.6.2" do
specify "When a shutdown function is called, the API invokes the shutdown function on the registered provider." do
provider1 = OpenFeature::SDK::Provider::InMemoryProvider.new
provider2 = OpenFeature::SDK::Provider::InMemoryProvider.new

OpenFeature::SDK.set_provider_and_wait(provider1)
OpenFeature::SDK.set_provider_and_wait(provider2, domain: "test-domain")

expect(provider1).to receive(:shutdown)
expect(provider2).to receive(:shutdown)

OpenFeature::SDK.shutdown
end

specify "After shutdown, providers are cleared." do
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
OpenFeature::SDK.set_provider_and_wait(provider)

OpenFeature::SDK.shutdown

expect(OpenFeature::SDK.provider).to be_nil
end
end
end

context "1.7 - Provider Status" do
context "Requirement 1.7.1" do
specify "The client MUST define a provider status accessor which indicates the readiness of the associated provider." do
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
OpenFeature::SDK.set_provider_and_wait(provider)
client = OpenFeature::SDK.build_client

expect(client).to respond_to(:provider_status)
expect(client.provider_status).to eq(OpenFeature::SDK::ProviderState::READY)
end
end

context "Requirement 1.7.6" do
specify "If the provider status is NOT_READY, the client should return the default value with PROVIDER_NOT_READY error." do
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
OpenFeature::SDK.set_provider_and_wait(provider)
client = OpenFeature::SDK.build_client

allow(OpenFeature::SDK.configuration).to receive(:provider_state).with(provider).and_return(OpenFeature::SDK::ProviderState::NOT_READY)

result = client.fetch_boolean_details(flag_key: "flag", default_value: false)

expect(result.value).to eq(false)
expect(result.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::PROVIDER_NOT_READY)
expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
end
end

context "Requirement 1.7.7" do
specify "If the provider status is FATAL, the client should return the default value with PROVIDER_FATAL error." do
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
OpenFeature::SDK.set_provider_and_wait(provider)
client = OpenFeature::SDK.build_client

allow(OpenFeature::SDK.configuration).to receive(:provider_state).with(provider).and_return(OpenFeature::SDK::ProviderState::FATAL)

result = client.fetch_string_details(flag_key: "flag", default_value: "default")

expect(result.value).to eq("default")
expect(result.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::PROVIDER_FATAL)
expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
end
end
end

context "Logger Methods" do
specify "delegates logger getter to configuration" do
logger = double("Logger")
Expand Down
Loading