ORM Object Management Framework User Guide
更新时间: 2024/12/14
在Gitcode上查看源码

ORM is an advanced object management framework of openUBMC. It is used in scenarios with many MDS objects, interdependencies among them, and complex lifecycle management. It is still in the experimental phase and is used in components such as network_adapter, thermal_mgmt, and storage. For details, see the component source code network_adapter.

NOTE
This document covers extensive openUBMC basics. You are advised to read the basic guide beforehand.

Applicable Scenarios

During the R&D of openUBMC, it is found that in some hardware management scenarios, especially those supporting hot swap, the basic component framework provides limitied support for unloading. As a result, a significant amount of control logic needs to be written to ensure that created resources, resident coroutines, and signal listening and callback features are processed in sequence during object unloading.

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

The preceding is a common scenario. When receiving a callback added by an MDS object, the app caches the MDS object handle, creates a resident coroutine, and updates the attributes of the MDS object every second. At the same time, the app monitors a signal slot. When a signal is sent, it changes the MDS attribute based on the signal data.

In normal service scenarios, the above logic presents no issues. However, problems arise during object unloading: The callback only releases the MDS object references, leaving resident coroutine and signal listening unprocessed.

  • When the resident coroutine is woken up again, the released MDS object is invoked, triggering a Lua exception. If the Lua exception is captured and ignored in the coroutine, the resident coroutine will exist in the memory, holding handles to the MDS object. Consequently, the MDS object cannot be garbage collected. In scenarios where hardware is inserted or removed, frequent loading and unloading of MDS objects occur. This consumes system resources (CPU and memory), eventually leading to out of memory.
  • Problems also arise with signal slot listening. Assigning values to the MDS object triggers Lua exceptions, which can invalidate other listening functions in the signal-slot listening chain and cause fault propagation.

Therefore, the correct approach is to manage all resident coroutines and signal listening created by the MDS object based on a unique identifier of the object. When the MDS object is unloaded, all corresponding resources are unloaded as well. In this example, the management logic is relatively simple. However, in real-world scenarios—where there are many MDS objects with complex relationships, and where MDS objects may trigger loading and unloading—the management logic grows exponentially. If any step is not properly processed, problems may occur.

Concept Design

Before using the ORM framework, you are advised to first understand its design concepts to gain a better understanding of how to use it effectively.

Lifecycle Management

The first design concept is the lifecycle management of MDS objects. Users do not need to pay attention to the lifecycle of MDS objects.

Unified Framework Management

Object loading and unloading are managed by the ORM framework. Callback functions such as on_add_object and on_delete_object are encapsulated by the framework.

The dependencies of multiple MDS objects are also managed by the ORM framework in a unified manner.

Service Logic Code

The MDS class is extended. Developers can directly write service logic in the MDS class. The original MDS class contains only the attributes defined in mds/model.json. Temporary data cannot be added. In the ORM framework, the MDS class can declare temporary memory attributes. The resource collaboration interface and CSR configuration attributes cannot be modified.

Resource Creation Agent

Resource creation mechanisms, such as coroutine creation and signal registration, have encapsulation functions. By calling the encapsulation functions, the ORM framework can record and manage the lifecycle of resources, which is imperceptible to developers.

Unified Resource Management

The ORM framework introduces the database management concept to simplify the object management mechanism. The framework also restricts resource creation.

Unique Key Based on Service Attributes

A unique key must be specified for each MDS class managed by the ORM framework to facilitate indexing and data storage. Developers can define unique keys based on service attributes to facilitate interconnection with corresponding service logic.

Explicit Declaration of Memory Resources

The ORM framework extends the MDS class and allows developers to add attributes required in the running memory. However, there are still strong constraints. The framework does not allow the MDS extension class to add memory attributes during running. Instead, the memory attributes must be explicitly declared in the constructor to prevent the creation of extra resources at the code-logic level.

Unified Query Method

The ORM framework provides global search for each MDS class. Based on the defined unique key, the MDS object can be obtained from anywhere in the component code. When implementing external interfaces, developers can obtain objects and corresponding attributes.

Unified Writing Framework

The ORM framework is a strong control framework, which has many restrictions on service logic writing. Developers can develop services only under the constraints of the framework. Therefore, the framework places high requirements on the MDS design.

How to Use

ORM is an advanced feature. You need to manually include it before use.

MDS Configuration

For the MDS class to be managed, you need to add the database table name, unique key, and the memory database.

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
            }
        }
    }
}

The MyCSRModel is modified in the same way as the persistence is added. The tableName bound to the MyCSRModel is declared, and the unique key Id is set in the CSR.

