diff --git a/gateway-ha/pom.xml b/gateway-ha/pom.xml index d8b56dc29..15b5af463 100644 --- a/gateway-ha/pom.xml +++ b/gateway-ha/pom.xml @@ -21,7 +21,6 @@ https://registry.npmmirror.com - 4.1.0 5.14.2 4.12.0 462 @@ -253,21 +252,9 @@ - org.jeasy - easy-rules-core - ${dep.jeasy.version} - - - - org.jeasy - easy-rules-mvel - ${dep.jeasy.version} - - - - org.jeasy - easy-rules-support - ${dep.jeasy.version} + org.mvel + mvel2 + 2.5.2.Final @@ -290,13 +277,6 @@ runtime - - org.mvel - mvel2 - 2.5.2.Final - runtime - - org.postgresql postgresql diff --git a/gateway-ha/src/main/java/io/trino/gateway/ha/router/FileReloadingMVELRoutingGroupSelector.java b/gateway-ha/src/main/java/io/trino/gateway/ha/router/FileReloadingMVELRoutingGroupSelector.java new file mode 100644 index 000000000..94c01e9f1 --- /dev/null +++ b/gateway-ha/src/main/java/io/trino/gateway/ha/router/FileReloadingMVELRoutingGroupSelector.java @@ -0,0 +1,57 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.gateway.ha.router; + +import io.trino.gateway.ha.config.RequestAnalyzerConfig; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; + +public class FileReloadingMVELRoutingGroupSelector + extends RulesRoutingGroupSelector +{ + Path rulesPath; + + FileReloadingMVELRoutingGroupSelector(String rulesPath, RequestAnalyzerConfig requestAnalyzerConfig) + { + super(requestAnalyzerConfig); + this.rulesPath = Paths.get(rulesPath); + + List ruleList = readRulesFromPath(this.rulesPath, MVELRoutingRule.class); + setRules(ruleList); + } + + @Override + void reloadRules(long lastUpdatedTimeMillis) + { + try { + BasicFileAttributes attr = Files.readAttributes(this.rulesPath, BasicFileAttributes.class); + if (attr.lastModifiedTime().toMillis() > lastUpdatedTimeMillis) { + synchronized (this) { + if (attr.lastModifiedTime().toMillis() > lastUpdatedTimeMillis) { + List ruleList = readRulesFromPath(this.rulesPath, MVELRoutingRule.class); + setRules(ruleList); + } + } + } + } + catch (IOException e) { + throw new RuntimeException("Could not access rules file", e); + } + } +} diff --git a/gateway-ha/src/main/java/io/trino/gateway/ha/router/MVELRoutingRule.java b/gateway-ha/src/main/java/io/trino/gateway/ha/router/MVELRoutingRule.java new file mode 100644 index 000000000..a41d3f463 --- /dev/null +++ b/gateway-ha/src/main/java/io/trino/gateway/ha/router/MVELRoutingRule.java @@ -0,0 +1,46 @@ +package io.trino.gateway.ha.router; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +import static org.mvel2.MVEL.compileExpression; +import static org.mvel2.MVEL.executeExpression; + +public class MVELRoutingRule + extends RoutingRule +{ + @JsonCreator + public MVELRoutingRule( + @JsonProperty("name") String name, + @JsonProperty("description") String description, + @JsonProperty("priority") Integer priority, + @JsonProperty("condition") Serializable condition, + @JsonProperty("actions") List actions) + { + super( + name, + description, + priority, + condition instanceof String stringCondition ? compileExpression(stringCondition) : condition, + actions.stream().map(action -> { + if (action instanceof String stringAction) { + return compileExpression(stringAction); + } + else { + return action; + }}).collect(ImmutableList.toImmutableList())); + } + + @Override + public void evaluate(Map variables) + { + if ((boolean) executeExpression(condition, variables)) { + actions.forEach(action -> executeExpression(action, variables)); + } + } +} diff --git a/gateway-ha/src/main/java/io/trino/gateway/ha/router/RoutingGroupSelector.java b/gateway-ha/src/main/java/io/trino/gateway/ha/router/RoutingGroupSelector.java index ae6285e14..ecd9040c7 100644 --- a/gateway-ha/src/main/java/io/trino/gateway/ha/router/RoutingGroupSelector.java +++ b/gateway-ha/src/main/java/io/trino/gateway/ha/router/RoutingGroupSelector.java @@ -39,7 +39,7 @@ static RoutingGroupSelector byRoutingGroupHeader() */ static RoutingGroupSelector byRoutingRulesEngine(String rulesConfigPath, RequestAnalyzerConfig requestAnalyzerConfig) { - return new RuleReloadingRoutingGroupSelector(rulesConfigPath, requestAnalyzerConfig); + return new FileReloadingMVELRoutingGroupSelector(rulesConfigPath, requestAnalyzerConfig); } /** diff --git a/gateway-ha/src/main/java/io/trino/gateway/ha/router/RoutingRule.java b/gateway-ha/src/main/java/io/trino/gateway/ha/router/RoutingRule.java new file mode 100644 index 000000000..433d51223 --- /dev/null +++ b/gateway-ha/src/main/java/io/trino/gateway/ha/router/RoutingRule.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.gateway.ha.router; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +import static java.util.Objects.requireNonNull; +import static java.util.Objects.requireNonNullElse; + +public abstract class RoutingRule + implements Comparable +{ + String name; + String description; + Integer priority; + Serializable condition; + List actions; + + @JsonCreator + public RoutingRule( + @JsonProperty("name") String name, + @JsonProperty("description") String description, + @JsonProperty("priority") Integer priority, + @JsonProperty("condition") Serializable condition, + @JsonProperty("actions") List actions) + { + this.name = requireNonNull(name, "name is null"); + this.description = requireNonNullElse(description, ""); + this.priority = requireNonNullElse(priority, 0); + this.condition = requireNonNull(condition, "condition is null"); + this.actions = requireNonNull(actions, "actions is null"); + } + + public abstract void evaluate(Map variables); + + @Override + public int compareTo(RoutingRule o) + { + if (o == null) { + return 1; + } + return priority.compareTo(o.priority); + } +} diff --git a/gateway-ha/src/main/java/io/trino/gateway/ha/router/RuleReloadingRoutingGroupSelector.java b/gateway-ha/src/main/java/io/trino/gateway/ha/router/RuleReloadingRoutingGroupSelector.java deleted file mode 100644 index 4be89f39c..000000000 --- a/gateway-ha/src/main/java/io/trino/gateway/ha/router/RuleReloadingRoutingGroupSelector.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.trino.gateway.ha.router; - -import io.airlift.log.Logger; -import io.trino.gateway.ha.config.RequestAnalyzerConfig; -import jakarta.servlet.http.HttpServletRequest; -import org.jeasy.rules.api.Facts; -import org.jeasy.rules.api.Rules; -import org.jeasy.rules.api.RulesEngine; -import org.jeasy.rules.core.DefaultRulesEngine; -import org.jeasy.rules.mvel.MVELRuleFactory; -import org.jeasy.rules.support.reader.YamlRuleDefinitionReader; - -import java.io.FileReader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.HashMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class RuleReloadingRoutingGroupSelector - implements RoutingGroupSelector -{ - private static final Logger log = Logger.get(RuleReloadingRoutingGroupSelector.class); - private final RulesEngine rulesEngine = new DefaultRulesEngine(); - private final MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader()); - private final String rulesConfigPath; - private volatile Rules rules = new Rules(); - private volatile long lastUpdatedTime; - private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); - private final RequestAnalyzerConfig requestAnalyzerConfig; - private final TrinoRequestUser.TrinoRequestUserProvider trinoRequestUserProvider; - - RuleReloadingRoutingGroupSelector(String rulesConfigPath, RequestAnalyzerConfig requestAnalyzerConfig) - { - this.rulesConfigPath = rulesConfigPath; - this.requestAnalyzerConfig = requestAnalyzerConfig; - trinoRequestUserProvider = new TrinoRequestUser.TrinoRequestUserProvider(requestAnalyzerConfig); - try { - rules = ruleFactory.createRules( - new FileReader(rulesConfigPath, UTF_8)); - BasicFileAttributes attr = Files.readAttributes(Path.of(rulesConfigPath), - BasicFileAttributes.class); - lastUpdatedTime = attr.lastModifiedTime().toMillis(); - } - catch (Exception e) { - throw new RuntimeException("Error opening rules configuration file at " - + rulesConfigPath + "\n" - + "Using routing group header as default.", e); - } - } - - @Override - public String findRoutingGroup(HttpServletRequest request) - { - try { - BasicFileAttributes attr = Files.readAttributes(Path.of(rulesConfigPath), - BasicFileAttributes.class); - log.debug("File modified time: %s. lastUpdatedTime: %s", attr.lastModifiedTime(), lastUpdatedTime); - if (attr.lastModifiedTime().toMillis() > lastUpdatedTime) { - Lock writeLock = readWriteLock.writeLock(); - writeLock.lock(); - try { - if (attr.lastModifiedTime().toMillis() > lastUpdatedTime) { - // This check is performed again to prevent parsing the rules twice in case another - // thread finds the condition true and acquires the lock before this one - log.info("Updating rules to file modified at %s", attr.lastModifiedTime()); - rules = ruleFactory.createRules( - new FileReader(rulesConfigPath, UTF_8)); - lastUpdatedTime = attr.lastModifiedTime().toMillis(); - } - } - finally { - writeLock.unlock(); - } - } - - Facts facts = new Facts(); - HashMap result = new HashMap(); - - facts.put("request", request); - if (requestAnalyzerConfig.isAnalyzeRequest()) { - TrinoQueryProperties trinoQueryProperties = new TrinoQueryProperties( - request, - requestAnalyzerConfig.isClientsUseV2Format(), - requestAnalyzerConfig.getMaxBodySize()); - TrinoRequestUser trinoRequestUser = trinoRequestUserProvider.getInstance(request); - facts.put("trinoQueryProperties", trinoQueryProperties); - facts.put("trinoRequestUser", trinoRequestUser); - } - facts.put("result", result); - Lock readLock = readWriteLock.readLock(); - readLock.lock(); - try { - rulesEngine.fire(rules, facts); - } - finally { - readLock.unlock(); - } - return result.get("routingGroup"); - } - catch (Exception e) { - log.error(e, "Error opening rules configuration file, using " - + "routing group header as default."); - // Invalid rules could lead to perf problems as every thread goes into the writeLock - // block until the issue is resolved - } - return request.getHeader(ROUTING_GROUP_HEADER); - } -} diff --git a/gateway-ha/src/main/java/io/trino/gateway/ha/router/RulesRoutingGroupSelector.java b/gateway-ha/src/main/java/io/trino/gateway/ha/router/RulesRoutingGroupSelector.java new file mode 100644 index 000000000..9192f376f --- /dev/null +++ b/gateway-ha/src/main/java/io/trino/gateway/ha/router/RulesRoutingGroupSelector.java @@ -0,0 +1,97 @@ +package io.trino.gateway.ha.router; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLParser; +import com.google.common.collect.ImmutableMap; +import io.trino.gateway.ha.config.RequestAnalyzerConfig; +import jakarta.servlet.http.HttpServletRequest; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.sort; + +public abstract class RulesRoutingGroupSelector + implements RoutingGroupSelector +{ + public static final String RESULTS_ROUTING_GROUP_KEY = "routingGroup"; + + private List rules; + final boolean analyzeRequest; + final boolean clientsUseV2Format; + final int maxBodySize; + final TrinoRequestUser.TrinoRequestUserProvider trinoRequestUserProvider; + private volatile long lastUpdatedTimeMillis; + + RulesRoutingGroupSelector(RequestAnalyzerConfig requestAnalyzerConfig) + { + this(new ArrayList<>(), requestAnalyzerConfig); + } + + public RulesRoutingGroupSelector(List rules, RequestAnalyzerConfig requestAnalyzerConfig) + { + setRules(rules); + analyzeRequest = requestAnalyzerConfig.isAnalyzeRequest(); + clientsUseV2Format = requestAnalyzerConfig.isClientsUseV2Format(); + maxBodySize = requestAnalyzerConfig.getMaxBodySize(); + trinoRequestUserProvider = new TrinoRequestUser.TrinoRequestUserProvider(requestAnalyzerConfig); + } + + abstract void reloadRules(long lastUpdatedTimeMillis); + + void setRules(List rules) + { + this.rules = new ArrayList<>(rules); + lastUpdatedTimeMillis = System.currentTimeMillis(); + sort(this.rules); + } + + // TODO: add CRUD operations for the rules + + @Override + public String findRoutingGroup(HttpServletRequest request) + { + reloadRules(lastUpdatedTimeMillis); + Map result = new HashMap<>(); + Map variables; + if (analyzeRequest) { + TrinoQueryProperties trinoQueryProperties = new TrinoQueryProperties( + request, + clientsUseV2Format, + maxBodySize); + TrinoRequestUser trinoRequestUser = trinoRequestUserProvider.getInstance(request); + variables = ImmutableMap.of("result", result, "request", request, "trinoQueryProperties", trinoQueryProperties, "trinoRequestUser", trinoRequestUser); + } + else { + variables = ImmutableMap.of("result", result, "request", request); + } + + rules.forEach(rule -> rule.evaluate(variables)); + return result.get(RESULTS_ROUTING_GROUP_KEY); + } + + public static List readRulesFromPath(Path rulesPath, Class ruleType) + { + ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory()); + try { + String content = Files.readString(rulesPath, UTF_8); + YAMLParser parser = new YAMLFactory().createParser(content); + List routingRulesList = new ArrayList<>(); + while (parser.nextToken() != null) { + RoutingRule routingRules = yamlReader.readValue(parser, ruleType); + routingRulesList.add(routingRules); + } + return routingRulesList; + } + catch (IOException e) { + throw new RuntimeException("Failed to read or parse routing rules configuration from path : " + rulesPath, e); + } + } +} diff --git a/gateway-ha/src/test/java/io/trino/gateway/ha/router/TestRoutingGroupSelector.java b/gateway-ha/src/test/java/io/trino/gateway/ha/router/TestRoutingGroupSelector.java index 1066d19a7..abb7fe0d9 100644 --- a/gateway-ha/src/test/java/io/trino/gateway/ha/router/TestRoutingGroupSelector.java +++ b/gateway-ha/src/test/java/io/trino/gateway/ha/router/TestRoutingGroupSelector.java @@ -67,7 +67,6 @@ static Stream provideRoutingRuleConfigFiles() String rulesDir = "src/test/resources/rules/"; return Stream.of( rulesDir + "routing_rules_atomic.yml", - rulesDir + "routing_rules_composite.yml", rulesDir + "routing_rules_priorities.yml", rulesDir + "routing_rules_if_statements.yml"); }