D-Bus 错误映射

头文件:#include <mc/engine/dbus_error_mapping.h>;命名空间:mc::engine。

概述

将 mcengine 内部错误映射为兼容的 D-Bus 错误名,供进程内派发、D-Bus、旧共享内存及新共享内存 MQ 通道统一使用。

模块位于 mc::engine,名称中的 D-Bus 表示映射目标,不限制实际传输通道,也不依赖 mcdbus。它不参与 Redfish、Web、SNMP 等北向接口的错误映射,不改变 error_engine 的错误注册与报告行为,也不依赖 error_message_converter 或北向错误注册表。

主要特性

  • 内部错误注册表、mc::error 和诊断 JSON 保持原有语义,只转换 RPC 对外错误名。
  • dispatcher、协议适配器、路由及传输产生的错误复用同一转换函数;external_proto 的外部回调错误消息也遵循此契约。
  • 标准名及合法的自定义业务错误名透传;空名、非法名统一返回 org.freedesktop.DBus.Error.Failed。
  • 转换幂等,返回的名字拥有独立存储;消息头和 error_payload.name 保持一致。
  • 不修改超时策略、重试策略或错误正文;转换不意味着失败调用可以安全重试。
  • 映射和 is_standard_error 使用同一张精确对应表,内部判断同时接受内部名与标准名,不进行反向改名。
  • RPC 响应经 mc::error_exception::from_response 构造:异常恢复以响应名为准,原始 JSON 不覆盖身份;普通内部构造的恢复规则不变。

精确映射表

内部错误名 对外错误名
mc.engine.internal_error org.freedesktop.DBus.Error.Failed
mc.engine.not_supported org.freedesktop.DBus.Error.NotSupported
mc.engine.invalid_args org.freedesktop.DBus.Error.InvalidArgs
mc.engine.unknown_method org.freedesktop.DBus.Error.UnknownMethod
mc.engine.unknown_object org.freedesktop.DBus.Error.UnknownObject
mc.engine.unknown_interface org.freedesktop.DBus.Error.UnknownInterface
mc.engine.unknown_property org.freedesktop.DBus.Error.UnknownProperty
mc.engine.property_read_only org.freedesktop.DBus.Error.PropertyReadOnly
mc.engine.service_unknown org.freedesktop.DBus.Error.ServiceUnknown
mc.engine.timeout org.freedesktop.DBus.Error.NoReply
mc.engine.transport_failed org.freedesktop.DBus.Error.Failed

org.freedesktop.DBus.Error.* 在此是沿用的跨通道公开契约,不表示实际使用 D-Bus 传输。映射按精确名字进行,不按 mc.engine.* 前缀批量替换。合法名字遵循 mc::is_valid_interface_name:不超过 255 字节,由至少两个点分段标识符组成,每段首字符为字母或下划线,其余为字母、数字或下划线。

未登记的合法 mc.engine.* 名称(例如 mc.engine.business_error)同样原样透传;此模块恢复已知标准错误的兼容语义,不负责隐藏整个内部命名空间。按前缀统一改成 Failed 会抹掉未登记错误的身份,影响已有调用方判断。新增需要标准化的框架错误时,必须在 dbus_error_mapping.cpp 的 mappings[] 中显式登记,并同步补充名称转换、双名称类别判断和幂等测试;只注册内部错误不会自动获得标准错误映射。

只有确认对象不存在时才返回 UnknownObject。路由至不拥有目标服务的 MQ 端点,仅能确认路由失败,返回 Failed,不伪装成对象未注册;这会改变依赖 UnknownObject 判断“对象未就绪/切换端点”的旧 MQ/共享内存客户端行为,相关客户端必须配套升级或保留过渡兼容。超时返回 NoReply,不能据此判断服务端操作未执行。

基本用法

1. 只转换错误名

#include <mc/engine/dbus_error_mapping.h>
#include <cassert>

int main()
{
    auto name = mc::engine::map_error_to_dbus_name("mc.engine.unknown_object");
    assert(name == "org.freedesktop.DBus.Error.UnknownObject");
    assert(mc::engine::map_error_to_dbus_name(name) == name);
}

2. 构造路由或传输错误响应

#include <mc/engine/dbus_error_mapping.h>
#include <cassert>

