Skip to content

feat(openfeature): add agentless feature flag configuration source - #19331

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 30 commits into
mainfrom
pavlo.khrebto/FFL-2699/agentless-ff-configs
Aug 7, 2026
Merged

feat(openfeature): add agentless feature flag configuration source#19331
gh-worker-dd-mergequeue-cf854d[bot] merged 30 commits into
mainfrom
pavlo.khrebto/FFL-2699/agentless-ff-configs

Conversation

@pavlokhrebto

@pavlokhrebto pavlokhrebto commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Description

Adds agentless delivery for the Feature Flagging & Experimentation (FFE) OpenFeature
provider: Universal Flag Configuration (UFC) is loaded directly from Datadog over HTTPS
with no Datadog Agent. Agentless becomes the default source; the existing Agent Remote
Configuration path remains available as explicit opt-in. Ports dd-trace-js #9397 and the
follow-ups #9481 (custom endpoints) / #9482 (grandfathering, fail-closed), adapted to
Python provider-lifecycle conventions.

Key pieces:

  • Config: DD_FEATURE_FLAGS_ENABLED (stable kill switch, default true),
    DD_FEATURE_FLAGS_CONFIGURATION_SOURCE (agentless default | remote_config; offline
    reserved), and agentless tunables ..._AGENTLESS_BASE_URL,
    ..._AGENTLESS_POLL_INTERVAL_SECONDS (30, capped at 1h),
    ..._AGENTLESS_REQUEST_TIMEOUT_SECONDS (5).
  • Source resolution: kill switch → explicit source → fail-closed for unsupported/offline
    → legacy grandfathering (DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED) → default agentless.
  • AgentlessConfigurationSource (PeriodicService): fixed-delay polling, in-tick
    retry/backoff (408/429/5xx/network, jittered), per-request timeout, gzip, strict JSON:API
    validation, ETag/304, last-known-good preserved on every failure, self-tracing suppressed.
  • Activation mirrors dd-trace-js: the agentless poller starts from the provider lifecycle
    (only when the OpenFeature provider is registered), so there is no default-on CDN polling.
    RC is subscribed only when remote_config is the resolved source. Both sources feed the
    same apply path.
  • Pre-fork: the poller restarts per worker with a jittered first poll so workers don't
    hit the CDN in lockstep.

JIRA: FFL-2699

Testing

  • Unit tests: endpoint derivation (incl. staging/GovCloud, dd_env), JSON:API accept/reject,
    gzip, poller behavior (200/304/401/malformed/retry/last-known-good/ETag/headers/no-trace),
    source-resolution matrix (kill switch, grandfathering, fail-closed), agentless factory
    (API-key handling), fork jitter.
  • End-to-end: JSON:API CDN response → parse → apply → OpenFeature evaluation; provider
    initialize()/shutdown() start/stop the poller; kill switch starts no source.
  • Full tests/openfeature suite passes (613). style/typing/spelling/registry/suitespec pass.

System tests

Ran the shared configuration-source contract against this branch, with the suite activated for
Python via DataDog/system-tests#7411 and binaries/python-load-from-local pointing at it:

PYTEST_XDIST_AUTO_NUM_WORKERS=1 TEST_LIBRARY=python ./run.sh PARAMETRIC \
  tests/parametric/test_ffe/test_configuration_sources.py
29 passed in 130.99s (0:02:10)

Covers default lazy agentless delivery, explicit and grandfathered Remote Configuration, the kill
switch, invalid and reserved-offline fail-closed behavior, custom-endpoint authentication,
recovery and ETag handling, timeout/retry, and non-overlapping polling.

No regressions in the rest of the FFE parametric directory: 30 passed, 12 xfailed, 29 xpassed
(test_dynamic_evaluation.py 24 passed, test_span_enrichment.py 6 passed + 12 pre-existing
xfails).

The suite is run sequentially; with parallel workers two cases intermittently error with test-agent
port-routing noise unrelated to the library.

