ORM对象管理框架使用指南
更新时间:2024/12/14
在Gitcode上查看源码

ORM对象管理框架是openUBMC的一款进阶型对象管理框架。用于MDS管理对象多、相互有依赖关系、生命管理逻辑复杂的场景。当前仍处于实验阶段,在network_adapterthermal_mgmtstorage等组件均有使用。实际案例可以参考network_adapter组件源码。

注意
作为异常说明显示 本篇文档中包含大量openUBMC的基础知识,请阅读完基础指南后再进行此部分内容的学习。

适用场景

在openUBMC的研发过程中,发现某些硬件管理场景,尤其是支持热插拔的硬件场景,基础的组件框架并没有很好的支持卸载动作,需要编写大量的控制逻辑来保证对象卸载时,其创建的资源、常驻协程、信号监听回调等逻辑都需要依次处理。

lua

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_objecton_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类,需要添加数据库表名和唯一键,同时指向内存数据库。

json
{
    "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配置

json
{
    "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框架

lua
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

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框架中规范的模式,对象的初始化逻辑在ctorinit阶段完成。通过关联on_add_object_complete信号和start函数,便可以将业务逻辑统一规划到start之后,保证了业务逻辑的有效性。

对象查询机制

ORM框架的另一大特色便是可以在任意的地方快速查询已管理的MDS对象,在编写RPC、IPMI命令时非常高效。

Lua
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支持两种查询语法,findfetch

注意
此处调用的是静态属性,因此要使用.而非:

find 查询第一个匹配的对象

find主要用于查找唯一对象,通过入参进行匹配,如果没有则返回nil。如果由多个则返回第一个。

注意
Lua语言中的表实现方式为哈希表,哈希种子每次虚拟机启动时都会随机分配
在多个返回值的场景下,在同一次程序运行中每次返回的理论上是一致的,但是不同程序运行中不保证一致。

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。如果由多个则返回列表。

lua
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

同样也可以用表达式作为查找条件

lua
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