Skip to content

Commit 73e9b07

Browse files
authored
fix: Return empty prerequisites for a flag that fails to evaluate in all_flags_state (#483)
1 parent cca37a8 commit 73e9b07

2 files changed

Lines changed: 43 additions & 1 deletion

File tree

ldclient/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,11 +640,13 @@ def all_flags_state(self, context: Context, **kwargs) -> FeatureFlagsState:
640640
try:
641641
result = self._evaluator.evaluate(flag, context, self._event_factory_default)
642642
detail = result.detail
643+
prerequisites = result.prerequisites
643644
except Exception as e:
644645
log.error("Error evaluating flag \"%s\" in all_flags_state: %s" % (key, repr(e)))
645646
log.debug(traceback.format_exc())
646647
reason = {'kind': 'ERROR', 'errorKind': 'EXCEPTION'}
647648
detail = EvaluationDetail(None, None, reason)
649+
prerequisites = []
648650

649651
requires_experiment_data = EventFactory.is_experiment(flag, detail.reason)
650652
flag_state = {
@@ -653,7 +655,7 @@ def all_flags_state(self, context: Context, **kwargs) -> FeatureFlagsState:
653655
'variation': detail.variation_index,
654656
'reason': detail.reason,
655657
'version': flag['version'],
656-
'prerequisites': result.prerequisites,
658+
'prerequisites': prerequisites,
657659
'trackEvents': flag.get('trackEvents', False) or requires_experiment_data,
658660
'trackReason': requires_experiment_data,
659661
'debugEventsUntilDate': flag.get('debugEventsUntilDate', None),

ldclient/testing/test_ldclient_evaluation.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,43 @@ def test_all_flags_returns_empty_state_if_feature_store_throws_error(caplog):
342342
assert state.valid is False
343343
errlog = get_log_lines(caplog, 'ERROR')
344344
assert errlog == ['Unable to read flags for all_flag_state: NotImplementedError()']
345+
346+
347+
def test_all_flags_state_degrades_per_flag_on_evaluator_error():
348+
# A flag whose evaluation raises degrades only that flag: the loop must not
349+
# raise UnboundLocalError when the first flag raises, and a failed flag must
350+
# not inherit a previous good flag's prerequisites.
351+
from unittest.mock import MagicMock
352+
353+
store = InMemoryFeatureStore()
354+
# Ordering matters: 'bad-first' raises on the first iteration (would
355+
# UnboundLocalError if result were read unconditionally); 'bad-last' raises
356+
# after a good flag set result (would reuse the good result's prerequisites).
357+
store.init({FEATURES: {
358+
'bad-first': {'key': 'bad-first', 'version': 1, 'on': True, 'fallthrough': {'variation': 0}, 'variations': ['x']},
359+
'good': {'key': 'good', 'version': 1, 'on': True, 'fallthrough': {'variation': 0}, 'variations': ['y']},
360+
'bad-last': {'key': 'bad-last', 'version': 1, 'on': True, 'fallthrough': {'variation': 0}, 'variations': ['z']},
361+
}})
362+
client = make_client(store)
363+
364+
good_result = MagicMock()
365+
good_result.detail = EvaluationDetail('y', 0, {'kind': 'FALLTHROUGH'})
366+
good_result.prerequisites = ['prereq-of-good']
367+
368+
def fake_evaluate(flag, context, event_factory):
369+
if flag['key'] == 'good':
370+
return good_result
371+
raise RuntimeError("boom")
372+
373+
client._evaluator.evaluate = MagicMock(side_effect=fake_evaluate)
374+
375+
# This must not raise UnboundLocalError.
376+
state = client.all_flags_state(user)
377+
assert state.valid
378+
379+
metadata = state.to_json_dict()['$flagsState']
380+
# The good flag carries its own prerequisites; the failed flags carry none
381+
# (not a neighbor's).
382+
assert metadata['good'].get('prerequisites') == ['prereq-of-good']
383+
assert 'prerequisites' not in metadata['bad-first']
384+
assert 'prerequisites' not in metadata['bad-last']

0 commit comments

Comments
 (0)