Risks

  • DD_FEATURE_FLAGS_ENABLED defaults true, but the agentless poller only activates when
    the OpenFeature provider is registered — users who don't use FFE are unaffected and there is
    no default-on CDN traffic.
  • Evaluation, hooks and telemetry writers are all gated on the resolved source, so the legacy
    DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED now only feeds source grandfathering. Existing users
    who set it keep Remote Configuration delivery and reporting; agentless users get reporting too.
  • Under pre-fork servers each worker polls independently; a shared-memory optimization is a
    planned follow-up.

Additional Notes

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codeowners resolved as

ddtrace/internal/openfeature/_agentless.py                              @DataDog/feature-flagging-and-experimentation-sdk
ddtrace/internal/openfeature/_agentless_source.py                       @DataDog/feature-flagging-and-experimentation-sdk
ddtrace/internal/openfeature/_native.py                                 @DataDog/feature-flagging-and-experimentation-sdk
ddtrace/internal/openfeature/_provider.py                               @DataDog/feature-flagging-and-experimentation-sdk
ddtrace/internal/openfeature/_source_selection.py                       @DataDog/feature-flagging-and-experimentation-sdk
ddtrace/internal/openfeature/product.py                                 @DataDog/feature-flagging-and-experimentation-sdk
ddtrace/internal/settings/_supported_configurations.py                  @DataDog/apm-sdk-capabilities-python @DataDog/apm-python
ddtrace/internal/settings/openfeature.py                                @DataDog/feature-flagging-and-experimentation-sdk
docs/configuration.rst                                                  @DataDog/python-guild
releasenotes/notes/agentless-feature-flag-configuration-source-6b2f9ae3c47d105e.yaml  @DataDog/apm-python
releasenotes/notes/fix-openfeature-init-blocking-70c8d5a99287cc49.yaml  @DataDog/apm-python
supported-configurations.json                                           @DataDog/apm-sdk-capabilities-python @DataDog/apm-python
tests/openfeature/conftest.py                                           @DataDog/feature-flagging-and-experimentation-sdk
tests/openfeature/test_agentless_poller.py                              @DataDog/feature-flagging-and-experimentation-sdk
tests/openfeature/test_agentless_provider_e2e.py                        @DataDog/feature-flagging-and-experimentation-sdk
tests/openfeature/test_agentless_source.py                              @DataDog/feature-flagging-and-experimentation-sdk
tests/openfeature/test_flag_eval_metrics.py                             @DataDog/feature-flagging-and-experimentation-sdk
tests/openfeature/test_provider_env_var.py                              @DataDog/feature-flagging-and-experimentation-sdk
tests/openfeature/test_provider_status.py                               @DataDog/feature-flagging-and-experimentation-sdk
tests/openfeature/test_source_selection.py                              @DataDog/feature-flagging-and-experimentation-sdk
tests/suitespec.yml                                                     @DataDog/python-guild @DataDog/apm-core-python

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Jul 28, 2026

Copy link
Copy Markdown

Circular import analysis

⚠️ Existing circular imports

There are 17 circular imports that already exist on the base branch and have not been changed by this PR.

Show existing cycles (showing 5 of 17 shortest)
ddtrace.contrib.internal.pytorch._distributed -> ddtrace.contrib.internal.pytorch._rank_root -> ddtrace.contrib.internal.pytorch._distributed
ddtrace.internal.ci_visibility.api._base -> ddtrace.internal.ci_visibility.api._session -> ddtrace.internal.ci_visibility.api._base
ddtrace.contrib.internal.asgi.middleware -> ddtrace.contrib.internal.starlette.patch -> ddtrace.contrib.internal.asgi.middleware
ddtrace.contrib.internal.pytest._plugin_v2 -> ddtrace.contrib.internal.pytest.plugin -> ddtrace.contrib.internal.pytest._plugin_v2
ddtrace.internal.coverage.installer -> ddtrace.internal.coverage.multiprocessing_coverage -> ddtrace.internal.coverage.installer

To see all cycles, download the cycles-base.json and cycles-pr.json artifacts from this CI job and run:

uv run --script scripts/import-analysis/cycles.py compare cycles-base.json cycles-pr.json

