Skip to content

Commit d09faa8

Browse files
authored
perf: optimize rule engine with Flow-based lazy evaluation (#677)
- Replace Sequence with Flow for suspend function compatibility - Enable early termination in aggregation modes (SINGLE, ANY, IntRuleMode) - Add AnyKit utilities for lenient type conversion (asInt, asBoolean) - Properly handle CancellationException for Flow exception transparency - Add comprehensive documentation to RuleModes and AnyKit This addresses performance issues by: - Short-circuiting rule evaluation when terminal operators find satisfying results - Avoiding eager collection of all rule results when only first is needed Closes #669
1 parent ccf01ca commit d09faa8

6 files changed

Lines changed: 567 additions & 225 deletions

File tree

src/main/kotlin/com/itangcent/easyapi/rule/RuleKey.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ package com.itangcent.easyapi.rule
1616
*/
1717
sealed class RuleKey<T>(
1818
val name: String,
19-
val mode: RuleMode,
19+
val mode: RuleMode<T>,
2020
val aliases: List<String> = emptyList()
2121
) {
2222
/** All config key names (primary + aliases) that should be looked up. */
@@ -26,7 +26,7 @@ sealed class RuleKey<T>(
2626
name: String,
2727
mode: StringRuleMode = StringRuleMode.SINGLE,
2828
aliases: List<String> = emptyList()
29-
) : RuleKey<String?>(name, mode, aliases) {
29+
) : RuleKey<String>(name, mode, aliases) {
3030
val stringMode: StringRuleMode get() = mode as StringRuleMode
3131
}
3232

@@ -41,7 +41,7 @@ sealed class RuleKey<T>(
4141
class IntKey(
4242
name: String,
4343
aliases: List<String> = emptyList()
44-
) : RuleKey<Int?>(name, IntRuleMode, aliases)
44+
) : RuleKey<Int>(name, IntRuleMode, aliases)
4545

4646
class EventKey(
4747
name: String,
@@ -56,9 +56,12 @@ sealed class RuleKey<T>(
5656
companion object {
5757
fun string(name: String, mode: StringRuleMode = StringRuleMode.SINGLE, aliases: List<String> = emptyList()) =
5858
StringKey(name, mode, aliases)
59+
5960
fun boolean(name: String, mode: BooleanRuleMode = BooleanRuleMode.ANY, aliases: List<String> = emptyList()) =
6061
BooleanKey(name, mode, aliases)
62+
6163
fun int(name: String, aliases: List<String> = emptyList()) = IntKey(name, aliases)
64+
6265
fun event(name: String, mode: EventRuleMode = EventRuleMode.IGNORE_ERROR, aliases: List<String> = emptyList()) =
6366
EventKey(name, mode, aliases)
6467
}
Lines changed: 115 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,61 @@
11
package com.itangcent.easyapi.rule
22

3+
import kotlinx.coroutines.flow.Flow
4+
import kotlinx.coroutines.flow.firstOrNull
5+
import kotlinx.coroutines.flow.toList
6+
import kotlinx.coroutines.flow.filter
7+
import kotlinx.coroutines.flow.mapNotNull
8+
import kotlinx.coroutines.flow.takeWhile
9+
310
/**
4-
* Base interface for rule evaluation modes.
11+
* Defines how multiple rule values are aggregated into a single result.
512
*
6-
* Determines how multiple rule values are aggregated when
7-
* the same rule is defined in multiple configuration files.
13+
* @param T the type of values being aggregated
814
*/
9-
sealed interface RuleMode
15+
sealed interface RuleMode<T> {
16+
/**
17+
* Aggregates multiple rule results into a single value.
18+
*
19+
* @param values The flow of results to aggregate
20+
* @return The aggregated result
21+
*/
22+
suspend fun aggregate(values: Flow<RuleResult<T>>): T?
23+
}
1024

1125
/**
1226
* Modes for rules that produce string values.
1327
*
1428
* Defines how multiple string values are combined.
1529
*/
16-
sealed class StringRuleMode : RuleMode {
17-
/**
18-
* Aggregates multiple string values into a single result.
19-
*
20-
* @param values The list of values to aggregate
21-
* @return The aggregated result
22-
*/
23-
abstract fun aggregate(values: List<String?>): String?
30+
sealed class StringRuleMode : RuleMode<String> {
31+
abstract override suspend fun aggregate(values: Flow<RuleResult<String>>): String?
2432

2533
/**
2634
* Returns the first non-empty value.
2735
* Use for rules where only one value should be used.
2836
*/
2937
data object SINGLE : StringRuleMode() {
30-
override fun aggregate(values: List<String?>): String? = values.firstOrNull { !it.isNullOrEmpty() }
38+
override suspend fun aggregate(values: Flow<RuleResult<String>>): String? =
39+
values.mapNotNull { it.result }.firstOrNull { it.isNotEmpty() }
3140
}
3241

3342
/**
3443
* Merges all non-empty values with newlines.
3544
* Use for rules where all values should be combined.
3645
*/
3746
data object MERGE : StringRuleMode() {
38-
override fun aggregate(values: List<String?>): String? = values.filterNotNull().filter { it.isNotEmpty() }.joinToString("\n").ifEmpty { null }
47+
override suspend fun aggregate(values: Flow<RuleResult<String>>): String? =
48+
values.mapNotNull { it.result }.filter { it.isNotEmpty() }.toList().joinToString("\n").ifEmpty { null }
3949
}
4050

4151
/**
4252
* Merges distinct non-empty values with newlines.
4353
* Use for rules where duplicate values should be removed.
4454
*/
4555
data object MERGE_DISTINCT : StringRuleMode() {
46-
override fun aggregate(values: List<String?>): String? = values.filterNotNull().filter { it.isNotEmpty() }.distinct().joinToString("\n").ifEmpty { null }
56+
override suspend fun aggregate(values: Flow<RuleResult<String>>): String? =
57+
values.mapNotNull { it.result }.filter { it.isNotEmpty() }.toList().distinct().joinToString("\n")
58+
.ifEmpty { null }
4759
}
4860
}
4961

@@ -52,30 +64,25 @@ sealed class StringRuleMode : RuleMode {
5264
*
5365
* Defines how multiple boolean values are combined.
5466
*/
55-
sealed class BooleanRuleMode : RuleMode {
56-
/**
57-
* Aggregates multiple boolean values into a single result.
58-
*
59-
* @param values The list of values to aggregate
60-
* @return The aggregated result
61-
*/
62-
abstract fun aggregate(values: List<Boolean?>): Boolean
67+
sealed class BooleanRuleMode : RuleMode<Boolean> {
68+
abstract override suspend fun aggregate(values: Flow<RuleResult<Boolean>>): Boolean
6369

6470
/**
6571
* Returns true if any value is true.
6672
* Use for rules like "ignore" or "required".
6773
*/
6874
data object ANY : BooleanRuleMode() {
69-
override fun aggregate(values: List<Boolean?>): Boolean = values.any { it == true }
75+
override suspend fun aggregate(values: Flow<RuleResult<Boolean>>): Boolean =
76+
values.mapNotNull { it.result }.firstOrNull { it } != null
7077
}
7178

7279
/**
7380
* Returns true only if all non-null values are true.
7481
* Use for rules that require all conditions to be met.
7582
*/
7683
data object ALL : BooleanRuleMode() {
77-
override fun aggregate(values: List<Boolean?>): Boolean {
78-
val nonNull = values.filterNotNull()
84+
override suspend fun aggregate(values: Flow<RuleResult<Boolean>>): Boolean {
85+
val nonNull = values.mapNotNull { it.result }.toList()
7986
return nonNull.isNotEmpty() && nonNull.all { it }
8087
}
8188
}
@@ -86,7 +93,10 @@ sealed class BooleanRuleMode : RuleMode {
8693
*
8794
* Event rules are executed for their side effects and don't produce values.
8895
*/
89-
sealed class EventRuleMode : RuleMode {
96+
sealed class EventRuleMode : RuleMode<Unit> {
97+
98+
abstract override suspend fun aggregate(values: Flow<RuleResult<Unit>>): Unit?
99+
90100
/**
91101
* Whether to throw an exception when an event handler fails.
92102
*/
@@ -96,13 +106,29 @@ sealed class EventRuleMode : RuleMode {
96106
* Ignores errors and continues execution.
97107
*/
98108
data object IGNORE_ERROR : EventRuleMode() {
109+
override suspend fun aggregate(values: Flow<RuleResult<Unit>>): Unit? {
110+
values.collect {}
111+
return null
112+
}
113+
99114
override val throwOnError: Boolean = false
100115
}
101116

102117
/**
103118
* Throws an exception on error.
104119
*/
105120
data object THROW_IN_ERROR : EventRuleMode() {
121+
override suspend fun aggregate(values: Flow<RuleResult<Unit>>): Unit? {
122+
var error: Throwable? = null
123+
values.collect { result ->
124+
if (result.error != null && error == null) {
125+
error = result.error
126+
}
127+
}
128+
error?.let { throw it }
129+
return null
130+
}
131+
106132
override val throwOnError: Boolean = true
107133
}
108134
}
@@ -112,12 +138,71 @@ sealed class EventRuleMode : RuleMode {
112138
*
113139
* Returns the first non-null value.
114140
*/
115-
data object IntRuleMode : RuleMode {
141+
data object IntRuleMode : RuleMode<Int> {
116142
/**
117143
* Returns the first non-null integer value.
118144
*
119-
* @param values The list of values to aggregate
145+
* @param values The flow of results to aggregate
120146
* @return The first non-null value, or null
121147
*/
122-
fun aggregate(values: List<Int?>): Int? = values.firstOrNull { it != null }
148+
override suspend fun aggregate(values: Flow<RuleResult<Int>>): Int? =
149+
values.mapNotNull { it.result }.firstOrNull()
150+
}
151+
152+
/**
153+
* Result of a rule evaluation.
154+
*
155+
* @param T the type of the result value
156+
*/
157+
interface RuleResult<T> {
158+
/**
159+
* The result value, or null if the rule didn't produce a value or failed.
160+
*/
161+
val result: T?
162+
163+
/**
164+
* The error that occurred during evaluation, or null if successful.
165+
*/
166+
val error: Throwable?
167+
168+
companion object {
169+
/**
170+
* Creates a successful result with the given value.
171+
*/
172+
fun <T> success(result: T?): RuleResult<T> =
173+
if (result == null) NULL() else Success(result)
174+
175+
/**
176+
* Creates a failure result with the given error.
177+
*/
178+
fun <T> failure(error: Throwable): RuleResult<T> = Failure(error)
179+
180+
/**
181+
* Creates a null result.
182+
*/
183+
fun <T> NULL() = NullInstance as RuleResult<T>
184+
}
185+
}
186+
187+
/**
188+
* A successful rule result.
189+
*/
190+
data class Success<T>(override val result: T) : RuleResult<T> {
191+
override val error: Throwable?
192+
get() = null
193+
}
194+
195+
/**
196+
* A failed rule result.
197+
*/
198+
data class Failure<T>(override val error: Throwable) : RuleResult<T> {
199+
override val result: T? = null
200+
}
201+
202+
/**
203+
* A null rule result.
204+
*/
205+
object NullInstance : RuleResult<Any> {
206+
override val result = null
207+
override val error: Throwable? = null
123208
}

0 commit comments

Comments
 (0)