Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 47 additions & 15 deletions ddtrace/internal/openfeature/_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from collections import OrderedDict
from collections.abc import MutableMapping
from importlib.metadata import version
import threading
import typing

from openfeature.evaluation_context import EvaluationContext
Expand Down Expand Up @@ -87,12 +88,21 @@ class DataDogProvider(AbstractProvider):
Feature Flags and Experimentation (FFE) product.
"""

def __init__(self, *args: typing.Any, **kwargs: typing.Any):
def __init__(self, *args: typing.Any, init_timeout: typing.Optional[float] = None, **kwargs: typing.Any):
Comment thread
leoromanovsky marked this conversation as resolved.
Outdated
super().__init__(*args, **kwargs)
self._metadata = Metadata(name="Datadog")
self._status = ProviderStatus.NOT_READY
self._config_received = False

# Init timeout: constructor arg takes priority, then env var (default 30s)
if init_timeout is not None:
self._init_timeout = init_timeout
else:
self._init_timeout = ffe_config.initialization_timeout_ms / 1000.0

# Event used to block initialize() until config arrives
self._config_event = threading.Event()
Comment thread
leoromanovsky marked this conversation as resolved.
Outdated

# Cache for reported exposures to prevent duplicates
# Stores mapping of (flag_key, subject_id) -> (allocation_key, variant_key)
# Using LRU cache with maxsize of 65536 to prevent unbounded memory growth
Expand All @@ -119,16 +129,17 @@ def initialize(self, evaluation_context: EvaluationContext) -> None:
"""
Initialize the provider.

Called by the OpenFeature SDK when the provider is set.
Provider Creation → NOT_READY
First Remote Config Payload
READY (emits PROVIDER_READY event)
Shutdown
NOT_READY
Blocks until Remote Config delivers the first FFE configuration or
the initialization timeout expires. This matches the behavior of the
Java (CountDownLatch), Go (sync.Cond), and Node.js (Promise) providers.
Comment thread
leoromanovsky marked this conversation as resolved.
Outdated
Comment thread
leoromanovsky marked this conversation as resolved.
Outdated

The timeout is configurable via:
- Constructor: DataDogProvider(init_timeout=10.0) # seconds
- Env var: DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS=10000

Provider lifecycle:
NOT_READY → initialize() blocks → config arrives → READY
NOT_READY → initialize() blocks → timeout → raises ProviderNotReadyError
"""
if not self._enabled:
return
Expand All @@ -139,12 +150,27 @@ def initialize(self, evaluation_context: EvaluationContext) -> None:
except ServiceStatusError:
logger.debug("Exposure writer is already running", exc_info=True)

# If configuration was already received before initialization, emit ready now
# Fast path: config already available (RC delivered before set_provider)
config = _get_ffe_config()
if config is not None and not self._config_received:
if config is not None:
self._config_received = True
self._status = ProviderStatus.READY
self._emit_ready_event()
return # SDK will dispatch PROVIDER_READY
Comment thread
dd-oleksii marked this conversation as resolved.

# Block until config arrives or timeout expires
logger.debug("Waiting up to %.1fs for initial FFE configuration from Remote Config", self._init_timeout)
Comment thread
leoromanovsky marked this conversation as resolved.
Outdated
if not self._config_event.wait(timeout=self._init_timeout):
Comment thread
dd-oleksii marked this conversation as resolved.
Outdated
# Timeout expired without receiving config
from openfeature.exception import ProviderNotReadyError

raise ProviderNotReadyError(
f"Provider timed out after {self._init_timeout:.1f}s waiting for "
"initial configuration from Remote Config"
)
Comment thread
dd-oleksii marked this conversation as resolved.

# Config received during wait
self._config_received = True
self._status = ProviderStatus.READY
Comment thread
leoromanovsky marked this conversation as resolved.
Outdated
Comment thread
leoromanovsky marked this conversation as resolved.
Outdated

def shutdown(self) -> None:
"""
Expand All @@ -168,6 +194,7 @@ def shutdown(self) -> None:
_unregister_provider(self)
Comment thread
dd-oleksii marked this conversation as resolved.
self._status = ProviderStatus.NOT_READY
self._config_received = False
self._config_event.clear()

def resolve_boolean_details(
self,
Expand Down Expand Up @@ -423,12 +450,17 @@ def on_configuration_received(self) -> None:
"""
Called when a Remote Configuration payload is received and processed.

Emits PROVIDER_READY event on first configuration.
Unblocks initialize() if it's waiting, and emits PROVIDER_READY for
late arrivals (config received after initialize() timed out).
"""
# Always signal the event to unblock initialize() if it's waiting
self._config_event.set()
Comment thread
dd-oleksii marked this conversation as resolved.
Outdated

if not self._config_received:
self._config_received = True
self._status = ProviderStatus.READY
logger.debug("First FFE configuration received, provider is now READY")
# Emit READY for late recovery: config arrived after init timed out
self._emit_ready_event()
Comment thread
leoromanovsky marked this conversation as resolved.
Outdated

def _emit_ready_event(self) -> None:
Expand Down
10 changes: 10 additions & 0 deletions ddtrace/internal/settings/openfeature.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,20 @@ class OpenFeatureConfig(DDConfig):
default=1.0,
)

# Provider initialization timeout in milliseconds.
# Controls how long initialize() blocks waiting for the first Remote Config payload.
# Default is 30000ms (30 seconds), matching Java, Go, and Node.js SDKs.
initialization_timeout_ms = DDConfig.var(
int,
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS",
Comment thread
dd-oleksii marked this conversation as resolved.
default=30000,
)

_openfeature_config_keys = [
"experimental_flagging_provider_enabled",
"ffe_intake_enabled",
"ffe_intake_heartbeat_interval",
"initialization_timeout_ms",
]


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
fixes:
Comment thread
brettlangdon marked this conversation as resolved.
- |
openfeature: This fix resolves an issue where ``DataDogProvider.initialize()`` returned immediately
Comment thread
dd-oleksii marked this conversation as resolved.
Outdated
without waiting for Remote Configuration data, causing the OpenFeature SDK to emit ``PROVIDER_READY``
before flag configuration was available. Flag evaluations in this window silently returned default
values. The provider now blocks in ``initialize()`` until the first configuration arrives or a
configurable timeout expires (default 30s), matching the behavior of the Java, Go, and Node.js
providers. The timeout is configurable via the ``DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS``
environment variable or the ``init_timeout`` constructor parameter.
Loading