Preface
I have been engaged in growth and promotion for a long time, and I am very tired when implementing various activities and gameplay. Every new gameplay requires filling in a bit of code, and every change requires an assembly line release, which is irritating and painful.
For such things where the input is uncertain, the process is uncertain, and the result is uncertain, it is often more efficient to build a rule engine to handle these problems.
Requirements: The function must be strong enough, the configuration must be simple enough, the performance must be high, and it must be accessible in a variety of ways. The most important thing is to be able to leave the work to the operation.
rules engine
Rule engines can be functionally divided 通用规则引擎
into 业务规则引擎
. As the name suggests, the former solves general problems, is more flexible and difficult to use. The latter is strongly bound to specific businesses, but there is often a backend for users to click on, which is more convenient to use. We are mainly talking about the general rule engine here.
Rule writing can also be divided into two types: 解析表达式
语言脚本
. The former uses expressions, which is relatively simple (the lower limit acceptable for operations). The latter is purely about writing code. The only advantage is that there is no need to release a version and it can be hot updated.
Let’s take a look at the more famous rule engines:
Expression based rule engine | expression ability | performance | other |
---|---|---|---|
drools (java) | It is difficult to write and is strongly related to Java | rete algorithm, executed sequentially | Old rule engine, I don’t use it anyway |
gengine (based on golang and AST) | Custom simple syntax, weakly related to golang | High, concurrent execution is supported both between rules and within rules. | B station is widely used and open sourced in 20 years |
Knetic/govaluate | Very expressive, easy to inject functions and variables | Powerful performance, benchmark | It has a star rating of 3.3 and has strong assembly capabilities. |
antonmedv/expr | Strong expressive ability | Powerful performance, benchmark | Star 4.4, with an expression editor, is used by major companies such as Google, Uber Byte, etc. |
There are also some script-based rule engines. Of course, they are not strictly rule engines. As long as they can run scripts, they can be regarded as rule engines. Common lua, tengo, even js and py can be run as rule scripts.
Rush
github:https://github.com/woshihaoren4/rush
There are many rule engines mentioned above, all of which are very powerful. Why do we need to build one ourselves? The reasons are as follows:
It needs to support multiple rule formats, which can use expressions or scripts, and is oriented to both development and operations.
Scalability must be strong. I'm not satisfied that the module can be modified.
The performance must be strong, concurrent, and safe. Just say the answer and show up.
rust
The overall design of Rush is as follows:
A rule is divided into
when
a conditional part andthen
an execution part. If an input satisfies when, the result is generated according to then.Rush is equivalent to a container that combines these calculations, executions, and functions.
Simple to use
//一个简单的规则
const SIMPLE_RULE: &'static str = "
rule COMPLEX_RULE
when
age > 18
then
stage = 'adult'
";
fn main(){
//ExprEngine是一个表达式解析器,将规则表达式,解析为上图中的 Calc 和 Assgin
//Rush是盛放规则的容器,它并不关心规则是如何解析和运行的,它只负责管理和调度
let rh = Rush::from(Into::<ExprEngine>::into([SIMPLE_RULE]));
// 执行一条规则
let res:HashMap<String,String> = rh.flow(r#"{"age":19}"#.parse::<Value>().unwrap()).unwrap();
assert_eq!(res.get("stage").unwrap().as_str(),"adult");
}
You can inject the function into rush just like you write a normal function.
let rh = rh
.register_function("abs", |i: i64| Ok(i.abs()));
Arrays come with two functions: contain: contains sub: whether there is a subset, as written below.
rule ARRAY_RULE
when
contain([1,2,3,4],status);
sub([2,3.1,'hello',true,2>>1],[1,'world']);
then
message = 'success'
The entire rush can be assembled by itself through abstraction, and the judgment conditions and generators can be customized as follows. Of course, you can use expressions as conditions to customize the generation.
struct CustomCalc;
impl CalcNode for CustomCalc{
fn when(&self, _fs: Arc<dyn FunctionSet>, input: &Value) -> anyhow::Result<bool> {
if let Value::String(s) = input{
return Ok(s == "true")
}
return Ok(false)
}
}
struct CustomExec;
impl Exec for CustomExec{
fn execute(&self, _fs: Arc<dyn FunctionSet>, _input: &Value, output: &mut Value) -> anyhow::Result<()> {
if let Value::Object(obj) = output{
obj.insert("result".to_string(),Value::from("success"));
}
Ok(())
}
}
#[test]
fn test_custom_calc_exec(){
let rh = Rush::new()
.register_rule("custom_rule",vec![CustomCalc],CustomExec);
let res:HashMap<String,String> = rh.flow("true".parse::<String>().unwrap()).unwrap();
assert_eq!(res.get("result").unwrap().as_str(),"success");
let res:HashMap<String,String> = rh.flow("false".parse::<String>().unwrap()).unwrap();
assert_eq!(res.get("result"),None);
}
Of course, because of our split design, the rules can be calculated in parallel.
let result = Into::<MultiRush>::into(rh)
.multi_flow(r#"{"country":"China"}"#.parse::<Value>().unwrap())
.await
.unwrap();
See more examples
Performance Testing
There is currently no optimization, and the performance is very strong. You can clone Rush git and cargo bench -- --verbose
test it in the example directory.
benchmark details
I did a benchmark test based on the local environment, mac i7 six-core 16g, from left to right [minimum value, average value, maximum value]
assign_simple_parse time: [620.70 ns 625.08 ns 630.18 ns]
rule_full_parse time: [7.5513 µs 7.5794 µs 7.6094 µs]
multi_flow time: [15.363 µs 15.721 µs 16.184 µs]
sync_flow time: [2.9953 µs 3.0295 µs 3.0700 µs]
single_parse time: [165.08 ns 174.83 ns 186.49 ns]
simple_parse time: [2.6358 µs 2.6470 µs 2.6591 µs]
full_parse time: [19.868 µs 20.089 µs 20.356 µs]
have_function_rush time: [6.9074 µs 6.9507 µs 7.0011 µs]
expression format
Note that currently only expression parsing is implemented, and lua and wasm are planned to be supported in the future. Interested parties are welcome to participate.
关键字不允许做它用, when,then
rule [name] [description] [engine/default:expr] [...]
when
[condition 1];
[condition 2];
...
[condition n];
then
[key1 = execute 1];
[key2 = execute 2];
...
[keyn = execute n];
coda
At present, Rush is still a relatively preliminary version. The API may change in the future, but the core content will not change. The author plans to support lua and wasm in the future. Interested friends are very welcome to participate.