Skip to content

feat: State change notification for flags client#3025

Merged
typotter merged 28 commits into
developfrom
typo/FFL-1442-android-sdk-of-wrapper-implement-observe-method
Dec 1, 2025
Merged

feat: State change notification for flags client#3025
typotter merged 28 commits into
developfrom
typo/FFL-1442-android-sdk-of-wrapper-implement-observe-method

Conversation

@typotter

@typotter typotter commented Nov 25, 2025

Copy link
Copy Markdown
Contributor

What does this PR do?

Implements state management for FlagsClient in the dd-sdk-android-flags module, enabling applications and OpenFeature providers to observe client lifecycle states.

Key Changes:

  • Adds FlagsClientState enum with states: NOT_READY, READY, RECONCILING, ERROR
  • Adds FlagsStateListener interface for receiving state change notifications
  • Extends FlagsClient interface with state management methods: getCurrentState(), addStateListener(), removeStateListener()
  • Implements FlagsStateChannel wrapper over DDCoreSubscription with semantic notification methods
  • Wires state transitions through EvaluationsManager to DatadogFlagsClient
  • Provides comprehensive unit test coverage for all state management components

Motivation

This change is a prerequisite for implementing the OpenFeature provider's observe() method. The OpenFeature specification requires providers to emit events (PROVIDER_READY, PROVIDER_RECONCILING, PROVIDER_ERROR) when their state changes.

Without state tracking in the underlying FlagsClient, the OpenFeature provider has no mechanism to know when flags are loaded, when context changes begin/complete, or when errors occur.

Benefits:

  • Enables OpenFeature provider compliance with the observe() specification
  • Provides visibility into flag loading lifecycle for debugging and monitoring
  • Allows applications to react to flag availability changes
  • Fully backwards compatible - existing clients continue to work unchanged

Additional Notes

Design Decisions:

  1. Left out STALE state: The OpenFeature spec includes a STALE state for "cached flags may be outdated", but we removed it because there's no current mechanism to determine staleness. Added a note in FlagsClientState docs that this may be added in a future release.

  2. DDCoreSubscription with synchronization: Used the existing DDCoreSubscription utility for listener management, wrapped with synchronized blocks to guarantee ordered delivery of state changes (critical for NOT_READY → RECONCILING → READY flows).

  3. FlagsStateChannel abstraction: Created a wrapper class with semantic methods (notifyReady(), notifyReconciling(), etc.) to abstract DDCoreSubscription internals and improve code readability.

State Transition Flow:

setEvaluationContext()
    ↓
EvaluationsManager.notifyReconciling()
    ↓
[Background fetch...]
    ↓
Success: EvaluationsManager.notifyReady()
Failure: EvaluationsManager.notifyError()
    ↓
FlagsStateChannel → All registered listeners notified

Thread Safety:

  • getCurrentState(): Lock-free atomic read
  • addStateListener()/removeStateListener(): Thread-safe via DDCoreSubscription
  • State notifications: Synchronized to guarantee ordered delivery

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

@typotter typotter changed the title Typo/ffl 1442 android sdk of wrapper implement observe method feat: State change notification for flags client Nov 25, 2025
@typotter typotter marked this pull request as ready for review November 25, 2025 06:50
@typotter typotter requested review from a team as code owners November 25, 2025 06:50
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Nov 25, 2025

Copy link
Copy Markdown

🎯 Code Coverage
Patch Coverage: 100.00%
Total Coverage: 71.37% (+0.05%)

View detailed report

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: c8b1143 | Docs | Datadog PR Page | Was this helpful? Give us feedback!

@typotter typotter force-pushed the typo/FFL-1442-android-sdk-of-wrapper-implement-observe-method branch from 8cfaa65 to be53823 Compare November 27, 2025 18:01

@typotter typotter left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks Nikita. ptal

testedManager.updateState(FlagsClientState.Ready)

// Then - no further notifications after removal
org.mockito.kotlin.verifyNoMoreInteractions(mockListener)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

applied

