gengine是一款基于golang和AST(抽象语法树)开发的规则引擎,使用一套自定义的简单语法来定义规则来实现语言无关,并且还执行规则执行的各种模式,功能也很强大。

核心API

对于gengine的使用,我们先掌握几个核心的API。

DataContextRuleBuilderDataContextGengineRuleBuilderGenginePoolGengineGenginePool

gengine的使用大致分为下面的步骤:

DataContextDataContextRuleBuilderGengine

接下来就以一个示例快速了解gengine的使用。

快速使用
User
type User struct {
	Name string
	Age  int64
	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 rule1 = `
rule "name test" "i can" salience 0
begin
	if 7 == User.GetNum(7) {
		User.Age = User.GetNum(89767) + 10000000
		User.Print("6666")
	} else {
		User.Name = "yyyy"
	}
end
`

按照上一部分的使用步骤,我们来编写代码。

DataContext
//规则要修改结构体数据,所以要传入指针
user := &User{
		Name: "Calo",
		Age:  0,
		Male: true,
	}
// 创建数据上下文,用来向规则中传入数据
dataContext := context.NewDataContext()
dataContext.Add("User", user)
RuleBuilder
ruleBuilder := builder.NewRuleBuilder(dataContext)
err := ruleBuilder.BuildRuleFromString(rule1)
if err != nil {
	log.Fatal(err)
}
Gengine
eng := engine.NewGengine()
err = eng.Execute(ruleBuilder, true) // true表示有多个规则的时候,其中一个执行失败也会继续执行其余的规则
if err != nil {
	log.Fatal(err)
}
log.Printf("%v\n", user)

使用起来并不复杂,甚至相当简单。对于Gengine的使用有了初步了解,接下来我们就进一步了解规则的语法。

规则语法

gengine使用的自定义的简单语法,整体框架如下:

const rule = `
rule "rulename" "rule-description" salience  10
begin

//规则体

end`
  • rulename:规则名,必须唯一。多个同名的规则编译后会只剩下一个。
  • rule-description:规则描述,非必须。
  • salience:关键字,后跟数字表示优先级,数字越大优先级越高。
  • begin和end包裹的就是规则体,规则的具体逻辑在这里编写。

规则体支持基本的数据类型,与go语言的基本类型一致。此外还支持了string类型。对于运算,规则体支持基本的算术四则运算,逻辑运算,比较运算,还有string的加法运算。

这些类型和运算的编写与go语言基本一致,不过规则体需要使用内置的isNil()函数来判断是否为nil

const rule_else_if_test =`
rule "elseif_test" "test"
begin

a = 8
if a < 1 {
	println("a < 1")
} else if a >= 1 && a <6 {
	println("a >= 1 && a <6")
} else if a >= 6 && a < 7 {
	println("a >= 6 && a < 7")
} else if a >= 7 && a < 10 {
	println("a >=7 && a < 10")
} else        {
	println("a > 10")
}
end`

规则体还支持conc并发语法块

const rule_conc_statement  = `
rule "conc_test" "test" 
begin
	conc  { 
		a = 3.0
		b = 4
		c = 6.8
		d = "hello world"
        e = "you will be happy here!"
	}
	println(a, b, c, d, e)
end`

conc块中的语句会并发执行,需要用户自己保证conc中调用的api是线程安全的。

多级调用

.

注释

规则体支持单行注释,与go语言一样以双斜杠(//)开头。

内置变量

  • @name: 指代当前规则名
  • @id:如果规则名是可以转为整数的字符串,那么@id就是这个整数,否则就是0.
  • @desc:当前规则的描述信息。
  • @sal:当前规则的优先级,类型为int64。

自定义变量

DataContext

return支持

return可以用来直接返回规则执行,并且gengine的return可以返回不同类型的结果。

	rule := `
rule "return_in_statements"
begin
	if a < 100 {
		return 5 + 6 * 6
	} else if a >= 100 && a < 200{
		return "hello world"
	} else {
		return true
	}
end
	`
规则更新与删除

当规则数量少的时候,每次修改规则后重新编译所有的规则并没有什么问题。但是当规则数量成千上万之后,如果每次修改一个规则,就要重新编译全部的规则,那么就会造成资源的浪费。

于是Gengine支持两种更新方式,全量更新与增量更新。并且规则更新是线程安全的。

  • 全量更新:无论是新增还是修改一个规则,都会移除上一次所有规则,然后重新编译这一批传递过来的所有规则
//单实例时
ruleBuilder := builder.NewRuleBuilder(dataContext)
e1 := ruleBuilder.BuildRuleFromString(rulesString) //这里全量更新

//pool中全量更新
//第一处
pool初始化的时候是全量更新的
//第二处
func (gp *GenginePool) UpdatePooledRules(ruleStr string) error
  • 增量更新:只对新增或者更新的规则进行编译,其余的规则则不进行任何操作
//单实例 rule_builder.go
func (builder *RuleBuilder)BuildRuleWithIncremental(ruleString string) error

//pool中的增量更新接口
func (gp *GenginePool) UpdatePooledRulesIncremental(ruleStr string) error 

当规则数量很大时,使用增量更新可以显著提高效率,降低资源消耗。

  • 批量删除
//ruleBuilder中
func (builder *RuleBuilder) RemoveRules(ruleNames []string) error 

//pool中
func (gp *GenginePool) RemoveRules(ruleNames []string) error 

执行模式

看一张官方的图,gengine支持的执行模式如何所示
image

这些执行模式都是基于gengine定义的规则的优先级。

  • 顺序执行模式:按照优先级从高到低的顺序依次执行。不过要注意:没有显式制定规定的优先级,那么规则的优先级不确定,相同优先级的执行顺序不一致。
  • 并发执行模式:不考虑优先级,全部并发执行。
  • 混合模式:选择一个优先级最高先执行,剩下的n-1个并发执行。
  • 逆混合模式:选择优先级高的n-1个并发执行,最后执行剩下的一个优先级最低的规则。

顺序模式下有如下的API:

ExecuteExecuteWithStopTagDirectExecuteExecuteSelectedRules

并发模式下API:

ExecuteConcurrentExecuteSelectedRulesConcurrent

混合模式API:

ExecuteMixModelExecuteMixModelWithStopTagDirectExecuteSelectedRulesMixModel

逆混合模式API:

ExecuteInverseMixModelExecuteSelectedRulesInverseMixModel
Gengine
//must default false
stag := &engine.Stag{StopTag: false}
dataContext.Add("stag", stag)

eng := engine.NewGengine()
e2 := eng.ExecuteWithStopTagDirect(ruleBuilder, true, stag)

NM执行模式

这是衍生出来的一种模式。按照优先级高低将规则分为两部分。第一阶段为前N个最高优先级的规则,剩下M个规则为第二阶段。
按照两个阶段执行模式的不同,产生了如下子模式

执行模式 说明
nSort - mConcurrent 前n个顺序执行,后m个并发执行
nConcurrent - mSort 前n个并发执行,后m个顺序执行
nConcurrent - mConcurrent 前n个并发执行,后m个也并发执行
nSort - mSort 就是普通的顺序执行

实际上就只有3个子模式,也就是3个api

ExecuteNSortMConcurrentExecuteNConcurrentMSortExecuteNConcurrentMConcurrent

与前面的api一样,这三个api也有对应的选择式,也就是使用规则名过滤出一部分规则,然后按照对应的执行模式执行。分别是:

ExecuteSelectedNSortMConcurrentExecuteSelectedNConcurrentMSortExecuteSelectedNConcurrentMConcurrent

DAG执行模式

DAG是有向无环图执行模式,图的存储主要有邻接矩阵和临接连表两种方式,但是图的存储都是比较复杂的,gengine对此进行了优化。看官网上的图很容易就理解
image

将DAG抽象为了图层,层内规则并发执行,层间规则顺序执行。用代码的语言来说就是

func (g *Gengine) ExecuteDAGModel(rb *builder.RuleBuilder, dag [][]string) error

func (gp *GenginePool) ExecuteDAGModel(dag [][]string, data map[string]interface{}) (error, map[string]interface{})
dag[0], dag[1]dag[0]dag[1]
引擎池

池化都是为了解决性能和并发安全问题的,例如数据库连接池。
gengine在执行规则的过程中是有状态的,因此不能在一个请求的规则执行结束之前就执行下一个请求的规则,否则会破坏当前请求执行结果的正确性。

为了提高性能和并发问题,引入了引擎池,提供的所有api都是线程安全的。

//初始化一个规则引擎池
func NewGenginePool(poolMinLen ,poolMaxLen int64, em int, rulesStr string, apiOuter map[string]interface{})

参数说明:

ExecuteSelectedRulesExecuteSelectedRulesConcurrent

对于单实例的gengine的执行api,都有对应的引擎池版本的api。以其中一个为例

func (gp *GenginePool) Execute(data map[string]interface{}, b bool) (error, map[string]interface{})
data