Skip to content

Commit 45d3356

Browse files
willmostlyprakhar10
andcommitted
Use MVEL for rule evaluation
Co-Authored-By: Prakhar Sapre <[email protected]>
1 parent c2ba1ea commit 45d3356

File tree

12 files changed

+435
-287
lines changed

12 files changed

+435
-287
lines changed

docs/routing-rules.md

Lines changed: 66 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ To enable the routing rules engine, find the following lines in
2424
* Set `rulesEngineEnabled` to `true`, then `rulesType` as `FILE` or `EXTERNAL`.
2525
* If you set `rulesType: FILE`, then set `rulesConfigPath` to the path to your
2626
rules config file.
27+
* The rules file will be re-read every minute by default. You may change this by setting
28+
`rulesRefreshPeriod: Duration`, where duration is an airlift style Duration such as `30s`.
2729
* If you set `rulesType: EXTERNAL`, set `rulesExternalConfiguration` to the URL
2830
of an external service for routing rules processing.
2931
* `rulesType` is by default `FILE` unless specified.
@@ -92,11 +94,10 @@ return a result with the following criteria:
9294

9395
### Configure routing rules with a file
9496

95-
To express and fire routing rules, we use the
96-
[easy-rules](https://github.com/j-easy/easy-rules) engine. These rules must be
97-
stored in a YAML file. Rules consist of a name, description, condition, and list
97+
Rules consist of a name, description, condition, and list
9898
of actions. If the condition of a particular rule evaluates to `true`, its
99-
actions are fired.
99+
actions are fired. Rules are stored as a
100+
[multi-document](https://www.yaml.info/learn/document.html) YAML file.
100101

101102
```yaml
102103
---
@@ -113,20 +114,37 @@ actions:
113114
- 'result.put("routingGroup", "etl-special")'
114115
```
115116

116-
In the condition, you can access the methods of a
117-
[HttpServletRequest](https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html)
118-
object called `request`. Rules may also utilize
117+
Three objects are available by default. They are
118+
* `request`, the incoming request as an [HttpServletRequest](https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html)
119+
* `state`, a `HashMap<String, Object>` that allows passing arbitrary state from one rule evaluation to the next
120+
* `result`, a `HashMap<String, String>` that is used to return the result of rule evaluation to the engine
121+
122+
In addition to the default objects, rules may optionally utilize
119123
[trinoRequestUser](#trinorequestuser) and
120124
[trinoQueryProperties](#trinoqueryproperties)
121-
objects, which provide information about the user and query respectively.
125+
, which provide information about the user and query respectively.
122126
You must include an action of the form `result.put(\"routingGroup\", \"foo\")`
123127
to trigger routing of a request that satisfies the condition to the specific
124128
routing group. Without this action, the default adhoc group is used and the
125129
whole routing rule is redundant.
126130

127131
The condition and actions are written in [MVEL](http://mvel.documentnode.com/),
128-
an expression language with Java-like syntax. In most cases, you can write
129-
conditions and actions in Java syntax and expect it to work. There are some
132+
an expression language with Java-like syntax. Classes from `java.util`, data-type
133+
classes from `java.lang` such as `Integer` and `String`, as well as `java.lang.Math`
134+
and `java.lang.StrictMath` are available in rules. Rules may not use `java.lang.System`
135+
and other classes that allow access the Java runtime and operating system.
136+
In most cases, you can write
137+
conditions and actions in Java syntax and expect it to work. One exception is
138+
parametrized types. Exclude type parameters, for example to add a `HashSet` to the
139+
`state` variable, use an action such as:
140+
```java
141+
actions:
142+
- |
143+
state.put("triggeredRules",new HashSet())
144+
```
145+
This is equivalent to `new HashSet<Object>()`.
146+
147+
There are some
130148
MVEL-specific operators. For example, instead of doing a null-check before
131149
accessing the `String.contains` method like this:
132150

@@ -296,8 +314,8 @@ actions:
296314
```
297315

298316
This can difficult to maintain with more rules. To have better control over the
299-
execution of rules, we can use rule priorities and composite rules. Overall,
300-
priorities, composite rules, and other constructs that MVEL support allows
317+
execution of rules, we can use rule priorities. Overall,
318+
priorities and other constructs that MVEL support allows
301319
you to express your routing logic.
302320

303321
#### Rule priority
@@ -328,99 +346,52 @@ that the first rule (priority 0) is fired before the second rule (priority 1).
328346
Thus `routingGroup` is set to `etl` and then to `etl-special`, so the
329347
`routingGroup` is always `etl-special` in the end.
330348

331-
More specific rules must be set to a lesser priority so they are evaluated last
332-
to set a `routingGroup`. To further control the execution of rules, for example
333-
to have only one rule fire, you can use composite rules.
349+
More specific rules must be set to a higher priority so they are evaluated last
350+
to set a `routingGroup`.
334351

335-
##### Composite rules
352+
##### Passing State
336353

337-
First, please refer to the [easy-rule composite rules documentation](https://github.com/j-easy/easy-rules/wiki/defining-rules#composite-rules).
338-
339-
The preceding section covers how to control the order of rule execution using
340-
priorities. In addition, you can configure evaluation so that only the first
341-
rule matched fires (the highest priority one) and the rest is ignored. You can
342-
use `ActivationRuleGroup` to achieve this:
354+
The `state` object may be used to pass information from one rule evaluation to
355+
the next. This allows an author to avoid duplicating logic in multiple rules.
356+
Priority should be used to ensure that `state` is updated before being used
357+
in downstream rules. For example, the atomic rules may be re-implemented as
343358

344359
```yaml
345360
---
346-
name: "airflow rule group"
347-
description: "routing rules for query from airflow"
348-
compositeRuleType: "ActivationRuleGroup"
349-
composingRules:
350-
- name: "airflow special"
351-
description: "if query from airflow with special label, route to etl-special group"
352-
priority: 0
353-
condition: 'request.getHeader("X-Trino-Source") == "airflow" && request.getHeader("X-Trino-Client-Tags") contains "label=special"'
354-
actions:
355-
- 'result.put("routingGroup", "etl-special")'
356-
- name: "airflow"
357-
description: "if query from airflow, route to etl group"
358-
priority: 1
359-
condition: 'request.getHeader("X-Trino-Source") == "airflow"'
360-
actions:
361-
- 'result.put("routingGroup", "etl")'
362-
```
363-
364-
Note that the priorities have switched. The more specific rule has a higher
365-
priority, since it should fire first. A query coming from airflow with special
366-
label is matched to the "airflow special" rule first, since it's higher
367-
priority, and the second rule is ignored. A query coming from airflow with no
368-
labels does not match the first rule, and is then tested and matched to the
369-
second rule.
370-
371-
You can also use `ConditionalRuleGroup` and `ActivationRuleGroup` to implement
372-
an if/else workflow. The following logic in pseudocode:
373-
374-
```text
375-
if source == "airflow":
376-
if clientTags["label"] == "foo":
377-
return "etl-foo"
378-
else if clientTags["label"] = "bar":
379-
return "etl-bar"
380-
else
381-
return "etl"
382-
```
383-
384-
This logic can be implemented with the following rules:
361+
name: "initialize state"
362+
description: "Add a set to the state map to track rules that have evaluated to true"
363+
priority: 0
364+
condition: "true"
365+
actions:
366+
- |
367+
state.put("triggeredRules",new HashSet())
368+
# MVEL does not support type parameters! HashSet<String>() would result in an error.
369+
---
370+
name: "airflow"
371+
description: "if query from airflow, route to etl group"
372+
priority: 1
373+
condition: |
374+
request.getHeader("X-Trino-Source") == "airflow"
375+
actions:
376+
- |
377+
result.put("routingGroup", "etl")
378+
- |
379+
state.get("triggeredRules").add("airflow")
380+
---
381+
name: "airflow special"
382+
description: "if query from airflow with special label, route to etl-special group"
383+
priority: 2
384+
condition: |
385+
state.get("triggeredRules").contains("airflow") && request.getHeader("X-Trino-Client-Tags") contains "label=special"
386+
actions:
387+
- |
388+
result.put("routingGroup", "etl-special")
385389
386-
```yaml
387-
name: "airflow rule group"
388-
description: "routing rules for query from airflow"
389-
compositeRuleType: "ConditionalRuleGroup"
390-
composingRules:
391-
- name: "main condition"
392-
description: "source is airflow"
393-
priority: 0 # rule with the highest priority acts as main condition
394-
condition: 'request.getHeader("X-Trino-Source") == "airflow"'
395-
actions:
396-
- ""
397-
- name: "airflow subrules"
398-
compositeRuleType: "ActivationRuleGroup" # use ActivationRuleGroup to simulate if/else
399-
composingRules:
400-
- name: "label foo"
401-
description: "label client tag is foo"
402-
priority: 0
403-
condition: 'request.getHeader("X-Trino-Client-Tags") contains "label=foo"'
404-
actions:
405-
- 'result.put("routingGroup", "etl-foo")'
406-
- name: "label bar"
407-
description: "label client tag is bar"
408-
priority: 0
409-
condition: 'request.getHeader("X-Trino-Client-Tags") contains "label=bar"'
410-
actions:
411-
- 'result.put("routingGroup", "etl-bar")'
412-
- name: "airflow default"
413-
description: "airflow queries default to etl"
414-
condition: "true"
415-
actions:
416-
- 'result.put("routingGroup", "etl")'
417390
```
418391

419392
##### If statements (MVEL Flow Control)
420393

421-
In the preceding section you see how `ConditionalRuleGroup` and
422-
`ActivationRuleGroup` are used to implement an `if/else` workflow. You can
423-
use MVEL support for `if` statements and other flow control. The following logic
394+
You can use MVEL support for `if` statements and other flow control. The following logic
424395
in pseudocode:
425396

426397
```text

gateway-ha/pom.xml

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -260,21 +260,9 @@
260260
</dependency>
261261

262262
<dependency>
263-
<groupId>org.jeasy</groupId>
264-
<artifactId>easy-rules-core</artifactId>
265-
<version>${dep.jeasy.version}</version>
266-
</dependency>
267-
268-
<dependency>
269-
<groupId>org.jeasy</groupId>
270-
<artifactId>easy-rules-mvel</artifactId>
271-
<version>${dep.jeasy.version}</version>
272-
</dependency>
273-
274-
<dependency>
275-
<groupId>org.jeasy</groupId>
276-
<artifactId>easy-rules-support</artifactId>
277-
<version>${dep.jeasy.version}</version>
263+
<groupId>org.mvel</groupId>
264+
<artifactId>mvel2</artifactId>
265+
<version>2.5.2.Final</version>
278266
</dependency>
279267

280268
<dependency>
@@ -311,13 +299,6 @@
311299
<scope>runtime</scope>
312300
</dependency>
313301

314-
<dependency>
315-
<groupId>org.mvel</groupId>
316-
<artifactId>mvel2</artifactId>
317-
<version>2.5.2.Final</version>
318-
<scope>runtime</scope>
319-
</dependency>
320-
321302
<dependency>
322303
<groupId>org.postgresql</groupId>
323304
<artifactId>postgresql</artifactId>

gateway-ha/src/main/java/io/trino/gateway/ha/config/RoutingRulesConfiguration.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,19 @@
1313
*/
1414
package io.trino.gateway.ha.config;
1515

16+
import io.airlift.units.Duration;
17+
18+
import static java.util.concurrent.TimeUnit.MINUTES;
19+
1620
public class RoutingRulesConfiguration
1721
{
1822
private boolean rulesEngineEnabled;
1923
private RulesType rulesType = RulesType.FILE;
2024
private String rulesConfigPath;
2125
private RulesExternalConfiguration rulesExternalConfiguration;
2226

27+
private Duration rulesRefreshPeriod = new Duration(1, MINUTES);
28+
2329
public RoutingRulesConfiguration() {}
2430

2531
public boolean isRulesEngineEnabled()
@@ -61,4 +67,14 @@ public void setRulesExternalConfiguration(RulesExternalConfiguration rulesExtern
6167
{
6268
this.rulesExternalConfiguration = rulesExternalConfiguration;
6369
}
70+
71+
public Duration getRulesRefreshPeriod()
72+
{
73+
return rulesRefreshPeriod;
74+
}
75+
76+
public void setRulesRefreshPeriod(Duration rulesRefreshPeriod)
77+
{
78+
this.rulesRefreshPeriod = rulesRefreshPeriod;
79+
}
6480
}

gateway-ha/src/main/java/io/trino/gateway/ha/module/HaGatewayProviderModule.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,10 @@ public RoutingGroupSelector getRoutingGroupSelector()
182182
if (routingRulesConfig.isRulesEngineEnabled()) {
183183
try {
184184
return switch (routingRulesConfig.getRulesType()) {
185-
case FILE -> {
186-
String rulesConfigPath = routingRulesConfig.getRulesConfigPath();
187-
yield RoutingGroupSelector.byRoutingRulesEngine(rulesConfigPath, configuration.getRequestAnalyzerConfig());
188-
}
185+
case FILE -> RoutingGroupSelector.byRoutingRulesEngine(
186+
routingRulesConfig.getRulesConfigPath(),
187+
routingRulesConfig.getRulesRefreshPeriod(),
188+
configuration.getRequestAnalyzerConfig());
189189
case EXTERNAL -> {
190190
RulesExternalConfiguration rulesExternalConfiguration = routingRulesConfig.getRulesExternalConfiguration();
191191
yield RoutingGroupSelector.byRoutingExternal(rulesExternalConfiguration, configuration.getRequestAnalyzerConfig());

0 commit comments

Comments
 (0)