@datadog-prod-us1-5

datadog-prod-us1-5 Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog auto-retried 1 job - 1 passed on retry View in Datadog

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 774fa3a | Docs | Datadog PR Page | Give us feedback!

@pavlokhrebto pavlokhrebto changed the title feat(openfeature): add agentless feature flag configuration options feat(openfeature): add agentless feature flag configuration source Jul 28, 2026
Comment thread tests/openfeature/test_agentless_source.py Fixed
@pr-commenter

pr-commenter Bot commented Jul 28, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-08-07 09:45:51

Comparing candidate commit 774fa3a in PR branch pavlo.khrebto/FFL-2699/agentless-ff-configs with baseline commit d800e66 in branch main.

Found 0 performance improvements and 4 performance regressions! Performance is the same for 606 metrics, 10 unstable metrics.

scenario:iastaspects-swapcase_aspect

  • 🟥 execution_time [+44.048µs; +48.149µs] or [+12.965%; +14.173%]

scenario:iastaspectsospath-ospathbasename_aspect

  • 🟥 execution_time [+71.224µs; +77.859µs] or [+16.906%; +18.480%]

scenario:span-start

  • 🟥 execution_time [+1.378ms; +1.528ms] or [+8.707%; +9.658%]

scenario:tracer-small

  • 🟥 execution_time [+31.736µs; +34.329µs] or [+9.473%; +10.247%]

@pavlokhrebto
pavlokhrebto marked this pull request as ready for review July 28, 2026 13:50
@pavlokhrebto
pavlokhrebto requested review from a team as code owners July 28, 2026 13:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 128172f5a9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ddtrace/internal/openfeature/_agentless_source.py
Comment thread ddtrace/internal/openfeature/_provider.py Outdated
Comment thread ddtrace/internal/openfeature/_provider.py
gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Aug 3, 2026
…19425)

## Description

Adds an optional `sleep_func` parameter to `ddtrace/internal/utils/retry.py`, used for the initial wait and for each backoff between attempts. It defaults to `time.sleep`, so every existing caller behaves identically — the change is purely additive.

The motivation is background-thread callers: `time.sleep` cannot be interrupted, so a service shutting down mid-backoff has to wait out the remaining delay. Passing an interruptible wait such as `threading.Event.wait` lets the caller abandon the backoff immediately.

Split out of #19331 at review request. The Feature Flagging agentless poller is the first consumer and uses it to make shutdown prompt instead of waiting out up to ~30s of backoff.

## Testing

Four tests added to `tests/internal/test_utils_retry.py`:

- `sleep_func` receives the initial wait and every backoff delay, in order.
- No backoff wait happens when the first attempt is accepted.
- The default remains bound to `time.sleep` for callers that do not pass it.
- An event-style wait can stop the remaining attempts (the interruptible-shutdown case).

`tests/internal/test_utils_retry.py` passes in full (9 tests).

## Risks

None expected. The parameter is optional and defaults to the previous behavior, and no existing caller passes it.

## Additional Notes

No release note: this is an internal utility with no customer-visible behavior change, so `changelog/no-changelog` is applied.


Co-authored-by: pavlo.khrebto <pavlo.khrebto@datadoghq.com>
Base automatically changed from pavlo.khrebto/adjust-retry-logic to main August 3, 2026 10:52
brettlangdon pushed a commit that referenced this pull request Aug 3, 2026
…19425)

## Description

Adds an optional `sleep_func` parameter to `ddtrace/internal/utils/retry.py`, used for the initial wait and for each backoff between attempts. It defaults to `time.sleep`, so every existing caller behaves identically — the change is purely additive.

The motivation is background-thread callers: `time.sleep` cannot be interrupted, so a service shutting down mid-backoff has to wait out the remaining delay. Passing an interruptible wait such as `threading.Event.wait` lets the caller abandon the backoff immediately.

Split out of #19331 at review request. The Feature Flagging agentless poller is the first consumer and uses it to make shutdown prompt instead of waiting out up to ~30s of backoff.

## Testing

Four tests added to `tests/internal/test_utils_retry.py`:

- `sleep_func` receives the initial wait and every backoff delay, in order.
- No backoff wait happens when the first attempt is accepted.
- The default remains bound to `time.sleep` for callers that do not pass it.
- An event-style wait can stop the remaining attempts (the interruptible-shutdown case).

`tests/internal/test_utils_retry.py` passes in full (9 tests).

## Risks

None expected. The parameter is optional and defaults to the previous behavior, and no existing caller passes it.

## Additional Notes

No release note: this is an internal utility with no customer-visible behavior change, so `changelog/no-changelog` is applied.


Co-authored-by: pavlo.khrebto <pavlo.khrebto@datadoghq.com>
@brettlangdon

Copy link
Copy Markdown
Member

@pavlokhrebto can you fix failing linting checks?

@pavlokhrebto

Copy link
Copy Markdown
Contributor Author

@pavlokhrebto can you fix failing linting checks?

@brettlangdon sure, done

@brettlangdon brettlangdon left a comment

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.

one nit on the docs, otherwise config, docs, and release note lgtm

Comment thread docs/configuration.rst Outdated

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

Two things to review or augment please.

Blocker: provider readiness is not canonical.

Python returns from initialize() before the first UFC configuration arrives. Its own test confirms that the SDK can emit PROVIDER_READY while the provider remains NOT_READY. The first evaluation can therefore return the default with PROVIDER_NOT_READY.

