[Everyone's project] Universal rules engine - Rush (1) A customizable rule engine, say goodbye to publishing, and quickly configure...

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 whena conditional part and thenan 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.

e285782a97adec2e80d3a4e793cf5e67.png

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.

Guess you like

Origin blog.csdn.net/u012067469/article/details/133445889