ORM对象管理框架是openUBMC的一款进阶型对象管理框架。用于MDS管理对象多、相互有依赖关系、生命管理逻辑复杂的场景。当前仍处于实验阶段,在network_adapter
,thermal_mgmt
,storage
等组件均有使用。实际案例可以参考network_adapter
组件源码。
注意:
作为异常说明显示 本篇文档中包含大量openUBMC的基础知识,请阅读完基础指南后再进行此部分内容的学习。
适用场景
在openUBMC的研发过程中,发现某些硬件管理场景,尤其是支持热插拔的硬件场景,基础的组件框架并没有很好的支持卸载动作,需要编写大量的控制逻辑来保证对象卸载时,其创建的资源、常驻协程、信号监听回调等逻辑都需要依次处理。
function app:start()
skynet.fork(function()
while true do
self.obj.PropA = SomeLogicToGetData()
skynet.sleeo(100)
end
end)
some_signal:on(function(value)
if value == "OK" then
self.obj.Status = "OK"
end
end)
end
function app:register_mds_listener()
object_manage.on_add_object(function(class, obj, position)
self.obj = obj
self:start()
end)
object_manage.on_delete_object(function(class, obj, position)
self.obj = nil
end)
end
上面是一个常见的场景,当app接收到一个MDS对象添加的回调时,将MDS对象句柄缓存,并创建了一个常驻协程,每1s更新一次MDS对象的属性。同时监听了某个信号槽,当有信号发送时,判断信号数据,并将MDS属性进行变更。
在正常的业务场景下,上述逻辑不会有任何的问题。然而当对象卸载时,问题就会出现:在对象卸载回调中,仅将MDS对象的引用进行了解除,但常驻协程、信号监听并没有进行处理。
- 当常驻协程再一次唤醒时,便会调用已经被释放掉的MDS对象,会触发Lua异常。如果在协程中对Lua异常做了捕捉和忽略,那么这个常驻协程会一直存在在内存中,且持有MDS对象的句柄,因此MDS对象也不会被GC。若经常出现MDS对象的加载和卸载业务,那么系统资源CPU和内存都会被逐渐消耗,最后导致OOM。
- 信号槽监听也是一样,在对MDS对象进行赋值时,便会触发Lua异常,导致信号槽监听链上其他的监听函数也会失效,导致故障扩散。
因此正确的做法是将MDS对象创建的所有常驻协程、信号监听等根据对象的某种唯一标识进行管理。在MDS对象卸载时,对应的资源全部卸载。在本例子中,管理逻辑相对简单,但当套入实际业务时,MDS对象多、关系复杂,且MDS对象可能会触发加载卸载的时候,管理逻辑就会成指数级增长,任意步骤处理疏忽、处理不当都会导致问题。
设计概念
在使用ORM框架前,首先先看一下ORM框架的设计概念,便于更好的理解和使用此框架。
生命周期管理
首当其冲的设计概念便是对MDS对象的生命周期管理。用户无需再关注MDS对象的生命周期。
框架统一管理
对象的加载和卸载由ORM框架进行管理,on_add_object
和on_delete_object
等回调均会被框架封装。
多个MDS对象的依赖关系也由ORM框架统一进行管理。
业务逻辑代码焦距
对MDS类进行了扩展,开发者直接在MDS类中进行业务逻辑编写。原有的MDS类仅包含mds/model.json
中定义的属性,无法添加临时数据。在ORM框架中,MDS类可以声明临时内存属性。资源协作接口、CSR配置属性无法变更。
资源创建代理
创建协程、信号注册等资源创建机制均有封装函数,通过调用封装函数创建,ORM框架可以记录并管理资源的生命周期,开发者无感知。
资源统一管理
ORM框架中引入了数据库管理思路,借用数据库的概念简化对象的管理机制。同时ORM框架对于资源的创建也进行了约束。
基于业务属性的唯一键
ORM框架管理的MDS类都需要指定MDS类的唯一键,便于索引和存储数据。开发者可以基于业务属性维度进行唯一键定义,方便对接对应业务逻辑。
内存资源显式声明
虽然ORM框架对MDS类进行了扩展,允许开发者添加运行内存中需要的属性,但仍有强约束。ORM框架不允许MDS扩展类在运行中途添加内存属性,而是必须要构造函数显式声明,从代码逻辑层面防止额外资源的创建。
统一的查询方法
ORM框架为每个MDS类提供了全局搜索能力,基于定义的唯一键,可以在组件代码任意处获取MDS对象。在对外接口实现时,开发者可以获取对象和对应的属性。
统一编写框架
ORM框架是一种强管控框架,对业务逻辑编写有很多的限制,开发者只能在框架的约束条件下进行开发。因此ORM框架对MDS的设计要求较高。
使用方法
由于ORM属于进阶特性,因此需要手动设置后,组件才可以使用。
MDS配置
对需要管理的MDS类,需要添加数据库表名和唯一键,同时指向内存数据库。
{
"MyCSRModel": {
"tableName": "t_my_csr_model",
"tableType": "Memory",
"path": "/bmc/demo/MyCSRModel/${id}"
"interfaces": {
"bmc.kepler.OpenUBMC.Reading": {
"properties":{
"TemperatureCelsius": {
"usage": ["CSR"]
}
}
}
},
"Properties": {
"Id": {
"usage": ["CSR"],
"type": "String",
"primaryKey": true
}
}
}
}
此处,我们对MyCSRModel
进行了改造,如同添加持久化的方式一致,声明MyCSRModel
绑定的tableName
,同时设置唯一键Id
,在CSR中进行配置。
CSR配置
{
"Object": {
"MyCSRModel_Demo1Obj": {
"TemperatureCelsius": 10,
"Id": "Obj1"
},
"MyCSRModel_Demo2Obj": {
"TemperatureCelsius": 20,
"Id": "Obj2"
},
"MyCSRModel_Demo3Obj": {
"TemperatureCelsius": 30,
"Id": "Obj3"
},
}
}
由于添加了Id,因此需要对应的在CSR配置中进行添加。此处需要确保唯一键唯一。对于同CSR可能会加载多份的场景(如Riser、硬盘背板),需要对唯一键进行设计,如果唯一键冲突,则会导致对象无法加载。
APP中启动ORM框架
打开组件对应的app.lua
文件,添加ORM框架
local class = require 'mc.class'
local c_service = require 'my_app.service'
local c_object_manage = require 'mc.orm.object_manage' -- 加载ORM框架
local app = class(c_service)
function app:ctor()
end
function app:init()
app.super.init(self)
self.orm = c_object_manage.new(self.bus, self.db) -- 创建ORM框架
self.orm.app = app -- ORM框架初始化赋值
self.orm.per_db = self.db -- ORM框架初始化赋值
self.orm:start() -- 启动ORM框架
end
return app
在组件启动阶段,创建ORM框架、初始化,并启动框架。
编写MDS对象管理逻辑
创建一个Lua文件,一般可以使用MDS类名作为文件名。此处创建lualib/MyCSRModel.lua
local c_object = require "mc.orm.object" -- 加载 ORM对象管理机制
local c_task = require "mc.tasks"
local my_csr_model = c_object("MyCSRModel") -- 通过ORM对象管理机制创建类
function my_csr_model:start() -- 业务逻辑的起始点
self.tasks:new_task(string.format("UpdateTask%d", self.Id))
:loop(function(task) -- 通过task机制创建一个常驻协程
if self.TemperatureCelsius > 120 then
task:stop() -- task内部提供停止操作
end
self.TemperatureCelsius = self.TemperatureCelsius + 1 -- 可以直接使用MDS对象的属性
self.rpc_message = string.format("current temp is %d", self.TemperatureCelsius)
end):set_timeout_ms(5000) -- 设置常驻协程轮询周期
end
function my_csr_model:dtor() -- 析构函数,允许用户自定义析构操作
end
function my_csr_model:init()
self:connect_signal(self.on_add_object_complete, function() -- 使用自身的函数进行信号监听
self:start() -- 当一个CSR的MyCSRModel都加载结束后,再开始业务逻辑
end)
my_csr_model.super.init(self)
end
function my_csr_model:ctor()
self.tasks = c_task.new() -- 创建task机制,用于创建协程
self.rpc_message = "" -- 内存资源需要显式声明
end
return my_csr_model
在这里,我们不再通过mc.class
来声明类,而是通过ORM框架来创建类,同时传入对应的MDS类的名字,以便ORM框架将两者关联起来。
同样,协程的创建也不再依赖Skynet提供的能力,而是利用封装的mc.tasks
机制来实现。
上述的结构便是ORM框架中规范的模式,对象的初始化逻辑在ctor
和init
阶段完成。通过关联on_add_object_complete
信号和start
函数,便可以将业务逻辑统一规划到start
之后,保证了业务逻辑的有效性。
对象查询机制
ORM框架的另一大特色便是可以在任意的地方快速查询已管理的MDS对象,在编写RPC、IPMI命令时非常高效。
local class = require 'mc.class'
local c_service = require 'my_app.service'
local c_object_manage = require 'mc.orm.object_manage'
local ipmi_struct = require 'my_app.ipmi.ipmi'
local ipmi_msg = require 'my_app.ipmi.ipmi_message'
local ipmi = require 'ipmi'
local c_my_csr_model = require 'MyCSRModel' -- 加载MyCSRModel类
local app = class(c_service)
function app:ctor()
end
function app:init()
app.super.init(self)
self.orm = c_object_manage.new(self.bus, self.db)
self.orm.app = app
self.orm.per_db = self.db
self.orm:start()
self:register_ipmi_and_rpc()
end
function app:register_ipmi_and_rpc()
self:register_ipmi_cmd(ipmi_struct.SomeIPMICmd, function(req, ctx, ...)
local obj = c_my_csr_model.collection:find({ Id = req.Id }) -- 使用MyCSRModel自带的collection能力进行搜索
if not obj then
return ipmi_msg.GetMessageRsp.new(ipmi.types.Cc.InvalidFieldRequest)
end
return ipmi_msg.GetMessageRsp.new(ipmi.types.Cc.Success, obj.rpc_message) -- 对象可以直接访问
end)
end
return app
通过ORM框架创建的类,会自带一个collection
静态属性。同故宫这个静态属性便可以进行对象的查询。输入的入参便是查询query表,也支持匹配函数。
collection
查询能力
collection
支持两种查询语法,find
和fetch
注意:
此处调用的是静态属性,因此要使用.
而非:
find
查询第一个匹配的对象
find
主要用于查找唯一对象,通过入参进行匹配,如果没有则返回nil
。如果由多个则返回第一个。
注意:
Lua语言中的表实现方式为哈希表,哈希种子每次虚拟机启动时都会随机分配
在多个返回值的场景下,在同一次程序运行中每次返回的理论上是一致的,但是不同程序运行中不保证一致。
local function find_by_id(id)
local obj1 = c_my_csr_model.collection:find({Id = id})
local obj2 = c_my_csr_model.collection:find(function(obj)
if obj.Id == id then
return
end
end)
assert(obj1 == obj2) -- 上述两种查询方式是一致的
end
fetch
查询所有匹配的对象
fetch
用于查询所有对象集,通过入参进行匹配,如果没有则返回nil
。如果由多个则返回列表。
local function find_all_by_temp(val)
local obj1 = c_my_csr_model.collection:fetch({ TemperatureCelsius = val})
local obj2 = c_my_csr_model.collection:fetch(function(obj)
if obj.TemperatureCelsius == val then
return
end
end)
assert(obj1 == obj2) -- 上述两种查询方式是一致的
end
同样也可以用表达式作为查找条件
local function find_all_large_id(id)
local obj_list = c_my_csr_model.collection:fetch(function(obj)
if obj.Id >= id then
return
end
end)
assert(type(obj_list) == 'table')
end