From 16179e3e68eb5bc18b5d12ec80caf511b7dec762 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Fri, 6 Dec 2024 15:47:34 +0100 Subject: [PATCH 1/6] feat(flagd-rpc): add caching (#110) add caching with tests --- Signed-off-by: Simon Schrottner --- .../openfeature/test-harness | 2 +- .../openfeature-provider-flagd/pyproject.toml | 2 + .../openfeature-provider-flagd/pytest.ini | 10 ++ .../contrib/provider/flagd/config.py | 45 +++++++- .../contrib/provider/flagd/provider.py | 8 +- .../contrib/provider/flagd/resolvers/grpc.py | 37 ++++++- .../tests/e2e/conftest.py | 9 ++ .../tests/e2e/flagd_container.py | 2 +- .../tests/e2e/steps.py | 10 ++ .../tests/e2e/test_config.py | 103 ++++++++++++++++++ ...rocess-file.py => test_in_process_file.py} | 3 +- .../tests/e2e/test_rpc.py | 1 + .../tests/test_config.py | 37 +++++-- 13 files changed, 244 insertions(+), 25 deletions(-) create mode 100644 providers/openfeature-provider-flagd/pytest.ini create mode 100644 providers/openfeature-provider-flagd/tests/e2e/test_config.py rename providers/openfeature-provider-flagd/tests/e2e/{test_in-process-file.py => test_in_process_file.py} (96%) diff --git a/providers/openfeature-provider-flagd/openfeature/test-harness b/providers/openfeature-provider-flagd/openfeature/test-harness index 6197b3d9..536d4845 160000 --- a/providers/openfeature-provider-flagd/openfeature/test-harness +++ b/providers/openfeature-provider-flagd/openfeature/test-harness @@ -1 +1 @@ -Subproject commit 6197b3d956d358bf662e5b8e0aebdc4800480f6b +Subproject commit 536d4845c0fa4255e3e98b7ee382d0eb73f7b4c0 diff --git a/providers/openfeature-provider-flagd/pyproject.toml b/providers/openfeature-provider-flagd/pyproject.toml index 7e74e810..738ba02a 100644 --- a/providers/openfeature-provider-flagd/pyproject.toml +++ b/providers/openfeature-provider-flagd/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "panzi-json-logic>=1.0.1", "semver>=3,<4", "pyyaml>=6.0.1", + "cachebox" ] requires-python = ">=3.8" @@ -59,6 +60,7 @@ cov = [ "cov-report", ] + [tool.hatch.envs.mypy] dependencies = [ "mypy[faster-cache]>=1.13.0", diff --git a/providers/openfeature-provider-flagd/pytest.ini b/providers/openfeature-provider-flagd/pytest.ini new file mode 100644 index 00000000..66da895f --- /dev/null +++ b/providers/openfeature-provider-flagd/pytest.ini @@ -0,0 +1,10 @@ +[pytest] +markers = + rpc: tests for rpc mode. + in-process: tests for rpc mode. + customCert: Supports custom certs. + unixsocket: Supports unixsockets. + events: Supports events. + sync: Supports sync. + caching: Supports caching. + offline: Supports offline. diff --git a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/config.py b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/config.py index a393d270..1bb73ece 100644 --- a/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/config.py +++ b/providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/config.py @@ -8,6 +8,13 @@ class ResolverType(Enum): IN_PROCESS = "in-process" +class CacheType(Enum): + LRU = "lru" + DISABLED = "disabled" + + +DEFAULT_CACHE = CacheType.LRU +DEFAULT_CACHE_SIZE = 1000 DEFAULT_DEADLINE = 500 DEFAULT_HOST = "localhost" DEFAULT_KEEP_ALIVE = 0 @@ -19,12 +26,14 @@ class ResolverType(Enum): DEFAULT_STREAM_DEADLINE = 600000 DEFAULT_TLS = False +ENV_VAR_CACHE_SIZE = "FLAGD_MAX_CACHE_SIZE" +ENV_VAR_CACHE_TYPE = "FLAGD_CACHE" ENV_VAR_DEADLINE_MS = "FLAGD_DEADLINE_MS" ENV_VAR_HOST = "FLAGD_HOST" ENV_VAR_KEEP_ALIVE_TIME_MS = "FLAGD_KEEP_ALIVE_TIME_MS" ENV_VAR_OFFLINE_FLAG_SOURCE_PATH = "FLAGD_OFFLINE_FLAG_SOURCE_PATH" ENV_VAR_PORT = "FLAGD_PORT" -ENV_VAR_RESOLVER_TYPE = "FLAGD_RESOLVER_TYPE" +ENV_VAR_RESOLVER_TYPE = "FLAGD_RESOLVER" ENV_VAR_RETRY_BACKOFF_MS = "FLAGD_RETRY_BACKOFF_MS" ENV_VAR_STREAM_DEADLINE_MS = "FLAGD_STREAM_DEADLINE_MS" ENV_VAR_TLS = "FLAGD_TLS" @@ -36,6 +45,14 @@ def str_to_bool(val: str) -> bool: return val.lower() == "true" +def convert_resolver_type(val: typing.Union[str, ResolverType]) -> ResolverType: + if isinstance(val, str): + v = val.lower() + return ResolverType(v) + else: + return ResolverType(val) + + def env_or_default( env_var: str, default: T, cast: typing.Optional[typing.Callable[[str], T]] = None ) -> typing.Union[str, T]: @@ -56,7 +73,9 @@ def __init__( # noqa: PLR0913 retry_backoff_ms: typing.Optional[int] = None, deadline: typing.Optional[int] = None, stream_deadline_ms: typing.Optional[int] = None, - keep_alive_time: typing.Optional[int] = None, + keep_alive: typing.Optional[int] = None, + cache_type: typing.Optional[CacheType] = None, + max_cache_size: typing.Optional[int] = None, ): self.host = env_or_default(ENV_VAR_HOST, DEFAULT_HOST) if host is None else host @@ -77,7 +96,9 @@ def __init__( # noqa: PLR0913 ) self.resolver_type = ( - ResolverType(env_or_default(ENV_VAR_RESOLVER_TYPE, DEFAULT_RESOLVER_TYPE)) + env_or_default( + ENV_VAR_RESOLVER_TYPE, DEFAULT_RESOLVER_TYPE, cast=convert_resolver_type + ) if resolver_type is None else resolver_type ) @@ -118,10 +139,22 @@ def __init__( # noqa: PLR0913 else stream_deadline_ms ) - self.keep_alive_time: int = ( + self.keep_alive: int = ( int( env_or_default(ENV_VAR_KEEP_ALIVE_TIME_MS, DEFAULT_KEEP_ALIVE, cast=int) ) - if keep_alive_time is None - else keep_alive_time + if keep_alive is None + else keep_alive + ) + + self.cache_type = ( + CacheType(env_or_default(ENV_VAR_CACHE_TYPE, DEFAULT_CACHE)) + if cache_type is None + else cache_type + ) + + self.max_cache_size: int = ( + int(env_or_default(ENV_VAR_CACHE_SIZE, DEFAULT_CACHE_SIZE, cast=int)) + if max_cache_size is None + else max_cache_size ) 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 c45b4a86..35fe2059 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 @@ -29,7 +29,7 @@ from openfeature.provider.metadata import Metadata from openfeature.provider.provider import AbstractProvider -from .config import Config, ResolverType +from .config import CacheType, Config, ResolverType from .resolvers import AbstractResolver, GrpcResolver, InProcessResolver T = typing.TypeVar("T") @@ -50,6 +50,8 @@ def __init__( # noqa: PLR0913 offline_flag_source_path: typing.Optional[str] = None, stream_deadline_ms: typing.Optional[int] = None, keep_alive_time: typing.Optional[int] = None, + cache_type: typing.Optional[CacheType] = None, + max_cache_size: typing.Optional[int] = None, ): """ Create an instance of the FlagdProvider @@ -82,7 +84,9 @@ def __init__( # noqa: PLR0913 resolver_type=resolver_type, offline_flag_source_path=offline_flag_source_path, stream_deadline_ms=stream_deadline_ms, - keep_alive_time=keep_alive_time, + keep_alive=keep_alive_time, + cache_type=cache_type, + max_cache_size=max_cache_size, ) self.resolver = self.setup_resolver() 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 9026e4b2..17f721a4 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 @@ -4,6 +4,7 @@ import typing import grpc +from cachebox import BaseCacheImpl, LRUCache from google.protobuf.json_format import MessageToDict from google.protobuf.struct_pb2 import Struct @@ -18,13 +19,13 @@ ProviderNotReadyError, TypeMismatchError, ) -from openfeature.flag_evaluation import FlagResolutionDetails +from openfeature.flag_evaluation import FlagResolutionDetails, Reason from openfeature.schemas.protobuf.flagd.evaluation.v1 import ( evaluation_pb2, evaluation_pb2_grpc, ) -from ..config import Config +from ..config import CacheType, Config from ..flag_type import FlagType if typing.TYPE_CHECKING: @@ -51,6 +52,11 @@ def __init__( self.emit_provider_ready = emit_provider_ready self.emit_provider_error = emit_provider_error self.emit_provider_configuration_changed = emit_provider_configuration_changed + self.cache: typing.Optional[BaseCacheImpl] = ( + LRUCache(maxsize=self.config.max_cache_size) + if self.config.cache_type == CacheType.LRU + else None + ) self.stub, self.channel = self._create_stub() self.retry_backoff_seconds = config.retry_backoff_ms * 0.001 self.streamline_deadline_seconds = config.stream_deadline_ms * 0.001 @@ -64,9 +70,13 @@ def _create_stub( channel_factory = grpc.secure_channel if config.tls else grpc.insecure_channel channel = channel_factory( f"{config.host}:{config.port}", - options=(("grpc.keepalive_time_ms", config.keep_alive_time),), + options=(("grpc.keepalive_time_ms", config.keep_alive),), ) stub = evaluation_pb2_grpc.ServiceStub(channel) + + if self.cache: + self.cache.clear() + return stub, channel def initialize(self, evaluation_context: EvaluationContext) -> None: @@ -75,6 +85,8 @@ def initialize(self, evaluation_context: EvaluationContext) -> None: def shutdown(self) -> None: self.active = False self.channel.close() + if self.cache: + self.cache.clear() def connect(self) -> None: self.active = True @@ -96,7 +108,6 @@ def connect(self) -> None: def listen(self) -> None: retry_delay = self.retry_backoff_seconds - call_args = ( {"timeout": self.streamline_deadline_seconds} if self.streamline_deadline_seconds > 0 @@ -148,6 +159,10 @@ def listen(self) -> None: def handle_changed_flags(self, data: typing.Any) -> None: changed_flags = list(data["flags"].keys()) + if self.cache: + for flag in changed_flags: + self.cache.pop(flag) + self.emit_provider_configuration_changed(ProviderEventDetails(changed_flags)) def resolve_boolean_details( @@ -190,13 +205,18 @@ def resolve_object_details( ) -> FlagResolutionDetails[typing.Union[dict, list]]: return self._resolve(key, FlagType.OBJECT, default_value, evaluation_context) - def _resolve( # noqa: PLR0915 + def _resolve( # noqa: PLR0915 C901 self, flag_key: str, flag_type: FlagType, default_value: T, evaluation_context: typing.Optional[EvaluationContext], ) -> FlagResolutionDetails[T]: + if self.cache is not None and flag_key in self.cache: + cached_flag: FlagResolutionDetails[T] = self.cache[flag_key] + cached_flag.reason = Reason.CACHED + return cached_flag + context = self._convert_context(evaluation_context) call_args = {"timeout": self.deadline} try: @@ -249,12 +269,17 @@ def _resolve( # noqa: PLR0915 raise GeneralError(message) from e # Got a valid flag and valid type. Return it. - return FlagResolutionDetails( + result = FlagResolutionDetails( value=value, reason=response.reason, variant=response.variant, ) + if response.reason == Reason.STATIC and self.cache is not None: + self.cache.insert(flag_key, result) + + return result + def _convert_context( self, evaluation_context: typing.Optional[EvaluationContext] ) -> Struct: diff --git a/providers/openfeature-provider-flagd/tests/e2e/conftest.py b/providers/openfeature-provider-flagd/tests/e2e/conftest.py index 142ec7f0..350f6f8d 100644 --- a/providers/openfeature-provider-flagd/tests/e2e/conftest.py +++ b/providers/openfeature-provider-flagd/tests/e2e/conftest.py @@ -11,6 +11,15 @@ SPEC_PATH = "../../openfeature/spec" +# running all gherkin tests, except the ones, not implemented +def pytest_collection_modifyitems(config): + marker = "not customCert and not unixsocket and not sync" + + # this seems to not work with python 3.8 + if hasattr(config.option, "markexpr") and config.option.markexpr == "": + config.option.markexpr = marker + + @pytest.fixture(autouse=True, scope="module") def setup(request, port, image): container: DockerContainer = FlagdContainer( diff --git a/providers/openfeature-provider-flagd/tests/e2e/flagd_container.py b/providers/openfeature-provider-flagd/tests/e2e/flagd_container.py index a9514363..e80fb0f7 100644 --- a/providers/openfeature-provider-flagd/tests/e2e/flagd_container.py +++ b/providers/openfeature-provider-flagd/tests/e2e/flagd_container.py @@ -11,7 +11,7 @@ class FlagdContainer(DockerContainer): def __init__( self, - image: str = "ghcr.io/open-feature/flagd-testbed:v0.5.13", + image: str = "ghcr.io/open-feature/flagd-testbed:v0.5.15", port: int = 8013, **kwargs, ) -> None: diff --git a/providers/openfeature-provider-flagd/tests/e2e/steps.py b/providers/openfeature-provider-flagd/tests/e2e/steps.py index 477d8c25..6e09fb57 100644 --- a/providers/openfeature-provider-flagd/tests/e2e/steps.py +++ b/providers/openfeature-provider-flagd/tests/e2e/steps.py @@ -82,6 +82,16 @@ def setup_key_and_default( return (key, default) +@when( + parsers.cfparse( + 'a string flag with key "{key}" is evaluated with details', + ), + target_fixture="key_and_default", +) +def setup_key_without_default(key: str) -> typing.Tuple[str, JsonPrimitive]: + return setup_key_and_default(key, "") + + @when( parsers.cfparse( 'an object flag with key "{key}" is evaluated with a null default value', diff --git a/providers/openfeature-provider-flagd/tests/e2e/test_config.py b/providers/openfeature-provider-flagd/tests/e2e/test_config.py new file mode 100644 index 00000000..25f0e03f --- /dev/null +++ b/providers/openfeature-provider-flagd/tests/e2e/test_config.py @@ -0,0 +1,103 @@ +import re +import sys +import typing + +import pytest +from asserts import assert_equal +from pytest_bdd import parsers, scenarios, then, when +from tests.e2e.conftest import TEST_HARNESS_PATH + +from openfeature.contrib.provider.flagd.config import CacheType, Config, ResolverType + + +def camel_to_snake(name): + name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower() + + +def str2bool(v): + return v.lower() in ("yes", "true", "t", "1") + + +def convert_resolver_type(val: typing.Union[str, ResolverType]) -> ResolverType: + if isinstance(val, str): + v = val.lower() + return ResolverType(v) + else: + return ResolverType(val) + + +type_cast = { + "Integer": int, + "Long": int, + "String": str, + "Boolean": str2bool, + "ResolverType": convert_resolver_type, + "CacheType": CacheType, +} + + +@pytest.fixture(autouse=True, scope="module") +def setup(request): + pass + + +@pytest.fixture() +def option_values() -> dict: + return {} + + +@when( + parsers.cfparse( + 'we have an option "{option}" of type "{type_info}" with value "{value}"', + ), +) +def option_with_value(option: str, value: str, type_info: str, option_values: dict): + value = type_cast[type_info](value) + option_values[camel_to_snake(option)] = value + + +@when( + parsers.cfparse( + 'we have an environment variable "{env}" with value "{value}"', + ), +) +def env_with_value(monkeypatch, env: str, value: str): + monkeypatch.setenv(env, value) + + +@when( + parsers.cfparse( + "we initialize a config", + ), + target_fixture="config", +) +def initialize_config(option_values): + return Config(**option_values) + + +@when( + parsers.cfparse( + 'we initialize a config for "{resolver_type}"', + ), + target_fixture="config", +) +def initialize_config_for(resolver_type: str, option_values: dict): + return Config(resolver_type=ResolverType(resolver_type), **option_values) + + +@then( + parsers.cfparse( + 'the option "{option}" of type "{type_info}" should have the value "{value}"', + ) +) +def check_option_value(option, value, type_info, config): + value = type_cast[type_info](value) + value = value if value != "null" else None + assert_equal(config.__getattribute__(camel_to_snake(option)), value) + + +if sys.version_info >= (3, 9): + scenarios( + f"{TEST_HARNESS_PATH}/gherkin/config.feature", + ) diff --git a/providers/openfeature-provider-flagd/tests/e2e/test_in-process-file.py b/providers/openfeature-provider-flagd/tests/e2e/test_in_process_file.py similarity index 96% rename from providers/openfeature-provider-flagd/tests/e2e/test_in-process-file.py rename to providers/openfeature-provider-flagd/tests/e2e/test_in_process_file.py index 86be937d..f73dc990 100644 --- a/providers/openfeature-provider-flagd/tests/e2e/test_in-process-file.py +++ b/providers/openfeature-provider-flagd/tests/e2e/test_in_process_file.py @@ -67,8 +67,7 @@ def setup(request, client_name, file_name, resolver_type): """nothing to boot""" api.set_provider( FlagdProvider( - resolver_type=resolver_type, - offline_flag_source_path=file_name.name, + resolver_type=resolver_type, offline_flag_source_path=file_name.name ), client_name, ) diff --git a/providers/openfeature-provider-flagd/tests/e2e/test_rpc.py b/providers/openfeature-provider-flagd/tests/e2e/test_rpc.py index 0a939d5a..3fefb300 100644 --- a/providers/openfeature-provider-flagd/tests/e2e/test_rpc.py +++ b/providers/openfeature-provider-flagd/tests/e2e/test_rpc.py @@ -29,4 +29,5 @@ def image(): f"{TEST_HARNESS_PATH}/gherkin/flagd.feature", f"{TEST_HARNESS_PATH}/gherkin/flagd-json-evaluator.feature", f"{SPEC_PATH}/specification/assets/gherkin/evaluation.feature", + f"{TEST_HARNESS_PATH}/gherkin/flagd-rpc-caching.feature", ) diff --git a/providers/openfeature-provider-flagd/tests/test_config.py b/providers/openfeature-provider-flagd/tests/test_config.py index 54d7ee3a..c923fb68 100644 --- a/providers/openfeature-provider-flagd/tests/test_config.py +++ b/providers/openfeature-provider-flagd/tests/test_config.py @@ -1,6 +1,8 @@ import pytest from openfeature.contrib.provider.flagd.config import ( + DEFAULT_CACHE, + DEFAULT_CACHE_SIZE, DEFAULT_DEADLINE, DEFAULT_HOST, DEFAULT_KEEP_ALIVE, @@ -11,6 +13,8 @@ DEFAULT_RETRY_BACKOFF, DEFAULT_STREAM_DEADLINE, DEFAULT_TLS, + ENV_VAR_CACHE_SIZE, + ENV_VAR_CACHE_TYPE, ENV_VAR_DEADLINE_MS, ENV_VAR_HOST, ENV_VAR_KEEP_ALIVE_TIME_MS, @@ -20,6 +24,7 @@ ENV_VAR_RETRY_BACKOFF_MS, ENV_VAR_STREAM_DEADLINE_MS, ENV_VAR_TLS, + CacheType, Config, ResolverType, ) @@ -27,9 +32,11 @@ def test_return_default_values_rpc(): config = Config() + assert config.cache_type == DEFAULT_CACHE + assert config.max_cache_size == DEFAULT_CACHE_SIZE assert config.deadline == DEFAULT_DEADLINE assert config.host == DEFAULT_HOST - assert config.keep_alive_time == DEFAULT_KEEP_ALIVE + assert config.keep_alive == DEFAULT_KEEP_ALIVE assert config.offline_flag_source_path == DEFAULT_OFFLINE_SOURCE_PATH assert config.port == DEFAULT_PORT_RPC assert config.resolver_type == DEFAULT_RESOLVER_TYPE @@ -40,9 +47,11 @@ def test_return_default_values_rpc(): def test_return_default_values_in_process(): config = Config(resolver_type=ResolverType.IN_PROCESS) + assert config.cache_type == DEFAULT_CACHE + assert config.max_cache_size == DEFAULT_CACHE_SIZE assert config.deadline == DEFAULT_DEADLINE assert config.host == DEFAULT_HOST - assert config.keep_alive_time == DEFAULT_KEEP_ALIVE + assert config.keep_alive == DEFAULT_KEEP_ALIVE assert config.offline_flag_source_path == DEFAULT_OFFLINE_SOURCE_PATH assert config.port == DEFAULT_PORT_IN_PROCESS assert config.resolver_type == ResolverType.IN_PROCESS @@ -56,7 +65,9 @@ def resolver_type(request): return request.param -def test_overrides_defaults_with_environment(monkeypatch, resolver_type): +def test_overrides_defaults_with_environment(monkeypatch, resolver_type): # noqa: PLR0915 + cache = CacheType.DISABLED + cache_size = 456 deadline = 1 host = "flagd" keep_alive = 2 @@ -66,6 +77,8 @@ def test_overrides_defaults_with_environment(monkeypatch, resolver_type): stream_deadline = 4 tls = True + monkeypatch.setenv(ENV_VAR_CACHE_TYPE, str(cache.value)) + monkeypatch.setenv(ENV_VAR_CACHE_SIZE, str(cache_size)) monkeypatch.setenv(ENV_VAR_DEADLINE_MS, str(deadline)) monkeypatch.setenv(ENV_VAR_HOST, host) monkeypatch.setenv(ENV_VAR_KEEP_ALIVE_TIME_MS, str(keep_alive)) @@ -77,9 +90,11 @@ def test_overrides_defaults_with_environment(monkeypatch, resolver_type): monkeypatch.setenv(ENV_VAR_TLS, str(tls)) config = Config() + assert config.cache_type == cache + assert config.max_cache_size == cache_size assert config.deadline == deadline assert config.host == host - assert config.keep_alive_time == keep_alive + assert config.keep_alive == keep_alive assert config.offline_flag_source_path == offline_path assert config.port == port assert config.resolver_type == resolver_type @@ -88,7 +103,9 @@ def test_overrides_defaults_with_environment(monkeypatch, resolver_type): assert config.tls is tls -def test_uses_arguments_over_environments_and_defaults(monkeypatch, resolver_type): +def test_uses_arguments_over_environments_and_defaults(monkeypatch, resolver_type): # noqa: PLR0915 + cache = CacheType.LRU + cache_size = 456 deadline = 1 host = "flagd" keep_alive = 2 @@ -98,6 +115,8 @@ def test_uses_arguments_over_environments_and_defaults(monkeypatch, resolver_typ stream_deadline = 4 tls = True + monkeypatch.setenv(ENV_VAR_CACHE_TYPE, str(cache.value) + "value") + monkeypatch.setenv(ENV_VAR_CACHE_SIZE, str(cache_size) + "value") monkeypatch.setenv(ENV_VAR_DEADLINE_MS, str(deadline) + "value") monkeypatch.setenv(ENV_VAR_HOST, host + "value") monkeypatch.setenv(ENV_VAR_KEEP_ALIVE_TIME_MS, str(keep_alive) + "value") @@ -109,6 +128,8 @@ def test_uses_arguments_over_environments_and_defaults(monkeypatch, resolver_typ monkeypatch.setenv(ENV_VAR_TLS, str(tls) + "value") config = Config( + cache_type=cache, + max_cache_size=cache_size, deadline=deadline, host=host, port=port, @@ -116,12 +137,14 @@ def test_uses_arguments_over_environments_and_defaults(monkeypatch, resolver_typ retry_backoff_ms=retry_backoff, stream_deadline_ms=stream_deadline, tls=tls, - keep_alive_time=keep_alive, + keep_alive=keep_alive, offline_flag_source_path=offline_path, ) + assert config.cache_type == cache + assert config.max_cache_size == cache_size assert config.deadline == deadline assert config.host == host - assert config.keep_alive_time == keep_alive + assert config.keep_alive == keep_alive assert config.offline_flag_source_path == offline_path assert config.port == port assert config.resolver_type == resolver_type From 51d765154dc585ab95ff4a329dcba101f3386cce Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Thu, 28 Nov 2024 19:15:40 +0100 Subject: [PATCH 2/6] feat(flagd-rpc): add caching with tests Signed-off-by: Simon Schrottner --- .../contrib/provider/flagd/resolvers/grpc.py | 14 ++ .../tests/e2e/config.feature | 189 ++++++++++++++++++ .../tests/e2e/rpc_cache.feature | 44 ++++ 3 files changed, 247 insertions(+) create mode 100644 providers/openfeature-provider-flagd/tests/e2e/config.feature create mode 100644 providers/openfeature-provider-flagd/tests/e2e/rpc_cache.feature 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 17f721a4..b1334a08 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 @@ -63,6 +63,12 @@ def __init__( self.deadline = config.deadline * 0.001 self.connected = False + self._cache: typing.Optional[BaseCacheImpl] = ( + LRUCache(maxsize=self.config.max_cache_size) + if self.config.cache_type == CacheType.LRU + else None + ) + def _create_stub( self, ) -> typing.Tuple[evaluation_pb2_grpc.ServiceStub, grpc.Channel]: @@ -81,6 +87,14 @@ def _create_stub( def initialize(self, evaluation_context: EvaluationContext) -> None: self.connect() + self.retry_backoff_seconds = 0.1 + self.connected = False + + self._cache = ( + LRUCache(maxsize=self.config.max_cache_size) + if self.config.cache_type == CacheType.LRU + else None + ) def shutdown(self) -> None: self.active = False diff --git a/providers/openfeature-provider-flagd/tests/e2e/config.feature b/providers/openfeature-provider-flagd/tests/e2e/config.feature new file mode 100644 index 00000000..f2bd715e --- /dev/null +++ b/providers/openfeature-provider-flagd/tests/e2e/config.feature @@ -0,0 +1,189 @@ +Feature: Configuration Test + + @rpc @in-process + Scenario Outline: Default Config + When we initialize a config + Then the option "