本文最后更新于:2021年5月31日 下午

本文主要讲了轻量级规则引擎设计过程

通用轻量级规则引擎:RulEngine

关于

什么是规则引擎?本质上就是个数据选择器,每个规则只关心自己匹配到部分,来进行进一步的操作和处理,更像是一个管道操作,本质是上一次操作的输出作为下一次的输入,整个流程如图所示。

Input -> Rule1: Input data
Note over Rule1: RulEngine.Run(Rule1)


Rule1 -> Rule2 : next, NewInput
Note over Rule2: RulEngine.Run(Rule2)
Rule2 --> Rule1 : stop

Rule2 -> Rule3 : next, NewInput
Note over Rule3: RulEngine.Run(Rule3)
Rule3 -> Rule2 : stop


Rule3 -> RuleN : next, NewInput
Note over RuleN: RulEngine.Run(RuleN)
RuleN -> Rule3 : stop

RuleN -> Output : Output

技术

规则引擎核心部件采用 golang 作为主要开发语言来实现,规则用 JavaScript 脚本来动态实现,本质上是 golang 启动一个 JavaScript解释器,然后执行规则定义代码。

设计

生命周期

规则引擎生命周期如下所示:

Start-> InitInEnds: Initial Input Resources
InitInEnds-> InitOutEnds: Initial Output Resources
InitOutEnds-> LoadRules: Load Rules
LoadRules -> Working : Work
Working -> Working : Working State
Working-> Destory: Free Resources
Destory-> Stop: Stop Rule Engine

外部接口

  • Start() : 启动规则引擎,返回值是个 MAP,里面包含了一些配置信息或者环境变量;
  • InitInEnd() list.List: 初始化输入端的资源,通常指规则引擎需要监听的端;
  • InitOutEnd() list.List: 初始化数据流出的目的地,比如数据库或者是转发;
  • LoadRule() list.List: 加载规则,在这里指的是 JavaScript 脚本;
  • Work(): 准备就绪开始工作;
  • Destory(): 规则引擎停止之前的释放资源操作;
  • Stop(): 停止规则引擎。

实现原理

RuleEngine 执行脚本过程:

data = InEnd.input
RuleEngine.work(inendId, data)
    -> RuleEngine.call(Js1, Input): next, NewInput | stop
    -> RuleEngine.call(Js2, Input): next, NewInput | stop
    -> .......
    -> RuleEngine.call(Jsn, Input): next, NewInput | stop

规则使用JavaScript来描述:

let name = "demo rule1";
let description = "ok, let's go";
let from = ["in_id1", "in_id2"];
let actions = [
    function a(params) {
        print("params1", params)
    },
    function b(params) {
        print("params2", params);
    }
    // …………
];

function success() {
    print("success callback rule call success");
}

function failed(error) {
    print("failed callback ,rule call error:", error);
}

字段解释:

  • name: 规则的名字
  • description:描述信息
  • from:来自哪个输入端,这是个数组,值是输入端的ID
  • actions:规则过滤函数
  • success:规则执行成功后回调
  • failed:规则执行失败后回调

JavaScript 规则脚本回调函数:

// 其中params表示输入的数据
function ${name)(params) {
   // todo
    return ${RETURN};
}
  • 回调函数不可以是匿名函数
  • 回调函数必须是 1个参数
  • 回调函数返回值必须是"stop" 或者 ["next", params]

注意:规则描述里面所有字段必须是必填的,缺一不可。

重要表结构

规则引擎为了加速和利用硬件性能,所有运行时数据全部存在某个内存表里面,因此需要合理设计几张内存表。

输入端

字段名 类型 备注
id string 唯一ID,系统分配
type string 类型
name string 名称
description string 描述文本
config Map string 配置信息,包含了输入端的有些配置,比如可能是MQTT的鉴权,Topic等
cts timestamp 创建时间

输入端一般指的是外部数据产生方,比如来自HTTP的请求,或者MQTT消息等。当创建一个输入端以后,系统会启动一个进程来维护这个输入端。

其中输入端业务必须实现接口:void RunEngine(data),最后数据流转的时候,回调这个接口:

inend.RunEngine(data){

​ id : = 自己的唯一ID

rulEngine.work(id, data)

}

规则

字段名 类型 备注
id string 唯一ID,系统分配
in_end string 输入端的ID
type string 类型
name string 名称
script string 规则脚本
description string 描述文本
config Map string 配置信息
cts timestamp 创建时间

规则表相对来说比较负责,除了基础字段,还包含了一个输入端ID。输入端ID主要用来标记该规则作用点,假设我们需要一个MQTT消息处理规则,则首先我们需要创建一个InEnd,然后创建一个规则,规则作用点为之前创建的InEnd。

输出端

字段名 类型 备注
id string 唯一ID,系统分配
type string 类型
name string 名称
description string 描述文本
config Map string 配置信息
cts timestamp 创建时间

输出端和输入端结构保持一致,原理就是把数据最终吐出到某个地方。

系统用户表

字段名 类型 备注
username string 用户名
password string 密码
email string Email
phone string 手机号码
extra_info string 扩展信息

因为用户管理不是本系统的主要功能,所以尽量做得简单,只做了个基础鉴权功能。如果觉得用户信息还需要扩展,那就在 extra 字段里面保存一些额外信息。

参考

[otto ] ( A JavaScript interpreter in Go)

[Rule engine] (OpenRules Rule Engine)