Open
feat: 增加有界异步 PLDM GetDateTime 业务适配模块 #12
liuxingsenAdd on 14 days ago
feat: 增加有界异步 PLDM GetDateTime 业务适配模块 #12
Open
liuxingsenAdd on 14 days ago
Total: 7 files changed+718-1
@@ -1,5 +1,8 @@
1# Changelog1# Changelog
2All notable changes to this project will be documented in this file.2All notable changes to this project will be documented in this file.
3+## [1.130.17] - 2026-10-26
4+新增 PLDM GetDateTime 异步业务适配器,约束 IPC 数量与期限并校验 UTC BCD 时间;传输入口接线另行提交。
5+ 
3## [1.130.15] - 2026-08-256## [1.130.15] - 2026-08-25
4更新版本号,触发流水线推包7更新版本号,触发流水线推包
5 8 
@@ -0,0 +1,13 @@
1+# PLDM 取时业务适配器
2+ 
3+`pldm_time.register(responder, bus)` 注册 BIOS GetDateTime(Type 3、Command 12)。适配器接受提供 `register` 方法的命令注册表;`bus` 必须是完整生产 `SDBus`,同时提供 `timeout_call` 和原始连接 `bus.bus:timeout_call`,不能只传原始D-Bus对象;本 PR 提供可独立测试的业务模块,尚未接入生产收包入口,不宣告 BMC 已支持该命令。传输解码、回复令牌、能力发布与入口接线另行提交。
4+ 
5+通过 MDB 发现唯一 Time 对象及服务,发现接口保留共享内存快路径,远程回退受剩余预算保护;通过单次有期限属性 RPC 读取 `SyncStatus` 和 `DateTime`,校验日历、时区并编码 UTC BCD。未授时/无效时间返回 `0x8A`;服务缺失、IPC 失败及业务超预算返回 `NOT_READY`。20ms 业务预算先于 50ms 命令期限,单调时钟防止 tick 提前结束或迟到结果被记为成功;最多保留 16 个未返回的 IPC,协议超时不冒充底层调用已取消。
6+ 
7+单测隔离命令注册接口,覆盖真实接口名称、板级对象路径、单次 RPC、时区/闰年、迟到回调、服务退出和有界等待。此前完整集成候选 `4620c09` 在专用 43000 四张 SP888 上完成 44 次真实 Base/时间/错误码请求,P95 12.38ms、最大 12.94ms;该结果属于集成候选,不冒充本独立模块已接线。真实未授时切换尚未执行。
8+ 
9+检视补充:单测先核实真实单调时钟 API、timespec 字段和 `SDBus.timeout_call` 存在,再替换时钟。IPC超时返回后释放调用槽位;DEBUG 日志区分对象/owner缺失或歧义、预算耗尽、IPC失败、属性缺失、无效时间及过载,带本地请求编号和阶段,不输出远程错误正文。该业务模块没有传输路由上下文,不伪造Network/EID/Tag等字段。
10+ 
11+偏移语法按 [RFC3339 §5.6](https://www.rfc-editor.org/rfc/rfc3339.html#section-5.6):数值偏移复用00~23的小时字段;不能把现实民用时区范围误当成协议语法的±14:00上限。新增±23:59的UTC跨日验证及24:00拒绝测试;未知偏移-00:00仍不作为有效绝对时间。
12+ 
13+评审回归:接受RFC3339的小写t/z;16个实际挂起调用到业务期限后仅完成回复,继续保留IPC槽位,防止把未取消的工作误计为已释放而突破上限。底层有期限调用返回后槽位归还,迟到完成不重复应答;若底层违约永久不返回,则保持有界拒绝而不继续堆积协程。该故障需要修复底层,不能靠提前释放计数掩盖。
@@ -2,7 +2,7 @@
2 "name": "mctpd",2 "name": "mctpd",
3 "type": "application",3 "type": "application",
4 "deployConfig": "framework.service",4 "deployConfig": "framework.service",
5- "version": "1.130.15",5+ "version": "1.130.17",
6 "author": "Huawei",6 "author": "Huawei",
7 "license": "Mulan PSL v2",7 "license": "Mulan PSL v2",
8 "options": {8 "options": {
@@ -0,0 +1,211 @@
1+-- Copyright (c) 2026 Huawei Technologies Co., Ltd.
2+-- openUBMC is licensed under Mulan PSL v2.
3+-- You can use this software according to the terms and conditions of the Mulan PSL v2.
4+-- You may obtain a copy of Mulan PSL v2 at:
5+-- http://license.coscl.org.cn/MulanPSL2
6+-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
7+-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
8+-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
9+-- See the Mulan PSL v2 for more details.
10+ 
11+-- SPDX-License-Identifier: MulanPSL-2.0
12+local skynet = require('skynet')
13+local context = require('mc.context')
14+local clock = require('utils.core')
15+local log = require('mc.logging')
16+local M = {}
17+local ROOT, INTERFACE = '/bmc/kepler/Managers', 'bmc.kepler.Managers.Time'
18+ 
19+local function days(year, month)
20+ if month == 2 and year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0) then
21+ return 29
22+ end
23+ return ({ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 })[month]
24+end
25+ 
26+local function bcd(value)
27+ return (value // 10) * 16 + value % 10
28+end
29+ 
30+function M.encode(snapshot)
31+ if type(snapshot) ~= 'table' or snapshot.SyncStatus ~= true or type(snapshot.DateTime) ~= 'string' then
32+ return
33+ end
34+ local y, m, d, h, minute, second, zone =
35+ snapshot.DateTime:match('^(%d%d%d%d)%-(%d%d)%-(%d%d)[Tt](%d%d):(%d%d):(%d%d)(.*)$')
36+ if not y then
37+ return
38+ end
39+ y, m, d, h, minute, second = tonumber(y), tonumber(m), tonumber(d), tonumber(h), tonumber(minute), tonumber(second)
40+ if y < 1 or m < 1 or m > 12 or d < 1 or d > days(y, m) or h > 23 or minute > 59 or second > 59 then
41+ return
42+ end
43+ zone = zone:gsub('^%.%d+', '')
44+ local offset = 0
45+ if zone ~= 'Z' and zone ~= 'z' then
46+ local sign, oh, om = zone:match('^([+-])(%d%d):(%d%d)$')
O
Oopenubmc-ci-bot13 days ago

💡 建议:补充"小数秒 + 数值时区偏移"组合的单测

M.encode 会先剥离小数秒再匹配数值偏移,但当前单测只分别覆盖了两者:2000-02-29T23:59:59.125Z(小数秒+Z)和 2026-01-01T00:00:00+23:59(无小数秒、数值偏移),没有把两者组合起来验证(如 2024-02-29T23:59:59.125+08:00)。

该组合正是生产场景常见格式(本地时间带小数秒)。一旦未来调整 zone:gsub('^%.%d+','') 的正则顺序或剥离逻辑,该路径回归不易被测试发现。

修复建议:在 TestPldmTime:test_utc_bcd_calendar_and_timezone 增加一条断言,例如 { '2024-02-29T23:59:59.125+08:00', '\x59\x59\x15\x29\x02\x24\x20' }, 覆盖"小数秒 + 数值偏移"的组合路径。

likedislike
liuxingsen
13 days ago Comments:
47+ if not sign or tonumber(oh) > 23 or tonumber(om) > 59 or zone == '-00:00' then
atomgit-botO
atomgit-botatomgit-bot14 days ago

🔵 Low Priority

变更行:src/lualib/pldm_time.lua:46 的时区校验 tonumber(oh) > 23 允许 ±23:59 的偏移。

影响的行为/契约:本模块的契约是"无效时间返回 0x8A"(issue #10:BCD 需符合 DSP0247 §8.12,时间统一为 UTC)。DateTime 使用 RFC3339 形式解析,而 RFC3339 允许的最大偏移只有 ±14:00;当前实现接受 ±14:01~±23:59 的偏移。

失败模式:若时间服务下发 '2026-01-01T00:00:00+20:00' 这类非法偏移,第49行仍算出 offset=1200、第51行随即把日期回退一天生成一个"看起来合法"的 UTC 时间并以 CC=0 上报给端点,而不是按设计返回 0x8A。端点会拿到一个错误的 BMC 时间且无任何错误指示,比拒绝更糟。触发前提是时间服务输出越界偏移(另一端缺陷),故置信度不高,属边界校验缺口。

建议:把时区偏移上界由 23 收紧为 14,并要求 14 时只能是 14:00:tonumber(oh) > 14 or (tonumber(oh) == 14 and tonumber(om) ~= 0),使越界偏移与其它非法时间一样返回 nil(最终 0x8A)。

Suggestions
47
- if not sign or tonumber(oh) > 23 or tonumber(om) > 59 or zone == '-00:00' then
47
+ if not sign or tonumber(oh) > 14 or tonumber(om) > 59 or (tonumber(oh) == 14 and tonumber(om) ~= 0) or zone == '-00:00' then
Apply suggestion
likedislike
Incorrect?
liuxingsen
14 days ago Comments:
Oopenubmc-ci-bot14 days ago

💡 建议:DateTime 的 T/Z 分隔符仅接受大写(RFC3339 允许小写)

L34-35 的匹配要求 T 且 Z 必须为大写,而 RFC3339(§5.6 Note 3)允许小写 t/z。若时间服务下发小写形式(如 2026-01-01t00:00:00z),本模块会按无效时间返回 0x8A,而非接受。

修复建议:可对 T/t 与 Z/z 做大小写不敏感处理,或在文档中明确只接受大写(当前行为)。鉴于现网服务通常输出大写,此项为可选优化,可按团队约定取舍。

likedislike
liuxingsen
14 days ago Comments:
48+ return
49+ end
50+ offset = (tonumber(oh) * 60 + tonumber(om)) * (sign == '+' and 1 or -1)
51+ end
52+ local minutes = h * 60 + minute - offset
53+ if minutes < 0 then
54+ minutes, d = minutes + 1440, d - 1
55+ if d == 0 then
56+ m = m - 1
57+ if m == 0 then
58+ y, m = y - 1, 12
59+ end
60+ d = days(y, m)
61+ end
62+ elseif minutes >= 1440 then
63+ minutes, d = minutes - 1440, d + 1
64+ if d > days(y, m) then
65+ d, m = 1, m + 1
66+ if m == 13 then
67+ y, m = y + 1, 1
68+ end
69+ end
70+ end
71+ if y < 1 or y > 9999 then
72+ return
73+ end
74+ return string.char(bcd(second), bcd(minutes % 60), bcd(minutes // 60), bcd(d), bcd(m), bcd(y % 100), bcd(y // 100))
75+end
76+ 
77+local function now()
78+ local value = clock.monotonic_time()
79+ return value.tv_sec * 1000 + value.tv_nsec // 1000000
80+end
81+ 
82+local function diagnostic(trace, reason, deadline)
83+ log:debug(
84+ 'pldm_time: request=%d reason=%s stage=%s remaining_ms=%d path=%s service=%s',
85+ trace.id,
86+ reason,
87+ trace.stage,
88+ deadline - now(),
89+ trace.path or '-',
90+ trace.service or '-'
91+ )
92+end
93+ 
94+local function read_time(bus, deadline, trace)
95+ local function unavailable(reason)
96+ diagnostic(trace, reason, deadline)
97+ return 4, ''
98+ end
99+ local remaining = deadline - now()
100+ if remaining <= 0 then
101+ return unavailable('budget_exhausted')
102+ end
103+ -- 保留 MDB 共享内存发现快路径;回退到远程调用时也受剩余预算限制。
104+ local objects = bus:timeout_call(
105+ remaining,
106+ 'bmc.kepler.maca',
107+ '/bmc/kepler/MdbService',
108+ 'bmc.kepler.Mdb',
109+ 'GetSubObjects',
110+ 'a{ss}sias',
111+ context.new(),
112+ ROOT,
113+ 1,
114+ { INTERFACE }
115+ )
atomgit-bot
atomgit-botatomgit-bot14 days ago

🟡 Medium Priority

变更行:src/lualib/pldm_time.lua:83-93 的 local objects = bus:call('bmc.kepler.maca', '/bmc/kepler/MdbService', 'bmc.kepler.Mdb', 'GetSubObjects', ...) 使用不带超时的 bus:call;只有第二次 GetAllWithContext 走了 bus.bus:timeout_call(remaining, ...)。模块自身的注释与 PR 描述都声称"单次有期限 RPC / 有界",但预算只作用在第二条 RPC 上。

影响的行为/契约:

  1. remaining = deadline - now()(第102行)只能在发现调用返回之后才能计算,发现调用本身完全不受 20ms 业务预算约束,实际整条链路的耗时上界是"发现调用耗时 + 20ms 剩余预算"。
  2. 更关键的是 busy 记账:busy = busy + 1(第142行)在 dispatch 中执行,而 busy = busy - 1 只出现在 fork 内 pcall(read_time, ...) 之后(第166行)。第20行 expire 触发 finish(4, '') 只做应答,不回收 busy(这是有意的"未返回 IPC 计数"),因此 busy 的释放完全依赖 bus:call / timeout_call 最终返回。

失败模式:当 mdb/maca 侧卡住(服务被挂起、消息丢失、mdb 未重启)时,前 16 个请求的 fork 会永久阻塞在无超时的 bus:call 上,busy 永远停在 16;此后 if busy >= 16 then done(4, '')(第138行)使所有 GetDateTime 请求永久返回 NOT_READY——即使时间服务和 mdb 已经恢复正常也无法自愈(同时泄漏 16 个 skynet 协程)。这与 issue #10 的"韧性要求:时间服务不可用时返回明确错误"以及本模块自称"有界"相冲突。

建议:为 GetSubObjects 发现调用套上与 deadline 一致的超时调用(例如改用 bus.bus:timeout_call(remaining, 'bmc.kepler.maca', '/bmc/kepler/MdbService', 'bmc.kepler.Mdb', 'GetSubObjects', 'a{ss}sias', context.new(), ROOT, 1, { INTERFACE }):read()),并确保 fork 无论超时还是异常都会执行 busy = busy - 1,避免 busy 配额被永久占用。

likedislike
Incorrect?
liuxingsen
14 days ago Comments:
116+ local path, owners = next(objects)
117+ if not path or next(objects, path) then
118+ return unavailable(path and 'ambiguous_object' or 'missing_object')
119+ end
120+ trace.path = path
121+ local service = next(owners)
122+ if not service or next(owners, service) then
123+ return unavailable(service and 'ambiguous_owner' or 'missing_owner')
124+ end
125+ trace.service, trace.stage = service, 'snapshot'
126+ remaining = deadline - now()
127+ if remaining <= 0 then
128+ return unavailable('budget_exhausted')
129+ end
130+ -- 显式一次 RPC;高层属性捷径会把 GetAll 拆成多个远程 Get。
131+ local values = bus.bus
O
Oopenubmc-ci-bot14 days ago

💡 建议:bus.bus 嵌套依赖未做防护,接线时易被掩盖

发现调用走 bus:timeout_call(...)(L104),而快照读取却走 bus.bus:timeout_call(...)(L131),要求 bus 同时具备 timeout_call 方法且携带 .bus 子对象。测试辅助函数 attach_raw_bus 会在 bus.bus 为 nil 时自动补建(test_pldm_time.lua),这会在单测中把"缺 .bus 字段"的接线错误掩盖掉,直到真机上所有取时都退化成 ipc_error。

修复建议:在 read_time 中对 bus.bus 做一次显式 nil 检查并返回明确的 unavailable('missing_bus')(区别于 IPC 失败),或在模块顶部注释/断言该 bus 的必需结构契约,避免在生产接线时静默失败。

likedislike
liuxingsen
14 days ago Comments:
132+ :timeout_call(
133+ remaining,
134+ service,
135+ path,
136+ 'bmc.kepler.Object.Properties',
137+ 'GetAllWithContext',
138+ 'a{ss}s',
139+ context.new(),
140+ INTERFACE
141+ )
142+ :read()
143+ if now() >= deadline then
144+ return unavailable('late_snapshot')
145+ end
146+ if not values.SyncStatus or not values.DateTime then
147+ return unavailable('missing_property')
148+ end
149+ local data = M.encode({ SyncStatus = values.SyncStatus:value(), DateTime = values.DateTime:value() })
150+ if not data then
151+ diagnostic(trace, 'invalid_time', deadline)
152+ end
153+ return data and 0 or 0x8a, data or ''
atomgit-bot
atomgit-botatomgit-bot14 days ago

🔵 Low Priority

变更行:src/lualib/pldm_time.lua:119-123(以及 96/100/104 的各失败分支)把所有失败原因——"Time 对象缺失"(96)、"多个 Time 对象/多 owner 歧义"(100)、"预算已耗尽"(104)、"RPC 返回晚于 deadline"(119)、"属性缺失"(119)——统一折叠成同一个完成码 4,整个模块没有任何日志输出(文件只 require 了 skynet / mc.context / utils.core)。

影响的行为/契约:issue #10 的可维护性要求明确为"GetDateTime 处理链路可诊断;日志须带 Network、接口、EID、Tag/TO、epoch 及本地 request_id"。本模块是唯一知道失败原因的位置(传输层只看到 CC=4),却在所有分支静默返回。

失败模式:现网出现 GetDateTime 全部返回 NOT_READY 时,无法从日志区分是"时间服务未发布 / 对象歧义(多 Time 对象或一对象多 owner)/ 20ms 预算被打爆 / 迟到成功 / 属性缺失 / busy 过载(第138行)",只能盲猜,排障成本放大。

建议:在 read_time 的四个失败分支和 dispatch 的 busy 过载分支增加日志(require('mc.logging')),记录失败原因、发现到的对象路径/服务名、剩余预算与耗时,使"服务缺失/歧义/超预算/迟到/无效时间/过载"可在现网区分。

likedislike
Incorrect?
liuxingsen
14 days ago Comments:
154+end
155+ 
156+-- bus 必须为生产 SDBus:timeout_call 用于发现,bus.bus:timeout_call 提供单次原始属性 RPC。
157+function M.register(responder, bus)
158+ local busy, sequence = 0, 0
159+ return responder:register({
160+ pldm_type = 3,
161+ command = 12,
162+ version = 0xF1F0F000,
163+ min_req = 0,
164+ max_req = 0,
165+ max_rsp = 7,
166+ timeout_ms = 50,
167+ dispatch = function(_, done)
168+ sequence = sequence % 2147483647 + 1
169+ local deadline = now() + 20
170+ local trace = { id = sequence, stage = 'discovery' }
171+ -- token 超时不等于底层 IPC 已取消,独立限制尚未返回的业务调用。
172+ if busy >= 16 then
173+ diagnostic(trace, 'busy', deadline)
174+ done(4, '')
175+ return
176+ end
177+ busy = busy + 1
178+ local completed = false
179+ local function finish(code, data)
180+ if not completed then
181+ completed = true
182+ done(code, data)
183+ end
184+ end
185+ -- tick 量化可能提前唤醒;按单调时钟核对完整的 20ms 预算。
186+ local function expire()
187+ if completed then
188+ return
189+ end
190+ local remaining = deadline - now()
191+ if remaining > 0 then
192+ skynet.timeout((remaining + 9) // 10, expire)
193+ else
194+ diagnostic(trace, 'budget_exhausted', deadline)
O
Oopenubmc-ci-bot14 days ago

💡 建议:业务预算到期时不释放 busy 槽位,恢复时机依赖 timeout_call 行为

busy = busy - 1 只出现在 fork 内(L200 pcall(read_time) 返回之后),而 expire() 在预算到期时(L193)只调用 finish(4,''),不会释放 busy。

这意味着 busy 的回落完全寄希望于底层 timeout_call 在超时时会抛错/返回。若远端 IPC 在剩余预算耗尽时仍阻塞不返回,busy 会持续累积到 16,之后所有请求都命中 busy >= 16 的 NOT_READY,即使远端已恢复也无法自愈。

现有的 test_discovery_uses_remaining_budget_and_recovers_after_timeout 把 timeout_call 模拟成 error('discovery timeout')(立即抛错),只覆盖了“超时即返回”这一种语义,未覆盖“超时阻塞不返回”的情形。

修复建议:在 expire() 预算到期分支考虑对已标记 completed 的任务回收槽位,或为 timeout_call 增加“超时必返回/抛错”的契约断言与兜底,确保 busy 不因底层行为差异而永久占满。

likedislike
liuxingsen
14 days ago Comments:
195+ finish(4, '')
196+ end
197+ end
198+ skynet.timeout(2, expire)
199+ skynet.fork(function()
200+ local ok, code, data = pcall(read_time, bus, deadline, trace)
201+ busy = busy - 1
202+ if not ok then
203+ diagnostic(trace, 'ipc_error', deadline)
204+ end
205+ finish(ok and code or 4, ok and data or '')
206+ end)
207+ end,
208+ })
209+end
210+ 
211+return M
@@ -31,5 +31,6 @@ require('test_routing_table')
31require('test_eid_table')31require('test_eid_table')
32require('test_mctp_dump')32require('test_mctp_dump')
33require('test_discovery')33require('test_discovery')
34+require('test_pldm_time')
34 35 
35os.exit(lu.LuaUnit.run())36os.exit(lu.LuaUnit.run())
@@ -114,6 +114,7 @@ function TestMctpDiscovery:test_mctp_discovery_success()
114 end)114 end)
115 115 
116 self.mctp_discovery:smbus_set_eid_to_endpoint(116 self.mctp_discovery:smbus_set_eid_to_endpoint(
117+ 'discovery-success',
117 target_eid,118 target_eid,
118 ref_chip,119 ref_chip,
119 medium_type,120 medium_type,
@@ -158,6 +159,7 @@ function TestMctpDiscovery:test_mctp_discovery_failed()
158 local ref_chip = { path = path }159 local ref_chip = { path = path }
159 160 
160 self.mctp_discovery:smbus_set_eid_to_endpoint(161 self.mctp_discovery:smbus_set_eid_to_endpoint(
162+ 'discovery-failed',
161 target_eid,163 target_eid,
162 ref_chip,164 ref_chip,
163 medium_type,165 medium_type,
@@ -0,0 +1,487 @@
1+-- Copyright (c) 2026 Huawei Technologies Co., Ltd.
2+-- openUBMC is licensed under Mulan PSL v2.
3+-- You can use this software according to the terms and conditions of the Mulan PSL v2.
4+-- You may obtain a copy of Mulan PSL v2 at:
5+-- http://license.coscl.org.cn/MulanPSL2
6+-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
7+-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
8+-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
9+-- See the Mulan PSL v2 for more details.
10+ 
11+-- SPDX-License-Identifier: MulanPSL-2.0
12+local lu = require('luaunit')
13+local skynet = require('skynet')
14+local clock = require('utils.core')
15+local log = require('mc.logging')
16+-- 本组隔离测试业务适配器;传输令牌/回复生命周期由 responder 集成测试覆盖。
17+local Responder = {}
18+function Responder.new()
19+ return {
20+ handlers = {},
21+ register = function(self, handler)
22+ lu.assertEquals({ handler.pldm_type, handler.command, handler.version }, { 3, 12, 0xF1F0F000 })
23+ lu.assertEquals({ handler.min_req, handler.max_req, handler.max_rsp, handler.timeout_ms }, { 0, 0, 7, 50 })
24+ self.handlers[handler.pldm_type * 256 + handler.command] = handler
25+ return true
26+ end,
27+ }
28+end
29+ 
30+local function attach_raw_bus(bus)
31+ bus.timeout_call = function(self, budget, ...)
32+ lu.assertTrue(budget > 0 and budget <= 20)
33+ return self:call(...)
34+ end
35+ if not bus.bus then
36+ bus.bus = {
37+ timeout_call = function(_, _, ...)
38+ local err, values = bus.pcall(bus, ...)
39+ if err then
40+ error(err)
41+ end
42+ return {
43+ read = function()
44+ return values
45+ end,
46+ }
47+ end,
48+ }
49+ end
50+end
51+ 
52+TestPldmTime = {}
53+ 
54+function TestPldmTime:setup()
55+ self.module_ok, self.time = pcall(require, 'pldm_time')
56+ self.original_debug, self.messages = log.debug, {}
57+ log.debug = function(_, fmt, ...)
58+ self.messages[#self.messages + 1] = string.format(fmt, ...)
59+ end
60+ self.original_fork = skynet.fork
61+ self.original_timeout = skynet.timeout
62+ self.original_clock = clock.monotonic_time
atomgit-bot
atomgit-botatomgit-bot14 days ago

🔵 Low Priority

变更行:test/unit/test_pldm_time.lua:53 与 55-57:self.original_clock = clock.monotonic_time 之后无条件覆写为桩函数,且没有任何断言校验该生产 API 真实存在。

影响的行为/契约与被掩盖的风险:src/lualib/pldm_time.lua:77 的 now() 依赖 require('utils.core').monotonic_time() 返回 { tv_sec, tv_nsec },这是全模块唯一的时钟源;而 now() 在 dispatch 首次调用处(pldm_time.lua:143 local deadline = now() + 20)与 expire(第156行)都在 pcall 之外。若 utils.core 并没有 monotonic_time(或返回值不是 timespec),生产路径会在 dispatch 中直接抛出 "attempt to call a nil value",异常逃逸到 responder(本 PR 未包含 responder,无法兜底),GetDateTime 将 100% 失败。

失败模式:本组隔离测试因为 setup 里凭空造出 clock.monotonic_time,对上述"API 不存在/签名不符"完全免疫——10 项测试全绿也无法证明生产时钟源可用,属于测试给出的假信号。仓库内除本模块外没有任何 monotonic_time / tv_nsec 使用(src、test 全量 grep 仅命中本 PR 文件),该 API 未被其他代码验证过。

建议:在 setup 中对生产时钟 API 增加存在性断言(type(self.original_clock)=='function'),或改用 skynet.now() 等确定的单调时钟源,避免桩函数掩盖生产时钟 API 缺失。

Suggestions
62
- self.original_clock = clock.monotonic_time
62
+ self.original_clock = clock.monotonic_time
63
+ lu.assertEquals(type(self.original_clock), 'function', 'utils.core.monotonic_time 不存在,生产 dispatch 会抛异常')
Apply suggestion
likedislike
Incorrect?
liuxingsen
14 days ago Comments:
63+ lu.assertEquals(type(self.original_clock), 'function', '真实单调时钟 API 缺失')
64+ local sample = self.original_clock()
65+ lu.assertEquals(type(sample.tv_sec), 'number')
66+ lu.assertEquals(type(sample.tv_nsec), 'number')
67+ lu.assertTrue(sample.tv_nsec >= 0 and sample.tv_nsec < 1000000000)
68+ lu.assertEquals(type(require('sd_bus').SDBus.timeout_call), 'function')
69+ self.now_ms = 100
70+ clock.monotonic_time = function()
71+ return { tv_sec = 0, tv_nsec = self.now_ms * 1000000 }
72+ end
73+ self.queue = {}
74+ self.timers = {}
75+ skynet.timeout = function(ticks, fn)
76+ self.timers[#self.timers + 1] = { ticks = ticks, run = fn }
77+ end
78+ skynet.fork = function(fn)
79+ self.queue[#self.queue + 1] = fn
80+ end
81+end
82+ 
83+function TestPldmTime:teardown()
84+ log.debug = self.original_debug
85+ skynet.fork = self.original_fork
86+ skynet.timeout = self.original_timeout
87+ clock.monotonic_time = self.original_clock
88+end
89+ 
90+function TestPldmTime:test_business_deadline_replies_once_before_late_ipc_returns()
91+ local responder, codes = Responder.new({}), {}
92+ self.time.register(responder, {})
93+ responder.handlers[3 * 256 + 12].dispatch('', function(cc)
94+ codes[#codes + 1] = cc
95+ end)
96+ lu.assertEquals(#self.timers, 1, '缺少独立业务等待期限')
97+ lu.assertEquals(self.timers[1].ticks, 2)
98+ self.now_ms = 120
99+ self.timers[1].run()
100+ lu.assertEquals(codes, { 4 })
101+ self.queue[1]()
102+ lu.assertEquals(codes, { 4 }, '迟到 IPC 不得再次完成')
103+end
104+ 
105+function TestPldmTime:test_early_scheduler_tick_does_not_shorten_business_budget()
106+ local responder, codes = Responder.new({}), {}
107+ self.time.register(responder, {})
108+ responder.handlers[3 * 256 + 12].dispatch('', function(cc)
109+ codes[#codes + 1] = cc
110+ end)
111+ self.now_ms = 110
112+ self.timers[1].run()
113+ lu.assertEquals(codes, {})
114+ lu.assertEquals(#self.timers, 2)
115+ lu.assertEquals(self.timers[2].ticks, 1)
116+ self.now_ms = 120
117+ self.timers[2].run()
118+ lu.assertEquals(codes, { 4 })
119+end
120+ 
121+function TestPldmTime:test_single_bounded_rpc_reads_time_snapshot()
122+ local path, intf = '/bmc/kepler/Managers/Time_Bmc_010101', 'bmc.kepler.Managers.Time'
123+ local result, bus = nil, { bus = {} }
124+ bus.call = function()
125+ self.now_ms = 103
126+ return { [path] = { ['bmc.kepler.bmc_time'] = { intf } } }
127+ end
128+ bus.bus.timeout_call = function(_, timeout, service, actual_path, iface, method, signature, _, target)
129+ lu.assertEquals({ timeout, service, actual_path, iface, method, signature, target }, {
130+ 17,
131+ 'bmc.kepler.bmc_time',
132+ path,
133+ 'bmc.kepler.Object.Properties',
134+ 'GetAllWithContext',
135+ 'a{ss}s',
136+ intf,
137+ })
138+ return {
139+ read = function()
140+ return {
141+ SyncStatus = {
142+ value = function()
143+ return true
144+ end,
145+ },
146+ DateTime = {
147+ value = function()
148+ return '2026-10-26T01:02:03Z'
149+ end,
150+ },
151+ }
152+ end,
153+ }
154+ end
155+ local responder = Responder.new({})
156+ attach_raw_bus(bus)
157+ self.time.register(responder, bus)
158+ responder.handlers[3 * 256 + 12].dispatch('', function(cc, data)
159+ result = { cc, data }
160+ end)
161+ self.queue[1]()
162+ lu.assertEquals(result, { 0, '\x03\x02\x01\x26\x10\x26\x20' })
163+end
164+ 
165+function TestPldmTime:test_late_ipc_result_cannot_beat_queued_timeout_callback()
166+ local result, responder = nil, Responder.new({})
167+ local bus = {
168+ call = function()
169+ return { ['/bmc/kepler/Managers/Time_Bmc'] = { ['bmc.kepler.bmc_time'] = {} } }
170+ end,
171+ pcall = function()
172+ self.now_ms = 121
173+ return nil,
174+ {
175+ SyncStatus = {
176+ value = function()
177+ return true
178+ end,
179+ },
180+ DateTime = {
181+ value = function()
182+ return '2026-10-26T01:02:03Z'
183+ end,
184+ },
185+ }
186+ end,
187+ }
188+ attach_raw_bus(bus)
189+ self.time.register(responder, bus)
190+ responder.handlers[3 * 256 + 12].dispatch('', function(cc)
191+ result = cc
192+ end)
193+ self.queue[1]()
194+ lu.assertEquals(result, 4)
195+end
196+ 
197+function TestPldmTime:test_utc_bcd_calendar_and_timezone()
198+ lu.assertTrue(self.module_ok, '缺少真实时间适配器')
199+ for _, case in ipairs({
200+ { '2024-02-29T00:00:01+08:00', '\x01\x00\x16\x28\x02\x24\x20' },
201+ { '2025-01-01T00:30:00+01:00', '\x00\x30\x23\x31\x12\x24\x20' },
202+ { '2024-12-31T23:30:59-01:00', '\x59\x30\x00\x01\x01\x25\x20' },
203+ { '2000-02-29T23:59:59.125Z', '\x59\x59\x23\x29\x02\x00\x20' },
204+ { '2026-01-01T00:00:00.125+08:00', '\x00\x00\x16\x31\x12\x25\x20' },
205+ { '2025-12-31T23:59:59.999-01:00', '\x59\x59\x00\x01\x01\x26\x20' },
206+ { '2000-03-01T00:00:00.001+00:30', '\x00\x30\x23\x29\x02\x00\x20' },
207+ }) do
208+ lu.assertEquals(self.time.encode({ SyncStatus = true, DateTime = case[1] }), case[2])
209+ end
210+end
211+ 
212+function TestPldmTime:test_invalid_time_never_uses_host_clock()
213+ lu.assertTrue(self.module_ok)
214+ lu.assertIsNil(self.time.encode({ SyncStatus = false, DateTime = '2026-01-01T00:00:00Z' }))
215+ for _, stamp in ipairs({
216+ '2023-02-29T00:00:00Z',
217+ '1900-02-29T00:00:00Z',
218+ '2026-04-31T00:00:00Z',
219+ '2026-01-01T24:00:00Z',
220+ '2026-01-01T00:00:60Z',
221+ '2026-01-01T00:00:00',
222+ '2026-01-01T00:00:00-00:00',
223+ '2026-01-01T00:00:00+24:00',
224+ '0000-01-01T00:00:00Z',
225+ '9999-12-31T23:00:00-02:00',
226+ '2026-01-01T00:00:00Zjunk',
227+ }) do
228+ lu.assertIsNil(self.time.encode({ SyncStatus = true, DateTime = stamp }), stamp)
229+ end
230+end
231+ 
232+function TestPldmTime:test_service_read_is_deferred_and_reports_invalid_or_unavailable()
233+ lu.assertTrue(self.module_ok)
234+ local responder, bus = Responder.new({}), {}
235+ local snapshot = { SyncStatus = true, DateTime = '2026-10-26T08:09:10Z' }
236+ bus.pcall = function(_, service, path, interface, method, signature, _, target)
237+ lu.assertEquals({ service, path, interface, method, signature, target }, {
238+ 'bmc.kepler.bmc_time',
239+ '/bmc/kepler/Managers/Time_Bmc',
240+ 'bmc.kepler.Object.Properties',
241+ 'GetAllWithContext',
242+ 'a{ss}s',
243+ 'bmc.kepler.Managers.Time',
244+ })
245+ local values = {}
246+ for k, v in pairs(snapshot) do
247+ values[k] = {
248+ value = function()
249+ return v
250+ end,
251+ }
252+ end
253+ return nil, values
254+ end
255+ attach_raw_bus(bus)
256+ self.time.register(responder, bus)
257+ local handler = responder.handlers[3 * 256 + 12]
258+ local result, reads
259+ reads = 0
260+ bus.call = function(actual_bus, _, _, _, _, _, _, path, depth, interfaces)
261+ lu.assertEquals(actual_bus, bus)
262+ lu.assertEquals({ path, depth }, { '/bmc/kepler/Managers', 1 })
263+ lu.assertEquals(interfaces, { 'bmc.kepler.Managers.Time' })
264+ reads = reads + 1
265+ return { ['/bmc/kepler/Managers/Time_Bmc'] = { ['bmc.kepler.bmc_time'] = { 'bmc.kepler.Managers.Time' } } }
266+ end
267+ handler.dispatch('', function(cc, data)
268+ result = { cc, data }
269+ end)
270+ lu.assertEquals(reads, 0)
271+ lu.assertIsNil(result)
272+ self.queue[1]()
273+ lu.assertEquals(result, { 0, '\x10\x09\x08\x26\x10\x26\x20' })
274+ snapshot.SyncStatus = false
275+ handler.dispatch('', function(cc, data)
276+ result = { cc, data }
277+ end)
278+ self.queue[2]()
279+ lu.assertEquals(result, { 0x8a, '' })
280+ bus.call = function()
281+ error('service unavailable')
282+ end
283+ handler.dispatch('', function(cc, data)
284+ result = { cc, data }
285+ end)
286+ self.queue[3]()
287+ lu.assertEquals(result, { 4, '' })
288+end
289+ 
290+function TestPldmTime:test_slow_service_work_is_bounded_after_tokens_expire()
291+ lu.assertTrue(self.module_ok)
292+ local responder = Responder.new({})
293+ self.time.register(responder, {})
294+ local handler, codes = responder.handlers[3 * 256 + 12], {}
295+ for _ = 1, 20 do
296+ handler.dispatch('', function(cc)
297+ codes[#codes + 1] = cc
298+ end)
299+ end
300+ lu.assertEquals(#self.queue, 16)
301+ lu.assertEquals(codes, { 4, 4, 4, 4 })
302+ self.queue[1]()
303+ handler.dispatch('', function() end)
304+ lu.assertEquals(#self.queue, 17)
305+end
306+ 
307+function TestPldmTime:test_real_interface_discovery_does_not_require_generated_time_client()
308+ lu.assertTrue(self.module_ok)
309+ local bus, responder, result = {}, Responder.new({}), nil
310+ bus.call = function(_, service, path, intf, method, signature, _, object_path, depth, interfaces)
311+ lu.assertEquals({ service, path, intf, method, signature, object_path, depth, interfaces }, {
312+ 'bmc.kepler.maca',
313+ '/bmc/kepler/MdbService',
314+ 'bmc.kepler.Mdb',
315+ 'GetSubObjects',
316+ 'a{ss}sias',
317+ '/bmc/kepler/Managers',
318+ 1,
319+ { 'bmc.kepler.Managers.Time' },
320+ })
321+ return { ['/bmc/kepler/Managers/Time_Bmc'] = { ['bmc.kepler.actual_time'] = { 'bmc.kepler.Managers.Time' } } }
322+ end
323+ bus.pcall = function(_, service)
324+ lu.assertEquals(service, 'bmc.kepler.actual_time')
325+ return nil,
326+ {
327+ SyncStatus = {
328+ value = function()
329+ return true
330+ end,
331+ },
332+ DateTime = {
333+ value = function()
334+ return '2026-10-26T01:02:03Z'
335+ end,
336+ },
337+ }
338+ end
339+ attach_raw_bus(bus)
340+ self.time.register(responder, bus)
341+ responder.handlers[3 * 256 + 12].dispatch('', function(cc, data)
342+ result = { cc, data }
343+ end)
344+ self.queue[1]()
345+ lu.assertEquals(result, { 0, '\x03\x02\x01\x26\x10\x26\x20' })
346+end
347+ 
348+function TestPldmTime:test_discovers_board_suffix_and_rejects_ambiguous_time_sources()
349+ local bus, responder, result = {}, Responder.new({}), nil
350+ local path = '/bmc/kepler/Managers/Time_Bmc_010101'
351+ local objects = { [path] = { ['bmc.kepler.bmc_time'] = { 'bmc.kepler.Managers.Time' } } }
352+ bus.call = function(_, _, _, _, method, signature, _, root, depth, interfaces)
353+ if method ~= 'GetSubObjects' then
354+ return {}
355+ end
356+ lu.assertEquals({ signature, root, depth, interfaces }, {
357+ 'a{ss}sias',
358+ '/bmc/kepler/Managers',
359+ 1,
360+ { 'bmc.kepler.Managers.Time' },
361+ })
362+ return objects
363+ end
364+ bus.pcall = function(_, service, actual_path)
365+ lu.assertEquals({ service, actual_path }, { 'bmc.kepler.bmc_time', path })
366+ return nil,
367+ {
368+ SyncStatus = {
369+ value = function()
370+ return true
371+ end,
372+ },
373+ DateTime = {
374+ value = function()
375+ return '2026-10-26T01:02:03Z'
376+ end,
377+ },
378+ }
379+ end
380+ attach_raw_bus(bus)
381+ self.time.register(responder, bus)
382+ local function query()
383+ responder.handlers[3 * 256 + 12].dispatch('', function(cc, data)
384+ result = { cc, data }
385+ end)
386+ self.queue[#self.queue]()
387+ return result
388+ end
389+ lu.assertEquals(query(), { 0, '\x03\x02\x01\x26\x10\x26\x20' })
390+ objects['/bmc/kepler/Managers/Time_Bmc_020202'] = objects[path]
391+ lu.assertEquals(query(), { 4, '' })
392+ objects = {}
393+ lu.assertEquals(query(), { 4, '' })
394+end
395+ 
396+function TestPldmTime:test_discovery_uses_remaining_budget_and_recovers_after_timeout()
397+ local responder, results, budgets = Responder.new(), {}, {}
398+ local bus = {
399+ timeout_call = function(_, budget)
400+ budgets[#budgets + 1] = budget
401+ error('discovery timeout')
402+ end,
403+ }
404+ self.time.register(responder, bus)
405+ local handler = responder.handlers[3 * 256 + 12]
406+ for i = 1, 20 do
407+ handler.dispatch('', function(code)
408+ results[#results + 1] = code
409+ end)
410+ self.queue[i]()
411+ end
412+ lu.assertEquals(#budgets, 20, '发现调用未受预算保护或槽位未归还')
413+ lu.assertEquals(budgets, { 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20 })
414+ lu.assertEquals(#results, 20)
415+ for _, code in ipairs(results) do
416+ lu.assertEquals(code, 4)
417+ end
418+end
419+ 
420+function TestPldmTime:test_rfc3339_numeric_offset_bounds()
421+ lu.assertEquals(
422+ self.time.encode({ SyncStatus = true, DateTime = '2026-01-01T00:00:00+23:59' }),
423+ '\x00\x01\x00\x31\x12\x25\x20'
424+ )
425+ lu.assertEquals(
426+ self.time.encode({ SyncStatus = true, DateTime = '2026-01-01T00:00:00-23:59' }),
427+ '\x00\x59\x23\x01\x01\x26\x20'
428+ )
429+ lu.assertIsNil(self.time.encode({ SyncStatus = true, DateTime = '2026-01-01T00:00:00+24:00' }))
430+end
431+ 
432+function TestPldmTime:test_diagnostic_reason_does_not_expose_remote_error()
433+ local responder = Responder.new()
434+ self.time.register(responder, {
435+ timeout_call = function()
436+ error('private service payload')
437+ end,
438+ })
439+ responder.handlers[3 * 256 + 12].dispatch('', function() end)
440+ self.queue[1]()
441+ local text = table.concat(self.messages, '\n')
442+ lu.assertStrContains(text, 'reason=ipc_error')
443+ lu.assertStrContains(text, 'stage=discovery')
444+ lu.assertStrContains(text, 'request=1')
445+ lu.assertIsNil(text:find('private service payload', 1, true))
446+end
447+ 
448+function TestPldmTime:test_lowercase_rfc3339_separators()
449+ for _, stamp in ipairs({ '2026-01-01t00:00:00z', '2026-01-01T00:00:00z', '2026-01-01t01:00:00+01:00' }) do
450+ lu.assertEquals(self.time.encode({ SyncStatus = true, DateTime = stamp }), '\x00\x00\x00\x01\x01\x26\x20')
451+ end
452+end
453+ 
454+function TestPldmTime:test_expired_tokens_do_not_release_still_running_ipc()
455+ local responder, codes, workers = Responder.new(), {}, {}
456+ self.time.register(responder, {
457+ timeout_call = function()
458+ coroutine.yield()
459+ error('bounded IPC returned timeout')
460+ end,
461+ })
462+ local function request()
463+ responder.handlers[3 * 256 + 12].dispatch('', function(code)
464+ codes[#codes + 1] = code
465+ end)
466+ end
467+ for i = 1, 16 do
468+ request()
469+ workers[i] = coroutine.create(self.queue[i])
470+ lu.assertTrue(coroutine.resume(workers[i]))
471+ lu.assertEquals(coroutine.status(workers[i]), 'suspended')
472+ end
473+ self.now_ms = 120
474+ for _, timeout in ipairs(self.timers) do
475+ timeout.run()
476+ end
477+ lu.assertEquals(#codes, 16)
478+ request()
479+ lu.assertEquals({ #codes, #self.queue }, { 17, 16 }, '仍在运行的IPC不能突破16个')
480+ lu.assertTrue(coroutine.resume(workers[1]))
481+ request()
482+ lu.assertEquals({ #codes, #self.queue }, { 17, 17 }, 'IPC返回后可接收新请求,迟到结果不重复回复')
483+ for i = 2, 16 do
484+ lu.assertTrue(coroutine.resume(workers[i]))
485+ end
486+ self.queue[17]()
487+end