Skip to content

Commit 16956f5

Browse files
committed
feat: introduce list for fatal status codes
Signed-off-by: Konvalinka <lea.konvalinka@dynatrace.com>
1 parent 7e3b06e commit 16956f5

File tree

8 files changed

+47
-16
lines changed

8 files changed

+47
-16
lines changed

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def __init__( # noqa: PLR0913
6565
default_authority: typing.Optional[str] = None,
6666
channel_credentials: typing.Optional[grpc.ChannelCredentials] = None,
6767
sync_metadata_disabled: typing.Optional[bool] = None,
68+
fatal_status_codes: typing.Optional[list[str]] = None,
6869
):
6970
"""
7071
Create an instance of the FlagdProvider
@@ -111,6 +112,7 @@ def __init__( # noqa: PLR0913
111112
default_authority=default_authority,
112113
channel_credentials=channel_credentials,
113114
sync_metadata_disabled=sync_metadata_disabled,
115+
fatal_status_codes=fatal_status_codes,
114116
)
115117
self.enriched_context: dict = {}
116118

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/grpc.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
GeneralError,
1919
InvalidContextError,
2020
ParseError,
21+
ProviderFatalError,
2122
ProviderNotReadyError,
2223
TypeMismatchError,
2324
)
@@ -61,6 +62,7 @@ def __init__(
6162
if self.config.cache == CacheType.LRU
6263
else None
6364
)
65+
logger.debug(self.config.fatal_status_codes)
6466

6567
self.retry_grace_period = config.retry_grace_period
6668
self.streamline_deadline_seconds = config.stream_deadline_ms * 0.001
@@ -235,6 +237,8 @@ def listen(self) -> None:
235237
except grpc.RpcError as e: # noqa: PERF203
236238
# although it seems like this error log is not interesting, without it, the retry is not working as expected
237239
logger.debug(f"SyncFlags stream error, {e.code()=} {e.details()=}")
240+
if e.code().name in self.config.fatal_status_codes:
241+
raise ProviderFatalError("fatal error") from e
238242
except ParseError:
239243
logger.exception(
240244
f"Could not parse flag data using flagd syntax: {message=}"
@@ -399,8 +403,11 @@ def _resolve( # noqa: PLR0915 C901
399403
except grpc.RpcError as e:
400404
code = e.code()
401405
message = f"received grpc status code {code}"
406+
logger.debug(message)
402407

403-
if code == grpc.StatusCode.NOT_FOUND:
408+
if code.name in self.config.fatal_status_codes:
409+
raise ProviderFatalError(message) from e
410+
elif code == grpc.StatusCode.NOT_FOUND:
404411
raise FlagNotFoundError(message) from e
405412
elif code == grpc.StatusCode.INVALID_ARGUMENT:
406413
raise TypeMismatchError(message) from e

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010

1111
from openfeature.evaluation_context import EvaluationContext
1212
from openfeature.event import ProviderEventDetails
13-
from openfeature.exception import ErrorCode, ParseError, ProviderNotReadyError
13+
from openfeature.exception import (
14+
ErrorCode,
15+
ParseError,
16+
ProviderFatalError,
17+
ProviderNotReadyError,
18+
)
1419
from openfeature.schemas.protobuf.flagd.sync.v1 import (
1520
sync_pb2,
1621
sync_pb2_grpc,
@@ -268,7 +273,8 @@ def listen(self) -> None:
268273
logger.debug("Terminating gRPC sync thread")
269274
return
270275
except grpc.RpcError as e: # noqa: PERF203
271-
logger.debug(f"SyncFlags stream error, {e.code()=} {e.details()=}")
276+
logger.warning(f"SyncFlags stream error, {e.code()=} {e.details()=}")
277+
self._raise_on_fatal_status_code(e)
272278
except json.JSONDecodeError:
273279
logger.exception(
274280
f"Could not parse JSON flag data from SyncFlags endpoint: {flag_str=}"
@@ -287,3 +293,8 @@ def generate_grpc_call_args(self) -> GrpcMultiCallableArgs:
287293
if metadata is not None:
288294
call_args["metadata"] = metadata
289295
return call_args
296+
297+
def _raise_on_fatal_status_code(self, e: grpc.RpcError) -> None:
298+
if e.code().name in self.config.fatal_status_codes:
299+
logger.error(f"Fatal gRPC status code received: {e.code()}")
300+
raise ProviderFatalError(f"Fatal gRPC status code: {e.code()}") from e

providers/openfeature-provider-flagd/tests/e2e/flagd_container.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
HEALTH_CHECK = 8014
1414
LAUNCHPAD = 8080
15+
FORBIDDEN = 9212
1516

1617

1718
class FlagdContainer(DockerContainer):
@@ -30,7 +31,7 @@ def __init__(
3031
self.ipr = 8015
3132
self.flagDir = Path("./flags")
3233
self.flagDir.mkdir(parents=True, exist_ok=True)
33-
self.with_exposed_ports(self.rpc, self.ipr, HEALTH_CHECK, LAUNCHPAD)
34+
self.with_exposed_ports(self.rpc, self.ipr, HEALTH_CHECK, LAUNCHPAD, FORBIDDEN)
3435
self.with_volume_mapping(os.path.abspath(self.flagDir.name), "/flags", "rw")
3536
self.waiting_for(LogMessageWaitStrategy("listening").with_startup_timeout(5))
3637

providers/openfeature-provider-flagd/tests/e2e/inprocess/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from tests.e2e.testfilter import TestFilter
55

66
resolver = ResolverType.IN_PROCESS
7-
feature_list = ["~targetURI", "~unixsocket", "~deprecated", "~forbidden"]
7+
feature_list = ["~targetURI", "~unixsocket", "~deprecated"]
88

99

1010
def pytest_collection_modifyitems(config, items):

providers/openfeature-provider-flagd/tests/e2e/rpc/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
"~sync",
1111
"~metadata",
1212
"~deprecated",
13-
"~forbidden",
1413
]
1514

1615

providers/openfeature-provider-flagd/tests/e2e/step/config_steps.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ def option_values() -> dict:
4646

4747

4848
@given(
49-
parsers.cfparse(
50-
'an option "{option}" of type "{type_info}" with value "{value}"',
49+
parsers.re(
50+
r'an option "(?P<option>[^"]+)" of type "(?P<type_info>[^"]+)" with value "(?P<value>[^"]*)"',
5151
),
5252
)
5353
def option_with_value(option: str, value: str, type_info: str, option_values: dict):
@@ -91,8 +91,8 @@ def initialize_config_for(resolver_type: str, option_values: dict):
9191

9292

9393
@then(
94-
parsers.cfparse(
95-
'the option "{option}" of type "{type_info}" should have the value "{value}"',
94+
parsers.re(
95+
r'the option "(?P<option>[^"]+)" of type "(?P<type_info>[^"]+)" should have the value "(?P<value>[^"]*)"',
9696
)
9797
)
9898
def check_option_value(option, value, type_info, config_or_error):

providers/openfeature-provider-flagd/tests/e2e/step/provider_steps.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
import pytest
88
import requests
9-
from pytest_bdd import given, parsers, when
9+
from asserts import assert_equal
10+
from pytest_bdd import given, parsers, then, when
1011
from tests.e2e.flagd_container import FlagdContainer
1112
from tests.e2e.step._utils import wait_for
1213

@@ -58,6 +59,7 @@ def get_default_options_for_provider(
5859
"retry_grace_period": 3,
5960
"port": container.get_port(resolver_type),
6061
}
62+
wait: bool = True
6163

6264
if t == TestProviderType.UNAVAILABLE:
6365
return {}, False
@@ -70,11 +72,10 @@ def get_default_options_for_provider(
7072
options["tls"] = True
7173
launchpad = "ssl"
7274
elif t == TestProviderType.FORBIDDEN:
73-
launchpad = "forbidden"
74-
options["port"] = container.get_port(9212)
75-
options["fatal_status_codes"] = ["FORBIDDEN"]
75+
options["port"] = container.get_exposed_port(9212)
76+
wait = False
7677
elif t == TestProviderType.SOCKET:
77-
return options, True
78+
return options, wait
7879
elif t == TestProviderType.METADATA:
7980
launchpad = "metadata"
8081
elif t == TestProviderType.SYNCPAYLOAD:
@@ -96,7 +97,7 @@ def get_default_options_for_provider(
9697
f"{container.get_launchpad_url()}/start?config={launchpad}", timeout=1
9798
)
9899
time.sleep(0.1)
99-
return options, True
100+
return options, wait
100101

101102

102103
@given(
@@ -144,6 +145,16 @@ def flagd_restart(
144145
pass
145146

146147

148+
@then(parsers.cfparse("the client should be in {status} state"))
149+
def client_state(status, client: OpenFeatureClient):
150+
expected_status = ProviderStatus[status.upper()]
151+
# wait_for(
152+
# lambda: client.get_provider_status() == expected_status,
153+
# timeout_sec=2,
154+
# )
155+
assert_equal(client.get_provider_status(), expected_status)
156+
157+
147158
@pytest.fixture(autouse=True, scope="package")
148159
def container(request):
149160
container = FlagdContainer()

0 commit comments

Comments
 (0)