Skip to content

Commit f8ace1a

Browse files
committed
fix: Ask the override layer for the key before the client has LaunchDarkly data
Before initialization the clients consult the override layer directly. A flag the layer does not hold gets the not-ready handling it had without an override source, for an invalid context, a store outage, and an uninitialized persistent store alike. The all-flags state is unavailable when the layer is empty. The sync client stops the components it started when the data system fails to start.
1 parent 8043a6d commit f8ace1a

10 files changed

Lines changed: 334 additions & 77 deletions

File tree

‎ldclient/async_client.py‎

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -505,36 +505,37 @@ def not_ready() -> Tuple[EvaluationDetail, Optional[FeatureFlag]]:
505505
self._send_event(event_factory.new_unknown_flag_event(key, context, default, reason))
506506
return EvaluationDetail(default, None, reason), None
507507

508-
no_launchdarkly_data = False
508+
flag: Any = None
509509
availability = await self._data_system.data_availability()
510510
if availability != DataAvailability.REFRESHED:
511511
if availability == DataAvailability.CACHED:
512512
if not self._eval_cached_data_warned:
513513
self._eval_cached_data_warned = True
514514
log.warning("Feature Flag evaluation attempted before client has initialized - using last known values from feature store for feature key: " + key + ". This message is logged once.")
515-
elif self._data_system.override_source_configured:
516-
# No data from LaunchDarkly is available. The store read below still finds an
517-
# entry that the override layer holds, and the SDK serves it. A miss returns the
518-
# not-ready default.
519-
no_launchdarkly_data = True
520515
else:
521-
return not_ready()
516+
# No data from LaunchDarkly is available. A flag that the override layer holds
517+
# is served anyway. A flag the layer does not hold gets the not-ready default,
518+
# as it does without an override source.
519+
layer = self._data_system.override_layer
520+
if layer is not None:
521+
flag = layer.get(FEATURES, key)
522+
if flag is None:
523+
return not_ready()
522524

523525
if not context.valid:
524526
log.warning("Context was invalid for flag evaluation (%s); returning default value" % context.error)
525527
return EvaluationDetail(default, None, error_reason('USER_NOT_SPECIFIED')), None
526528

527-
try:
528-
flag = await self._data_system.store.get(FEATURES, key)
529-
except Exception as e:
530-
log.error("Unexpected error while retrieving feature flag \"%s\": %s" % (key, repr(e)))
531-
log.debug(traceback.format_exc())
532-
reason = error_reason('EXCEPTION')
533-
self._send_event(event_factory.new_unknown_flag_event(key, context, default, reason))
534-
return EvaluationDetail(default, None, reason), None
529+
if flag is None:
530+
try:
531+
flag = await self._data_system.store.get(FEATURES, key)
532+
except Exception as e:
533+
log.error("Unexpected error while retrieving feature flag \"%s\": %s" % (key, repr(e)))
534+
log.debug(traceback.format_exc())
535+
reason = error_reason('EXCEPTION')
536+
self._send_event(event_factory.new_unknown_flag_event(key, context, default, reason))
537+
return EvaluationDetail(default, None, reason), None
535538
if not flag:
536-
if no_launchdarkly_data:
537-
return not_ready()
538539
reason = error_reason('FLAG_NOT_FOUND')
539540
self._send_event(event_factory.new_unknown_flag_event(key, context, default, reason))
540541
return EvaluationDetail(default, None, reason), None
@@ -591,13 +592,15 @@ async def all_flags_state(self, context: Context, **kwargs) -> FeatureFlagsState
591592
if not self._all_flags_cached_data_warned:
592593
self._all_flags_cached_data_warned = True
593594
log.warning("all_flags_state() called before client has finished initializing! Using last known values from feature store. This message is logged once.")
594-
elif self._data_system.override_source_configured:
595-
# No data from LaunchDarkly is available. The store read below returns only the
596-
# entries that the override layer holds. The result decides the state.
597-
overrides_only = True
598595
else:
599-
log.warning("all_flags_state() called before client has finished initializing! Feature store unavailable - returning empty state")
600-
return FeatureFlagsState(False)
596+
# No data from LaunchDarkly is available. When the override layer holds entries,
597+
# the state is built from them alone. Otherwise the state is unavailable, as it
598+
# is without an override source.
599+
layer = self._data_system.override_layer
600+
if layer is None or layer.is_empty:
601+
log.warning("all_flags_state() called before client has finished initializing! Feature store unavailable - returning empty state")
602+
return FeatureFlagsState(False)
603+
overrides_only = True
601604

602605
if not context.valid:
603606
log.warning("Context was invalid for all_flags_state (%s); returning default value" % context.error)

‎ldclient/client.py‎

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,13 @@ def __start_up(self, start_wait: float):
212212
self.__register_plugins(environment_metadata)
213213

214214
update_processor_ready = threading.Event()
215-
self._data_system.start(update_processor_ready)
215+
try:
216+
self._data_system.start(update_processor_ready)
217+
except BaseException:
218+
# A data system that fails to start fails the constructor. The components that
219+
# are already running stop here, so the failure leaves no threads behind.
220+
self._close_components()
221+
raise
216222

217223
if not self._config.offline and not self._config.use_ldd:
218224
if start_wait > 60:
@@ -282,6 +288,15 @@ def close(self):
282288
self._data_system.stop()
283289
self.__big_segment_store_manager.stop()
284290

291+
def _close_components(self):
292+
"""
293+
Stops the SDK components after a failed start-up. The event processor is last because
294+
it may still be sending events the other components generated.
295+
"""
296+
self._data_system.stop()
297+
self.__big_segment_store_manager.stop()
298+
self._event_processor.stop()
299+
285300
# These magic methods allow a client object to be automatically cleaned up by the "with" scope operator
286301
def __enter__(self):
287302
return self
@@ -474,7 +489,7 @@ def not_ready() -> Tuple[EvaluationDetail, Optional[FeatureFlag]]:
474489
self._send_event(event_factory.new_unknown_flag_event(key, context, default, reason))
475490
return EvaluationDetail(default, None, reason), None
476491

477-
no_launchdarkly_data = False
492+
flag: Any = None
478493
availability = self._data_system.data_availability
479494
if availability != DataAvailability.REFRESHED:
480495
if availability == DataAvailability.CACHED:
@@ -483,29 +498,30 @@ def not_ready() -> Tuple[EvaluationDetail, Optional[FeatureFlag]]:
483498
if not self._eval_cached_data_warned:
484499
self._eval_cached_data_warned = True
485500
log.warning("Feature Flag evaluation attempted before client has initialized - using last known values from feature store for feature key: " + key + ". This message is logged once.")
486-
elif self._data_system.override_source_configured:
487-
# No data from LaunchDarkly is available. The store read below still finds an
488-
# entry that the override layer holds, and the SDK serves it. A miss returns the
489-
# not-ready default.
490-
no_launchdarkly_data = True
491501
else:
492-
return not_ready()
502+
# No data from LaunchDarkly is available. A flag that the override layer holds
503+
# is served anyway. A flag the layer does not hold gets the not-ready default,
504+
# as it does without an override source.
505+
layer = self._data_system.override_layer
506+
if layer is not None:
507+
flag = layer.get(FEATURES, key)
508+
if flag is None:
509+
return not_ready()
493510

494511
if not context.valid:
495512
log.warning("Context was invalid for flag evaluation (%s); returning default value" % context.error)
496513
return EvaluationDetail(default, None, error_reason('USER_NOT_SPECIFIED')), None
497514

498-
try:
499-
flag = self._data_system.store.get(FEATURES, key)
500-
except Exception as e:
501-
log.error("Unexpected error while retrieving feature flag \"%s\": %s" % (key, repr(e)))
502-
log.debug(traceback.format_exc())
503-
reason = error_reason('EXCEPTION')
504-
self._send_event(event_factory.new_unknown_flag_event(key, context, default, reason))
505-
return EvaluationDetail(default, None, reason), None
515+
if flag is None:
516+
try:
517+
flag = self._data_system.store.get(FEATURES, key)
518+
except Exception as e:
519+
log.error("Unexpected error while retrieving feature flag \"%s\": %s" % (key, repr(e)))
520+
log.debug(traceback.format_exc())
521+
reason = error_reason('EXCEPTION')
522+
self._send_event(event_factory.new_unknown_flag_event(key, context, default, reason))
523+
return EvaluationDetail(default, None, reason), None
506524
if not flag:
507-
if no_launchdarkly_data:
508-
return not_ready()
509525
reason = error_reason('FLAG_NOT_FOUND')
510526
self._send_event(event_factory.new_unknown_flag_event(key, context, default, reason))
511527
return EvaluationDetail(default, None, reason), None
@@ -566,13 +582,15 @@ def all_flags_state(self, context: Context, **kwargs) -> FeatureFlagsState:
566582
if not self._all_flags_cached_data_warned:
567583
self._all_flags_cached_data_warned = True
568584
log.warning("all_flags_state() called before client has finished initializing! Using last known values from feature store. This message is logged once.")
569-
elif self._data_system.override_source_configured:
570-
# No data from LaunchDarkly is available. The store read below returns only the
571-
# entries that the override layer holds. The result decides the state.
572-
overrides_only = True
573585
else:
574-
log.warning("all_flags_state() called before client has finished initializing! Feature store unavailable - returning empty state")
575-
return FeatureFlagsState(False)
586+
# No data from LaunchDarkly is available. When the override layer holds entries,
587+
# the state is built from them alone. Otherwise the state is unavailable, as it
588+
# is without an override source.
589+
layer = self._data_system.override_layer
590+
if layer is None or layer.is_empty:
591+
log.warning("all_flags_state() called before client has finished initializing! Feature store unavailable - returning empty state")
592+
return FeatureFlagsState(False)
593+
overrides_only = True
576594

