Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion ld_openfeature/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
34 changes: 34 additions & 0 deletions tests/test_data_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
Loading