feat!: add StateManagingProvider and deprecate SDK-synthesized lifecycle events - #260
Closed
mtonko-flx wants to merge 3 commits into
Closed
feat!: add StateManagingProvider and deprecate SDK-synthesized lifecycle events#260mtonko-flx wants to merge 3 commits into
mtonko-flx wants to merge 3 commits into
Conversation
Spec requirement 5.1.1 requires the provider event set to include PROVIDER_RECONCILING and PROVIDER_CONTEXT_CHANGED, and 5.2.3 requires event details to carry the provider name. Neither existed, so the SDK could not represent context reconciliation as events nor attribute an event to its provider. Signed-off-by: Mark Tonkonoh <mark.tonkonoh@fluxon.com>
BREAKING CHANGE: statusFlow is now StateFlow<OpenFeatureStatus> and conflates consecutive equal values. Signed-off-by: Mark Tonkonoh <mark.tonkonoh@fluxon.com>
…cle events The SDK inferred provider readiness from lifecycle methods returning: it published Ready once initialize returned, and drove the Reconciling/Ready transitions around onContextSet itself. A provider doing work in the background could report something in that window and have the SDK's own conclusion overwrite it. Providers can now declare, by implementing StateManagingProvider, that they report their own lifecycle events. For those the SDK publishes nothing of its own and reports exactly what the provider signals, in the order signalled. Note the marker carries no status accessor: the provider owns the events, the SDK still owns the status. Providers that do not implement it keep working unchanged. Everything the SDK used to do for all providers now lives in LegacyProviderWrapper, installed for them at registration and nowhere else, so withdrawing the behavior later is a deletion rather than an untangling. That includes coalescing overlapping context reconciliations, which moved out of the API instance along with its generation counters. Where such a provider already reports a lifecycle event itself, the wrapper stands aside instead of reporting the same transition twice. Registering a legacy provider logs a deprecation warning. setProviderAndWait now settles on the provider's reported event rather than on initialize returning. A provider that returns from initialize without reporting anything is in breach of its contract; the SDK logs that and keeps waiting rather than substituting a status, since substituting one is what caused the original race. Applications needing a bound can impose their own. Two consequences worth noting for anyone reading the test diff. Status is now observable only once the event carrying it has been dispatched, so tests asserting status straight after a gated lifecycle call have to let the dispatcher run. Registration settling on an event also means a collector started alongside it observes NotReady first. SpyProvider gained real metadata: provider names are now read during registration to attribute events, and a test double throwing from metadata broke that. BREAKING CHANGE: providers wanting to own their lifecycle events implement StateManagingProvider; the SDK synthesizing them is deprecated. setProviderAndWait waits for the provider's lifecycle event rather than for initialize to return. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueComment |
This was referenced Aug 28, 2026
Author
|
Superseded by #262, which delivers this work as a single change. Closing the stack. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
The SDK infers provider readiness from lifecycle methods returning: it publishes
Readyonceinitializereturns, and drives theRECONCILING/READYtransitions aroundonContextSetitself. A provider doing work in the background can report something in that window and have the SDK's own conclusion overwrite it — the race in spec #365. This PR lets a provider declare that it reports its own lifecycle events, in which case the SDK publishes nothing of its own and reports exactly what the provider signals, in the order signalled.Motivation
spec #385 resolves #365 by making provider status derived entirely from provider-emitted events, with an opt-in marker so existing providers keep working. Notably it was chosen over #380, which moved the status itself onto the provider: state stays in the SDK precisely so that providers are not burdened with concurrency-safe status accessors. So
StateManagingProviderhere carries no status accessor — the provider owns the events, the SDK still owns the status.The Kotlin SDK is the first to implement this; the Java, JS and Go PoCs on #365 predate #385 and still follow the rejected shape. The marker keeps their name for cross-SDK recognisability despite the changed meaning; happy to revisit if the ecosystem settles on something else.
Spec Requirements
PROVIDER_READY/PROVIDER_ERRORbeforeinitializeterminatesPROVIDER_CONTEXT_CHANGED/PROVIDER_ERRORforon context changedinitializeand its resulting lifecycle event to be processedinitializeis abstract, so every provider defines it; the legacy path covers the behaviourChanges
Implementation
StateManagingProvider: marker extendingFeatureProvider, redeclaringobserve()without a default so opting in requires a real event stream — implementing #408's advice to couple lifecycle methods with event support.LegacyProviderWrapper(internal): holds all deprecated behaviour for providers without the marker — synthesising readiness and reconciliation events, and coalescing overlappingonContextSetinvocations. The generation-counter arbitration previously inOpenFeatureAPIInstancemoves here, so withdrawing the legacy path later is a deletion rather than an untangling.getProvider()unwraps so callers always get the instance they registered.setProviderAndWaitsettles on the provider's reported event rather than oninitializereturning.Mixed mode
Appendix E permits duplicate events where a legacy provider also emits its own. Its sanction covers the derived status, which absorbs a repeat — but the event stream would deliver both to application handlers, which on
mainit does not. So the wrapper stands aside when the provider has already reported a transition itself. Everything the wrapper publishes goes through one queue with a single consumer, so forwarded and synthesised events keep a total order rather than racing.Usage Examples
Testing
initializeis not overwritten afterwards — the #365 regression test.PROVIDER_FATALbecomesFATAL, notERROR.RECONCILING→CONTEXT_CHANGED, with nothing added by the SDK.getProvider()returns the registered instance, never the wrapper.initializethrew can be registered again and succeed;shutdownis repeatable.ktlintCheckandapiCheckgreen,apiDumpregenerated (LegacyProviderWrapperstays internal).A note on the test diff
Two consequences are worth knowing when reading it. Status is now observable only once the event carrying it has been dispatched, so tests asserting status straight after a gated lifecycle call have to let the dispatcher run. And registration settling on an event means a collector started alongside it observes
NOT_READYfirst.Breaking Changes
Providers wanting to own their lifecycle events implement
StateManagingProvider; the SDK synthesising them is deprecated but still the default for existing providers, so no provider needs changing yet.setProviderAndWaitnow waits for the provider's lifecycle event rather than forinitializeto return — a provider that returns without reporting anything is in breach of 2.8.2, and the SDK logs that and keeps waiting rather than substituting a status, since substituting one is what caused the original race. Applications needing a bound can impose their own withwithTimeout.