Skip to content

feat: Centralize event distribution#246

Open
maxnrp wants to merge 6 commits into
open-feature:mainfrom
maxnrp:centralized-event-distribution
Open

feat: Centralize event distribution#246
maxnrp wants to merge 6 commits into
open-feature:mainfrom
maxnrp:centralized-event-distribution

Conversation

@maxnrp
Copy link
Copy Markdown
Contributor

@maxnrp maxnrp commented Apr 30, 2026

Intent

Fix redundant FeatureProvider.observe() subscriptions when multiple callers collect OpenFeatureAPI.observe() / Client.observe() (#245).

Motivation

Each collector used to drive its own flatMapLatest into provider.observe() — which can be an overhead for providers where observe() is costly.

Changes

  • shareIn on providersFlow.flatMapLatest { it.observe() }.shareIn(providerEventsScope, SharingStarted.Eagerly, replay = 0).

Tests

flushDispatchersDefault(durationMs): runTest doesn’t pump Dispatchers.Default, where shareIn runs.
Existing event-order / client–API parity / filter tests kept; they still define expected behavior.

Breaking changes

None intended. Concurrent collectors now share one hot stream (replay = 0);

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a shared flow for provider events in the OpenFeatureAPI, utilizing the shareIn operator to centralize event observation. While this simplifies the public API and reduces overhead for multiple observers, the implementation hardcodes Dispatchers.Default, which has introduced the need for real-time delays in the test suite to ensure deterministic behavior. Feedback suggests making the dispatcher configurable to improve test reliability and further unifying internal status tracking with this shared flow to eliminate redundant subscriptions to the underlying provider events.

Comment thread kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/OpenFeatureAPI.kt Outdated
@maxnrp maxnrp force-pushed the centralized-event-distribution branch 2 times, most recently from 56cf79b to e61ebd7 Compare May 1, 2026 00:11
Signed-off-by: Max Pinheiro <max.pinheiro@fluxon.com>
@maxnrp maxnrp force-pushed the centralized-event-distribution branch from e61ebd7 to 59db46c Compare May 1, 2026 01:26
@maxnrp maxnrp changed the title feat: Centralize event distribution to avoid overhead feat: Centralize event distribution May 1, 2026
@bencehornak
Copy link
Copy Markdown
Contributor

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors event management in OpenFeatureAPI by introducing a shared flow to handle provider events globally, replacing the previous per-provider observation logic. Key changes include the addition of sharedProviderEvents and the use of a dedicated CoroutineScope for event collection. Feedback identifies that using Dispatchers.Unconfined for the event scope may lead to unpredictable behavior and suggests switching to Dispatchers.Default. Additionally, there is a noted discrepancy between the implementation and the PR description regarding the sharing strategy, which may cause late subscribers to miss events.

/**
* [Dispatchers.Unconfined] keeps the shared [observe] collector on the emitting thread when possible.
*/
private val providerEventsScope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Dispatchers.Unconfined for a global event scope can lead to unpredictable behavior if subscribers perform heavy work or cause recursive emissions, as they will execute on the emitting thread. Consider using Dispatchers.Default to offload this work and ensure the provider remains responsive.

Suggested change
private val providerEventsScope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)
private val providerEventsScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

Copy link
Copy Markdown
Contributor Author

@maxnrp maxnrp May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried with Default before (can check gemini's insights on this marked as resolved above), but tests looked messy, and I thought that Unconfined wasn't a bad option, may seen unstable but the jumping from thread to thread is kinda appealing, isn't?

Though as you suggested in a comment, now that API is becoming a class we could just pass Dispatcher as an argument eventually. I've updated the tests, just to keep the PR merge-ready, if #243 goes first, we may retouch it again.

Copy link
Copy Markdown
Contributor

@bencehornak bencehornak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First batch of thoughts :)

Comment thread kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/OpenFeatureAPI.kt Outdated
Comment thread kotlin-sdk/src/commonMain/kotlin/dev/openfeature/kotlin/sdk/OpenFeatureAPI.kt Outdated
/**
* [Dispatchers.Unconfined] keeps the shared [observe] collector on the emitting thread when possible.
*/
private val providerEventsScope = CoroutineScope(SupervisorJob() + Dispatchers.Unconfined)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not hardcode any dispatchers to make the code testable. In #243 this object will become a class, so we could take the opportunity to set the Dispatcher as a constructor argument.

maxnrp added 3 commits May 19, 2026 14:58
Signed-off-by: Max Pinheiro <max.pinheiro@fluxon.com>
Signed-off-by: Max Pinheiro <max.pinheiro@fluxon.com>
Signed-off-by: Max Pinheiro <max.pinheiro@fluxon.com>
@maxnrp maxnrp force-pushed the centralized-event-distribution branch from 69015ea to 1dc461b Compare May 19, 2026 14:29
@maxnrp maxnrp requested a review from bencehornak May 19, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants