From cb8ef1b164a421c9aefff9e22008a95ff67642f4 Mon Sep 17 00:00:00 2001 From: Pavel Date: Tue, 11 Aug 2026 16:06:36 +0300 Subject: [PATCH 1/2] fix(openfeature): use 2s agentless request timeout and degrade on bad numeric config --- .../internal/openfeature/_agentless_source.py | 4 +- ddtrace/internal/settings/openfeature.py | 41 ++++++++++++++++++- docs/configuration.rst | 2 +- ...uest-timeout-default-3f1c8a24b6d90e75.yaml | 13 ++++++ supported-configurations.json | 2 +- tests/openfeature/test_source_selection.py | 41 +++++++++++++++++++ 6 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/openfeature-agentless-request-timeout-default-3f1c8a24b6d90e75.yaml diff --git a/ddtrace/internal/openfeature/_agentless_source.py b/ddtrace/internal/openfeature/_agentless_source.py index b680a6a95d4..ac39bce1b0c 100644 --- a/ddtrace/internal/openfeature/_agentless_source.py +++ b/ddtrace/internal/openfeature/_agentless_source.py @@ -39,7 +39,9 @@ # Polling / retry policy (mirrors the dd-trace-js reference implementation). MAX_POLL_INTERVAL_SECONDS = 60 * 60 DEFAULT_POLL_INTERVAL_SECONDS = 30.0 -DEFAULT_REQUEST_TIMEOUT_SECONDS = 5.0 +# 2s per the Feature Flagging RFC. dd-trace-js declares 5s, which is where an earlier +# revision of this file took it from; the RFC and dd-trace-java are the contract. +DEFAULT_REQUEST_TIMEOUT_SECONDS = 2.0 MAX_ATTEMPTS = 3 FIRST_RETRY_MIN_S = 2.0 diff --git a/ddtrace/internal/settings/openfeature.py b/ddtrace/internal/settings/openfeature.py index 78a653263f5..3d6f2bfe786 100644 --- a/ddtrace/internal/settings/openfeature.py +++ b/ddtrace/internal/settings/openfeature.py @@ -2,11 +2,43 @@ OpenFeature configuration settings. """ +from typing import Callable from typing import Optional +from ddtrace.internal.logger import get_logger from ddtrace.internal.settings._core import DDConfig +log = get_logger(__name__) + + +# AIDEV-NOTE: numeric settings here parse leniently on purpose. This class is instantiated at +# module scope (see the bottom of this file), so letting envier raise on an unparsable value +# turns it into an ImportError for ddtrace.openfeature and takes the whole application down at +# startup rather than degrading one setting. dd-trace-java substitutes the default in the same +# situation; match that. +def _lenient_int(env_name: str, default: int) -> Callable[[str], int]: + def parse(raw: str) -> int: + try: + return int(raw) + except ValueError: + log.warning("Invalid value for %s: %r is not an integer; using the default", env_name, raw) + return default + + return parse + + +def _lenient_float(env_name: str, default: float) -> Callable[[str], float]: + def parse(raw: str) -> float: + try: + return float(raw) + except ValueError: + log.warning("Invalid value for %s: %r is not a number; using the default", env_name, raw) + return default + + return parse + + class OpenFeatureConfig(DDConfig): """ Configuration for OpenFeature provider and exposure reporting. @@ -48,6 +80,7 @@ class OpenFeatureConfig(DDConfig): float, "DD_FFE_INTAKE_HEARTBEAT_INTERVAL", default=1.0, + parser=_lenient_float("DD_FFE_INTAKE_HEARTBEAT_INTERVAL", 1.0), ) # Provider initialization timeout in milliseconds. Controls how long initialize() @@ -61,6 +94,7 @@ class OpenFeatureConfig(DDConfig): int, "DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS", default=10000, + parser=_lenient_int("DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS", 10000), ) # Stable Feature Flagging kill switch. When False, the provider is disabled @@ -96,13 +130,16 @@ class OpenFeatureConfig(DDConfig): int, "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS", default=30, + parser=_lenient_int("DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS", 30), ) - # Agentless UFC per-request timeout in seconds. + # Agentless UFC per-request timeout in seconds. 2s per the Feature Flagging RFC, which + # dd-trace-java also implements. configuration_source_agentless_request_timeout_seconds = DDConfig.var( int, "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS", - default=5, + default=2, + parser=_lenient_int("DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS", 2), ) _openfeature_config_keys = [ diff --git a/docs/configuration.rst b/docs/configuration.rst index 16b5e0593ca..da373edf011 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -1252,7 +1252,7 @@ Feature Flagging DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: type: Integer - default: 5 + default: 2 description: | The per-request timeout in seconds for agentless Universal Flag Configuration polls. diff --git a/releasenotes/notes/openfeature-agentless-request-timeout-default-3f1c8a24b6d90e75.yaml b/releasenotes/notes/openfeature-agentless-request-timeout-default-3f1c8a24b6d90e75.yaml new file mode 100644 index 00000000000..672d9d2d124 --- /dev/null +++ b/releasenotes/notes/openfeature-agentless-request-timeout-default-3f1c8a24b6d90e75.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + openfeature: This fix resolves an issue where the default agentless Universal Flag + Configuration request timeout was 5 seconds instead of the specified 2 seconds. Polls that + take longer than 2 seconds now time out and are retried. Set + ``DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS`` to restore a + longer timeout. + - | + openfeature: This fix resolves an issue where an invalid value for a numeric Feature + Flagging environment variable raised an exception while importing ``ddtrace.openfeature``, + preventing the application from starting. Such values are now logged and the documented + default is used instead. diff --git a/supported-configurations.json b/supported-configurations.json index a83e55a36a8..a28da103666 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -1730,7 +1730,7 @@ { "implementation": "A", "type": "int", - "default": "5" + "default": "2" } ], "DD_FEATURE_FLAGS_ENABLED": [ diff --git a/tests/openfeature/test_source_selection.py b/tests/openfeature/test_source_selection.py index dd743b2efae..fbe64042e7e 100644 --- a/tests/openfeature/test_source_selection.py +++ b/tests/openfeature/test_source_selection.py @@ -130,3 +130,44 @@ def test_create_invalid_endpoint_returns_none(bad_url): cfg = _config(DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL=bad_url) with override_global_config({"_dd_api_key": "secret"}): assert create_agentless_source(cfg, lambda _: None) is None + + +# --------------------------------------------------------------------------- +# Numeric settings degrade instead of breaking the import +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("env_name", "attribute", "expected"), + [ + ( + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS", + "configuration_source_agentless_poll_interval_seconds", + 30, + ), + ( + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS", + "configuration_source_agentless_request_timeout_seconds", + 2, + ), + ( + "DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS", + "initialization_timeout_ms", + 10000, + ), + ], +) +def test_unparsable_integer_setting_falls_back_to_default(monkeypatch, env_name, attribute, expected): + # OpenFeatureConfig is built at module scope, so raising here would surface as an + # ImportError for ddtrace.openfeature and take the application down at startup. + monkeypatch.setenv(env_name, "0.2") + assert getattr(OpenFeatureConfig(), attribute) == expected + + +def test_unparsable_float_setting_falls_back_to_default(monkeypatch): + monkeypatch.setenv("DD_FFE_INTAKE_HEARTBEAT_INTERVAL", "not-a-number") + assert OpenFeatureConfig().ffe_intake_heartbeat_interval == 1.0 + + +def test_request_timeout_default_matches_the_rfc(): + assert _config().configuration_source_agentless_request_timeout_seconds == 2 From 5ebbc5492b7c1ab1365886d8dfb82e2c8fda057e Mon Sep 17 00:00:00 2001 From: Pavel Date: Tue, 11 Aug 2026 18:06:40 +0300 Subject: [PATCH 2/2] fix(openfeature): degrade instead of raising on invalid numeric config --- ddtrace/internal/openfeature/_agentless_source.py | 4 +--- ddtrace/internal/settings/openfeature.py | 7 +++---- docs/configuration.rst | 2 +- ...ss-request-timeout-default-3f1c8a24b6d90e75.yaml | 13 ------------- ...ure-invalid-numeric-config-3f1c8a24b6d90e75.yaml | 7 +++++++ supported-configurations.json | 2 +- tests/openfeature/test_source_selection.py | 6 +----- 7 files changed, 14 insertions(+), 27 deletions(-) delete mode 100644 releasenotes/notes/openfeature-agentless-request-timeout-default-3f1c8a24b6d90e75.yaml create mode 100644 releasenotes/notes/openfeature-invalid-numeric-config-3f1c8a24b6d90e75.yaml diff --git a/ddtrace/internal/openfeature/_agentless_source.py b/ddtrace/internal/openfeature/_agentless_source.py index ac39bce1b0c..b680a6a95d4 100644 --- a/ddtrace/internal/openfeature/_agentless_source.py +++ b/ddtrace/internal/openfeature/_agentless_source.py @@ -39,9 +39,7 @@ # Polling / retry policy (mirrors the dd-trace-js reference implementation). MAX_POLL_INTERVAL_SECONDS = 60 * 60 DEFAULT_POLL_INTERVAL_SECONDS = 30.0 -# 2s per the Feature Flagging RFC. dd-trace-js declares 5s, which is where an earlier -# revision of this file took it from; the RFC and dd-trace-java are the contract. -DEFAULT_REQUEST_TIMEOUT_SECONDS = 2.0 +DEFAULT_REQUEST_TIMEOUT_SECONDS = 5.0 MAX_ATTEMPTS = 3 FIRST_RETRY_MIN_S = 2.0 diff --git a/ddtrace/internal/settings/openfeature.py b/ddtrace/internal/settings/openfeature.py index 3d6f2bfe786..2037b7ca785 100644 --- a/ddtrace/internal/settings/openfeature.py +++ b/ddtrace/internal/settings/openfeature.py @@ -133,13 +133,12 @@ class OpenFeatureConfig(DDConfig): parser=_lenient_int("DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS", 30), ) - # Agentless UFC per-request timeout in seconds. 2s per the Feature Flagging RFC, which - # dd-trace-java also implements. + # Agentless UFC per-request timeout in seconds. configuration_source_agentless_request_timeout_seconds = DDConfig.var( int, "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS", - default=2, - parser=_lenient_int("DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS", 2), + default=5, + parser=_lenient_int("DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS", 5), ) _openfeature_config_keys = [ diff --git a/docs/configuration.rst b/docs/configuration.rst index 76264027e89..d3c4afa1b55 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -1260,7 +1260,7 @@ Feature Flagging DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: type: Integer - default: 2 + default: 5 description: | The per-request timeout in seconds for agentless Universal Flag Configuration polls. diff --git a/releasenotes/notes/openfeature-agentless-request-timeout-default-3f1c8a24b6d90e75.yaml b/releasenotes/notes/openfeature-agentless-request-timeout-default-3f1c8a24b6d90e75.yaml deleted file mode 100644 index 672d9d2d124..00000000000 --- a/releasenotes/notes/openfeature-agentless-request-timeout-default-3f1c8a24b6d90e75.yaml +++ /dev/null @@ -1,13 +0,0 @@ ---- -fixes: - - | - openfeature: This fix resolves an issue where the default agentless Universal Flag - Configuration request timeout was 5 seconds instead of the specified 2 seconds. Polls that - take longer than 2 seconds now time out and are retried. Set - ``DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS`` to restore a - longer timeout. - - | - openfeature: This fix resolves an issue where an invalid value for a numeric Feature - Flagging environment variable raised an exception while importing ``ddtrace.openfeature``, - preventing the application from starting. Such values are now logged and the documented - default is used instead. diff --git a/releasenotes/notes/openfeature-invalid-numeric-config-3f1c8a24b6d90e75.yaml b/releasenotes/notes/openfeature-invalid-numeric-config-3f1c8a24b6d90e75.yaml new file mode 100644 index 00000000000..13913827e35 --- /dev/null +++ b/releasenotes/notes/openfeature-invalid-numeric-config-3f1c8a24b6d90e75.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + openfeature: This fix resolves an issue where an invalid value for a numeric Feature + Flagging environment variable raised an exception while importing ``ddtrace.openfeature``, + preventing the application from starting. Such values are now logged and the documented + default is used instead. diff --git a/supported-configurations.json b/supported-configurations.json index 0920887d5b8..9af7a007192 100644 --- a/supported-configurations.json +++ b/supported-configurations.json @@ -1730,7 +1730,7 @@ { "implementation": "A", "type": "int", - "default": "2" + "default": "5" } ], "DD_FEATURE_FLAGS_ENABLED": [ diff --git a/tests/openfeature/test_source_selection.py b/tests/openfeature/test_source_selection.py index fbe64042e7e..04a07b34a10 100644 --- a/tests/openfeature/test_source_selection.py +++ b/tests/openfeature/test_source_selection.py @@ -148,7 +148,7 @@ def test_create_invalid_endpoint_returns_none(bad_url): ( "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS", "configuration_source_agentless_request_timeout_seconds", - 2, + 5, ), ( "DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS", @@ -167,7 +167,3 @@ def test_unparsable_integer_setting_falls_back_to_default(monkeypatch, env_name, def test_unparsable_float_setting_falls_back_to_default(monkeypatch): monkeypatch.setenv("DD_FFE_INTAKE_HEARTBEAT_INTERVAL", "not-a-number") assert OpenFeatureConfig().ffe_intake_heartbeat_interval == 1.0 - - -def test_request_timeout_default_matches_the_rfc(): - assert _config().configuration_source_agentless_request_timeout_seconds == 2