Gengine

the rule engine based on golang

  • this is a rule engine named Gengine based on golang and AST, it can help you to load your code(rules) to run while you did not need to restart your application.
  • Gengine's code structure is Modular design, logic is easy to understand, and it passed the necessary testing!
  • it is also a high performance engine

the execute model of rules

avatar

Grammar support by Gengine

  • support the priority of the rules list and the priority scope is -int64 to int64
  • support rule's description
  • support define a local variable in a rule, and it invisible between rules
  • support 'if..else' and it's Nested structure
  • support complex logic operation
  • support complex Arithmetic (+ - * /)
  • support method of golang's structure
  • support single line comment(//)
  • support elegant check error, if there is an error in one rule, gengine will not load the rules to run to forbidden the harm to data
  • to make it easy to use,Gengine just supports one return value function's or method's Assignment and support return struct, but support call multi return value function
  • support directly inject function to run, and rename function
  • support switch to help user to decide when a rule execute error in the list whether continue to execute the last rules
  • support use '@name' to get rule's name in rule content
  • support map, slice, array
  • support rule pool, code location: gengine/engine/gengine_pool.go, test case: gengine/test/Pool_test.go

Gengine not support grammar

  • not support 'else if', beacase the creator hates 'else if'
  • not support Multi-level call such as 'user.ip.ipRisk()',because it not meet the "Dimit rule", and multi-level call make it hard to understand, so it just support this call type: 'ip.ipRisk()'
  • not support multi line comment (/* comment */)
  • not support multi return value, if you want, you can use return struct
  • not support nil

something need your attention

  • if you not declare the rules' priority, the rules will be execute in unknown sort
  • every rule's name should be different from each other

support data type

  • string
  • bool
  • int, int8, int16, int32, int64
  • float32, float64

support logic operation

  • &&
  • ||
  • !

support compared operation

  • ==
  • !=
  • >
  • <
  • >=
  • <=

support math operation

  • +
  • -
  • *
  • /
  • support string and string's plus

attention and in action

  • if you want get high performance, please do as the test case do: separate the rule build process and the rule execute process
  • when you rules contains Very time-consuming operation, such as operate database, you should use engine.ExecuteConcurrent(...), if not ,you should still use engine.Execute(...)

use

  • go mod or go vendor

Gengine rule example

//rule
rule "测试" "测试描述"  salience 0 
begin
		// rename function test; @name represent the rule name "测试"
		Sout(@name)
		// common function test
		Hello()
		//struct's method  test
		User.Say()
		// if
		if !(7 == User.GetNum(7)) || !(7 > 8)  {
			//define variable and string's plus; @name is just a string
			variable = "hello" + (" world" + "zeze")+"@name"
			// inner function
			User.Name = "hhh" + strconv.FormatBool(true)
			//struct's field 
			User.Age = User.GetNum(8976) / 1000+ 3*(1+1) 
			//bool set test
			User.Male = false
			//use inner variable test
			User.Print(variable)
			//float test		
			f = 9.56			
			PrintReal(f)
			//if-else test
			if false	{
				Sout("sout true")
			}else{
				Sout("sout false")
			}
		}else{ //else
			//struct field set value test
			User.Name = "yyyy"
		}
end

Gengine complete test

  • you can find all code in test package
import (
	"fmt"
	"gengine/base"
	"gengine/builder"
	"gengine/context"
	"gengine/engine"
	"github.com/sirupsen/logrus"
	"testing"
	"time"
)

type User struct {
	Name string
	Age  int
	Male bool
}

func (u *User)GetNum(i int64) int64 {
	return i
}

func (u *User)Print(s string){
	fmt.Println(s)
}

func (u *User)Say(){
	fmt.Println("hello world")
}

const (
	rule2 = `
rule "测试" "测试描述"  salience 0 
begin
		// 重命名函数 测试
		Sout(@name)
		// 普通函数 测试
		Hello()
		//结构提方法 测试
		User.Say()
		// if
		if 7 == User.GetNum(7){
			//自定义变量 和 加法 测试
			variable = "hello" + " world"
			// 加法 与 内建函数 测试
			User.Name = "hhh" + strconv.FormatBool(true)
			//结构体属性、方法调用 和 除法 测试
			User.Age = User.GetNum(89767999999) / 10000000
			//布尔值设置 测试
			User.Male = false
			//规则内自定义变量调用 测试
			User.Print(variable)
			//float测试	也支持科学计数法		
			f = 9.56			
			PrintReal(f)
			//嵌套if-else测试
			if false	{
				Sout("嵌套if测试")
			}else{
				Sout("嵌套else测试")
			}
		}else{ //else
			//字符串设置 测试
			User.Name = "yyyy"
		}
end`)

func Hello()  {
	fmt.Println("hello")
}

func PrintReal(real float64){
	fmt.Println(real)
}

func exe(user *User){
	/**
	 不要注入除函数和结构体指针以外的其他类型(如变量)
	 */
	dataContext := context.NewDataContext()
	//inject struct
	dataContext.Add("User", user)
	//rename and inject
	dataContext.Add("Sout",fmt.Println)
	//直接注入函数
	dataContext.Add("Hello",Hello)
	dataContext.Add("PrintReal",PrintReal)

	//init rule engine 
	knowledgeContext := base.NewKnowledgeContext()
	ruleBuilder := builder.NewRuleBuilder(knowledgeContext, dataContext)

	//resolve rules from string 
	err := ruleBuilder.BuildRuleFromString(rule2)
	if err != nil{
		logrus.Errorf("err:%s ", err)
	}else{
		eng := engine.NewGengine()

		start := time.Now().UnixNano()
		// true: means when there are many rules, if one rule execute error,continue to execute rules after the occur error rule
		err := eng.Execute(ruleBuilder, true)
		end := time.Now().UnixNano()
		if err != nil{
			logrus.Errorf("execute rule error: %v", err)
		}
		logrus.Infof("execute rule cost %d ns",end-start)
		logrus.Infof("user.Age=%d,Name=%s,Male=%t", user.Age, user.Name, user.Male)
	}
}

func Test_Base(t *testing.T){
	user := &User{
		Name: "Calo",
		Age:  0,
		Male: true,
	}
	exe(user)
}

Licence

  • Officially authorized by bilibili (www.bilibili.com)
  • BSD licensed

Question Connection

  • write issue or connect
  • renyunyi@bilibili.com (become some reason,this mail box may can't get mail)

基于golang的规则引擎

  • Gengine是一款基于AST(Abstract Syntax Tree)和golang语言实现的规则引擎。能够让你在golang这种静态语言上,在不停服务的情况下实现动态加载与配置规则。
  • 代码结构松散,逻辑极其简单,但经过了必要且详尽的测试
  • Gengine所支持的规则,就是一门DSL(领域专用语言)

Gengine支持的规则语法

  • 支持规则优先级和规则执行条件,优先级高的先执行,优先级低的后执行;
  • 支持的优先级范围 -int64 ~ int64
  • 支持中文规则名与中文规则描述
  • 支持规则内定义变量,但规则内定义的变量在规则与规则之间的不可见
  • 支持 if../if..else.. 代码块和其代码块嵌套
  • 支持复杂逻辑运算
  • 支持复杂数学四则运算(+ - * /)
  • 支持结构体方法调用
  • 支持单行注释(//)
  • 支持优雅的规则检错机制(是的,你没看错,就是检错,不是检测!):如果待加载的一批规则中有一个规则有语法错误,那么规则引擎就不会加载这批规则去执行,防止对数据造成不可预知的危害
  • 支持仅有一个返回值的函数赋值,且返回值为基础类型或结构体; 支持多返回值函数的调用,但无法处理其返回值
  • 支持直接注入函数并执行,并允许函数重命名
  • 支持规则链中有规则执行失败时,是否继续执行后续规则开关
  • 支持一些内置函数
  • 支持使用@name 在规则体内获得当前规则的名称
  • 支持基础类型的map, slice, array
  • 支持规则池,代码位置gengine/engine/gengine_pool.go, 测试用例 gengine/test/Pool_test.go

使用

  • go mod 或者 go vendor

Gengine不支持的规则语法

  • 不支持else if, 因为作者讨厌使用else if
  • 不支持形如user.ip.ipRisk()这种多级调用,因为它不符合"迪米特法则",并且会使代码变得难以理解;只支持ip.ipRisk()这种单级调用
  • 不支持函数多个返回值,当需要返回多个值时,请使用返回结构体
  • 不支持多行注释,因为不想支持
  • 不支持nil

书写规则需要注意的事情

  • 如果规则的优先级不指定,多个规则将以未知次序执行
  • 同一批待加载的规则中不能有重名规则

支持的基本数据类型

  • string
  • bool
  • int, int8, int16, int32, int64
  • float32, float64

支持的逻辑运算符

  • && 且
  • || 或
  • ! 非

支持的比较运算符

  • == 等于
  • != 不等于
  • > 大于
  • < 小于
  • >= 大于等于
  • <= 小于等于

支持的算术运算符

  • + 加
  • - 减
  • * 乘
  • / 除
  • 支持int,uint,float任意两者之间的加减乘除, 以及string与string之间的加法

支持的括号

  • 圆括号

运算优先级

  • 单目运算符非(!) > 算术运算符 > 比较运算符 > 逻辑运算符

Gengine规则示例

//规则
rule "测试" "测试描述"  salience 0 
begin
		// 重命名函数 测试
		Sout("XXXXXXXXXX")
		// 普通函数 测试
		Hello()
		//结构提方法 测试
		User.Say()
		// if
		if !(7 == User.GetNum(7)) || !(7 > 8)  {
			//自定义变量 和 加法 测试
			variable = "hello" + (" world" + "zeze")
			// 加法 与 内建函数 测试
			User.Name = "hhh" + strconv.FormatBool(true)
			//结构体属性、方法调用 和 除法 测试
			User.Age = User.GetNum(8976) / 1000+ 3*(1+1) 
			//布尔值设置 测试
			User.Male = false
			//规则内自定义变量调用 测试
			User.Print(variable)
			//float测试	也支持科学计数法		
			f = 9.56			
			PrintReal(f)
			//嵌套if-else测试
			if false	{
				Sout("嵌套if测试")
			}else{
				Sout("嵌套else测试")
			}
		}else{ //else
			//字符串设置 测试
			User.Name = "yyyy"
		}
end

Gengine完整的规则加载并执行的代码示例

  • 所有代码,在test测试包可见
import (
	"fmt"
	"gengine/base"
	"gengine/builder"
	"gengine/context"
	"gengine/engine"
	"github.com/sirupsen/logrus"
	"testing"
	"time"
)

type User struct {
	Name string
	Age  int
	Male bool
}

func (u *User)GetNum(i int64) int64 {
	return i
}

func (u *User)Print(s string){
	fmt.Println(s)
}

func (u *User)Say(){
	fmt.Println("hello world")
}

const (
	rule2 = `
rule "测试" "测试描述"  salience 0 
begin
		// 重命名函数 测试
		Sout("XXXXXXXXXX")
		// 普通函数 测试
		Hello()
		//结构提方法 测试
		User.Say()
		// if
		if 7 == User.GetNum(7){
			//自定义变量 和 加法 测试
			variable = "hello" + " world"
			// 加法 与 内建函数 测试
			User.Name = "hhh" + strconv.FormatBool(true)
			//结构体属性、方法调用 和 除法 测试
			User.Age = User.GetNum(89767999999) / 10000000
			//布尔值设置 测试
			User.Male = false
			//规则内自定义变量调用 测试
			User.Print(variable)
			//float测试	也支持科学计数法		
			f = 9.56			
			PrintReal(f)
			//嵌套if-else测试
			if false	{
				Sout("嵌套if测试")
			}else{
				Sout("嵌套else测试")
			}
		}else{ //else
			//字符串设置 测试
			User.Name = "yyyy"
		}
end`)

func Hello()  {
	fmt.Println("hello")
}

func PrintReal(real float64){
	fmt.Println(real)
}

func exe(user *User){
	/**
	 不要注入除函数和结构体指针以外的其他类型(如变量)
	 */
	dataContext := context.NewDataContext()
	//inject struct
	dataContext.Add("User", user)
	//rename and inject
	dataContext.Add("Sout",fmt.Println)
	//直接注入函数
	dataContext.Add("Hello",Hello)
	dataContext.Add("PrintReal",PrintReal)

	//init rule engine 
	knowledgeContext := base.NewKnowledgeContext()
	ruleBuilder := builder.NewRuleBuilder(knowledgeContext, dataContext)

	//resolve rules from string 
	err := ruleBuilder.BuildRuleFromString(rule2)
	if err != nil{
		logrus.Errorf("err:%s ", err)
	}else{
		eng := engine.NewGengine()

		start := time.Now().UnixNano()
		// true: means when there are many rules, if one rule execute error,continue to execute rules after the occur error rule
		err := eng.Execute(ruleBuilder, true)
		end := time.Now().UnixNano()
		if err != nil{
			logrus.Errorf("execute rule error: %v", err)
		}
		logrus.Infof("execute rule cost %d ns",end-start)
		logrus.Infof("user.Age=%d,Name=%s,Male=%t", user.Age, user.Name, user.Male)
	}
}

func Test_Base(t *testing.T){
	user := &User{
		Name: "Calo",
		Age:  0,
		Male: true,
	}
	exe(user)
}

注意 和最佳实践

  • 如果你想获得高执行效率,请将 规则的构建过程和规则的执行过程相分离
  • 如果你的规则中包含耗时,比如操作数据库,那么建议你用engine.ExecuteConcurrent(...) ,如果没有,建议你仍然用engine.Execute(...)
  • 规则引擎支持混合执行模式,优先执行一个最高优先级的规则,剩下的规则以并发的模式执行

授权

  • 哔哩哔哩官方授权开源 ( www.bilibili.com)
  • 基于BSD开源协议授权

问题联系

  • 提issue,或者联系
  • renyunyi@bilibili.com (因为公司屏蔽等原因,可能会接受不到邮件)