Skip to content

Commit f9c32ad

Browse files
josecolellaclaude
andauthored
feat: add shutdown API, provider status, and status short-circuit (#223)
Signed-off-by: Jose Colella <jose.colella@gusto.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 28518a0 commit f9c32ad

5 files changed

Lines changed: 132 additions & 4 deletions

File tree

lib/open_feature/sdk/api.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ def logger
6868
def logger=(new_logger)
6969
configuration.logger = new_logger
7070
end
71+
72+
def shutdown
73+
configuration.shutdown
74+
end
7175
end
7276
end
7377
end

lib/open_feature/sdk/client.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ def initialize(provider:, domain: nil, evaluation_context: nil)
2828
@hooks = []
2929
end
3030

31+
def provider_status
32+
OpenFeature::SDK.configuration.provider_state(@provider)
33+
end
34+
3135
def add_handler(event_type, handler = nil, &block)
3236
actual_handler = handler || block
3337
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
5458
def fetch_details(type:, flag_key:, default_value:, evaluation_context: nil, invocation_hooks: [], hook_hints: nil)
5559
validate_default_value_type(type, default_value)
5660

61+
if OpenFeature::SDK.configuration.provider_tracked?(@provider)
62+
error_code = short_circuit_error_code(provider_status)
63+
if error_code
64+
resolution = Provider::ResolutionDetails.new(
65+
value: default_value,
66+
error_code: error_code,
67+
reason: Provider::Reason::ERROR
68+
)
69+
return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
70+
end
71+
end
72+
5773
built_context = EvaluationContextBuilder.new.call(
5874
api_context: OpenFeature::SDK.evaluation_context,
5975
client_context: self.evaluation_context,
@@ -109,6 +125,13 @@ def evaluate_flag(type:, flag_key:, default_value:, evaluation_context:)
109125
EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution_details)
110126
end
111127

128+
def short_circuit_error_code(state)
129+
case state
130+
when ProviderState::NOT_READY then Provider::ErrorCode::PROVIDER_NOT_READY
131+
when ProviderState::FATAL then Provider::ErrorCode::PROVIDER_FATAL
132+
end
133+
end
134+
112135
def validate_default_value_type(type, default_value)
113136
expected_classes = TYPE_CLASS_MAP[type]
114137
unless expected_classes.any? { |klass| default_value.is_a?(klass) }

lib/open_feature/sdk/configuration.rb

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,26 @@ def set_provider_and_wait(provider, domain: nil)
7676
set_provider_internal(provider, domain: domain, wait_for_init: true)
7777
end
7878

79+
def provider_state(provider)
80+
@provider_state_registry.get_state(provider)
81+
end
82+
83+
def provider_tracked?(provider)
84+
@provider_state_registry.tracked?(provider)
85+
end
86+
87+
def shutdown
88+
providers_to_shutdown = @provider_mutex.synchronize { @providers.values.uniq }
89+
90+
providers_to_shutdown.each do |prov|
91+
prov.shutdown if prov.respond_to?(:shutdown)
92+
rescue => e
93+
@logger&.warn("Error shutting down provider #{prov&.class&.name || "unknown"}: #{e.message}")
94+
end
95+
96+
reset
97+
end
98+
7999
private
80100

81101
def reset
@@ -171,10 +191,6 @@ def dispatch_provider_event(provider, event_type, details = {})
171191
run_handlers_for_provider(provider, event_type, event_details)
172192
end
173193

174-
def provider_state(provider)
175-
@provider_state_registry.get_state(provider)
176-
end
177-
178194
private
179195

180196
def extract_provider_name(provider)

lib/open_feature/sdk/provider_state_registry.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ def remove_provider(provider)
5454
end
5555
end
5656

57+
def tracked?(provider)
58+
return false unless provider
59+
60+
@mutex.synchronize do
61+
@states.key?(provider.object_id)
62+
end
63+
end
64+
5765
def ready?(provider)
5866
get_state(provider) == ProviderState::READY
5967
end

spec/specification/flag_evaluation_api_spec.rb

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,83 @@
270270
end
271271
end
272272

273+
context "1.6 - Shutdown" do
274+
context "Requirement 1.6.1" do
275+
specify "The API MUST define a mechanism to propagate a shutdown request to registered providers." do
276+
expect(OpenFeature::SDK).to respond_to(:shutdown)
277+
end
278+
end
279+
280+
context "Requirement 1.6.2" do
281+
specify "When a shutdown function is called, the API invokes the shutdown function on the registered provider." do
282+
provider1 = OpenFeature::SDK::Provider::InMemoryProvider.new
283+
provider2 = OpenFeature::SDK::Provider::InMemoryProvider.new
284+
285+
OpenFeature::SDK.set_provider_and_wait(provider1)
286+
OpenFeature::SDK.set_provider_and_wait(provider2, domain: "test-domain")
287+
288+
expect(provider1).to receive(:shutdown)
289+
expect(provider2).to receive(:shutdown)
290+
291+
OpenFeature::SDK.shutdown
292+
end
293+
294+
specify "After shutdown, providers are cleared." do
295+
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
296+
OpenFeature::SDK.set_provider_and_wait(provider)
297+
298+
OpenFeature::SDK.shutdown
299+
300+
expect(OpenFeature::SDK.provider).to be_nil
301+
end
302+
end
303+
end
304+
305+
context "1.7 - Provider Status" do
306+
context "Requirement 1.7.1" do
307+
specify "The client MUST define a provider status accessor which indicates the readiness of the associated provider." do
308+
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
309+
OpenFeature::SDK.set_provider_and_wait(provider)
310+
client = OpenFeature::SDK.build_client
311+
312+
expect(client).to respond_to(:provider_status)
313+
expect(client.provider_status).to eq(OpenFeature::SDK::ProviderState::READY)
314+
end
315+
end
316+
317+
context "Requirement 1.7.6" do
318+
specify "If the provider status is NOT_READY, the client should return the default value with PROVIDER_NOT_READY error." do
319+
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
320+
OpenFeature::SDK.set_provider_and_wait(provider)
321+
client = OpenFeature::SDK.build_client
322+
323+
allow(OpenFeature::SDK.configuration).to receive(:provider_state).with(provider).and_return(OpenFeature::SDK::ProviderState::NOT_READY)
324+
325+
result = client.fetch_boolean_details(flag_key: "flag", default_value: false)
326+
327+
expect(result.value).to eq(false)
328+
expect(result.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::PROVIDER_NOT_READY)
329+
expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
330+
end
331+
end
332+
333+
context "Requirement 1.7.7" do
334+
specify "If the provider status is FATAL, the client should return the default value with PROVIDER_FATAL error." do
335+
provider = OpenFeature::SDK::Provider::InMemoryProvider.new
336+
OpenFeature::SDK.set_provider_and_wait(provider)
337+
client = OpenFeature::SDK.build_client
338+
339+
allow(OpenFeature::SDK.configuration).to receive(:provider_state).with(provider).and_return(OpenFeature::SDK::ProviderState::FATAL)
340+
341+
result = client.fetch_string_details(flag_key: "flag", default_value: "default")
342+
343+
expect(result.value).to eq("default")
344+
expect(result.error_code).to eq(OpenFeature::SDK::Provider::ErrorCode::PROVIDER_FATAL)
345+
expect(result.reason).to eq(OpenFeature::SDK::Provider::Reason::ERROR)
346+
end
347+
end
348+
end
349+
273350
context "Logger Methods" do
274351
specify "delegates logger getter to configuration" do
275352
logger = double("Logger")

0 commit comments

Comments
 (0)