Skip to content

Commit 4039ff4

Browse files
Merge pull request #181 from dhis2/DHIS2-21245
perf: Analyze rules before running to limit context [DHIS2-21245]
2 parents 86be3ee + 379f78d commit 4039ff4

12 files changed

Lines changed: 554 additions & 18 deletions

File tree

api/rule-engine.api

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,32 @@ public final class org/hisp/dhis/rules/api/ItemValueType : java/lang/Enum {
6363
public static fun values ()[Lorg/hisp/dhis/rules/api/ItemValueType;
6464
}
6565

66+
public final class org/hisp/dhis/rules/api/RuleContextRequirements {
67+
public static final field Companion Lorg/hisp/dhis/rules/api/RuleContextRequirements$Companion;
68+
public fun <init> (ZZZZZ)V
69+
public final fun component1 ()Z
70+
public final fun component2 ()Z
71+
public final fun component3 ()Z
72+
public final fun component4 ()Z
73+
public final fun component5 ()Z
74+
public final fun copy (ZZZZZ)Lorg/hisp/dhis/rules/api/RuleContextRequirements;
75+
public static synthetic fun copy$default (Lorg/hisp/dhis/rules/api/RuleContextRequirements;ZZZZZILjava/lang/Object;)Lorg/hisp/dhis/rules/api/RuleContextRequirements;
76+
public fun equals (Ljava/lang/Object;)Z
77+
public final fun getNeedsAllEvents ()Z
78+
public final fun getNeedsAttributes ()Z
79+
public final fun getNeedsDataValues ()Z
80+
public final fun getNeedsEnrollment ()Z
81+
public final fun getNeedsOrgUnitGroups ()Z
82+
public fun hashCode ()I
83+
public fun toString ()Ljava/lang/String;
84+
}
85+
86+
public final class org/hisp/dhis/rules/api/RuleContextRequirements$Companion {
87+
}
88+
6689
public abstract interface class org/hisp/dhis/rules/api/RuleEngine {
6790
public static final field Companion Lorg/hisp/dhis/rules/api/RuleEngine$Companion;
91+
public abstract fun analyzeContextRequirements (Ljava/util/List;Ljava/util/List;)Lorg/hisp/dhis/rules/api/RuleContextRequirements;
6892
public abstract fun evaluate (Lorg/hisp/dhis/rules/models/RuleEnrollment;Ljava/util/List;Lorg/hisp/dhis/rules/api/RuleEngineContext;)Ljava/util/List;
6993
public abstract fun evaluate (Lorg/hisp/dhis/rules/models/RuleEvent;Lorg/hisp/dhis/rules/models/RuleEnrollment;Ljava/util/List;Lorg/hisp/dhis/rules/api/RuleEngineContext;)Ljava/util/List;
7094
public abstract fun evaluateAll (Lorg/hisp/dhis/rules/models/RuleEnrollment;Ljava/util/List;Lorg/hisp/dhis/rules/api/RuleEngineContext;)Ljava/util/List;

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ repositories {
1212
maven { url = uri("https://central.sonatype.com/repository/maven-snapshots/") }
1313
}
1414

15-
version = "3.7.2-SNAPSHOT"
15+
version = "3.8.0-SNAPSHOT"
1616
group = "org.hisp.dhis.rules"
1717

1818
if (project.hasProperty("removeSnapshotSuffix")) {

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ sonarqube = "7.2.3.7755"
1010
kotlinxDatetime = "0.7.1"
1111
kotlinJsWrappers = "1.0.0-pre.830"
1212
slf4jApi = "1.7.36"
13-
expressionParser = "1.4.0"
13+
expressionParser = "1.4.1"
1414

1515
[libraries]
1616
kotlin-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.hisp.dhis.rules.api
2+
3+
import kotlin.js.JsExport
4+
5+
@JsExport
6+
data class RuleContextRequirements(
7+
val needsAllEvents: Boolean,
8+
val needsEnrollment: Boolean,
9+
val needsDataValues: Boolean,
10+
val needsAttributes: Boolean,
11+
val needsOrgUnitGroups: Boolean,
12+
) {
13+
companion object {
14+
internal val NONE = RuleContextRequirements(
15+
needsAllEvents = false,
16+
needsEnrollment = false,
17+
needsDataValues = false,
18+
needsAttributes = false,
19+
needsOrgUnitGroups = false,
20+
)
21+
}
22+
}

src/commonMain/kotlin/org/hisp/dhis/rules/api/RuleEngine.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import org.hisp.dhis.rules.models.*
55
import kotlin.jvm.JvmStatic
66

77
interface RuleEngine {
8+
fun analyzeContextRequirements(rules: List<Rule>, variables: List<RuleVariable>): RuleContextRequirements
9+
810
fun validate(
911
expression: String,
1012
dataItemStore: Map<String, DataItem>,

src/commonMain/kotlin/org/hisp/dhis/rules/engine/DefaultRuleEngine.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.hisp.dhis.lib.expression.spi.IllegalExpressionException
66
import org.hisp.dhis.lib.expression.spi.ParseException
77
import org.hisp.dhis.lib.expression.spi.ValueType
88
import org.hisp.dhis.rules.api.DataItem
9+
import org.hisp.dhis.rules.api.RuleContextRequirements
910
import org.hisp.dhis.rules.api.RuleEngine
1011
import org.hisp.dhis.rules.api.RuleEngineContext
1112
import org.hisp.dhis.rules.models.*
@@ -74,6 +75,10 @@ internal class DefaultRuleEngine : RuleEngine {
7475
)
7576
}
7677

78+
override fun analyzeContextRequirements(rules: List<Rule>, variables: List<RuleVariable>): RuleContextRequirements {
79+
return RuleEngineAnalyzer.analyzeContextRequirements(rules, variables)
80+
}
81+
7782
override fun validate(
7883
expression: String,
7984
dataItemStore: Map<String, DataItem>,
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package org.hisp.dhis.rules.engine
2+
3+
import org.hisp.dhis.lib.expression.Expression
4+
import org.hisp.dhis.rules.api.RuleContextRequirements
5+
import org.hisp.dhis.rules.models.Rule
6+
import org.hisp.dhis.rules.models.RuleVariable
7+
import org.hisp.dhis.rules.models.RuleVariableAttribute
8+
import org.hisp.dhis.rules.models.RuleVariableCalculatedValue
9+
import org.hisp.dhis.rules.models.RuleVariableCurrentEvent
10+
import org.hisp.dhis.rules.models.RuleVariableNewestEvent
11+
import org.hisp.dhis.rules.models.RuleVariableNewestStageEvent
12+
import org.hisp.dhis.rules.models.RuleVariablePreviousEvent
13+
import org.hisp.dhis.rules.utils.RuleEngineUtils.ENV_VAR_ENROLLMENT_COUNT
14+
import org.hisp.dhis.rules.utils.RuleEngineUtils.ENV_VAR_ENROLLMENT_DATE
15+
import org.hisp.dhis.rules.utils.RuleEngineUtils.ENV_VAR_ENROLLMENT_ID
16+
import org.hisp.dhis.rules.utils.RuleEngineUtils.ENV_VAR_ENROLLMENT_STATUS
17+
import org.hisp.dhis.rules.utils.RuleEngineUtils.ENV_VAR_EVENT_COUNT
18+
import org.hisp.dhis.rules.utils.RuleEngineUtils.ENV_VAR_TEI_COUNT
19+
20+
internal object RuleEngineAnalyzer {
21+
private val MULTI_EVENT_ENV_VARS = setOf(ENV_VAR_EVENT_COUNT)
22+
private val ENROLLMENT_ENV_VARS = setOf(
23+
ENV_VAR_ENROLLMENT_COUNT,
24+
ENV_VAR_ENROLLMENT_DATE,
25+
ENV_VAR_ENROLLMENT_ID,
26+
ENV_VAR_ENROLLMENT_STATUS,
27+
ENV_VAR_TEI_COUNT,
28+
)
29+
30+
fun analyzeContextRequirements(
31+
rules: List<Rule>,
32+
variables: List<RuleVariable>,
33+
): RuleContextRequirements {
34+
val byName = variables.associateBy { it.name }
35+
val hasNonCalculatedVariables = variables.any { it !is RuleVariableCalculatedValue }
36+
val acc = RequirementsAccumulator(byName)
37+
38+
for (rule in rules) {
39+
if (acc.isComplete) break
40+
for (expr in rule.expressions()) {
41+
acc.processEnvVars(expr)
42+
if (hasNonCalculatedVariables) acc.processVarNames(expr)
43+
acc.processOrgUnitGroups(expr)
44+
}
45+
}
46+
47+
return acc.toRequirements()
48+
}
49+
50+
private fun Rule.expressions(): List<Expression> = buildList {
51+
conditionExpression.getOrNull()?.let { add(it) }
52+
actions.forEach { action -> action.dataExpression.getOrNull()?.let { add(it) } }
53+
}
54+
55+
private class RequirementsAccumulator(private val byName: Map<String, RuleVariable>) {
56+
var needsAllEvents = false
57+
var needsEnrollment = false
58+
var needsDataValues = false
59+
var needsAttributes = false
60+
var needsOrgUnitGroups = false
61+
62+
val isComplete get() = needsAllEvents && needsEnrollment && needsDataValues && needsAttributes && needsOrgUnitGroups
63+
64+
fun processEnvVars(expr: Expression) {
65+
if (needsAllEvents && needsEnrollment) return
66+
for (envVar in expr.collectProgramVariablesNames()) {
67+
needsAllEvents = needsAllEvents || envVar in MULTI_EVENT_ENV_VARS
68+
needsEnrollment = needsEnrollment || envVar in ENROLLMENT_ENV_VARS
69+
if (needsAllEvents && needsEnrollment) break
70+
}
71+
}
72+
73+
fun processVarNames(expr: Expression) {
74+
if (needsAllEvents && needsDataValues && needsAttributes) return
75+
for (name in expr.collectProgramRuleVariableNames()) {
76+
val v = byName[name]
77+
val isMultiEvent = v is RuleVariableNewestEvent || v is RuleVariableNewestStageEvent || v is RuleVariablePreviousEvent
78+
needsAllEvents = needsAllEvents || isMultiEvent
79+
needsDataValues = needsDataValues || isMultiEvent || v is RuleVariableCurrentEvent
80+
needsAttributes = needsAttributes || v is RuleVariableAttribute
81+
needsEnrollment = needsEnrollment || needsAttributes
82+
if (needsAllEvents && needsDataValues && needsAttributes) break
83+
}
84+
}
85+
86+
fun processOrgUnitGroups(expr: Expression) {
87+
if (!needsOrgUnitGroups) needsOrgUnitGroups = expr.collectInOrgUnitGroups().isNotEmpty()
88+
}
89+
90+
fun toRequirements() = RuleContextRequirements(
91+
needsAllEvents, needsEnrollment, needsDataValues, needsAttributes, needsOrgUnitGroups,
92+
)
93+
}
94+
}

src/commonMain/kotlin/org/hisp/dhis/rules/utils/RuleEngineUtils.kt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,23 @@ package org.hisp.dhis.rules.utils
3232
* @author Zubair Asghar
3333
*/
3434
internal object RuleEngineUtils {
35-
const val ENV_VAR_CURRENT_DATE = "current_date"
3635
const val ENV_VAR_COMPLETED_DATE = "completed_date"
37-
const val ENV_VAR_EVENT_DATE = "event_date"
38-
const val ENV_VAR_EVENT_COUNT = "event_count"
36+
const val ENV_VAR_CURRENT_DATE = "current_date"
3937
const val ENV_VAR_DUE_DATE = "due_date"
40-
const val ENV_VAR_EVENT_ID = "event_id"
38+
const val ENV_VAR_ENROLLMENT_COUNT = "enrollment_count"
4139
const val ENV_VAR_ENROLLMENT_DATE = "enrollment_date"
4240
const val ENV_VAR_ENROLLMENT_ID = "enrollment_id"
43-
const val ENV_VAR_ENROLLMENT_COUNT = "enrollment_count"
44-
const val ENV_VAR_INCIDENT_DATE = "incident_date"
45-
const val ENV_VAR_TEI_COUNT = "tei_count"
41+
const val ENV_VAR_ENROLLMENT_STATUS = "enrollment_status"
42+
const val ENV_VAR_ENVIRONMENT = "environment"
43+
const val ENV_VAR_EVENT_COUNT = "event_count"
44+
const val ENV_VAR_EVENT_DATE = "event_date"
45+
const val ENV_VAR_EVENT_ID = "event_id"
4646
const val ENV_VAR_EVENT_STATUS = "event_status"
47+
const val ENV_VAR_INCIDENT_DATE = "incident_date"
4748
const val ENV_VAR_OU = "org_unit"
48-
const val ENV_VAR_ENROLLMENT_STATUS = "enrollment_status"
49+
const val ENV_VAR_OU_CODE = "orgunit_code"
50+
const val ENV_VAR_PROGRAM_NAME = "program_name"
4951
const val ENV_VAR_PROGRAM_STAGE_ID = "program_stage_id"
5052
const val ENV_VAR_PROGRAM_STAGE_NAME = "program_stage_name"
51-
const val ENV_VAR_PROGRAM_NAME = "program_name"
52-
const val ENV_VAR_ENVIRONMENT = "environment"
53-
const val ENV_VAR_OU_CODE = "orgunit_code"
53+
const val ENV_VAR_TEI_COUNT = "tei_count"
5454
}

src/commonMain/kotlin/org/hisp/dhis/rules/utils/RuleEventUtils.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,3 @@ internal fun filterRules(
5858
ruleEvent: RuleEvent,
5959
): List<Rule> =
6060
rules.filter { it.programStage.isNullOrEmpty() || it.programStage == ruleEvent.programStage }
61-
62-
internal fun isAllowedAction(ruleAction: RuleAction, attributeType: AttributeType): Boolean =
63-
ruleAction.attributeType() == null ||
64-
ruleAction.attributeType() == attributeType.name ||
65-
ruleAction.attributeType() == AttributeType.UNKNOWN.name

0 commit comments

Comments
 (0)