@Test
fun `M delegate to state manager W state_removeListener()`() {
// Given
val mockListener = mock(FlagsStateListener::class.java)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

applied

@Test
fun `M delegate to state manager W state_addListener()`() {
// Given
val mockListener = mock(FlagsStateListener::class.java)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

applied

Comment on lines +40 to +41
override fun addListener(listener: FlagsStateListener) { /* no-op */ }
override fun removeListener(listener: FlagsStateListener) { /* no-op */ }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

applied

Comment on lines +153 to +167
/**
* Observable interface for tracking client state changes.
*
* Provides three ways to observe state:
* - Synchronous: [StateObservable.getCurrentState] for immediate queries (Java-friendly)
* - Reactive: [StateObservable.flow] for coroutine-based updates (Kotlin)
* - Callback: [StateObservable.addListener] for traditional observers (Java-friendly)
*
* Example:
* ```kotlin
* // Synchronous
* val current = client.state.getCurrentState()
*
* // Reactive Flow
* client.state.flow.collect { state -> /* ... */ }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

fun `set up`() {
whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger
whenever(mockSdkCore.createSingleThreadExecutorService(FLAGS_CLIENT_EXECUTOR_NAME)) doReturn
whenever(mockSdkCore.createSingleThreadExecutorService(org.mockito.kotlin.any())) doReturn

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

applied

@Test
fun `M not block W hasFlags() { persistence still loading }`() {
// Given
val startTime = System.currentTimeMillis()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

thanks. good catch

Comment thread detekt_custom_safe_calls.yml Outdated
Comment on lines +1086 to +1087
- "kotlinx.coroutines.flow.MutableStateFlow(com.datadog.android.flags.model.FlagsClientState)"
- "kotlinx.coroutines.flow.MutableStateFlow.asStateFlow()"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

Comment on lines +153 to +167
/**
* Observable interface for tracking client state changes.
*
* Provides three ways to observe state:
* - Synchronous: [StateObservable.getCurrentState] for immediate queries (Java-friendly)
* - Reactive: [StateObservable.flow] for coroutine-based updates (Kotlin)
* - Callback: [StateObservable.addListener] for traditional observers (Java-friendly)
*
* Example:
* ```kotlin
* // Synchronous
* val current = client.state.getCurrentState()
*
* // Reactive Flow
* client.state.flow.collect { state -> /* ... */ }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

// region addListener / removeListener

@Test
fun `M notify listener W addListener() and notify`() {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

@typotter typotter requested a review from 0xnm December 1, 2025 16:23
0xnm
0xnm previously approved these changes Dec 1, 2025
@typotter

typotter commented Dec 1, 2025

Copy link
Copy Markdown
Contributor Author

/merge

@typotter typotter requested a review from 0xnm December 1, 2025 16:47
@dd-devflow-routing-codex

dd-devflow-routing-codex Bot commented Dec 1, 2025

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2025-12-01 16:47:44 UTC ℹ️ Start processing command /merge


2025-12-01 16:47:50 UTC ℹ️ MergeQueue: waiting for PR to be ready

This pull request is not mergeable according to GitHub. Common reasons include pending required checks, missing approvals, or merge conflicts — but it could also be blocked by other repository rules or settings.
It will be added to the queue as soon as checks pass and/or get approvals.
Note: if you pushed new commits since the last approval, you may need additional approval.
You can remove it from the waiting list with /remove command.


2025-12-01 18:07:30 UTC ⚠️ MergeQueue: This merge request was unqueued

tyler.potter@datadoghq.com unqueued this merge request

@typotter

typotter commented Dec 1, 2025

Copy link
Copy Markdown
Contributor Author

Thanks. Just need one more stamp for the final change

@typotter

typotter commented Dec 1, 2025

Copy link
Copy Markdown
Contributor Author

/remove

@dd-devflow-routing-codex

dd-devflow-routing-codex Bot commented Dec 1, 2025

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2025-12-01 18:07:25 UTC ℹ️ Start processing command /remove


2025-12-01 18:07:28 UTC ℹ️ Devflow: /remove

@typotter typotter dismissed aleksandr-gringauz’s stale review December 1, 2025 18:46

Requested changes have been applied; reviewer is on PTO.

@typotter

typotter commented Dec 1, 2025

Copy link
Copy Markdown
Contributor Author

/merge

@dd-devflow-routing-codex

dd-devflow-routing-codex Bot commented Dec 1, 2025

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2025-12-01 20:12:44 UTC ℹ️ Start processing command /merge


2025-12-01 20:12:48 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in develop is approximately 1h (p90).


2025-12-01 20:29:08 UTCMergeQueue: The checks failed on this merge request

Tests failed on this commit 1909853:

What to do next?

  • Investigate the failures and when ready, re-add your pull request to the queue!
  • If your PR checks are green, try to rebase/merge. It might be because the CI run is a bit old.
  • Any question, go check the FAQ.

typotter added a commit that referenced this pull request Dec 1, 2025
Implements state observation API for FlagsClient:
- Add FlagsClientState sealed class (NotReady, Reconciling, Ready, Stale, Error)
- Add FlagsStateListener interface for state change callbacks
- Add StateObservable interface bundling getCurrentState, addListener, removeListener, and Flow
- Implement FlagsStateManager with ExecutorService for ordered state notifications
- Add synchronized blocks for defensive thread safety
- Expose state via client.state property
- Add StateFlow support for Kotlin coroutines
- Ensure Java compatibility
- Update NoOpFlagsClient with Ready state
- Add comprehensive tests

PR #3025
@typotter typotter merged commit bd9768e into develop Dec 1, 2025
26 of 29 checks passed
@typotter typotter deleted the typo/FFL-1442-android-sdk-of-wrapper-implement-observe-method branch December 1, 2025 23:15
typotter added a commit that referenced this pull request Dec 8, 2025
Implements state observation API for FlagsClient:
- Add FlagsClientState sealed class (NotReady, Reconciling, Ready, Stale, Error)
- Add FlagsStateListener interface for state change callbacks
- Add StateObservable interface bundling getCurrentState, addListener, removeListener, and Flow
- Implement FlagsStateManager with ExecutorService for ordered state notifications
- Add synchronized blocks for defensive thread safety
- Expose state via client.state property
- Add StateFlow support for Kotlin coroutines
- Ensure Java compatibility
- Update NoOpFlagsClient with Ready state
- Add comprehensive tests

PR #3025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants