Skip to content

Commit 5aeb04f

Browse files
committed
fix: Keep the override flags in the all flags state when the store cannot list its flags
When the store's all-items read fails, the client logs the failure as before and, if the override layer holds flags, returns a state of those flags marked invalid instead of an empty one. Overrides only add to the state, and a client-side SDK bootstrapped from it uses its flags whatever $valid says, so the overridden values still reach it. With no override flags the state is the same invalid, empty state as without a source.
1 parent 838de68 commit 5aeb04f

4 files changed

Lines changed: 61 additions & 10 deletions

File tree

‎ldclient/async_client.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ async def all_flags_state(self, context: Context, **kwargs) -> FeatureFlagsState
608608
log.warning("Context was invalid for all_flags_state (%s); returning default value" % context.error)
609609
return FeatureFlagsState(False)
610610

611-
state = FeatureFlagsState(True)
611+
valid = True
612612
client_only = kwargs.get('client_side_only', False)
613613
with_reasons = kwargs.get('with_reasons', False)
614614
details_only_if_tracked = kwargs.get('details_only_for_tracked_flags', False)
@@ -624,8 +624,18 @@ async def all_flags_state(self, context: Context, **kwargs) -> FeatureFlagsState
624624
raise ValueError("feature store error")
625625
except Exception as e:
626626
log.error("Unable to read flags for all_flag_state: %s" % repr(e))
627-
return FeatureFlagsState(False)
627+
# Overrides only add to the state. Without them the state is invalid and empty
628+
# here, so it holds the override layer's flags and stays invalid. A client-side
629+
# SDK bootstrapped from the state uses its flags whatever $valid says (the
630+
# JavaScript client SDKs 3.x and 4.x only warn), so the overridden values still
631+
# reach it.
632+
layer = self._data_system.override_layer
633+
flags_map = layer.all(FEATURES) if layer is not None else {}
634+
if len(flags_map) == 0:
635+
return FeatureFlagsState(False)
636+
valid = False
628637

638+
state = FeatureFlagsState(valid)
629639
for key, flag in flags_map.items():
630640
if client_only and not flag.get('clientSide', False):
631641
continue

‎ldclient/client.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ def all_flags_state(self, context: Context, **kwargs) -> FeatureFlagsState:
598598
log.warning("Context was invalid for all_flags_state (%s); returning default value" % context.error)
599599
return FeatureFlagsState(False)
600600

601-
state = FeatureFlagsState(True)
601+
valid = True
602602
client_only = kwargs.get('client_side_only', False)
603603
with_reasons = kwargs.get('with_reasons', False)
604604
details_only_if_tracked = kwargs.get('details_only_for_tracked_flags', False)
@@ -616,8 +616,18 @@ def all_flags_state(self, context: Context, **kwargs) -> FeatureFlagsState:
616616
raise ValueError("feature store error")
617617
except Exception as e:
618618
log.error("Unable to read flags for all_flag_state: %s" % repr(e))
619-
return FeatureFlagsState(False)
619+
# Overrides only add to the state. Without them the state is invalid and empty
620+
# here, so it holds the override layer's flags and stays invalid. A client-side
621+
# SDK bootstrapped from the state uses its flags whatever $valid says (the
622+
# JavaScript client SDKs 3.x and 4.x only warn), so the overridden values still
623+
# reach it.
624+
layer = self._data_system.override_layer
625+
flags_map = layer.all(FEATURES) if layer is not None else {}
626+
if len(flags_map) == 0:
627+
return FeatureFlagsState(False)
628+
valid = False
620629

630+
state = FeatureFlagsState(valid)
621631
for key, flag in flags_map.items():
622632
if client_only and not flag.get('clientSide', False):
623633
continue

‎ldclient/testing/test_async_client_overrides.py‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,14 +252,32 @@ async def test_all_flags_state_before_initialization_excludes_launchdarkly_data_
252252

253253

