diff --git a/lib/open_feature/sdk/api.rb b/lib/open_feature/sdk/api.rb index f714ee09..f4a67134 100644 --- a/lib/open_feature/sdk/api.rb +++ b/lib/open_feature/sdk/api.rb @@ -68,6 +68,10 @@ def logger def logger=(new_logger) configuration.logger = new_logger end + + def shutdown + configuration.shutdown + end end end end diff --git a/lib/open_feature/sdk/client.rb b/lib/open_feature/sdk/client.rb index 4e76b244..75c51777 100644 --- a/lib/open_feature/sdk/client.rb +++ b/lib/open_feature/sdk/client.rb @@ -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) @@ -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, @@ -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) } diff --git a/lib/open_feature/sdk/configuration.rb b/lib/open_feature/sdk/configuration.rb index 22610c78..10591459 100644 --- a/lib/open_feature/sdk/configuration.rb +++ b/lib/open_feature/sdk/configuration.rb @@ -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 @@ -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) diff --git a/lib/open_feature/sdk/provider_state_registry.rb b/lib/open_feature/sdk/provider_state_registry.rb index 5ee20da5..c4b5940c 100644 --- a/lib/open_feature/sdk/provider_state_registry.rb +++ b/lib/open_feature/sdk/provider_state_registry.rb @@ -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 diff --git a/spec/specification/flag_evaluation_api_spec.rb b/spec/specification/flag_evaluation_api_spec.rb index e06cc229..722e0657 100644 --- a/spec/specification/flag_evaluation_api_spec.rb +++ b/spec/specification/flag_evaluation_api_spec.rb @@ -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")