CSR Configuration

json
{
    "Object": {
        "MyCSRModel_Demo1Obj": {
            "TemperatureCelsius": 10,
            "Id": "Obj1"
        },
        "MyCSRModel_Demo2Obj": {
            "TemperatureCelsius": 20,
            "Id": "Obj2"
        },
        "MyCSRModel_Demo3Obj": {
            "TemperatureCelsius": 30,
            "Id": "Obj3"
        },
    }
}

The Id is added. You also need to add it in the corresponding CSR configuration. Ensure that the unique key is unique. In scenarios where multiple copies of the CSR may be loaded (such as riser cards and drive backplanes), the unique key needs to be designed. If unique keys conflict, the object cannot be loaded.

Starting the ORM Framework in the App

Open the app.lua file corresponding to the component and add the ORM framework.

lua
local class = require 'mc.class'
local c_service = require 'my_app.service'
local c_object_manage = require 'c.orm.object_manage' -- Load the ORM framework.

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) --Create the ORM framework.
    self.orm.app = app -- Assign values during ORM framework initialization.
    self.orm.per_db = self.db -- Assign values during ORM framework initialization.
    self.orm:start() -- Start the ORM framework.
end

return app

In the component startup phase, create, initialize, and start the ORM framework.

Writing the MDS Object Management Logic

Create a Lua file. Generally, the MDS class name can be used as the file name. Create lualib/MyCSRModel.lua.

lua
local c_object = require "mc.orm.object" -- Load the ORM object management mechanism.
local c_task = require "mc.tasks"


local my_csr_model = c_object("MyCSRModel") -- Create a class through the ORM object management mechanism.

function my_csr_model:start() --Start point of the service logic.
    self.tasks:new_task(string.format("UpdateTask%d", self.Id))
        :loop(function(task) -- Create a resident coroutine using the task mechanism.
            if self.TemperatureCelsius > 120 then
                task:stop() -- The task internally provides the stop operation.
            end
            self.TemperatureCelsius = self.TemperatureCelsius + 1 -- The attributes of the MDS object can be used.
            self.rpc_message = string.format("current temp is %d", self.TemperatureCelsius)
        end):set_timeout_ms(5000) -- Set the polling period of the resident coroutine.
end

function my_csr_model:dtor() -- Destructor, which allows users to customize destruct operations.
end

function my_csr_model:init()
    self:connect_signal(self.on_add_object_complete, function() -- The class provides the callback signal function to listen to signals.
        self:start() -- Start the service logic after all MyCSRModels of a CSR are loaded.
    end)
    my_csr_model.super.init(self)
end

function my_csr_model:ctor()
    self.tasks = c_task.new() -- Create the task mechanism to create coroutines.
    self.rpc_message = "" --Explicitly declare memory resources.
end

return my_csr_model

Here, we do not use mc.class to declare a class. Instead, we use the ORM framework to create a class and transfer the name of the corresponding MDS class so that the ORM framework can associate them.

Similarly, the creation of coroutines does not depend on Skynet. Instead, it is implemented by using the encapsulated mc.tasks mechanism.

The preceding structure is the standard mode in the ORM framework. The object initialization logic is completed in the ctor and init phases. By associating the on_add_object_complete signal with the start function, the service logic can be uniformly planned after the start function finishes, thereby ensuring validity of the service logic.

Object Query Mechanism

Another feature of the ORM framework is that managed MDS objects can be quickly queried anywhere, which is very efficient in writing RPC and IPMI commands.

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' -- Load the MyCSRModel class.

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 }) -- Use the collection capability of MyCSRModel for search.
        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) --The object can be directly accessed.
    end)
end

return app

A class created using the ORM framework has a collection static attribute. You can query objects using this static attribute. The input parameter is the query table, and the matching function is also supported.

collection query capability

collection supports two query syntaxes: find and fetch.

NOTE
The static attribute is invoked. Therefore, use . instead of :.

find queries the first matched object.

find is used to query a unique object based on input parameters. If no value is generated, nil is returned. If there are multiple values, the first one is returned.

NOTE
Because Lua uses hash tables for its table type, the hash seed is randomly allocated each time the VM is started.
In scenarios where there are multiple values, the return values are theoretically the same in the same program, but may not be consistent in different programs.

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) -- The two query methods above produce the same result.
end
fetch queries all matched objects.

fetch is used to query all object sets based on input parameters. If no value is generated, nil is returned. If there are multiple values, a list is returned.

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) -- The two query methods above produce the same result.
end

You can also use expressions as query conditions.

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