Skip to content
Merged
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
6 changes: 6 additions & 0 deletions ldclient/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ def reason(self) -> dict:
* ``inExperiment``: This is true if the evaluation resulted in an experiment rollout and
served one of the variations in the experiment. Otherwise it is false.

* ``overrideAffected``: This is true if a flag override affected the evaluation, directly
or indirectly. It is present only when true. An evaluation is affected when the flag
itself, a prerequisite flag at any depth, or a segment consulted during rule matching
came from the SDK's override store. Flag overrides are currently experimental and
subject to change.

* ``bigSegmentsStatus``: describes the validity of Big Segment information, if and only if
the flag evaluation required querying at least one Big Segment; otherwise it returns None.
Allowable values are defined in :class:`BigSegmentsStatus`. For more information, read the
Expand Down
38 changes: 33 additions & 5 deletions ldclient/impl/async_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
_match_clause_by_kind,
_match_single_context_value,
_maybe_negate,
check_targets
check_targets,
mark_override_affected
)
from ldclient.impl.events.types import EventFactory
from ldclient.impl.model import *
Expand Down Expand Up @@ -56,15 +57,23 @@ def __init__(
async def evaluate(self, flag: FeatureFlag, context: Context, event_factory: EventFactory) -> EvalResult:
state = EvalResult()
state.original_flag_key = flag.key
# Reading the flag's own definition is the first read of the evaluation, so the marking
# starts from the flag's override marker.
state.override_affected = flag.is_override
try:
state.detail = await self._evaluate(flag, context, state, event_factory)
except EvaluationException as e:
if self.__logger is not None:
self.__logger.error('Could not evaluate flag "%s": %s' % (flag.key, e.message))
state.detail = EvaluationDetail(None, None, {'kind': 'ERROR', 'errorKind': e.error_kind})
# An evaluation that fails is still marked when it read an override definition.
if state.override_affected:
mark_override_affected(state.detail)
return state
if state.big_segments_status is not None:
state.detail.reason['bigSegmentsStatus'] = state.big_segments_status
if state.override_affected:
mark_override_affected(state.detail)
return state

async def _evaluate(self, flag: FeatureFlag, context: Context, state: EvalResult, event_factory: EventFactory) -> EvaluationDetail:
Expand Down Expand Up @@ -120,9 +129,21 @@ async def _check_prerequisites(self, flag: FeatureFlag, context: Context, state:
log.warning("Missing prereq flag: " + prereq_key)
failed_prereq = prereq
else:
state.depth += 1
prereq_res = await self._evaluate(prereq_flag, context, state, event_factory)
state.depth -= 1
# The prerequisite is an evaluation in its own right. Its marking starts from
# its own definition and covers only the definitions its subtree reads. The
# parent's marking is restored and merged afterward, so the marking propagates
# upward only. A failure inside the subtree still merges what was read.
parent_override_affected = state.override_affected
state.override_affected = prereq_flag.is_override
try:
state.depth += 1
prereq_res = await self._evaluate(prereq_flag, context, state, event_factory)
state.depth -= 1
finally:
prereq_override_affected = state.override_affected
state.override_affected = parent_override_affected or prereq_override_affected
if prereq_override_affected:
mark_override_affected(prereq_res)
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
# off variation was. But we still need to evaluate it in order to generate an event.
if (not prereq_flag.on) or prereq_res.variation_index != prereq.variation:
Expand All @@ -146,7 +167,14 @@ async def _clause_matches_context(self, clause: Clause, context: Context, state:
if clause.op == 'segmentMatch':
for seg_key in clause.values:
segment = await self.__get_segment(seg_key)
if segment is not None and await self._segment_matches_context(segment, context, state):
if segment is None:
continue
# The segment definition was read at this point, so an override segment marks the
# evaluation whether or not it matches. A segment that does not match still shapes
# the outcome, for example through a negated clause.
if segment.is_override:
state.override_affected = True
if await self._segment_matches_context(segment, context, state):
return _maybe_negate(clause, True)
return _maybe_negate(clause, False)

Expand Down
38 changes: 33 additions & 5 deletions ldclient/impl/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
_maybe_negate,
_variation_index_for_context,
check_targets,
error_reason
error_reason,
mark_override_affected
)
from ldclient.impl.events.types import EventFactory
from ldclient.impl.model import *
Expand Down Expand Up @@ -57,15 +58,23 @@ def __init__(
def evaluate(self, flag: FeatureFlag, context: Context, event_factory: EventFactory) -> EvalResult:
state = EvalResult()
state.original_flag_key = flag.key
# Reading the flag's own definition is the first read of the evaluation, so the marking
# starts from the flag's override marker.
state.override_affected = flag.is_override
try:
state.detail = self._evaluate(flag, context, state, event_factory)
except EvaluationException as e:
if self.__logger is not None:
self.__logger.error('Could not evaluate flag "%s": %s' % (flag.key, e.message))
state.detail = EvaluationDetail(None, None, {'kind': 'ERROR', 'errorKind': e.error_kind})
# An evaluation that fails is still marked when it read an override definition.
if state.override_affected:
mark_override_affected(state.detail)
return state
if state.big_segments_status is not None:
state.detail.reason['bigSegmentsStatus'] = state.big_segments_status
if state.override_affected:
mark_override_affected(state.detail)
return state

def _evaluate(self, flag: FeatureFlag, context: Context, state: EvalResult, event_factory: EventFactory) -> EvaluationDetail:
Expand Down Expand Up @@ -118,9 +127,21 @@ def _check_prerequisites(self, flag: FeatureFlag, context: Context, state: EvalR
log.warning("Missing prereq flag: " + prereq_key)
failed_prereq = prereq
else:
state.depth += 1
prereq_res = self._evaluate(prereq_flag, context, state, event_factory)
state.depth -= 1
# The prerequisite is an evaluation in its own right. Its marking starts from
# its own definition and covers only the definitions its subtree reads. The
# parent's marking is restored and merged afterward, so the marking propagates
# upward only. A failure inside the subtree still merges what was read.
parent_override_affected = state.override_affected
state.override_affected = prereq_flag.is_override
try:
state.depth += 1
prereq_res = self._evaluate(prereq_flag, context, state, event_factory)
state.depth -= 1
finally:
prereq_override_affected = state.override_affected
state.override_affected = parent_override_affected or prereq_override_affected
if prereq_override_affected:
mark_override_affected(prereq_res)
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
# off variation was. But we still need to evaluate it in order to generate an event.
if (not prereq_flag.on) or prereq_res.variation_index != prereq.variation:
Expand All @@ -144,7 +165,14 @@ def _clause_matches_context(self, clause: Clause, context: Context, state: EvalR
if clause.op == 'segmentMatch':
for seg_key in clause.values:
segment = self.__get_segment(seg_key)
if segment is not None and self._segment_matches_context(segment, context, state):
if segment is None:
continue
# The segment definition was read at this point, so an override segment marks the
# evaluation whether or not it matches. A segment that does not match still shapes
# the outcome, for example through a negated clause.
if segment.is_override:
state.override_affected = True
if self._segment_matches_context(segment, context, state):
return _maybe_negate(clause, True)
return _maybe_negate(clause, False)

Expand Down
16 changes: 15 additions & 1 deletion ldclient/impl/evaluator_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
# prerequisite evaluations, and the cached state of any Big Segments query that we may have
# ended up having to do for the context.
class EvalResult:
__slots__ = ['detail', 'events', 'big_segments_status', 'big_segments_membership', 'original_flag_key', 'prereq_stack', 'segment_stack', 'depth', 'prerequisites']
__slots__ = ['detail', 'events', 'big_segments_status', 'big_segments_membership', 'original_flag_key', 'prereq_stack', 'segment_stack', 'depth', 'prerequisites', 'override_affected']

def __init__(self):
self.detail = None
Expand All @@ -37,6 +37,11 @@ def __init__(self):
self.segment_stack = None # type: Optional[List[str]]
self.depth = 0
self.prerequisites = [] # type: List[str]
# True when the evaluation in progress has read a flag or segment definition that carries
# the override marker. While a prerequisite is evaluated, this holds the marking of the
# prerequisite's own subtree. The evaluator merges it into the parent's marking afterward,
# so the marking propagates upward only.
self.override_affected = False

def record_prerequisite(self, key: str):
if self.depth == 0:
Expand Down Expand Up @@ -255,3 +260,12 @@ def check_targets(flag: FeatureFlag, context: Context) -> Optional[EvaluationDet

def error_reason(error_kind: str) -> dict:
return {'kind': 'ERROR', 'errorKind': error_kind}


def mark_override_affected(detail: EvaluationDetail) -> None:
"""
Sets the override-affected indicator on the evaluation reason. The indicator is present
only when true, so a reason that was not affected by an override is left untouched and
serializes exactly as before.
"""
detail.reason['overrideAffected'] = True
Loading
Loading