From 16956f5816750027415162ffa0e3d83cba41fa24 Mon Sep 17 00:00:00 2001 From: lea konvalinka Date: Thu, 12 Mar 2026 15:38:39 +0100 Subject: [PATCH 1/3] feat: introduce list for fatal status codes Signed-off-by: Konvalinka --- .../contrib/provider/flagd/provider.py | 2 ++ .../contrib/provider/flagd/resolvers/grpc.py | 9 +++++++- .../process/connector/grpc_watcher.py | 15 ++++++++++-- .../tests/e2e/flagd_container.py | 3 ++- .../tests/e2e/inprocess/conftest.py | 2 +- .../tests/e2e/rpc/conftest.py | 1 - .../tests/e2e/step/config_steps.py | 8 +++---- .../tests/e2e/step/provider_steps.py | 23 ++++++++++++++----- 8 files changed, 47 insertions(+), 16 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 5efa995c..aadf3318 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 @@ -65,6 +65,7 @@ def __init__( # noqa: PLR0913 default_authority: typing.Optional[str] = None, channel_credentials: typing.Optional[grpc.ChannelCredentials] = None, sync_metadata_disabled: typing.Optional[bool] = None, + fatal_status_codes: typing.Optional[list[str]] = None, ): """ Create an instance of the FlagdProvider @@ -111,6 +112,7 @@ def __init__( # noqa: PLR0913 default_authority=default_authority, channel_credentials=channel_credentials, sync_metadata_disabled=sync_metadata_disabled, + fatal_status_codes=fatal_status_codes, ) self.enriched_context: dict = {} 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 e4341d48..c4a3430a 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 @@ -18,6 +18,7 @@ GeneralError, InvalidContextError, ParseError, + ProviderFatalError, ProviderNotReadyError, TypeMismatchError, ) @@ -61,6 +62,7 @@ def __init__( if self.config.cache == CacheType.LRU else None ) + logger.debug(self.config.fatal_status_codes) self.retry_grace_period = config.retry_grace_period self.streamline_deadline_seconds = config.stream_deadline_ms * 0.001 @@ -235,6 +237,8 @@ def listen(self) -> None: except grpc.RpcError as e: # noqa: PERF203 # although it seems like this error log is not interesting, without it, the retry is not working as expected logger.debug(f"SyncFlags stream error, {e.code()=} {e.details()=}") + if e.code().name in self.config.fatal_status_codes: + raise ProviderFatalError("fatal error") from e except ParseError: logger.exception( f"Could not parse flag data using flagd syntax: {message=}" @@ -399,8 +403,11 @@ def _resolve( # noqa: PLR0915 C901 except grpc.RpcError as e: code = e.code() message = f"received grpc status code {code}" + logger.debug(message) - if code == grpc.StatusCode.NOT_FOUND: + if code.name in self.config.fatal_status_codes: + raise ProviderFatalError(message) from e + elif code == grpc.StatusCode.NOT_FOUND: raise FlagNotFoundError(message) from e elif code == grpc.StatusCode.INVALID_ARGUMENT: raise TypeMismatchError(message) from e 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 4d9d27ed..a45e87b8 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 @@ -10,7 +10,12 @@ from openfeature.evaluation_context import EvaluationContext from openfeature.event import ProviderEventDetails -from openfeature.exception import ErrorCode, ParseError, ProviderNotReadyError +from openfeature.exception import ( + ErrorCode, + ParseError, + ProviderFatalError, + ProviderNotReadyError, +) from openfeature.schemas.protobuf.flagd.sync.v1 import ( sync_pb2, sync_pb2_grpc, @@ -268,7 +273,8 @@ def listen(self) -> None: logger.debug("Terminating gRPC sync thread") return except grpc.RpcError as e: # noqa: PERF203 - logger.debug(f"SyncFlags stream error, {e.code()=} {e.details()=}") + logger.warning(f"SyncFlags stream error, {e.code()=} {e.details()=}") + self._raise_on_fatal_status_code(e) except json.JSONDecodeError: logger.exception( f"Could not parse JSON flag data from SyncFlags endpoint: {flag_str=}" @@ -287,3 +293,8 @@ def generate_grpc_call_args(self) -> GrpcMultiCallableArgs: if metadata is not None: call_args["metadata"] = metadata return call_args + + def _raise_on_fatal_status_code(self, e: grpc.RpcError) -> None: + if e.code().name in self.config.fatal_status_codes: + logger.error(f"Fatal gRPC status code received: {e.code()}") + raise ProviderFatalError(f"Fatal gRPC status code: {e.code()}") from e diff --git a/providers/openfeature-provider-flagd/tests/e2e/flagd_container.py b/providers/openfeature-provider-flagd/tests/e2e/flagd_container.py index bc0dbb28..4a94ef9b 100644 --- a/providers/openfeature-provider-flagd/tests/e2e/flagd_container.py +++ b/providers/openfeature-provider-flagd/tests/e2e/flagd_container.py @@ -12,6 +12,7 @@ HEALTH_CHECK = 8014 LAUNCHPAD = 8080 +FORBIDDEN = 9212 class FlagdContainer(DockerContainer): @@ -30,7 +31,7 @@ def __init__( self.ipr = 8015 self.flagDir = Path("./flags") self.flagDir.mkdir(parents=True, exist_ok=True) - self.with_exposed_ports(self.rpc, self.ipr, HEALTH_CHECK, LAUNCHPAD) + self.with_exposed_ports(self.rpc, self.ipr, HEALTH_CHECK, LAUNCHPAD, FORBIDDEN) self.with_volume_mapping(os.path.abspath(self.flagDir.name), "/flags", "rw") self.waiting_for(LogMessageWaitStrategy("listening").with_startup_timeout(5)) diff --git a/providers/openfeature-provider-flagd/tests/e2e/inprocess/conftest.py b/providers/openfeature-provider-flagd/tests/e2e/inprocess/conftest.py index d087b512..49811e10 100644 --- a/providers/openfeature-provider-flagd/tests/e2e/inprocess/conftest.py +++ b/providers/openfeature-provider-flagd/tests/e2e/inprocess/conftest.py @@ -4,7 +4,7 @@ from tests.e2e.testfilter import TestFilter resolver = ResolverType.IN_PROCESS -feature_list = ["~targetURI", "~unixsocket", "~deprecated", "~forbidden"] +feature_list = ["~targetURI", "~unixsocket", "~deprecated"] def pytest_collection_modifyitems(config, items): diff --git a/providers/openfeature-provider-flagd/tests/e2e/rpc/conftest.py b/providers/openfeature-provider-flagd/tests/e2e/rpc/conftest.py index e16f2bdd..8804bb94 100644 --- a/providers/openfeature-provider-flagd/tests/e2e/rpc/conftest.py +++ b/providers/openfeature-provider-flagd/tests/e2e/rpc/conftest.py @@ -10,7 +10,6 @@ "~sync", "~metadata", "~deprecated", - "~forbidden", ] diff --git a/providers/openfeature-provider-flagd/tests/e2e/step/config_steps.py b/providers/openfeature-provider-flagd/tests/e2e/step/config_steps.py index a3b378ce..7da4f8c3 100644 --- a/providers/openfeature-provider-flagd/tests/e2e/step/config_steps.py +++ b/providers/openfeature-provider-flagd/tests/e2e/step/config_steps.py @@ -46,8 +46,8 @@ def option_values() -> dict: @given( - parsers.cfparse( - 'an option "{option}" of type "{type_info}" with value "{value}"', + parsers.re( + r'an option "(?P