Node.js waits for configuration through setProviderAndWait(). Java completes an initial poll and waits for configuration during provider initialization.

 References: Python provider
 (https://github.com/DataDog/dd-trace-py/blob/b5cb1213cf2f8fa18294d3ecf7d56444885606c0/ddtrace/internal/openfeature/_provider.py#L222-L286), Python
 readiness test (https://github.com/DataDog/dd-trace-py/blob/b5cb1213cf2f8fa18294d3ecf7d56444885606c0/tests/openfeature/test_provider_status.py#L143-L165),
 Node.js contract
 (https://github.com/DataDog/dd-trace-js/blob/d678882c997c1c0f5b7f6f779cff9995bc84f806/integration-tests/openfeature/app/configuration-source-evaluation.js#L34-L41),
 Java provider
 (https://github.com/DataDog/dd-trace-java/blob/f54355526dabf515d0c8884f74a6e01ced61c1ff/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java#L109-L133).

Should fix: shutdown does not cancel an active HTTP request.

 [Python ](https://github.com/DataDog/dd-trace-py/blob/b5cb1213cf2f8fa18294d3ecf7d56444885606c0/tests/openfeature/test_provider_status.py#L143-L165)interrupts retry waits, but shutdown() joins the poller without cancelling the current connection. Shutdown can block until the configured request
 timeout. Node.js aborts the request. Java cancels the active call.

Blocker: final L3 validation is stale.

The documented 29 passed result is useful, but it predates the current head and later retry changes. The current head is also 19 commits behind Python main. Latest main includes a periodic-thread crash fix (#19445), which directly affects this new poller.

 Merge current main, then rerun all 29 tests with [system-tests #7411](https://github.com/DataDog/system-tests/pull/7411) (https://github.com/DataDog/system-tests/pull/7411) against the exact final head.

@pavlokhrebto

pavlokhrebto commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@leoromanovsky made the fixes:

  1. Provider readiness is not canonical — fixed

initialize() now blocks on the first configuration payload instead of returning immediately, so PROVIDER_READY implies the provider can actually resolve flags. This restores the contract Node gets from setProviderAndWait() and Java from its initial poll.

Two details worth calling out, because they're deliberate:

The wait does not raise on expiry. A previous revision (#18613 reverted it) waited and then raised ProviderNotReadyError, which surfaced as PROVIDER_ERROR — and, for the common top-level set_provider() call, as an exception at import time — for what is usually transient delivery slowness. Now, timing out just returns: status stays NOT_READY, on_configuration_received() promotes it whenever the payload lands, and evaluations until then return the caller-provided default with ErrorCode.PROVIDER_NOT_READY. So the failure mode is exactly today's behavior; only the success path is strengthened.

The default stays 10s rather than Java/Node's 30s. This is a Python-specific constraint, not drift. Gunicorn's default worker timeout is also 30s, so a 30s blocking initialize() at import time sits right on the boundary and gets the worker SIGKILLed with [CRITICAL] WORKER TIMEOUT — which is why #18169 moved Python off 30s in the first place. 10s is comfortably inside that budget and still ample for a healthy delivery path. DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS is the same env var Node uses, so anyone who wants parity can set it explicitly.

Side effects of the change: the initialization_timeout constructor argument was accepted and silently discarded, and the initialization_timeout_ms setting was dead — both are wired up now, with the constructor argument taking precedence. The release note for #18613 still claimed "the provider now waits for configuration before returning," which was false on main; it's accurate again and now documents the timeout behavior. New tests cover: config arriving mid-wait → READY; timeout → NOT_READY and no raise; late delivery still promotes; timeout read from the env var; constructor override.

If you'd rather standardize on a single cross-SDK number, I'm happy to — but I'd argue it should be 10s for everyone rather than 30s for Python, and that's a guild decision rather than something to settle in this PR.

  1. Shutdown does not cancel an active HTTP request — fixed

836b9d7. _stop_service now sets the stop flag first, then half-closes the socket of the poll in flight (socket.shutdown(SHUT_RDWR)) before joining. close() alone wouldn't do it — the worker is already parked inside a syscall on that descriptor — so the half-close is what makes the pending recv return immediately. The worker still owns cleanup: _request closes the connection in its finally block and reports the resulting error as a failed poll, which preserves last-known-good.

One window is genuinely uncancellable and documented in the code: http.client connects lazily inside request(), so a stop landing before the socket exists has nothing to half-close and the worker can block in connect() for up to the request timeout. Everything past connect — the long pole, since a poll spends its time reading the UFC body — cancels at once. Claiming the connection slot and re-reading the stop flag happen under one lock, so a poll can't start after shutdown has already looked for something to cancel. Seven new tests cover the cancel path, the no-op paths, and the ordering guarantee.

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

Functional result:

  • Startup timeout now raises the SDK’s canonical ProviderNotReadyError.
  • OpenFeature 0.8 reports provider status ERROR.
  • OpenFeature 0.10 set_provider_and_wait() raises the error.
  • OpenFeature 0.10 set_provider() returns immediately. Background initialization then reports ERROR.
  • A later valid configuration changes the provider to READY.
  • The implementation emits one recovery event.

Validation:

  • OpenFeature 0.8 full suite: 625 passed, 2 skipped.
  • OpenFeature 0.10 focused suite: 18 passed.
  • L3 at system-tests commit ef1cb447c9: 29 passed in 168.20s.

@leoromanovsky

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Aug 7, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-08-07 01:36:20 UTC ℹ️ Start processing command /merge


2026-08-07 01:36:37 UTC ℹ️ MergeQueue: Pull request is not mergeable yet

It will be processed automatically as soon as GitHub reports it as mergeable. View in MergeQueue UI.

  • Run /code blockers to see what is blocking it.
  • Run /remove to cancel it.

2026-08-07 02:15:09 UTC ℹ️ MergeQueue: merge request added to the queue

The expected merge time in main is approximately 57m (p90).


2026-08-07 03:28:56 UTCMergeQueue: The checks failed on this merge request

Tests failed on this commit 23f1a14:

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.

@pavlokhrebto

Copy link
Copy Markdown
Contributor Author

/code blockers

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Aug 7, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-08-07 10:29:15 UTC ℹ️ Start processing command /code blockers


2026-08-07 10:29:16 UTC ℹ️ Devflow:

Checking merge blockers for #19331...


2026-08-07 10:29:36 UTC ℹ️ Devflow: /code blockers

No merge blockers detected.

@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit e48932d into main Aug 7, 2026
1293 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the pavlo.khrebto/FFL-2699/agentless-ff-configs branch August 7, 2026 10:35
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.

5 participants