254254
@pytest.mark.asyncio
255-
async def test_all_flags_state_is_invalid_when_the_store_fails_even_with_overrides(caplog):
256-
# A daemon-mode client whose store cannot list its flags reports an invalid state and logs
257-
# the failure, as it does without a source, rather than a valid state of the overrides alone.
255+
async def test_all_flags_state_holds_the_overrides_and_is_invalid_when_the_store_fails(caplog):
256+
# A daemon-mode client whose store cannot list its flags logs the failure, as it does without
257+
# a source. Overrides only add to the state, so it holds the override flags, and it stays
258+
# invalid because the store's flags are missing.
258259
source = MockOverrideSource(flags={'overridden-flag': single_value_flag('overridden-flag', True)})
259260
datasystem = AsyncDataSystemConfig(override_source=source.builder, data_store=AsyncStoreThatCannotListItems(), data_store_mode=DataStoreMode.READ_ONLY)
260261
config = AsyncConfig('SDK_KEY', datasystem_config=datasystem, event_processor_class=lambda config: MockAsyncEventProcessor())
261262
client = AsyncLDClient(config)
262263
await client.start(start_wait=0)
264+
try:
265+
state = await client.all_flags_state(user, with_reasons=True)
266+
assert state.valid is False
267+
assert state.to_values_map() == {'overridden-flag': True}
268+
assert state.get_flag_reason('overridden-flag') == {'kind': 'FALLTHROUGH', 'overrideAffected': True}
269+
assert records_containing(caplog, logging.ERROR, 'Unable to read flags for all_flag_state') != []
270+
finally:
271+
await client.close()
272+
273+
274+
@pytest.mark.asyncio
275+
async def test_all_flags_state_is_invalid_and_empty_when_the_store_fails_and_the_layer_holds_no_flags(caplog):
276+
source = MockOverrideSource()
277+
datasystem = AsyncDataSystemConfig(override_source=source.builder, data_store=AsyncStoreThatCannotListItems(), data_store_mode=DataStoreMode.READ_ONLY)
278+
config = AsyncConfig('SDK_KEY', datasystem_config=datasystem, event_processor_class=lambda config: MockAsyncEventProcessor())
279+
client = AsyncLDClient(config)
280+
await client.start(start_wait=0)
263281
try:
264282
state = await client.all_flags_state(user)
265283
assert state.valid is False

‎ldclient/testing/test_ldclient_overrides.py‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,25 @@ def test_all_flags_state_before_initialization_excludes_launchdarkly_data_from_a
208208
assert state.to_values_map() == {'overridden-flag': True}
209209

210210

211-
def test_all_flags_state_is_invalid_when_the_store_fails_even_with_overrides(caplog):
212-
# A daemon-mode client whose store cannot list its flags reports an invalid state and logs
213-
# the failure, as it does without a source, rather than a valid state of the overrides alone.
211+
def test_all_flags_state_holds_the_overrides_and_is_invalid_when_the_store_fails(caplog):
212+
# A daemon-mode client whose store cannot list its flags logs the failure, as it does without
213+
# a source. Overrides only add to the state, so it holds the override flags, and it stays
214+
# invalid because the store's flags are missing.
214215
source = MockOverrideSource(flags={'overridden-flag': single_value_flag('overridden-flag', True)})
215216
datasystem = daemon(StoreThatCannotListItems()).overrides(source.builder).build()
216217
config = Config(sdk_key='SDK_KEY', datasystem_config=datasystem, event_processor_class=MockEventProcessor)
218+
with LDClient(config, start_wait=0) as client:
219+
state = client.all_flags_state(user, with_reasons=True)
220+
assert state.valid is False
221+
assert state.to_values_map() == {'overridden-flag': True}
222+
assert state.get_flag_reason('overridden-flag') == {'kind': 'FALLTHROUGH', 'overrideAffected': True}
223+
assert errors_containing(caplog, 'Unable to read flags for all_flag_state') != []
224+
225+
226+
def test_all_flags_state_is_invalid_and_empty_when_the_store_fails_and_the_layer_holds_no_flags(caplog):
227+
source = MockOverrideSource()
228+
datasystem = daemon(StoreThatCannotListItems()).overrides(source.builder).build()
229+
config = Config(sdk_key='SDK_KEY', datasystem_config=datasystem, event_processor_class=MockEventProcessor)
217230
with LDClient(config, start_wait=0) as client:
218231
state = client.all_flags_state(user)
219232
assert state.valid is False

0 commit comments

Comments
 (0)