int main()
{
    mc::engine::message request;
    request.header.type = mc::engine::message_type::method_call;
    request.header.sender = "org.example.Client";
    request.header.destination = "org.example.Server";
    request.header.serial = 7;
    auto reply = mc::engine::make_dbus_error_response(request, "mc.engine.timeout", "deadline exceeded");
    assert(reply.header.reply_serial == 7);
    assert(reply.header.error_name == "org.freedesktop.DBus.Error.NoReply");
}

3. 新通道标准化已有错误消息

#include <mc/engine/dbus_error_mapping.h>
#include <cassert>

int main()
{
    mc::engine::message reply;
    reply.header.type = mc::engine::message_type::error;
    reply.body = mc::engine::make_payload<mc::engine::error_payload>(
        "mc.engine.unknown_object", R"({"name":"mc.engine.unknown_object"})");
    mc::engine::map_error_response_to_dbus(reply);
    assert(reply.header.error_name == "org.freedesktop.DBus.Error.UnknownObject");
    assert(reply.as<mc::engine::error_payload>().name == reply.header.error_name);
    assert(reply.as<mc::engine::error_payload>().message == R"({"name":"mc.engine.unknown_object"})");
}

API 详细说明

错误类别判断

standard_error 枚举包含 failed、not_supported、invalid_args、unknown_method、unknown_object、unknown_interface、unknown_property、property_read_only、service_unknown、no_reply,分别对应映射表中的对外错误类别。internal_error 与 transport_failed 都属于 failed,不能据此反推原始原因。

bool is_standard_error(mc::string_view name, standard_error category) noexcept;
参数 说明
name 要识别的内部名或响应名;不读取诊断 JSON
category 预期标准错误类别

返回精确匹配结果;未知业务名、空名、非法名及无效枚举值均返回 false。不分配内存、不改写输入、不抛异常,线程安全。名称合法化时的 Failed 兜底不是原始错误分类依据。

#include <mc/engine/dbus_error_mapping.h>
#include <cassert>

int main()
{
    using namespace mc::engine;
    assert(is_standard_error("mc.engine.unknown_object", standard_error::unknown_object));
    assert(is_standard_error("org.freedesktop.DBus.Error.UnknownObject", standard_error::unknown_object));
    assert(!is_standard_error("bad name", standard_error::failed));
}

对外名称转换

mc::string map_error_to_dbus_name(mc::string_view name);
参数 说明
name 内部名、标准名或业务自定义名;允许空名或非法名

返回标准化后的自持有字符串。不查错误注册表,不构造或解析 JSON。可能抛出字符串内存分配相关异常;线程安全,无可变全局状态。

void map_error_response_to_dbus(message& response);
参数 说明
response 原地标准化的消息;非 error 消息不作修改

无返回值。非空 header.error_name 优先,否则采用 error_payload.name;两者均无则为 Failed。

先以不分配存储的视图计算目标名,再按以下规则处理:

  • header 已等于目标名,且没有 error_payload 或其 name 也等于目标名时,直接返回,不分配名字或重建 payload。没有 error_payload 的消息保持原样,不保证所有 error 消息均有此 payload;此规则也适用于透传的合法自定义名。
  • 缺少 error_payload 且 header 需要标准化时,补建使用目标名、空正文的 error_payload。
  • 已有 error_payload 的 name 与目标名不同时,创建新 payload 对齐名称,正文按原字节保留,包括 JSON 中的内部 name;不递归转换其字段,也不修改其他消息共享的 payload。

header/payload 相等但仍为已登记内部名或非法名时,仍须执行转换。不改路由字段及序列号。需要修改时可能抛出内存分配异常;调用方必须独占待修改的 message,不同消息可并发处理。

message make_dbus_error_response(const message& request,
                                 mc::string_view name,
                                 mc::string_view text);
参数 说明
request 原始请求,仅用于复制响应关联信息
name 待标准化的错误名
text 原始诊断正文,可为空;不会自动生成或重写 JSON

返回 type 为 error 的消息。交换 sender/destination,复制 path/interface/member,reply_serial = request.serial,serial = request.serial + 1(无符号整数语义;实际通道仍负责其发送序列号)。header 和 payload 使用同一个标准名。可能抛出内存分配异常;输入未被其他线程同时修改时可并发调用。

message make_dbus_error_response(const message& request, const mc::exception& error);
参数 说明
request 原始请求,关联信息复制规则同上述重载
error 已确认应转换为 RPC 响应的异常;不改变原始异常

