diff --git a/src/OpenFeature/Api.cs b/src/OpenFeature/Api.cs index 4226a499..8d0e22f5 100644 --- a/src/OpenFeature/Api.cs +++ b/src/OpenFeature/Api.cs @@ -220,5 +220,14 @@ public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler) { this.EventExecutor.RemoveApiLevelHandler(type, handler); } + + /// + /// Sets the logger for the API + /// + /// The logger to be used + public void SetLogger(ILogger logger) + { + this.EventExecutor.Logger = logger; + } } } diff --git a/src/OpenFeature/EventExecutor.cs b/src/OpenFeature/EventExecutor.cs index 01a3451f..8a6df9a4 100644 --- a/src/OpenFeature/EventExecutor.cs +++ b/src/OpenFeature/EventExecutor.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using OpenFeature.Constant; using OpenFeature.Model; @@ -27,8 +27,11 @@ internal class EventExecutor private readonly Dictionary> _apiHandlers = new Dictionary>(); private readonly Dictionary>> _clientHandlers = new Dictionary>>(); + internal ILogger Logger { get; set; } + public EventExecutor() { + this.Logger = new Logger(new NullLoggerFactory()); this._shutdownDelegate = this.SignalShutdownAsync; var eventProcessing = new Thread(this.ProcessEventAsync); eventProcessing.Start(); @@ -208,14 +211,14 @@ private void EmitOnRegistration(FeatureProviderReference provider, ProviderEvent { handler.Invoke(new ProviderEventPayload { - ProviderName = provider.Provider.GetMetadata().Name, + ProviderName = provider.Provider?.GetMetadata()?.Name, Type = eventType, Message = message }); } catch (Exception exc) { - Debug.WriteLine(exc); + this.Logger?.LogError("Error running handler: " + exc); } } } @@ -320,7 +323,7 @@ private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e) } catch (Exception exc) { - Debug.WriteLine(exc); + this.Logger?.LogError("Error running handler: " + exc); } } diff --git a/src/OpenFeature/Model/ProviderEvents.cs b/src/OpenFeature/Model/ProviderEvents.cs index f2fee853..da68aef4 100644 --- a/src/OpenFeature/Model/ProviderEvents.cs +++ b/src/OpenFeature/Model/ProviderEvents.cs @@ -17,18 +17,22 @@ public class ProviderEventPayload /// Name of the provider. /// public string ProviderName { get; set; } + /// /// Type of the event /// public ProviderEventTypes Type { get; set; } + /// /// A message providing more information about the event. /// public string Message { get; set; } + /// /// A List of flags that have been changed. /// - public List FlagChanges { get; set; } + public List FlagsChanged { get; set; } + /// /// Metadata information for the event. /// diff --git a/test/OpenFeature.Tests/OpenFeatureEventTests.cs b/test/OpenFeature.Tests/OpenFeatureEventTests.cs index e660789b..ca691c52 100644 --- a/test/OpenFeature.Tests/OpenFeatureEventTests.cs +++ b/test/OpenFeature.Tests/OpenFeatureEventTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -20,18 +21,23 @@ public async Task Event_Executor_Should_Propagate_Events_ToGlobal_Handler() { var eventHandler = Substitute.For(); - eventHandler.Invoke(Arg.Any()); - var eventExecutor = new EventExecutor(); - eventExecutor.AddApiLevelHandler(ProviderEventTypes.ProviderReady, eventHandler); + eventExecutor.AddApiLevelHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); - var eventPayload = new Event { EventPayload = new ProviderEventPayload { Type = ProviderEventTypes.ProviderReady } }; - eventExecutor.EventChannel.Writer.TryWrite(eventPayload); + var eventMetadata = new Dictionary { { "foo", "bar" } }; + var myEvent = new Event { EventPayload = new ProviderEventPayload + { + Type = ProviderEventTypes.ProviderConfigurationChanged, + Message = "The provider is ready", + EventMetadata = eventMetadata, + FlagsChanged = new List{ "flag1", "flag2" } + } }; + eventExecutor.EventChannel.Writer.TryWrite(myEvent); Thread.Sleep(1000); - eventHandler.Received().Invoke(Arg.Is(payload => payload.Type == ProviderEventTypes.ProviderReady)); + eventHandler.Received().Invoke(Arg.Is(payload => payload == myEvent.EventPayload)); // shut down the event executor await eventExecutor.Shutdown(); @@ -55,8 +61,6 @@ public async Task API_Level_Event_Handlers_Should_Be_Registered() { var eventHandler = Substitute.For(); - eventHandler.Invoke(Arg.Any()); - Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); Api.Instance.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); Api.Instance.AddHandler(ProviderEventTypes.ProviderError, eventHandler); @@ -131,7 +135,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_ [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] [Specification("5.3.2", "If the provider's `initialize` function terminates abnormally, `PROVIDER_ERROR` handlers MUST run.")] [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] - public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_After_Registering_Provider_Error() + public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Error_State_After_Registering_Provider_Error() { var eventHandler = Substitute.For(); @@ -157,7 +161,7 @@ public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_ [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] [Specification("5.3.3", "Handlers attached after the provider is already in the associated state, MUST run immediately.")] - public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Ready_State_After_Registering_Provider_Stale() + public async Task API_Level_Event_Handlers_Should_Be_Informed_About_Stale_State_After_Registering_Provider_Stale() { var eventHandler = Substitute.For(); @@ -188,15 +192,21 @@ public async Task API_Level_Event_Handlers_Should_Be_Exchangeable() var eventHandler = Substitute.For(); Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + Api.Instance.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, eventHandler); var testProvider = new TestProvider(); await Api.Instance.SetProvider(testProvider); + testProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + var newTestProvider = new TestProvider(); await Api.Instance.SetProvider(newTestProvider); + newTestProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + Thread.Sleep(1000); - eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderReady)); + eventHandler.Received(2).Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)); } [Fact] @@ -230,32 +240,22 @@ public async Task API_Level_Event_Handlers_Should_Be_Removable() [Specification("5.2.5", "If a `handler function` terminates abnormally, other `handler` functions MUST run.")] public async Task API_Level_Event_Handlers_Should_Be_Executed_When_Other_Handler_Fails() { - try - { - var fixture = new Fixture(); - - var failingEventHandler = Substitute.For(); - var eventHandler = Substitute.For(); + var fixture = new Fixture(); - failingEventHandler.When(x => x.Invoke(Arg.Any())) - .Do(x => throw new Exception()); + var failingEventHandler = Substitute.For(); + var eventHandler = Substitute.For(); - eventHandler.Invoke(Arg.Any()); + failingEventHandler.When(x => x.Invoke(Arg.Any())) + .Do(x => throw new Exception()); - Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); - Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); + Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); - var testProvider = new TestProvider(fixture.Create()); - await Api.Instance.SetProvider(testProvider); + var testProvider = new TestProvider(fixture.Create()); + await Api.Instance.SetProvider(testProvider); - failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); - eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); - } - catch (Exception e) - { - // make sure the exception is passed on correctly - Assert.NotNull(e); - } + failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); } [Fact] @@ -268,8 +268,6 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered() var fixture = new Fixture(); var eventHandler = Substitute.For(); - eventHandler.Invoke(Arg.Any()); - var myClient = Api.Instance.GetClient(fixture.Create()); var testProvider = new TestProvider(); @@ -288,35 +286,24 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered() [Specification("5.2.5", "If a `handler function` terminates abnormally, other `handler` functions MUST run.")] public async Task Client_Level_Event_Handlers_Should_Be_Executed_When_Other_Handler_Fails() { - try - { - var fixture = new Fixture(); - - var failingEventHandler = Substitute.For(); - var eventHandler = Substitute.For(); - - failingEventHandler.When(x => x.Invoke(Arg.Any())) - .Do(x => throw new Exception()); + var fixture = new Fixture(); - eventHandler.Invoke(Arg.Any()); + var failingEventHandler = Substitute.For(); + var eventHandler = Substitute.For(); - var myClient = Api.Instance.GetClient(fixture.Create()); + failingEventHandler.When(x => x.Invoke(Arg.Any())) + .Do(x => throw new Exception()); - myClient.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); - myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); + var myClient = Api.Instance.GetClient(fixture.Create()); - var testProvider = new TestProvider(); - await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider); + myClient.AddHandler(ProviderEventTypes.ProviderReady, failingEventHandler); + myClient.AddHandler(ProviderEventTypes.ProviderReady, eventHandler); - failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); - eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); - } - catch (Exception e) - { - // make sure the exception is passed on correctly - Assert.NotNull(e); - } + var testProvider = new TestProvider(); + await Api.Instance.SetProvider(myClient.GetMetadata().Name, testProvider); + failingEventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); + eventHandler.Received().Invoke(Arg.Is(payload => payload.ProviderName == testProvider.GetMetadata().Name)); } [Fact] @@ -331,8 +318,6 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered_To_Default_Pr var eventHandler = Substitute.For(); var clientEventHandler = Substitute.For(); - eventHandler.Invoke(Arg.Any()); - var myClientWithNoBoundProvider = Api.Instance.GetClient(fixture.Create()); var myClientWithBoundProvider = Api.Instance.GetClient(fixture.Create()); @@ -354,6 +339,50 @@ public async Task Client_Level_Event_Handlers_Should_Be_Registered_To_Default_Pr clientEventHandler.DidNotReceive().Invoke(Arg.Is(payload => payload.ProviderName == apiProvider.GetMetadata().Name)); } + [Fact] + [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] + [Specification("5.1.3", "When a `provider` signals the occurrence of a particular `event`, event handlers on clients which are not associated with that provider MUST NOT run.")] + [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] + [Specification("5.2.3", "The `event details` MUST contain the `provider name` associated with the event.")] + [Specification("5.2.4", "The `handler function` MUST accept a `event details` parameter.")] + [Specification("5.2.6", "Event handlers MUST persist across `provider` changes.")] + public async Task Client_Level_Event_Handlers_Should_Be_Receive_Events_From_Named_Provider_Instead_of_Default() + { + var fixture = new Fixture(); + var clientEventHandler = Substitute.For(); + + var client = Api.Instance.GetClient(fixture.Create()); + + var defaultProvider = new TestProvider(fixture.Create()); + var clientProvider = new TestProvider(fixture.Create()); + + // set the default provider + await Api.Instance.SetProvider(defaultProvider); + + client.AddHandler(ProviderEventTypes.ProviderConfigurationChanged, clientEventHandler); + + defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + + Thread.Sleep(1000); + + // verify that the client received the event from the default provider as there is no named provider registered yet + clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)); + + // set the other provider specifically for the client + await Api.Instance.SetProvider(client.GetMetadata().Name, clientProvider); + + // now, send another event for the default handler + defaultProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + clientProvider.SendEvent(ProviderEventTypes.ProviderConfigurationChanged); + + Thread.Sleep(1000); + + // now the client should have received only the event from the named provider + clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == clientProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)); + // for the default provider, the number of received events should stay unchanged + clientEventHandler.Received(1).Invoke(Arg.Is(payload => payload.ProviderName == defaultProvider.GetMetadata().Name && payload.Type == ProviderEventTypes.ProviderConfigurationChanged)); + } + [Fact] [Specification("5.1.2", "When a `provider` signals the occurrence of a particular `event`, the associated `client` and `API` event handlers MUST run.")] [Specification("5.2.1", "The `client` MUST provide a function for associating `handler functions` with a particular `provider event type`.")] @@ -366,8 +395,6 @@ public async Task Client_Level_Event_Handlers_Should_Be_Informed_About_Ready_Sta var fixture = new Fixture(); var eventHandler = Substitute.For(); - eventHandler.Invoke(Arg.Any()); - var myClient = Api.Instance.GetClient(fixture.Create()); var testProvider = new TestProvider();