Skip to content

feat!: Add SMP + Legacy Provider adaptor#241

Open
maxnrp wants to merge 14 commits into
open-feature:mainfrom
maxnrp:smp-with-legacy-adaptor
Open

feat!: Add SMP + Legacy Provider adaptor#241
maxnrp wants to merge 14 commits into
open-feature:mainfrom
maxnrp:smp-with-legacy-adaptor

Conversation

@maxnrp
Copy link
Copy Markdown
Contributor

@maxnrp maxnrp commented Apr 22, 2026

Intent

Address the race condition in the current SDK implementation, where lifecycle state is split between the provider and the API, causing undefined event ordering in concurrent scenarios.

This is achieved by narrowing event emission to a single source of truth — the provider itself — via the status: StateFlow<OpenFeatureStatus> property on StateManagingProvider, eliminating the window between initialize() returning and the API updating its internal state. State transitions are the authoritative origin of events, which are emitted in response to each lifecycle stage change.

Introduce a small helper for StateManagingProvider implementations, the ProviderStatusTracker: one place to drive OpenFeatureStatus and the provider event stream with the same event → status rules as the rest of the SDK.

Motivation

open-feature/spec#365 — Provider lifecycle race condition in multi-threaded SDKs
The issue above describes how split ownership of state and events between the API and the provider can produce undefined ordering under concurrency (e.g. API publishing “ready” while the provider emits something else from background work when it actually finishes initializing).

This change aligns the Kotlin SDK with the direction there: the provider is the party that can own a single, authoritative OpenFeatureStatus stream. The API forwards it instead of racing to update the same conceptual state after initialize for implementations. That does not rewrite the whole spec, but it reduces split ownership for the new path while preserving the old behavior for legacy providers.

Providers that only implement FeatureProvider (and not StateManagingProvider) are automatically wrapped by LegacyFeatureProviderAdapter at registration time. The adapter derives status from the provider's observe() events — preserving all existing behavior including status buffering, Ready/Error handling around initialize() and onContextSet(), and event mirroring — while making the provider fully StateManagingProvider-compliant internally. This wrapping is transparent: getProvider() always returns the original provider instance.

Changes

  • Introduced StateManagingProvider as an extension of FeatureProvider, exposing status: StateFlow<OpenFeatureStatus> as the single source of truth for lifecycle state.
  • Introduced LegacyFeatureProviderAdapter, an internal adapter that wraps plain FeatureProvider implementations, deriving their status from observe() events and preserving legacy lifecycle semantics.
  • OpenFeatureAPI now:
    Delegates getStatus() and statusFlow directly to the active provider's status: StateFlow.
    Normalizes all registered providers to StateManagingProvider at registration time — either directly if already implementing it, or via LegacyFeatureProviderAdapter.
    Updates setProvider and setProviderAndWait to suspend until the provider emits a state other than NotReady after initialize() completes.
    getProvider() unwraps LegacyFeatureProviderAdapter to always return the original registered provider.
  • NoOpProvider and MultiProvider implement StateManagingProvider.
  • ProviderStatusTracker updates derived StateFlow via toOpenFeatureStatus() and broadcasts with tryEmit on a MutableSharedFlow.

Tests

StateManagingProviderStatusTests — Legacy init, observe mirrored into SDK status via LegacyFeatureProviderAdapter, onContextSet, getStatus from provider status, provider replacement/shutdown.

LegacyFeatureProviderStatusTests — Legacy init, observe mirrored into SDK status, onContextSet, getStatus from SDK buffer, provider replacement/shutdown.

StatusTests — End-to-end transitions, clear/shutdown, concurrent context updates with a state-managing slow provider, provider replacement (including async setProvider).

MultiProviderTests — Existing multi-provider behavior (strategies, lifecycle, track, error aggregation, init with long-running flows).

ProviderStatusTrackerTest — send → status mapping, replay + subsequent sends, not-ready/timeout, two collectors, ProviderConfigurationChanged not changing status, error details preserved under replay, etc.

Together, these unite legacy and state-managing behavior and cover init, status, and context and shutdown, under concurrency where relevant.

Breaking changes vs legacy compliance

  • Intended backward compatible.
  • StateManagingProvider (standard for custom providers)
  • LegacyFeatureProviderAdapter (internal — not part of public API)
  • API update: new public type: StateManagingProvider

Dependency

This change requires a follow-up merge in: open-feature/kotlin-sdk-contrib

@maxnrp maxnrp changed the title Add SMP + Legacy Provider adaptor feat!: Add SMP + Legacy Provider adaptor Apr 22, 2026
@maxnrp maxnrp force-pushed the smp-with-legacy-adaptor branch from 2bf7987 to 7965fd2 Compare April 22, 2026 17:32
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 the StateManagingProvider interface, which allows providers to manage their own lifecycle status via a StateFlow. The OpenFeatureAPI has been refactored to derive the SDK's overall status from the active provider, and a LegacyFeatureProviderAdapter was added to wrap existing FeatureProvider implementations, which are now deprecated. Feedback was provided regarding a redundant status check in the legacy adapter's initialization logic.

@maxnrp maxnrp force-pushed the smp-with-legacy-adaptor branch from 7965fd2 to 2290aff Compare April 22, 2026 18:18
@toddbaert
Copy link
Copy Markdown
Member

Related spec PR: open-feature/spec#367

@maxnrp maxnrp force-pushed the smp-with-legacy-adaptor branch 2 times, most recently from 2d2e563 to 9bc01c7 Compare April 24, 2026 17:10
@typotter typotter requested a review from dd-oleksii April 29, 2026 20:14
@maxnrp maxnrp force-pushed the smp-with-legacy-adaptor branch from 482c01f to 135d7b3 Compare May 4, 2026 10:41
maxnrp and others added 12 commits May 6, 2026 11:37
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>
Signed-off-by: Max Pinheiro <max.pinheiro@fluxon.com>
Signed-off-by: Max Pinheiro <max.pinheiro@fluxon.com>
Co-authored-by: Oleksii Shmalko <oleksii.shmalko@datadoghq.com>
Signed-off-by: Max <73404116+maxnrp@users.noreply.github.com>
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>
Signed-off-by: Tyler Potter <tyler.john.potter@gmail.com>
Signed-off-by: Max Pinheiro <max.pinheiro@fluxon.com>
Signed-off-by: Max <73404116+maxnrp@users.noreply.github.com>
@maxnrp maxnrp force-pushed the smp-with-legacy-adaptor branch from fe104e3 to 996c782 Compare May 6, 2026 10:46
Signed-off-by: Max Pinheiro <max.pinheiro@fluxon.com>
@maxnrp maxnrp force-pushed the smp-with-legacy-adaptor branch from e2b6b8d to d08f327 Compare May 6, 2026 11:19
@maxnrp maxnrp requested a review from dd-oleksii May 8, 2026 15:45
Signed-off-by: Max Pinheiro <max.pinheiro@fluxon.com>
@maxnrp maxnrp force-pushed the smp-with-legacy-adaptor branch from a818129 to c63c8e5 Compare May 8, 2026 15:56
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.

4 participants