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
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: 2026 FinCore Engine Authors

package com.fincore.decision.eval

import com.fincore.decision.domain.DecimalValue
import com.fincore.decision.domain.EvaluationInput
import com.fincore.decision.parser.RuleParser
import io.kotest.assertions.withClue
import io.kotest.matchers.longs.shouldBeLessThan
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
import java.math.BigDecimal

private const val RULE_CONDITIONS = 100
private const val WARMUP_ITERATIONS = 5_000
private const val MEASURED_ITERATIONS = 10_000
private const val P99_CEILING_MILLIS = 10L
private const val NANOS_PER_MILLI = 1_000_000L

class EvaluatorLatencyGateTest {
private val evaluator = RuleEvaluator()
private val rule = RuleParser().parse(buildDsl())
private val input = buildInput()

@Test
fun `should produce a matching decision for the latency gate rule`() {
evaluator.evaluate(rule, input).matched shouldBe true
}

@Test
fun `should evaluate a 100 condition rule within the p99 latency ceiling`() {
repeat(WARMUP_ITERATIONS) { evaluator.evaluate(rule, input) }

val durations = LongArray(MEASURED_ITERATIONS)
var matchedAll = true
for (i in 0 until MEASURED_ITERATIONS) {
val start = System.nanoTime()
val result = evaluator.evaluate(rule, input)
durations[i] = System.nanoTime() - start
matchedAll = matchedAll && result.matched
}

matchedAll shouldBe true
durations.sort()
val p99Nanos = durations[durations.size - durations.size / 100 - 1]
withClue("p99=${p99Nanos / NANOS_PER_MILLI}ms ceiling=${P99_CEILING_MILLIS}ms") {
p99Nanos shouldBeLessThan P99_CEILING_MILLIS * NANOS_PER_MILLI
}
}

private fun buildDsl(): String {
val conditions = (1..RULE_CONDITIONS).joinToString(",") { """{"attr":"a$it","op":"gte","value":0}""" }
return """{"condition":{"all":[$conditions]},"outcome":{"label":"OK"}}"""
}

private fun buildInput(): EvaluationInput = EvaluationInput((1..RULE_CONDITIONS).associate { "a$it" to DecimalValue(BigDecimal.ONE) })
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.fincore.decision.domain.DecisionResult
import com.fincore.decision.store.persistence.DecisionLogEntity
import com.fincore.decision.store.persistence.DecisionLogRepository
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
import java.time.Instant
import java.util.UUID

Expand All @@ -20,6 +21,7 @@ class DecisionLogWriter(
private val decisionLogRepository: DecisionLogRepository,
private val objectMapper: ObjectMapper,
) {
@Transactional
fun write(
ruleVersionId: UUID,
inputHash: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import com.fincore.decision.store.persistence.DecisionRuleRepository
import com.fincore.decision.store.persistence.RuleVersionEntity
import com.fincore.decision.store.persistence.RuleVersionRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class EvaluationServiceImpl(
Expand All @@ -23,7 +22,6 @@ class EvaluationServiceImpl(
private val inputMapper: InputMapper,
private val logWriter: DecisionLogWriter,
) : EvaluationService {
@Transactional
override fun evaluate(
ruleKey: String,
attributes: Map<String, JsonNode>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: 2026 FinCore Engine Authors

package com.fincore.decision.store.application

import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.nulls.shouldNotBeNull
import org.junit.jupiter.api.Test
import org.springframework.core.annotation.AnnotatedElementUtils
import org.springframework.transaction.annotation.Transactional
import kotlin.reflect.jvm.javaMethod

class EvaluationTransactionBoundaryTest {
@Test
fun `should not wrap evaluate in a transaction so the bounded eval holds no connection`() {
val method = requireNotNull(EvaluationServiceImpl::evaluate.javaMethod)

AnnotatedElementUtils.findMergedAnnotation(method, Transactional::class.java).shouldBeNull()
AnnotatedElementUtils
.findMergedAnnotation(EvaluationServiceImpl::class.java, Transactional::class.java)
.shouldBeNull()
}

@Test
fun `should write the audit log in its own transaction`() {
val method = requireNotNull(DecisionLogWriter::write.javaMethod)

AnnotatedElementUtils.findMergedAnnotation(method, Transactional::class.java).shouldNotBeNull()
}
}
Loading