diff --git a/src/api/api.rs b/src/api/api.rs index 3344be7..db09abb 100644 --- a/src/api/api.rs +++ b/src/api/api.rs @@ -195,12 +195,6 @@ mod tests { api.set_provider(provider).await; } - #[spec( - number = "1.1.2.3", - text = "The provider mutator function MUST invoke the shutdown function on the previously registered provider once it's no longer being used to resolve flag values." - )] - #[test] - fn invoke_shutdown_on_old_provider_checked_by_type_system() {} #[spec( number = "1.1.3", @@ -320,13 +314,78 @@ mod tests { text = "The API MUST define a shutdown function which, when called, must call the respective shutdown function on the active provider." )] #[tokio::test] - async fn shutdown() { + async fn shutdown_calls_provider_shutdown() { + let mut provider = MockFeatureProvider::new(); + provider.expect_initialize().returning(|_| {}); + provider.expect_shutdown().once().returning(|| {}); + let mut api = OpenFeature::default(); - api.set_provider(NoOpProvider::default()).await; + api.set_provider(provider).await; + + api.shutdown().await; + } + + #[spec( + number = "1.6.1", + text = "The API MUST define a shutdown function which, when called, must call the respective shutdown function on the active provider." + )] + #[tokio::test] + async fn shutdown_calls_shutdown_on_named_providers() { + let mut default_provider = MockFeatureProvider::new(); + default_provider.expect_initialize().returning(|_| {}); + default_provider.expect_shutdown().once().returning(|| {}); + + let mut named_provider = MockFeatureProvider::new(); + named_provider.expect_initialize().returning(|_| {}); + named_provider.expect_shutdown().once().returning(|| {}); + + let mut api = OpenFeature::default(); + api.set_provider(default_provider).await; + api.set_named_provider("test", named_provider).await; api.shutdown().await; } + #[spec( + number = "1.1.2.3", + text = "The provider mutator function MUST invoke the shutdown function on the previously registered provider once it's no longer being used to resolve flag values." + )] + #[tokio::test] + async fn set_provider_calls_shutdown_on_old_provider() { + let mut old_provider = MockFeatureProvider::new(); + old_provider.expect_initialize().returning(|_| {}); + old_provider.expect_shutdown().once().returning(|| {}); + + let mut new_provider = MockFeatureProvider::new(); + new_provider.expect_initialize().returning(|_| {}); + + let mut api = OpenFeature::default(); + api.set_provider(old_provider).await; + + // When we set a new provider, the old one's shutdown should be called + api.set_provider(new_provider).await; + } + + #[spec( + number = "1.1.2.3", + text = "The provider mutator function MUST invoke the shutdown function on the previously registered provider once it's no longer being used to resolve flag values." + )] + #[tokio::test] + async fn set_named_provider_calls_shutdown_on_old_provider() { + let mut old_provider = MockFeatureProvider::new(); + old_provider.expect_initialize().returning(|_| {}); + old_provider.expect_shutdown().once().returning(|| {}); + + let mut new_provider = MockFeatureProvider::new(); + new_provider.expect_initialize().returning(|_| {}); + + let mut api = OpenFeature::default(); + api.set_named_provider("test", old_provider).await; + + // When we set a new provider with the same name, the old one's shutdown should be called + api.set_named_provider("test", new_provider).await; + } + #[spec( number = "3.2.1.1", text = "The API, Client and invocation MUST have a method for supplying evaluation context." diff --git a/src/api/provider_registry.rs b/src/api/provider_registry.rs index 753dd83..e69c8b3 100644 --- a/src/api/provider_registry.rs +++ b/src/api/provider_registry.rs @@ -32,20 +32,25 @@ impl ProviderRegistry { } pub async fn set_default(&self, mut provider: T) { - let mut map = self.providers.write().await; - map.remove(""); + // Shutdown the old provider before replacing it. + if let Some(old_provider) = self.providers.write().await.remove("") { + old_provider.get().shutdown().await; + } provider .initialize(self.global_evaluation_context.get().await.borrow()) .await; - map.insert(String::default(), FeatureProviderWrapper::new(provider)); + self.providers + .write() + .await + .insert(String::default(), FeatureProviderWrapper::new(provider)); } pub async fn set_named(&self, name: &str, mut provider: T) { - // Drop the already registered provider if any. - if self.get_named(name).await.is_some() { - self.providers.write().await.remove(name); + // Shutdown the old provider before replacing it. + if let Some(old_provider) = self.providers.write().await.remove(name) { + old_provider.get().shutdown().await; } provider @@ -74,7 +79,10 @@ impl ProviderRegistry { } pub async fn clear(&self) { - self.providers.write().await.clear(); + let providers: Vec<_> = self.providers.write().await.drain().collect(); + for (_, provider) in providers { + provider.get().shutdown().await; + } } } diff --git a/src/provider/feature_provider.rs b/src/provider/feature_provider.rs index ecf2261..dbb34d6 100644 --- a/src/provider/feature_provider.rs +++ b/src/provider/feature_provider.rs @@ -89,6 +89,13 @@ pub trait FeatureProvider: Send + Sync + 'static { flag_key: &str, evaluation_context: &EvaluationContext, ) -> EvaluationResult>; + + /// The provider MAY define a shutdown function which performs any cleanup necessary + /// before the provider is disposed of. This may include flushing telemetry events, + /// closing connections, or other resource cleanup. + /// + /// See [spec 2.5.1](https://openfeature.dev/specification/sections/providers#requirement-251). + async fn shutdown(&self) {} } // ============================================================ diff --git a/src/provider/no_op_provider.rs b/src/provider/no_op_provider.rs index 133ec8f..fed72a0 100644 --- a/src/provider/no_op_provider.rs +++ b/src/provider/no_op_provider.rs @@ -201,6 +201,10 @@ mod tests { number = "2.5.1", text = "The provider MAY define a mechanism to gracefully shutdown and dispose of resources." )] - #[test] - fn shutdown_covered_by_drop_trait() {} + #[tokio::test] + async fn shutdown() { + let provider = NoOpProvider::default(); + // NoOpProvider has a default empty shutdown implementation + provider.shutdown().await; + } }