-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add events of the OpenFeature into the provider #45
Changes from 4 commits
4a509d5
1fc3c18
02afc24
d770f73
1d98d35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,37 +18,57 @@ import dev.openfeature.sdk.ProviderEvaluation | |
import dev.openfeature.sdk.ProviderMetadata | ||
import dev.openfeature.sdk.Reason | ||
import dev.openfeature.sdk.Value | ||
import dev.openfeature.sdk.events.EventHandler | ||
import dev.openfeature.sdk.events.EventsPublisher | ||
import dev.openfeature.sdk.events.OpenFeatureEvents | ||
import dev.openfeature.sdk.exceptions.OpenFeatureError.FlagNotFoundError | ||
import dev.openfeature.sdk.exceptions.OpenFeatureError.InvalidContextError | ||
import dev.openfeature.sdk.exceptions.OpenFeatureError.ParseError | ||
import kotlinx.coroutines.CoroutineDispatcher | ||
import kotlinx.coroutines.CoroutineExceptionHandler | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.withContext | ||
import kotlinx.coroutines.SupervisorJob | ||
import kotlinx.coroutines.cancel | ||
import kotlinx.coroutines.launch | ||
|
||
@Suppress( | ||
"TooManyFunctions", | ||
"LongParameterList" | ||
) | ||
class ConfidenceFeatureProvider private constructor( | ||
override val hooks: List<Hook<*>>, | ||
override val metadata: ProviderMetadata, | ||
private val cache: ProviderCache, | ||
private val client: ConfidenceClient, | ||
private val flagApplier: FlagApplier, | ||
private val dispatcher: CoroutineDispatcher | ||
private val eventsPublisher: EventsPublisher, | ||
dispatcher: CoroutineDispatcher | ||
) : FeatureProvider { | ||
override suspend fun initialize(initialContext: EvaluationContext?) { | ||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher) | ||
private val networkExceptionHandler by lazy { | ||
CoroutineExceptionHandler { _, _ -> | ||
// network failed, provider is ready but with default/cache values | ||
eventsPublisher.publish(OpenFeatureEvents.ProviderReady) | ||
Comment on lines
+51
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe in some cases we will want to emit ProviderError here. I think we can merge this as it is for now, but we should perhaps take another look the overall OpenFeature design of Events and make sure they are useful for users to really understand what is happening in the Provider at any time There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this depends on the strategy of the provider. maybe we are fine with the network to fail. if user doesn't have internet they should still be able to use a working provider with flags from the cache. and provider is ready. |
||
} | ||
} | ||
override fun initialize(initialContext: EvaluationContext?) { | ||
if (initialContext == null) return | ||
withContext(dispatcher) { | ||
try { | ||
val resolveResponse = client.resolve(listOf(), initialContext) | ||
if (resolveResponse is ResolveResponse.Resolved) { | ||
val (flags, resolveToken) = resolveResponse.flags | ||
cache.refresh(flags.list, resolveToken, initialContext) | ||
} | ||
} catch (_: Throwable) { | ||
// Can't refresh cache at this time. Do we retry at a later time? | ||
coroutineScope.launch(networkExceptionHandler) { | ||
val resolveResponse = client.resolve(listOf(), initialContext) | ||
if (resolveResponse is ResolveResponse.Resolved) { | ||
val (flags, resolveToken) = resolveResponse.flags | ||
cache.refresh(flags.list, resolveToken, initialContext) | ||
eventsPublisher.publish(OpenFeatureEvents.ProviderReady) | ||
} | ||
} | ||
} | ||
|
||
override suspend fun onContextSet( | ||
override fun shutdown() { | ||
coroutineScope.cancel() | ||
} | ||
|
||
override fun onContextSet( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should emit "ProviderStale" if |
||
oldContext: EvaluationContext?, | ||
newContext: EvaluationContext | ||
) { | ||
|
@@ -163,7 +183,7 @@ class ConfidenceFeatureProvider private constructor( | |
private class ConfidenceMetadata(override var name: String? = "confidence") : ProviderMetadata | ||
|
||
@Suppress("LongParameterList") | ||
suspend fun create( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good to have a "create" method not suspend 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the problem with this however is that the cache is reading from file in the caller thread now. we should probably make the read from file in the cache explicit. we can discuss this offline. |
||
fun create( | ||
context: Context, | ||
clientSecret: String, | ||
region: ConfidenceRegion = ConfidenceRegion.EUROPE, | ||
|
@@ -172,6 +192,7 @@ class ConfidenceFeatureProvider private constructor( | |
metadata: ProviderMetadata = ConfidenceMetadata(), | ||
cache: ProviderCache? = null, | ||
flagApplier: FlagApplier? = null, | ||
eventsPublisher: EventsPublisher = EventHandler.eventsPublisher(Dispatchers.IO), | ||
dispatcher: CoroutineDispatcher = Dispatchers.IO | ||
): ConfidenceFeatureProvider { | ||
val configuredClient = client ?: ConfidenceRemoteClient( | ||
|
@@ -191,6 +212,7 @@ class ConfidenceFeatureProvider private constructor( | |
cache = cache ?: StorageFileCache.create(context), | ||
client = configuredClient, | ||
flagApplier = flagApplierWithRetries, | ||
eventsPublisher, | ||
dispatcher | ||
) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,8 +6,6 @@ import dev.openfeature.sdk.EvaluationContext | |
import kotlinx.serialization.encodeToString | ||
import kotlinx.serialization.json.Json | ||
import java.io.File | ||
import kotlin.coroutines.resume | ||
import kotlin.coroutines.suspendCoroutine | ||
|
||
const val FLAGS_FILE_NAME = "confidence_flags_cache.json" | ||
|
||
|
@@ -43,10 +41,10 @@ class StorageFileCache private constructor(context: Context) : InMemoryCache() { | |
} | ||
|
||
companion object { | ||
suspend fun create(context: Context): StorageFileCache = suspendCoroutine { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Curious about removing suspend here: reading file could be a slow operation, do we really want to make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. explained in another comment. we can and i believe we should explicitly call read from file in the provider initialization on a background thread instead of doing it in the init of the cache. |
||
fun create(context: Context): StorageFileCache { | ||
val storage = StorageFileCache(context) | ||
storage.readFile() | ||
it.resume(storage) | ||
return storage | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,9 @@ import dev.openfeature.sdk.ImmutableContext | |
import dev.openfeature.sdk.OpenFeatureAPI | ||
import dev.openfeature.sdk.Reason | ||
import dev.openfeature.sdk.Value | ||
import dev.openfeature.sdk.async.awaitProviderReady | ||
import dev.openfeature.sdk.events.EventHandler | ||
import dev.openfeature.sdk.events.OpenFeatureEvents | ||
import kotlinx.coroutines.runBlocking | ||
import org.junit.Assert.assertEquals | ||
import org.junit.Assert.assertNotEquals | ||
|
@@ -27,26 +30,34 @@ class ConfidenceIntegrationTests { | |
@Before | ||
fun setup() { | ||
whenever(mockContext.filesDir).thenReturn(Files.createTempDirectory("tmpTests").toFile()) | ||
EventHandler.eventsPublisher().publish(OpenFeatureEvents.ProviderShutDown) | ||
} | ||
|
||
@Test | ||
fun testSimpleResolveInMemoryCache() { | ||
runBlocking { | ||
OpenFeatureAPI.setProvider( | ||
ConfidenceFeatureProvider.create(mockContext, clientSecret, cache = InMemoryCache()), | ||
ImmutableContext( | ||
targetingKey = UUID.randomUUID().toString(), | ||
attributes = mutableMapOf( | ||
"user" to Value.Structure( | ||
mapOf( | ||
"country" to Value.String("SE") | ||
) | ||
EventHandler.eventsPublisher().publish(OpenFeatureEvents.ProviderShutDown) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's this for? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tests are running on the same machine, if they run together and one after another, the next one will fail because provider was already ready in the previous test. we need to clean up the events for the new test case. |
||
OpenFeatureAPI.setProvider( | ||
ConfidenceFeatureProvider.create(mockContext, clientSecret, cache = InMemoryCache()), | ||
ImmutableContext( | ||
targetingKey = UUID.randomUUID().toString(), | ||
attributes = mutableMapOf( | ||
"user" to Value.Structure( | ||
mapOf( | ||
"country" to Value.String("SE") | ||
) | ||
) | ||
) | ||
) | ||
) | ||
runBlocking { | ||
awaitProviderReady() | ||
} | ||
val intDetails = OpenFeatureAPI.getClient().getIntegerDetails("test-flag-1.my-integer", 0) | ||
|
||
val intDetails = OpenFeatureAPI.getClient() | ||
.getIntegerDetails( | ||
"test-flag-1.my-integer", | ||
0 | ||
) | ||
assertNull(intDetails.errorCode) | ||
assertNull(intDetails.errorMessage) | ||
assertNotNull(intDetails.value) | ||
|
@@ -59,21 +70,23 @@ class ConfidenceIntegrationTests { | |
fun testSimpleResolveStoredCache() { | ||
val cacheFile = File(mockContext.filesDir, FLAGS_FILE_NAME) | ||
assertEquals(0L, cacheFile.length()) | ||
runBlocking { | ||
OpenFeatureAPI.setProvider( | ||
ConfidenceFeatureProvider.create(mockContext, clientSecret), | ||
ImmutableContext( | ||
targetingKey = UUID.randomUUID().toString(), | ||
attributes = mutableMapOf( | ||
"user" to Value.Structure( | ||
mapOf( | ||
"country" to Value.String("SE") | ||
) | ||
OpenFeatureAPI.setProvider( | ||
ConfidenceFeatureProvider.create(mockContext, clientSecret), | ||
ImmutableContext( | ||
targetingKey = UUID.randomUUID().toString(), | ||
attributes = mutableMapOf( | ||
"user" to Value.Structure( | ||
mapOf( | ||
"country" to Value.String("SE") | ||
) | ||
) | ||
) | ||
) | ||
) | ||
runBlocking { | ||
awaitProviderReady() | ||
} | ||
|
||
assertNotEquals(0L, cacheFile.length()) | ||
val intDetails = OpenFeatureAPI.getClient().getIntegerDetails("test-flag-1.my-integer", 0) | ||
assertNull(intDetails.errorCode) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am sill concerned about exposing this non-spec-standard API from the base SDK: we should use the Events API directly or try add this API in the OpenFeature Sepc. But this comment is not related to this PR under review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this makes the life of provider devs much easier. but we can always remove it.