返回标准化的 error message。timeout_exception_code 按超时语义返回 NoReply,其他异常使用 error.name() 进行精确映射;普通异常正文为 error.what(),preserves_response_body() 为 true 的响应异常使用原始 JSON 或 top_message()。例如旧共享内存等待超时的内部异常名为短名 timeout,须根据类型识别,不能把所有名为 timeout 的非法业务错误都当成传输超时。异常与线程安全约束同前一重载。

通道接入要求

正常响应由 dispatcher 和统一错误响应工厂负责名称转换;接收边界负责兼容旧端返回的内部名;传输出口的幂等转换是对自定义 handler 等旁路的兜底。三处职责不同,不能以“已经转换过”为由去掉旧端接收兼容。

app_proto 在选择 D-Bus/MQ 路由之前执行标准化,直接 send/push 的错误消息同样适用;不依赖 dispatcher 已经处理过,也不依赖接收端补救。无来源标记的双通道发送使用相同的公开错误名,原始诊断正文保持不变。

新 RPC 通道必须在向调用者交付错误结果前调用上述 API;不能另写一份映射表。正常 dispatcher 输出已经标准化,但通道本身产生的路由失败、超时以及自定义 handler 返回值仍须覆盖。无 engine message 的适配层使用 map_error_to_dbus_name;有 message 的出口使用 map_error_response_to_dbus。编解码器本身保留原始消息格式,不承担策略转换。

这里规范的是 RPC 错误响应;本地 API 参数异常、future 取消及传输回调自身的 rejected future 保留既有控制流,不强行变成成功完成的 error message。将这些失败转换为 RPC 响应时,必须使用相同的标准化入口。

异常与诊断正文

service::call 和 proxy 使用 mc::error_exception::from_response 构造错误异常,可通过 mc::error_exception 或 mc::exception 捕获,不要求物理上跨进程。工厂设定 name_policy() == preserve_exception_name;mc::error::from_exception 恢复 JSON 中支持的诊断字段,但保留响应 name(),不会把 Failed 猜测为某个内部原因。

正文转发是另一项明确契约:工厂同时设定 preserves_response_body() == true;dispatcher 检查这项标记,直接转发原始 get_json_error(),非结构化正文则转发 top_message()(包括空正文和展示用 JSON)。不根据名称策略推断正文处理方式,也不重新编码原始正文。策略只存于私有实现,不加入通信格式,没有额外的远端异常子类或来源 RTTI 判断。

普通内部 mc::error_exception 继续从 JSON 恢复内部错误;显式解码原始诊断 JSON 也仍能看到内部名称。两种用途不可混淆。按 error_exception 值复制、动态复制及重抛均保留策略;按通用 exception 值复制会丢失派生类的 JSON 数据,仍应避免。

GetPropertiesByNames 捕获 getter 异常后,通过策略感知的 from_exception 构造局部 Errors:转发响应异常保留响应名,普通内部异常仍恢复 JSON 中的名称。这不意味着递归映射所有成功响应中的业务数据或所有嵌套错误字典。

新版客户端可识别内部名及标准名,不代表未升级的旧客户端自动具备这种能力。例如,旧版 C++ 批量读取回退只匹配内部 UnknownMethod/UnknownInterface,收到新版服务端的标准名可能不再回退 GetAll。应配套升级并重启相关进程;需要滚动升级时,应先向旧客户端回移双名称判断及响应身份恢复修复,并验证实际异版本互操作,不能仅凭模拟旧错误名的测试认定兼容。

最佳实践

  • ✅ 使用 make_dbus_error_response 生成失败结果,统一关联字段、header 和 payload。
  • ✅ 将业务错误正文和合法业务错误名原样保留;诊断时同时记录公开名与原始正文。
  • ✅ 用 is_standard_error(ex.name(), standard_error::unknown_interface) 判断类别,避免业务代码维护两套错误名。
  • ❌ 在 D-Bus、MQ、旧共享内存各维护一份错误映射表,导致新通道遗漏兼容。
  • ❌ 将所有失败改成 UnknownObject,或遇到 NoReply 自动重放有副作用的 RPC。
  • ❌ 从诊断 JSON 的 name 决定通信回退分支,或把所有 Failed 反向改成某一种内部错误。