diff --git a/src/LaunchDarkly/Impl/Evaluation/Evaluator.php b/src/LaunchDarkly/Impl/Evaluation/Evaluator.php index 19551ee6..4450dd8d 100644 --- a/src/LaunchDarkly/Impl/Evaluation/Evaluator.php +++ b/src/LaunchDarkly/Impl/Evaluation/Evaluator.php @@ -70,13 +70,26 @@ public function evaluate(FeatureFlag $flag, LDContext $context, ?callable $prere return $evalResult; } catch (EvaluationException $e) { - return new EvalResult(new EvaluationDetail(null, null, EvaluationReason::error($e->getErrorKind())), false, $state); + $reason = self::withBigSegmentsStatus(EvaluationReason::error($e->getErrorKind()), $state); + return new EvalResult(new EvaluationDetail(null, null, $reason), false, $state); } catch (\Throwable $e) { Util::logExceptionAtErrorLevel($this->_logger, $e, 'Unexpected error when evaluating flag ' . $flag->getKey()); - return new EvalResult(new EvaluationDetail(null, null, EvaluationReason::exception($e)), false, $state); + $reason = self::withBigSegmentsStatus(EvaluationReason::exception($e), $state); + return new EvalResult(new EvaluationDetail(null, null, $reason), false, $state); } } + /** + * Adds the big segments status to an error reason. A big segment query can run before the error occurs. + */ + private static function withBigSegmentsStatus(EvaluationReason $reason, EvaluatorState $state): EvaluationReason + { + if ($state->bigSegmentsEvaluationStatus === null) { + return $reason; + } + return $reason->withBigSegmentsEvaluationStatus($state->bigSegmentsEvaluationStatus); + } + private function evaluateInternal( FeatureFlag $flag, LDContext $context, diff --git a/tests/Impl/Evaluation/EvaluatorBigSegmentsTest.php b/tests/Impl/Evaluation/EvaluatorBigSegmentsTest.php index 216ae4b0..51feb0e9 100644 --- a/tests/Impl/Evaluation/EvaluatorBigSegmentsTest.php +++ b/tests/Impl/Evaluation/EvaluatorBigSegmentsTest.php @@ -2,6 +2,8 @@ namespace LaunchDarkly\Tests\Impl\Evaluation; +use LaunchDarkly\BigSegmentsEvaluationStatus; +use LaunchDarkly\EvaluationReason; use LaunchDarkly\Impl\BigSegments\StoreManager; use LaunchDarkly\Impl\Evaluation\Evaluator; use LaunchDarkly\Impl\Model\Segment; @@ -160,6 +162,61 @@ public function testSegmentLogicFallsThroughToRulesWhenMembershipHasNoEntryForSe $this->assertTrue(self::bigSegmentsMatchesContext($store, $segment, $defaultContext)); } + public function testBigSegmentsStatusIsKeptWhenLaterSegmentCausesEvaluationError(): void + { + global $defaultContext; + $bigSegment = ModelBuilders::segmentBuilder('big') + ->generation(100) + ->unbounded(true) + ->build(); + // The circular segment causes an EvaluationException after the big segment query. + $circularSegment = ModelBuilders::segmentBuilder('circular') + ->rule( + ModelBuilders::segmentRuleBuilder() + ->clause(ModelBuilders::clause(null, '', 'segmentMatch', 'circular')) + ->build() + ) + ->build(); + + $store = new BigSegmentsStoreImpl([new BigSegmentsStoreMetadata(10)], [[]]); + $evaluator = self::getEvaluator($store, $defaultContext, [$bigSegment, $circularSegment]); + $flag = ModelBuilders::booleanFlagWithRules( + ModelBuilders::flagRuleWithClauses(1, ModelBuilders::clauseMatchingSegment($bigSegment)), + ModelBuilders::flagRuleWithClauses(1, ModelBuilders::clauseMatchingSegment($circularSegment)) + ); + + $reason = $evaluator->evaluate($flag, $defaultContext, EvaluatorTestUtil::expectNoPrerequisiteEvals()) + ->getDetail()->getReason(); + $this->assertEquals(EvaluationReason::ERROR, $reason->getKind()); + $this->assertEquals(EvaluationReason::MALFORMED_FLAG_ERROR, $reason->getErrorKind()); + $this->assertEquals(BigSegmentsEvaluationStatus::STALE, $reason->bigSegmentsEvaluationStatus()); + } + + public function testBigSegmentsStatusIsKeptWhenLaterSegmentLookupThrows(): void + { + global $defaultContext; + $bigSegment = ModelBuilders::segmentBuilder('big') + ->generation(100) + ->unbounded(true) + ->build(); + // The requester does not know this segment, so the lookup throws. + $unknownSegment = ModelBuilders::segmentBuilder('unknown')->build(); + + $store = new BigSegmentsStoreImpl([new BigSegmentsStoreMetadata(10)], [[]]); + $evaluator = self::getEvaluator($store, $defaultContext, [$bigSegment]); + $flag = ModelBuilders::booleanFlagWithRules( + ModelBuilders::flagRuleWithClauses(1, ModelBuilders::clauseMatchingSegment($bigSegment)), + ModelBuilders::flagRuleWithClauses(1, ModelBuilders::clauseMatchingSegment($unknownSegment)) + ); + + $reason = $evaluator->evaluate($flag, $defaultContext, EvaluatorTestUtil::expectNoPrerequisiteEvals()) + ->getDetail()->getReason(); + $this->assertEquals(EvaluationReason::ERROR, $reason->getKind()); + $this->assertEquals(EvaluationReason::EXCEPTION_ERROR, $reason->getErrorKind()); + $this->assertInstanceOf(\InvalidArgumentException::class, $reason->getException()); + $this->assertEquals(BigSegmentsEvaluationStatus::STALE, $reason->bigSegmentsEvaluationStatus()); + } + private static function bigSegmentsMatchesContext(BigSegmentsStore $store, Segment $segment, LDContext $context): bool { $evaluator = self::getEvaluator($store, $context, [$segment]);