Skip to content

feat(openfeature): emit provider events from the DevCycle config lifecycle - #199

Closed
jonathannorris wants to merge 2 commits into
mainfrom
feat/openfeature-provider-events
Closed

feat(openfeature): emit provider events from the DevCycle config lifecycle#199
jonathannorris wants to merge 2 commits into
mainfrom
feat/openfeature-provider-events

Conversation

@jonathannorris

@jonathannorrisjonathannorris commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

  • DevCycleProvider now extends EventProvider and emits PROVIDER_CONFIGURATION_CHANGED, PROVIDER_STALE, PROVIDER_ERROR, and PROVIDER_READY
  • Adds a ConfigUpdateListener to EnvironmentConfigManager so config fetch outcomes are no longer swallowed into the logger
  • Replaces the 2 second busy-wait in initialize() with a latch released by the first config load, and fails fast with FatalError when the SDK key is unauthorized
  • 124 tests pass (117 before)

Without these events, nothing downstream could tell a config refreshing normally from one that had been failing for hours.

EventWhen it is emitted
PROVIDER_READYA config has been loaded, or fetching recovered after a failure.
PROVIDER_CONFIGURATION_CHANGEDA newly fetched config has a different ETag than the one in use.
PROVIDER_STALEA fetch failed while a previously fetched config is still being served.
PROVIDER_ERRORA fetch failed and no config was ever loaded. PROVIDER_FATAL when the SDK key is unauthorized.

Implementation

EnvironmentConfigManager calls onConfigLoaded(etag, firstLoad, changed) on every successful fetch and onConfigError(error, fatal) from the polling catch block. changed is derived from the ETag, so a 304 or an out-of-order config doesn't report a change. DevCycleLocalClient forwards to the provider once one exists, and replays a fatal error to a provider created after the failure.

Worth calling out for review:

  1. initialize() never emits PROVIDER_READY/PROVIDER_ERROR itself.FeatureProviderStateManager already sets those states from the initialize() outcome, and EventProvider's javadoc says the same.
  2. A successful fetch after a failed initialize() emits PROVIDER_READY explicitly. The SDK doesn't call initialize() again, so otherwise the provider stays in ERROR forever after the config arrives.
  3. shutdown() calls super.shutdown().EventProvider.shutdown() is concrete, so an override that forgets it compiles fine and leaks the emitter thread pool. Regression test included.
  4. flagsChanged is intentionally unset. DevCycle resolves variables per-user at evaluation time, so the changed key set isn't knowable at fetch time. The ETag goes in eventMetadata instead.

DevCycleCloudClient is unaffected: no config to change, isInitialized() always true. Docs added to OpenFeature.md.

@jonathannorris
jonathannorrisforce-pushed the fix/sse-polling-interval branch from 093cb83 to 25c4380CompareAugust 6, 2026 13:43
Base automatically changed from fix/sse-polling-interval to mainAugust 6, 2026 13:47
@jonathannorris
jonathannorrisforce-pushed the feat/openfeature-provider-events branch from 6a831c0 to c19d2b9CompareAugust 6, 2026 17:19
@jonathannorris
jonathannorris marked this pull request as ready for review August 7, 2026 15:24
@jonathannorris
jonathannorris requested a review from a team as a code ownerAugust 7, 2026 15:24
CopilotAI review requested due to automatic review settings August 7, 2026 15:24

CopilotAI left a comment

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.

Pull request overview

This PR enhances the Java Server SDK’s OpenFeature Local Bucketing integration by emitting standard OpenFeature provider lifecycle events driven by the DevCycle config polling/SSE lifecycle, allowing downstream consumers to distinguish healthy config refresh from prolonged failure.

Changes:

  • Make DevCycleProvider an EventProvider and emit PROVIDER_READY, PROVIDER_CONFIGURATION_CHANGED, PROVIDER_STALE, and PROVIDER_ERROR based on config fetch outcomes.
  • Add a ConfigUpdateListener hook to EnvironmentConfigManager and forward those callbacks through DevCycleLocalClient to the provider (including replay of fatal errors).
  • Replace the initialize() busy-wait with a latch-based first-config signal and fail fast for unauthorized SDK keys; add integration/unit tests and documentation.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
src/main/java/com/devcycle/sdk/server/openfeature/DevCycleProvider.javaConverts provider to EventProvider, tracks degraded/failed init state, and emits provider events from config lifecycle callbacks.
src/main/java/com/devcycle/sdk/server/local/managers/EnvironmentConfigManager.javaAdds ConfigUpdateListener wiring and notifies on config load/error instead of only logging failures.
src/main/java/com/devcycle/sdk/server/local/managers/ConfigUpdateListener.javaIntroduces an interface for config lifecycle callbacks (success + failure, with fatal flag).
src/main/java/com/devcycle/sdk/server/local/api/DevCycleLocalClient.javaForwards config lifecycle callbacks to the OpenFeature provider and replays fatal errors if provider is created late.
src/test/java/com/devcycle/sdk/server/openfeature/DevCycleProviderTest.javaAdds unit coverage for provider shutdown behavior and init-time failure when the client never initializes.
src/test/java/com/devcycle/sdk/server/openfeature/DevCycleProviderEventsTest.javaAdds integration tests validating emitted OpenFeature provider events across config updates, failures, recovery, and unauthorized keys.
src/test/java/com/devcycle/sdk/server/helpers/LocalConfigServer.javaExtends the test config server to support ETag changes and controllable HTTP error responses.
OpenFeature.mdDocuments emitted provider events and their meaning for Local vs Cloud bucketing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/main/java/com/devcycle/sdk/server/openfeature/DevCycleProvider.java Outdated
Comment threadsrc/test/java/com/devcycle/sdk/server/helpers/LocalConfigServer.java Outdated

private DevCycleProvider openFeatureProvider;
// volatile: read from the config polling and SSE threads via configUpdateListener
private volatile DevCycleProvider openFeatureProvider;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we really need to mark this volatile? This seems like a quick and dirty method instead of properly accessing the various callbacks safely. I'm worried about potential side effects in the actual regular use of the provider from this.

@jonathannorris
jonathannorrisforce-pushed the feat/openfeature-provider-events branch from 6dc4426 to 37d447bCompareAugust 7, 2026 19:39
Sign up for freeto 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.

3 participants

@jonathannorris@JamieSinn