577595
if not context.valid:
578596
log.warning("Context was invalid for all_flags_state (%s); returning default value" % context.error)

‎ldclient/impl/datasystem/__init__.py‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
if TYPE_CHECKING:
1212
from ldclient.impl.aio.concurrency import AsyncEvent
13+
from ldclient.impl.overrides.layer import OverrideLayer
1314

1415
from ldclient.impl.listeners import Listeners
1516
from ldclient.interfaces import (
@@ -160,12 +161,12 @@ def environment_id(self) -> Optional[str]:
160161

161162
@property
162163
@abstractmethod
163-
def override_source_configured(self) -> bool:
164+
def override_layer(self) -> Optional["OverrideLayer"]:
164165
"""
165-
Reports whether the data system was built with an override source. The value is
166-
fixed at construction. When true, the store served by :attr:`store` overlays the
167-
override layer, and the client serves an overridden flag before it has LaunchDarkly
168-
data.
166+
Returns the override layer when the data system was built with an override source,
167+
and None otherwise. The store served by :attr:`store` overlays this layer. The client
168+
consults the layer directly before it has LaunchDarkly data, so that a flag the layer
169+
holds is served and a flag it does not hold gets the not-ready handling.
169170
"""
170171
raise NotImplementedError
171172

@@ -245,10 +246,10 @@ def store(self) -> AsyncReadOnlyStore:
245246

246247
@property
247248
@abstractmethod
248-
def override_source_configured(self) -> bool:
249+
def override_layer(self) -> Optional["OverrideLayer"]:
249250
"""
250-
Reports whether the data system was built with an override source. The value is
251-
fixed at construction.
251+
Returns the override layer when the data system was built with an override source,
252+
and None otherwise. See :attr:`DataSystem.override_layer`.
252253
"""
253254
raise NotImplementedError
254255

‎ldclient/impl/datasystem/async_fdv1.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,9 @@ def store(self) -> AsyncReadOnlyStore:
125125
return self._store_view
126126

127127
@property
128-
def override_source_configured(self) -> bool:
128+
def override_layer(self) -> None:
129129
# Overrides are an option of the FDv2 data system only.
130-
return False
130+
return None
131131

132132
def set_diagnostic_accumulator(self, diagnostic_accumulator: DiagnosticAccumulator):
133133
"""

‎ldclient/impl/datasystem/async_fdv2.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -677,8 +677,8 @@ def store(self) -> AsyncReadOnlyStore:
677677
return self._store_view
678678

679679
@property
680-
def override_source_configured(self) -> bool:
681-
return self._override_source is not None
680+
def override_layer(self) -> Optional[OverrideLayer]:
681+
return self._override_layer
682682

683683
async def data_availability(self) -> DataAvailability: # type: ignore[override]
684684
"""Reports what form of data is currently available, awaiting the store's

‎ldclient/impl/datasystem/fdv1.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,9 @@ def store(self) -> ReadOnlyStore:
234234
return self._store_view
235235

236236
@property
237-
def override_source_configured(self) -> bool:
237+
def override_layer(self) -> None:
238238
# Overrides are an option of the FDv2 data system only.
239-
return False
239+
return None
240240

241241
@property
242242
def environment_id(self) -> Optional[str]:

‎ldclient/impl/datasystem/fdv2.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -680,8 +680,8 @@ def store(self) -> ReadOnlyStore:
680680
return self._store_view
681681

682682
@property
683-
def override_source_configured(self) -> bool:
684-
return self._override_source is not None
683+
def override_layer(self) -> Optional[OverrideLayer]:
684+
return self._override_layer
685685

686686

687687
__all__ = [

‎ldclient/testing/mock_components.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,21 @@ class MockOverrideSource:
6969
on every later call to set_overrides, and records its lifecycle calls.
7070
"""
7171

72-
def __init__(self, flags: Optional[Dict[str, Any]] = None, segments: Optional[Dict[str, Any]] = None):
72+
def __init__(self, flags: Optional[Dict[str, Any]] = None, segments: Optional[Dict[str, Any]] = None, start_error: Optional[Exception] = None):
73+
"""
74+
:param start_error: when given, start raises it instead of supplying the contents
75+
"""
7376
self._flags: Dict[str, Any] = dict(flags or {})
7477
self._segments: Dict[str, Any] = dict(segments or {})
78+
self._start_error = start_error
7579
self._sink: Optional[OverrideSink] = None
7680
self.start_count = 0
7781
self.close_count = 0
7882

7983
def start(self, sink: OverrideSink) -> None:
8084
self.start_count += 1
85+
if self._start_error is not None:
86+
raise self._start_error
8187
self._sink = sink
8288
sink.set_overrides(self._flags, self._segments)
8389

0 commit comments

Comments
 (0)