diff --git a/ld_openfeature/provider.py b/ld_openfeature/provider.py index 5865b98..fa3d421 100644 --- a/ld_openfeature/provider.py +++ b/ld_openfeature/provider.py @@ -41,7 +41,10 @@ def __handle_data_source_status(self, status: DataSourceStatus): elif state == DataSourceState.OFF: error_message = self.__get_message(status, "the provider has encountered a permanent error or has been shutdown") - self.emit_provider_error(ProviderEventDetails(error_code=ErrorCode.PROVIDER_FATAL, + # This is not reported as a fatal error. A fatal provider prevents the OpenFeature client + # from evaluating flags at all, but the LaunchDarkly client can keep evaluating the flag + # data it already has. + self.emit_provider_error(ProviderEventDetails(error_code=ErrorCode.GENERAL, message=error_message)) elif state == DataSourceState.INTERRUPTED: error_message = self.__get_message(status, "encountered an unknown error") diff --git a/tests/test_data_sources.py b/tests/test_data_sources.py index 2b24cea..2440083 100644 --- a/tests/test_data_sources.py +++ b/tests/test_data_sources.py @@ -74,6 +74,40 @@ def initialized(self): return False +class InitializedThenFailingDataSource(UpdateProcessor): + def __init__(self, config: Config, store, ready: threading.Event): + self._data_source_update_sink: Optional[DataSourceUpdateSink] = config.data_source_update_sink + self._ready = ready + + def start(self): + self._ready.set() + self._data_source_update_sink.init( + {FEATURES: {"cached-boolean": TestData().data_source().flag("cached-boolean").on(True)._build(1)}}) + self._data_source_update_sink.update_status(DataSourceState.VALID, None) + + def data_source_failure(): + self._data_source_update_sink.update_status( + DataSourceState.OFF, + DataSourceErrorInfo( + DataSourceErrorKind.ERROR_RESPONSE, + 401, + time.time(), + str("Bad things") + ) + ) + + threading.Timer(0.1, data_source_failure).start() + + def stop(self): + pass + + def is_alive(self): + return False + + def initialized(self): + return True + + class StaleDataSource(UpdateProcessor): def __init__(self, config: Config, store, ready: threading.Event): self._data_source_update_sink: Optional[DataSourceUpdateSink] = config.data_source_update_sink diff --git a/tests/test_provider.py b/tests/test_provider.py index c0e1d5c..f4496f2 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -11,10 +11,11 @@ from openfeature.event import ProviderEvent, EventDetails from openfeature.exception import ErrorCode from openfeature.flag_evaluation import Reason +from openfeature.provider import ProviderStatus from openfeature import api from ld_openfeature import LaunchDarklyProvider, Config -from tests.test_data_sources import FailingDataSource, StaleDataSource, UpdatingDataSource, DelayedFailingDataSource +from tests.test_data_sources import FailingDataSource, InitializedThenFailingDataSource, StaleDataSource, UpdatingDataSource, DelayedFailingDataSource @pytest.fixture @@ -232,6 +233,28 @@ def handle_status(details: EventDetails): api.shutdown() +def test_evaluations_continue_after_the_data_source_permanently_fails(): + thread_event = threading.Event() + + def handle_status(details: EventDetails): + if details.provider_name == 'launchdarkly-openfeature-server': + thread_event.set() + + api.add_handler(ProviderEvent.PROVIDER_ERROR, handle_status) + + provider = LaunchDarklyProvider( + Config("", update_processor_class=InitializedThenFailingDataSource, send_events=False)) + api.set_provider(provider) + client = api.get_client() + + assert thread_event.wait(timeout=5) + + assert client.get_provider_status() == ProviderStatus.ERROR + assert client.get_boolean_value("cached-boolean", False, EvaluationContext('user-key')) is True + + api.shutdown() + + def test_provider_emits_stale_event(): thread_event = threading.Event()