From a5af90e2e82053d1ba3d076a9f1a51ee37eb1fa1 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Tue, 18 Feb 2025 18:30:16 +0100 Subject: [PATCH 1/2] feat(flagd): Context value hydration Signed-off-by: Simon Schrottner --- .gitmodules | 2 +- .../openfeature/test-harness | 2 +- .../contrib/provider/flagd/provider.py | 18 +++++++++++++++++- .../contrib/provider/flagd/resolvers/grpc.py | 2 +- .../provider/flagd/resolvers/in_process.py | 2 +- .../process/connector/file_watcher.py | 5 +++-- .../process/connector/grpc_watcher.py | 12 ++++++++++-- .../provider/flagd/sync_metadata_hook.py | 14 ++++++++++++++ .../tests/e2e/file/conftest.py | 1 + 9 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/sync_metadata_hook.py diff --git a/.gitmodules b/.gitmodules index c774b2cb..d063bd57 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,7 +5,7 @@ [submodule "providers/openfeature-provider-flagd/test-harness"] path = providers/openfeature-provider-flagd/openfeature/test-harness url = git@github.com:open-feature/flagd-testbed.git - branch = v2.2.0 + branch = v2.5.0 [submodule "providers/openfeature-provider-flagd/spec"] path = providers/openfeature-provider-flagd/openfeature/spec url = https://github.com/open-feature/spec diff --git a/providers/openfeature-provider-flagd/openfeature/test-harness b/providers/openfeature-provider-flagd/openfeature/test-harness index f5afee5a..9d35a07f 160000 --- a/providers/openfeature-provider-flagd/openfeature/test-harness +++ b/providers/openfeature-provider-flagd/openfeature/test-harness @@ -1 +1 @@ -Subproject commit f5afee5aa8e94bb9fc0becbb5928a5b4bd44729a +Subproject commit 9d35a07f43c6b5e1810a5e83029aae62a5dbd494 diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py index 83b03897..322661fa 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py @@ -25,12 +25,15 @@ import warnings from openfeature.evaluation_context import EvaluationContext +from openfeature.event import ProviderEventDetails from openfeature.flag_evaluation import FlagResolutionDetails +from openfeature.hook import Hook from openfeature.provider import AbstractProvider from openfeature.provider.metadata import Metadata from .config import CacheType, Config, ResolverType from .resolvers import AbstractResolver, GrpcResolver, InProcessResolver +from .sync_metadata_hook import SyncMetadataHook T = typing.TypeVar("T") @@ -96,8 +99,16 @@ def __init__( # noqa: PLR0913 max_cache_size=max_cache_size, cert_path=cert_path, ) + self.enriched_context: dict = {} self.resolver = self.setup_resolver() + self.hooks: list[Hook] = [SyncMetadataHook(self.get_enriched_context)] + + def get_enriched_context(self) -> EvaluationContext: + return EvaluationContext(attributes=self.enriched_context) + + def get_provider_hooks(self) -> list[Hook]: + return self.hooks def setup_resolver(self) -> AbstractResolver: if self.config.resolver == ResolverType.RPC: @@ -114,7 +125,7 @@ def setup_resolver(self) -> AbstractResolver: ): return InProcessResolver( self.config, - self.emit_provider_ready, + self.on_provider_ready, self.emit_provider_error, self.emit_provider_stale, self.emit_provider_configuration_changed, @@ -184,3 +195,8 @@ def resolve_object_details( return self.resolver.resolve_object_details( key, default_value, evaluation_context ) + + def on_provider_ready(self, details: ProviderEventDetails, metadata: dict) -> None: + self.enriched_context = metadata + self.emit_provider_ready(details) + pass diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py index 4eef2420..af29e5de 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py @@ -276,7 +276,7 @@ def _resolve( # noqa: PLR0915 C901 return cached_flag context = self._convert_context(evaluation_context) - call_args = {"timeout": self.deadline} + call_args = {"timeout": self.deadline, "wait_for_ready": True} try: request: Message if flag_type == FlagType.BOOLEAN: diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py index cacf37ec..a555896d 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/in_process.py @@ -21,7 +21,7 @@ class InProcessResolver: def __init__( self, config: Config, - emit_provider_ready: typing.Callable[[ProviderEventDetails], None], + emit_provider_ready: typing.Callable[[ProviderEventDetails, dict], None], emit_provider_error: typing.Callable[[ProviderEventDetails], None], emit_provider_stale: typing.Callable[[ProviderEventDetails], None], emit_provider_configuration_changed: typing.Callable[ diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/file_watcher.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/file_watcher.py index befc69ff..eaf2b6d3 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/file_watcher.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/file_watcher.py @@ -24,7 +24,7 @@ def __init__( self, config: Config, flag_store: FlagStore, - emit_provider_ready: typing.Callable[[ProviderEventDetails], None], + emit_provider_ready: typing.Callable[[ProviderEventDetails, dict], None], emit_provider_error: typing.Callable[[ProviderEventDetails], None], ): if config.offline_flag_source_path is None: @@ -94,7 +94,8 @@ def _load_data(self, modified_time: typing.Optional[float] = None) -> None: self.emit_provider_ready( ProviderEventDetails( message="Reloading file contents recovered from error state" - ) + ), + {}, ) self.should_emit_ready_on_success = False diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py index 138a5ddb..11776a1d 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py @@ -5,6 +5,7 @@ import typing import grpc +from google.protobuf.json_format import MessageToDict from openfeature.evaluation_context import EvaluationContext from openfeature.event import ProviderEventDetails @@ -26,7 +27,7 @@ def __init__( self, config: Config, flag_store: FlagStore, - emit_provider_ready: typing.Callable[[ProviderEventDetails], None], + emit_provider_ready: typing.Callable[[ProviderEventDetails, dict], None], emit_provider_error: typing.Callable[[ProviderEventDetails], None], emit_provider_stale: typing.Callable[[ProviderEventDetails], None], ): @@ -157,6 +158,12 @@ def listen(self) -> None: while self.active: try: + context_values_request = sync_pb2.GetMetadataRequest() + context_values_response: sync_pb2.GetMetadataResponse = ( + self.stub.GetMetadata(context_values_request, wait_for_ready=True) + ) + context_values = MessageToDict(context_values_response) + request = sync_pb2.SyncFlagsRequest(**request_args) logger.debug("Setting up gRPC sync flags connection") @@ -173,7 +180,8 @@ def listen(self) -> None: self.emit_provider_ready( ProviderEventDetails( message="gRPC sync connection established" - ) + ), + context_values["metadata"], ) self.connected = True diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/sync_metadata_hook.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/sync_metadata_hook.py new file mode 100644 index 00000000..cd873c40 --- /dev/null +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/sync_metadata_hook.py @@ -0,0 +1,14 @@ +import typing + +from openfeature.evaluation_context import EvaluationContext +from openfeature.hook import Hook, HookContext, HookHints + + +class SyncMetadataHook(Hook): + def __init__(self, context_supplier: typing.Callable[[], EvaluationContext]): + self.context_supplier = context_supplier + + def before( + self, hook_context: HookContext, hints: HookHints + ) -> typing.Optional[EvaluationContext]: + return self.context_supplier() diff --git a/providers/openfeature-provider-flagd/tests/e2e/file/conftest.py b/providers/openfeature-provider-flagd/tests/e2e/file/conftest.py index f687cb17..03f1332a 100644 --- a/providers/openfeature-provider-flagd/tests/e2e/file/conftest.py +++ b/providers/openfeature-provider-flagd/tests/e2e/file/conftest.py @@ -12,6 +12,7 @@ "~sync", "~caching", "~grace", + "~contextEnrichment", } From 1d46ae89f70a74f92e730f7defdd91f41c943d6d Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Tue, 4 Mar 2025 14:31:28 +0100 Subject: [PATCH 2/2] fixup: naming Signed-off-by: Simon Schrottner --- .../src/openfeature/contrib/provider/flagd/provider.py | 8 +++++--- .../openfeature/contrib/provider/flagd/resolvers/grpc.py | 5 ++++- .../flagd/resolvers/process/connector/grpc_watcher.py | 5 ++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py index 322661fa..ae3cf323 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py @@ -125,7 +125,7 @@ def setup_resolver(self) -> AbstractResolver: ): return InProcessResolver( self.config, - self.on_provider_ready, + self.emit_provider_ready_with_context, self.emit_provider_error, self.emit_provider_stale, self.emit_provider_configuration_changed, @@ -196,7 +196,9 @@ def resolve_object_details( key, default_value, evaluation_context ) - def on_provider_ready(self, details: ProviderEventDetails, metadata: dict) -> None: - self.enriched_context = metadata + def emit_provider_ready_with_context( + self, details: ProviderEventDetails, context: dict + ) -> None: + self.enriched_context = context self.emit_provider_ready(details) pass diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py index af29e5de..3d8ebe1f 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py @@ -137,7 +137,10 @@ def monitor(self) -> None: def _state_change_callback(self, new_state: ChannelConnectivity) -> None: logger.debug(f"gRPC state change: {new_state}") - if new_state == ChannelConnectivity.READY: + if ( + new_state == grpc.ChannelConnectivity.READY + or new_state == grpc.ChannelConnectivity.IDLE + ): if not self.thread or not self.thread.is_alive(): self.thread = threading.Thread( target=self.listen, diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py index 11776a1d..f5aeba22 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py @@ -107,7 +107,10 @@ def monitor(self) -> None: def _state_change_callback(self, new_state: grpc.ChannelConnectivity) -> None: logger.debug(f"gRPC state change: {new_state}") - if new_state == grpc.ChannelConnectivity.READY: + if ( + new_state == grpc.ChannelConnectivity.READY + or new_state == grpc.ChannelConnectivity.IDLE + ): if not self.thread or not self.thread.is_alive(): self.thread = threading.Thread( target=self.listen,