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

@jonathannorris jonathannorris 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.

Event When it is emitted
PROVIDER_READY A config has been loaded, or fetching recovered after a failure.
PROVIDER_CONFIGURATION_CHANGED A newly fetched config has a different ETag than the one in use.
PROVIDER_STALE A fetch failed while a previously fetched config is still being served.
PROVIDER_ERROR A 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
jonathannorris force-pushed the fix/sse-polling-interval branch from 093cb83 to 25c4380 Compare August 6, 2026 13:43
Base automatically changed from fix/sse-polling-interval to main August 6, 2026 13:47
@jonathannorris
jonathannorris force-pushed the feat/openfeature-provider-events branch from 6a831c0 to c19d2b9 Compare August 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 owner August 7, 2026 15:24
Copilot AI review requested due to automatic review settings August 7, 2026 15:24

Copilot AI 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
File Description
src/main/java/com/devcycle/sdk/server/openfeature/DevCycleProvider.java Converts 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.java Adds ConfigUpdateListener wiring and notifies on config load/error instead of only logging failures.
src/main/java/com/devcycle/sdk/server/local/managers/ConfigUpdateListener.java Introduces an interface for config lifecycle callbacks (success + failure, with fatal flag).
src/main/java/com/devcycle/sdk/server/local/api/DevCycleLocalClient.java Forwards 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.java Adds unit coverage for provider shutdown behavior and init-time failure when the client never initializes.
src/test/java/com/devcycle/sdk/server/openfeature/DevCycleProviderEventsTest.java Adds integration tests validating emitted OpenFeature provider events across config updates, failures, recovery, and unauthorized keys.
src/test/java/com/devcycle/sdk/server/helpers/LocalConfigServer.java Extends the test config server to support ETag changes and controllable HTTP error responses.
OpenFeature.md Documents 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 thread src/test/java/com/devcycle/sdk/server/openfeature/DevCycleProviderEventsTest.java Outdated
Comment thread src/main/java/com/devcycle/sdk/server/openfeature/DevCycleProvider.java Outdated
Comment thread src/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
jonathannorris force-pushed the feat/openfeature-provider-events branch from 6dc4426 to 37d447b Compare August 7, 2026 19:39
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.

3 participants