protobuf 中 enum 类型默认值的一个坑

2017-12-05

在最近的项目中,我们使用 protobuf 的 enum 类型来表示接口的返回码,类似这样:

enum RtnCode {
    SUCCESS = 0,
    ERROR_1 = 1,
    ERROR_2 = 2,
}
message AwesomeInterfaceRsp {
    optional RtnCode rtn = 1 [default = SUCCESS];
    // 其他字段...
}

开始一切正常,直到有一天服务端增加了一个错误码 ERROR_3 = 3,我们发现,服务端返回这个新的错误时,客户端认为请求成功。

检查客户端代码:

if (rsp.rtn() == SUCCESS) {
    log(kInfo, "SUCCESS");
    // ....
} else {
    log(kError, "Failed!");
    // ....
}

似乎也没啥问题,可是总是执行到 SUCCESS 的分支。百思不得其解,我们决定把 rsp 先打印出来。服务端打印的是:

rtn: ERROR_3

客户端打印的是:

1: 3

嗯???

服务已经上线,改协议是不可能了,怎么补救呢?

思路 1:以后修改协议时,永远保证客户端先上线,服务端后上线。然而这样显然不可靠,现在刚出了问题记得,没准过两个月就忽略了。

思路 2:pb 不改,检查 has_rtn(),这种情况会返回 false。这样同样不可靠,rtn 字段设置了 SUCCESS 的默认值,不排除服务端在成功的情况下没有 set。即使现在服务端保证总是显式设置,我作为客户端的开发,保险起见也不应该依赖这种行为。

思路 3:pb 小改,去掉默认值,检查 has_rtn()。可行。

思路 4:pb 不改,客户端检查 UnknownFieldSet。既然已经知道这个值被放到了 UnknownFieldSet,我们可以检查到是否有未识别的枚举值:

template <typename MessageType>
bool CheckRtnCode(const MessageType& msg) {
    if (msg.has_rtn())
        return true;

    const auto& unknown_fields = msg.unknown_fields();
    for (int i = 0; i < unknown_fields.field_count(); ++i) {
        const auto& field = unknown_fields.field(i);
        if (field.number() == msg.kRtnFieldNumber) {
            log(kError, "Unknown RtnCode found: %d; full message: %s",
                int(field.varint()), msg.ShortDebugString().c_str());
            return false;
        }
    }

    return true;
}

当然,这只是权宜之计,客户端开发还是很容易忘了调用 has_rtn/CheckRtnCode。最好还是在设计 pb 时就考虑到这个问题,例如定义一个“未知错误”的代码作为默认值:

enum RtnCode {
    UNKNOWN_ERROR = -1,
    SUCCESS = 0,
    ERROR_1 = 1,
    ERROR_2 = 2,
}
message AwesomeInterfaceRsp {
    optional RtnCode rtn = 1 [default = UNKNOWN_ERROR];
}

知道 pb enum 的这个特性以后,还可以得出一个推论:枚举类型绝对不要用 required 关键字!仍然是上面那个例子,如果 rtn 没有设置默认值,而是设置为 required 字段,那么客户端在 parse 时就会直接报错,根本不给补救机会。