Skip to content

Commit e821140

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

File tree

10 files changed

+381
-271
lines changed

10 files changed

+381
-271
lines changed

docs/routing-rules.md

Lines changed: 64 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,10 @@ return a result with the following criteria:
9292

9393
### Configure routing rules with a file
9494

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
95+
Rules consist of a name, description, condition, and list
9896
of actions. If the condition of a particular rule evaluates to `true`, its
99-
actions are fired.
97+
actions are fired. Rules are stored as a
98+
[multi-document](https://www.yaml.info/learn/document.html) YAML file.
10099

101100
```yaml
102101
---
@@ -113,20 +112,37 @@ actions:
113112
- 'result.put("routingGroup", "etl-special")'
114113
```
115114

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
115+
Three objects are available by default. They are
116+
* `request`, the incoming request as an [HttpServletRequest](https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html)
117+
* `state`, a `HashMap<String, Object>` that allows passing arbitrary state from one rule evaluation to the next
118+
* `result`, a `HashMap<String, String>` that is used to return the result of rule evaluation to the engine
119+
120+
In addition to the default objects, rules may optionally utilize
119121
[trinoRequestUser](#trinorequestuser) and
120122
[trinoQueryProperties](#trinoqueryproperties)
121-
objects, which provide information about the user and query respectively.
123+
, which provide information about the user and query respectively.
122124
You must include an action of the form `result.put(\"routingGroup\", \"foo\")`
123125
to trigger routing of a request that satisfies the condition to the specific
124126
routing group. Without this action, the default adhoc group is used and the
125127
whole routing rule is redundant.
126128

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

@@ -296,8 +312,8 @@ actions:
296312
```
297313

298314
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
315+
execution of rules, we can use rule priorities. Overall,
316+
priorities and other constructs that MVEL support allows
301317
you to express your routing logic.
302318

303319
#### Rule priority
@@ -328,99 +344,52 @@ that the first rule (priority 0) is fired before the second rule (priority 1).
328344
Thus `routingGroup` is set to `etl` and then to `etl-special`, so the
329345
`routingGroup` is always `etl-special` in the end.
330346

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.
347+
More specific rules must be set to a higher priority so they are evaluated last
348+
to set a `routingGroup`.
334349

335-
##### Composite rules
350+
##### Passing State
336351

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:
352+
The `state` object may be used to pass information from one rule evaluation to
353+
the next. This allows an author to avoid duplicating logic in multiple rules.
354+
Priority should be used to ensure that `state` is updated before being used
355+
in downstream rules. For example, the atomic rules may be re-implemented as
343356

344357
```yaml
345358
---
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:
359+
name: "initialize state"
360+
description: "Add a set to the state map to track rules that have evaluated to true"
361+
priority: 0
362+
condition: "true"
363+
actions:
364+
- |
365+
state.put("triggeredRules",new HashSet())
366+
# MVEL does not support type parameters! HashSet<String>() would result in an error.
367+
---
368+
name: "airflow"
369+
description: "if query from airflow, route to etl group"
370+
priority: 1
371+
condition: |
372+
request.getHeader("X-Trino-Source") == "airflow"
373+
actions:
374+
- |
375+
result.put("routingGroup", "etl")
376+
- |
377+
state.get("triggeredRules").add("airflow")
378+
---
379+
name: "airflow special"
380+
description: "if query from airflow with special label, route to etl-special group"
381+
priority: 2
382+
condition: |
383+
state.get("triggeredRules").contains("airflow") && request.getHeader("X-Trino-Client-Tags") contains "label=special"
384+
actions:
385+
- |
386+
result.put("routingGroup", "etl-special")
385387
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")'
417388
```
418389

419390
##### If statements (MVEL Flow Control)
420391

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
392+
You can use MVEL support for `if` statements and other flow control. The following logic
424393
in pseudocode:
425394

426395
```text

gateway-ha/pom.xml

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
<frontend.pnpmRegistryURL>https://registry.npmmirror.com</frontend.pnpmRegistryURL>
2222

2323
<!-- dependency versions -->
24-
<dep.jeasy.version>4.1.0</dep.jeasy.version>
2524
<dep.mockito.version>5.14.2</dep.mockito.version>
2625
<dep.okhttp3.version>4.12.0</dep.okhttp3.version>
2726
<dep.trino.version>464</dep.trino.version>
@@ -253,21 +252,9 @@
253252
</dependency>
254253

255254
<dependency>
256-
<groupId>org.jeasy</groupId>
257-
<artifactId>easy-rules-core</artifactId>
258-
<version>${dep.jeasy.version}</version>
259-
</dependency>
260-
261-
<dependency>
262-
<groupId>org.jeasy</groupId>
263-
<artifactId>easy-rules-mvel</artifactId>
264-
<version>${dep.jeasy.version}</version>
265-
</dependency>
266-
267-
<dependency>
268-
<groupId>org.jeasy</groupId>
269-
<artifactId>easy-rules-support</artifactId>
270-
<version>${dep.jeasy.version}</version>
255+
<groupId>org.mvel</groupId>
256+
<artifactId>mvel2</artifactId>
257+
<version>2.5.2.Final</version>
271258
</dependency>
272259

273260
<dependency>
@@ -290,13 +277,6 @@
290277
<scope>runtime</scope>
291278
</dependency>
292279

293-
<dependency>
294-
<groupId>org.mvel</groupId>
295-
<artifactId>mvel2</artifactId>
296-
<version>2.5.2.Final</version>
297-
<scope>runtime</scope>
298-
</dependency>
299-
300280
<dependency>
301281
<groupId>org.postgresql</groupId>
302282
<artifactId>postgresql</artifactId>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package io.trino.gateway.ha.router;
15+
16+
import com.fasterxml.jackson.databind.ObjectMapper;
17+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
18+
import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
19+
import com.google.common.collect.ImmutableMap;
20+
import io.trino.gateway.ha.config.RequestAnalyzerConfig;
21+
import jakarta.servlet.http.HttpServletRequest;
22+
23+
import java.io.IOException;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.nio.file.Paths;
27+
import java.nio.file.attribute.BasicFileAttributes;
28+
import java.util.ArrayList;
29+
import java.util.HashMap;
30+
import java.util.List;
31+
import java.util.Map;
32+
33+
import static java.nio.charset.StandardCharsets.UTF_8;
34+
import static java.util.Collections.sort;
35+
36+
public class FileBasedRoutingGroupSelector
37+
implements RoutingGroupSelector
38+
{
39+
public static final String RESULTS_ROUTING_GROUP_KEY = "routingGroup";
40+
41+
private List<RoutingRule> rules;
42+
final boolean analyzeRequest;
43+
final boolean clientsUseV2Format;
44+
final int maxBodySize;
45+
final TrinoRequestUser.TrinoRequestUserProvider trinoRequestUserProvider;
46+
private volatile long lastUpdatedTimeMillis;
47+
Path rulesPath;
48+
49+
public FileBasedRoutingGroupSelector(String rulesPath, RequestAnalyzerConfig requestAnalyzerConfig)
50+
{
51+
analyzeRequest = requestAnalyzerConfig.isAnalyzeRequest();
52+
clientsUseV2Format = requestAnalyzerConfig.isClientsUseV2Format();
53+
maxBodySize = requestAnalyzerConfig.getMaxBodySize();
54+
trinoRequestUserProvider = new TrinoRequestUser.TrinoRequestUserProvider(requestAnalyzerConfig);
55+
this.rulesPath = Paths.get(rulesPath);
56+
57+
setRules(readRulesFromPath(this.rulesPath));
58+
}
59+
60+
void setRules(List<RoutingRule> rules)
61+
{
62+
this.rules = new ArrayList<>(rules);
63+
lastUpdatedTimeMillis = System.currentTimeMillis();
64+
sort(this.rules);
65+
}
66+
67+
@Override
68+
public String findRoutingGroup(HttpServletRequest request)
69+
{
70+
reloadRules(lastUpdatedTimeMillis);
71+
Map<String, String> result = new HashMap<>();
72+
Map<String, Object> state = new HashMap<>();
73+
74+
Map<String, Object> data;
75+
if (analyzeRequest) {
76+
TrinoQueryProperties trinoQueryProperties = new TrinoQueryProperties(
77+
request,
78+
clientsUseV2Format,
79+
maxBodySize);
80+
TrinoRequestUser trinoRequestUser = trinoRequestUserProvider.getInstance(request);
81+
data = ImmutableMap.of("request", request, "trinoQueryProperties", trinoQueryProperties, "trinoRequestUser", trinoRequestUser);
82+
}
83+
else {
84+
data = ImmutableMap.of("request", request);
85+
}
86+
87+
rules.forEach(rule -> {
88+
if (rule.evaluateCondition(data, state)) {
89+
rule.evaluateAction(result, data, state);
90+
}
91+
});
92+
return result.get(RESULTS_ROUTING_GROUP_KEY);
93+
}
94+
95+
void reloadRules(long lastUpdatedTimeMillis)
96+
{
97+
try {
98+
BasicFileAttributes attr = Files.readAttributes(this.rulesPath, BasicFileAttributes.class);
99+
if (attr.lastModifiedTime().toMillis() <= lastUpdatedTimeMillis) {
100+
return;
101+
}
102+
synchronized (this) {
103+
// Prevent re-entry in case another thread passes the first check while rules are being updated
104+
if (attr.lastModifiedTime().toMillis() > lastUpdatedTimeMillis) {
105+
List<RoutingRule> ruleList = readRulesFromPath(this.rulesPath);
106+
setRules(ruleList);
107+
}
108+
}
109+
}
110+
catch (IOException e) {
111+
throw new RuntimeException("Could not access rules file", e);
112+
}
113+
}
114+
115+
public List<RoutingRule> readRulesFromPath(Path rulesPath)
116+
{
117+
ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory());
118+
try {
119+
String content = Files.readString(rulesPath, UTF_8);
120+
YAMLParser parser = new YAMLFactory().createParser(content);
121+
List<RoutingRule> routingRulesList = new ArrayList<>();
122+
while (parser.nextToken() != null) {
123+
MVELRoutingRule routingRules = yamlReader.readValue(parser, MVELRoutingRule.class);
124+
routingRulesList.add(routingRules);
125+
}
126+
return routingRulesList;
127+
}
128+
catch (IOException e) {
129+
throw new RuntimeException("Failed to read or parse routing rules configuration from path: " + rulesPath, e);
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)