From 6292faa8495ed3b851945c3c6d57bc77327fb39d Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Thu, 7 Dec 2023 11:56:55 +0100 Subject: [PATCH] make the eventing provider specific instead of being singletone Signed-off-by: vahid torkaman --- .../dev/openfeature/sdk/FeatureProvider.kt | 5 +- .../java/dev/openfeature/sdk/NoOpProvider.kt | 9 ++ .../dev/openfeature/sdk/OpenFeatureAPI.kt | 9 -- .../dev/openfeature/sdk/async/AsyncClient.kt | 9 +- .../dev/openfeature/sdk/async/Extensions.kt | 41 +++++--- .../openfeature/sdk/events/EventHandler.kt | 28 +----- .../dev/openfeature/sdk/EventsHandlerTest.kt | 95 +++++++++++-------- .../sdk/helpers/AlwaysBrokenProvider.kt | 8 ++ .../sdk/helpers/DoSomethingProvider.kt | 9 ++ build.gradle.kts | 4 +- 10 files changed, 128 insertions(+), 89 deletions(-) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt index 81ed906..9de1a04 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/FeatureProvider.kt @@ -1,6 +1,9 @@ package dev.openfeature.sdk -interface FeatureProvider { +import dev.openfeature.sdk.events.EventObserver +import dev.openfeature.sdk.events.ProviderStatus + +interface FeatureProvider : EventObserver, ProviderStatus { val hooks: List> val metadata: ProviderMetadata diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt index c1d617f..a5632ab 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt @@ -1,5 +1,10 @@ package dev.openfeature.sdk +import dev.openfeature.sdk.events.OpenFeatureEvents +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlin.reflect.KClass + class NoOpProvider(override val hooks: List> = listOf()) : FeatureProvider { override val metadata: ProviderMetadata = NoOpProviderMetadata("No-op provider") override fun initialize(initialContext: EvaluationContext?) { @@ -57,5 +62,9 @@ class NoOpProvider(override val hooks: List> = listOf()) : FeatureProvid return ProviderEvaluation(defaultValue, "Passed in default", Reason.DEFAULT.toString()) } + override fun observe(kClass: KClass): Flow = flow { } + + override fun isProviderReady(): Boolean = true + data class NoOpProviderMetadata(override val name: String?) : ProviderMetadata } \ No newline at end of file diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt index 649d83b..fc247df 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt @@ -1,10 +1,5 @@ package dev.openfeature.sdk -import dev.openfeature.sdk.events.EventHandler -import dev.openfeature.sdk.events.OpenFeatureEvents -import dev.openfeature.sdk.events.observe -import kotlinx.coroutines.CoroutineDispatcher - @Suppress("TooManyFunctions") object OpenFeatureAPI { private var provider: FeatureProvider? = null @@ -23,10 +18,6 @@ object OpenFeatureAPI { return provider } - inline fun observeEvents(dispatcher: CoroutineDispatcher) = - EventHandler.eventsObserver(dispatcher) - .observe() - fun clearProvider() { provider = null } diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt index d730d6e..22b6d40 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt @@ -1,6 +1,7 @@ package dev.openfeature.sdk.async -import dev.openfeature.sdk.OpenFeatureClient +import dev.openfeature.sdk.Client +import dev.openfeature.sdk.FeatureProvider import dev.openfeature.sdk.Value import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -16,10 +17,12 @@ interface AsyncClient { } internal class AsyncClientImpl( - private val client: OpenFeatureClient, + private val client: Client, + private val provider: FeatureProvider, private val dispatcher: CoroutineDispatcher ) : AsyncClient { - private fun observeEvents(callback: () -> T) = observeProviderReady(dispatcher) + private fun observeEvents(callback: () -> T) = provider + .observeProviderReady(dispatcher) .map { callback() } .distinctUntilChanged() diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt index 52645cc..f448c02 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt @@ -1,33 +1,53 @@ package dev.openfeature.sdk.async +import dev.openfeature.sdk.OpenFeatureAPI import dev.openfeature.sdk.OpenFeatureClient -import dev.openfeature.sdk.events.EventHandler +import dev.openfeature.sdk.events.EventObserver import dev.openfeature.sdk.events.OpenFeatureEvents import dev.openfeature.sdk.events.observe import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine -fun OpenFeatureClient.toAsync(dispatcher: CoroutineDispatcher = Dispatchers.IO): AsyncClient { - return AsyncClientImpl(this, dispatcher) +fun OpenFeatureClient.toAsync( + dispatcher: CoroutineDispatcher = Dispatchers.IO +): AsyncClient? { + val provider = OpenFeatureAPI.getProvider() + return provider?.let { + AsyncClientImpl( + this, + it, + dispatcher + ) + } } -internal fun observeProviderReady( - dispatcher: CoroutineDispatcher = Dispatchers.IO -) = EventHandler.eventsObserver(dispatcher) - .observe() +internal fun EventObserver.observeProviderReady() = observe() .onStart { - if (EventHandler.providerStatus().isProviderReady()) { + if (isProviderReady()) { this.emit(OpenFeatureEvents.ProviderReady) } } -suspend fun awaitProviderReady( +suspend fun OpenFeatureAPI.awaitProviderReady( + dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + val provider = getProvider() + requireNotNull(provider) + return provider.awaitProviderReady(dispatcher) +} + +inline fun OpenFeatureAPI.observeEvents(): Flow? { + return getProvider()?.observe() +} + +suspend fun EventObserver.awaitProviderReady( dispatcher: CoroutineDispatcher = Dispatchers.IO ) = suspendCancellableCoroutine { continuation -> val coroutineScope = CoroutineScope(dispatcher) @@ -40,8 +60,7 @@ suspend fun awaitProviderReady( } coroutineScope.launch { - EventHandler.eventsObserver() - .observe() + observe() .take(1) .collect { continuation.resumeWith(Result.failure(it.error)) diff --git a/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventHandler.kt b/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventHandler.kt index d5813a0..700ff26 100644 --- a/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventHandler.kt +++ b/OpenFeature/src/main/java/dev/openfeature/sdk/events/EventHandler.kt @@ -2,7 +2,6 @@ package dev.openfeature.sdk.events import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.Flow @@ -12,14 +11,14 @@ import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch import kotlin.reflect.KClass -interface ProviderStatus { - fun isProviderReady(): Boolean -} - interface EventObserver { fun observe(kClass: KClass): Flow } +interface ProviderStatus { + fun isProviderReady(): Boolean +} + interface EventsPublisher { fun publish(event: OpenFeatureEvents) } @@ -62,23 +61,4 @@ class EventHandler(dispatcher: CoroutineDispatcher) : EventObserver, EventsPubli override fun isProviderReady(): Boolean { return isProviderReady.value } - - companion object { - @Volatile - private var instance: EventHandler? = null - - private fun getInstance(dispatcher: CoroutineDispatcher) = - instance ?: synchronized(this) { - instance ?: create(dispatcher).also { instance = it } - } - - fun eventsObserver(dispatcher: CoroutineDispatcher = Dispatchers.IO): EventObserver = - getInstance(dispatcher) - internal fun providerStatus(dispatcher: CoroutineDispatcher = Dispatchers.IO): ProviderStatus = - getInstance(dispatcher) - fun eventsPublisher(dispatcher: CoroutineDispatcher = Dispatchers.IO): EventsPublisher = - getInstance(dispatcher) - - private fun create(dispatcher: CoroutineDispatcher) = EventHandler(dispatcher) - } } \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt index e019bc1..92dcf7c 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/EventsHandlerTest.kt @@ -25,34 +25,33 @@ class EventsHandlerTest { @Test fun observing_event_observer_works() = runTest { - val eventObserver = EventHandler.eventsObserver() - val eventPublisher = EventHandler.eventsPublisher() + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val eventHandler = EventHandler(dispatcher) var emitted = false - val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - eventObserver.observe() + val job = backgroundScope.launch(dispatcher) { + eventHandler.observe() .take(1) .collect { emitted = true } } - - eventPublisher.publish(OpenFeatureEvents.ProviderReady) + eventHandler.publish(OpenFeatureEvents.ProviderReady) job.join() Assert.assertTrue(emitted) } @Test fun multiple_subscribers_works() = runTest { - val eventObserver = EventHandler.eventsObserver() - val eventPublisher = EventHandler.eventsPublisher() + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val eventHandler = EventHandler(dispatcher) val numberOfSubscribers = 10 val parentJob = Job() var emitted = 0 repeat(numberOfSubscribers) { CoroutineScope(parentJob).launch(UnconfinedTestDispatcher(testScheduler)) { - eventObserver.observe() + eventHandler.observe() .take(1) .collect { emitted += 1 @@ -60,28 +59,28 @@ class EventsHandlerTest { } } - eventPublisher.publish(OpenFeatureEvents.ProviderReady) + eventHandler.publish(OpenFeatureEvents.ProviderReady) parentJob.children.forEach { it.join() } Assert.assertTrue(emitted == 10) } @Test fun canceling_one_subscriber_does_not_cancel_others() = runTest { - val eventObserver = EventHandler.eventsObserver() - val eventPublisher = EventHandler.eventsPublisher() + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val eventHandler = EventHandler(dispatcher) val numberOfSubscribers = 10 val parentJob = Job() var emitted = 0 val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - eventObserver.observe() + eventHandler.observe() .take(1) .collect {} } repeat(numberOfSubscribers) { CoroutineScope(parentJob).launch(UnconfinedTestDispatcher(testScheduler)) { - eventObserver.observe() + eventHandler.observe() .take(1) .collect { emitted += 1 @@ -89,39 +88,41 @@ class EventsHandlerTest { } } job.cancel() - eventPublisher.publish(OpenFeatureEvents.ProviderReady) + eventHandler.publish(OpenFeatureEvents.ProviderReady) parentJob.children.forEach { it.join() } Assert.assertTrue(emitted == 10) } @Test fun the_provider_status_stream_works() = runTest { - val eventPublisher = EventHandler.eventsPublisher() + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val eventHandler = EventHandler(dispatcher) var isProviderReady = false // observing the provider status after the provider ready event is published val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - EventHandler.eventsObserver() - .observe() + eventHandler.observe() .take(1) .collect { isProviderReady = true } } - eventPublisher.publish(OpenFeatureEvents.ProviderReady) + eventHandler.publish(OpenFeatureEvents.ProviderReady) job.join() Assert.assertTrue(isProviderReady) } @Test + @kotlinx.coroutines.FlowPreview fun the_provider_status_stream_not_emitting_without_event_published() = runTest { var isProviderReady = false + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val eventHandler = EventHandler(dispatcher) // observing the provider status after the provider ready event is published val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - EventHandler.eventsObserver() - .observe() + eventHandler.observe() .timeout(10L.milliseconds) .collect { isProviderReady = true @@ -134,13 +135,14 @@ class EventsHandlerTest { @Test fun the_provider_status_stream_is_replays_current_status() = runTest { - val eventPublisher = EventHandler.eventsPublisher() - eventPublisher.publish(OpenFeatureEvents.ProviderReady) + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val eventHandler = EventHandler(dispatcher) + eventHandler.publish(OpenFeatureEvents.ProviderReady) var isProviderReady = false // observing the provider status after the provider ready event is published val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - observeProviderReady() + eventHandler.observeProviderReady() .take(1) .collect { isProviderReady = true @@ -153,41 +155,47 @@ class EventsHandlerTest { @Test fun the_provider_becomes_stale() = runTest { - val eventPublisher = EventHandler.eventsPublisher() + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val eventHandler = EventHandler(dispatcher) var isProviderStale = false - val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - EventHandler.eventsObserver() - .observe() + val job = backgroundScope.launch(dispatcher) { + eventHandler.observe() .take(1) .collect { isProviderStale = true } } - eventPublisher.publish(OpenFeatureEvents.ProviderReady) - eventPublisher.publish(OpenFeatureEvents.ProviderStale) + eventHandler.publish(OpenFeatureEvents.ProviderReady) + eventHandler.publish(OpenFeatureEvents.ProviderStale) job.join() - Assert.assertTrue(isProviderStale) } @Test fun observe_string_value_from_client_works() = runTest { val testDispatcher = UnconfinedTestDispatcher(testScheduler) - val eventPublisher = EventHandler.eventsPublisher(testDispatcher) - eventPublisher.publish(OpenFeatureEvents.ProviderReady) + val eventHandler = EventHandler(testDispatcher) + eventHandler.publish(OpenFeatureEvents.ProviderReady) val key = "mykey" val default = "default" val resultTexts = mutableListOf() + OpenFeatureAPI.setProvider( + mock { + on { isProviderReady() } doReturn eventHandler.isProviderReady() + on { observeProviderReady() } doReturn eventHandler.observeProviderReady(testDispatcher) + } + ) + val mockOpenFeatureClient = mock { on { getStringValue(key, default) } doReturn "text1" } // observing the provider status after the provider ready event is published val job = backgroundScope.launch(testDispatcher) { - mockOpenFeatureClient.toAsync() + mockOpenFeatureClient.toAsync(testDispatcher) .observeStringValue(key, default) .take(2) .collect { @@ -198,15 +206,15 @@ class EventsHandlerTest { `when`(mockOpenFeatureClient.getStringValue(key, default)) .thenReturn("text2") - eventPublisher.publish(OpenFeatureEvents.ProviderReady) + eventHandler.publish(OpenFeatureEvents.ProviderReady) job.join() Assert.assertEquals(listOf("text1", "text2"), resultTexts) } @Test fun observe_string_value_from_client_waits_until_provider_ready() = runTest { - val testDispatcher = UnconfinedTestDispatcher(testScheduler) - val eventPublisher = EventHandler.eventsPublisher(testDispatcher) + val dispatcher = UnconfinedTestDispatcher(testScheduler) + val eventHandler = EventHandler(dispatcher) val key = "mykey" val default = "default" val resultTexts = mutableListOf() @@ -215,9 +223,16 @@ class EventsHandlerTest { on { getStringValue(key, default) } doReturn "text1" } + OpenFeatureAPI.setProvider( + mock { + on { isProviderReady() } doReturn eventHandler.isProviderReady() + on { observeProviderReady() } doReturn eventHandler.observeProviderReady(dispatcher) + } + ) + // observing the provider status after the provider ready event is published - val job = backgroundScope.launch(testDispatcher) { - mockOpenFeatureClient.toAsync() + val job = backgroundScope.launch(dispatcher) { + mockOpenFeatureClient.toAsync(dispatcher) .observeStringValue(key, default) .take(1) .collect { @@ -225,7 +240,7 @@ class EventsHandlerTest { } } - eventPublisher.publish(OpenFeatureEvents.ProviderReady) + eventHandler.publish(OpenFeatureEvents.ProviderReady) job.join() Assert.assertEquals(listOf("text1"), resultTexts) } diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt index cfd6831..6a6053d 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/AlwaysBrokenProvider.kt @@ -6,7 +6,11 @@ import dev.openfeature.sdk.Hook import dev.openfeature.sdk.ProviderEvaluation import dev.openfeature.sdk.ProviderMetadata import dev.openfeature.sdk.Value +import dev.openfeature.sdk.events.OpenFeatureEvents import dev.openfeature.sdk.exceptions.OpenFeatureError.FlagNotFoundError +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlin.reflect.KClass class AlwaysBrokenProvider(override var hooks: List> = listOf(), override var metadata: ProviderMetadata = AlwaysBrokenProviderMetadata()) : FeatureProvider { @@ -65,5 +69,9 @@ class AlwaysBrokenProvider(override var hooks: List> = listOf(), overrid throw FlagNotFoundError(key) } + override fun observe(kClass: KClass): Flow = flow { } + + override fun isProviderReady(): Boolean = true + class AlwaysBrokenProviderMetadata(override val name: String? = "test") : ProviderMetadata } \ No newline at end of file diff --git a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt index 7328dab..8fc762b 100644 --- a/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt +++ b/OpenFeature/src/test/java/dev/openfeature/sdk/helpers/DoSomethingProvider.kt @@ -6,6 +6,10 @@ import dev.openfeature.sdk.Hook import dev.openfeature.sdk.ProviderEvaluation import dev.openfeature.sdk.ProviderMetadata import dev.openfeature.sdk.Value +import dev.openfeature.sdk.events.OpenFeatureEvents +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlin.reflect.KClass class DoSomethingProvider( override val hooks: List> = listOf(), @@ -65,5 +69,10 @@ class DoSomethingProvider( ): ProviderEvaluation { return ProviderEvaluation(Value.Null) } + + override fun observe(kClass: KClass): Flow = flow { } + + override fun isProviderReady(): Boolean = true + class DoSomethingProviderMetadata(override val name: String? = "something") : ProviderMetadata } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index e40f414..311d6f0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,7 +18,9 @@ nexusPublishing { this.repositories { sonatype { nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) + snapshotRepositoryUrl.set( + uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") + ) username = System.getenv("OSSRH_USERNAME") password = System.getenv("OSSRH_PASSWORD") }