Проверка нулевого значения Enum в буфере протокола

Проверка нулевого значения Enum в буфере протокола

2 марта 2022 г.
  • Как проверить, что сообщение Protobuf не содержит полей перечисления с нулевым значением? Оказывается, Protobuf не поддерживает его напрямую! Нам нужно изучить, как protojson пакет реализован.*

Все больше и больше компаний используют gRPC с Protobuf для связи между внутренними службами. Он обладает преимуществами высокой производительности, поддержки нескольких языков программирования и поддержки Google с отличной экосистемой.


Для связи с интерфейсными и внешними службами Protobuf можно маршалировать в формат JSON. Браузер понимает только формат JSON, и мы не можем ожидать, что другие компании будут использовать Protobuf напрямую от нас. (Конечно, вы можете, если вы достаточно большой!)


Давайте поговорим о перечислении и некоторых проблемах, с которыми мы недавно столкнулись.


Пример кода написан на Go.


Из Protobuf руководства по стилю перечисление с нулевым значением должно иметь суффикс UNSPECIFIED. Это связано с тем, что enum реализован как uint32, а значение 0 считается неуказанным. Это похоже на nil для сообщения или пустой строки. При кодировании Protobuf в формате JSON сообщение nil, перечисление UNSPECIFIED или пустая строка игнорируются.


Мы следовали этому соглашению, пока однажды не сделали этого.


При отправке сообщений внешних веб-перехватчиков мы решили не использовать 0 как UNSPECIFIED. Одна из причин заключается в том, что мы используем EmitUnpopulated: true, чтобы убедиться, что все поля включены в представление JSON при отправке сообщений веб-перехватчиков внешним сторонам. И мы не хотим, чтобы это значение UNSPECIFIED отображалось в сообщениях веб-перехватчиков, если мы каким-то образом забыли установить для поля перечисления значение 0. Модульные тесты не могут выявить все ошибки; мы, инженеры, это знаем.


Это вызывает много проблем, поэтому нам пришлось вернуться и сделать значение 0 как UNSPECIFIED снова. Одна из проблем заключается в том, что это вынуждает использовать EmitUnpopulated: true везде! И есть места, где мы не хотим выделять все незаселенные поля. Например, вызов некоторых сторонних API. В некоторых сообщениях перечисления UNSPECIFIED смешиваются с перечислениями, не относящимися к UNSPECIFIED; нет способов отправить правильный формат с этим. Используйте EmitUnpopulated: true, сторонние API не понимают UNSPECIFIED; используйте EmitUnpopulated: false, а некоторые обязательные поля с не-UNSPECIFIED опущены. Конечно, все они могут быть удалены с помощью рефакторинга, но будет проще просто принудительно использовать `UNSPECIFIED в начале.


Почему бы просто не проверить, что все поля перечисления не установлены в 0 в сообщениях веб-перехватчиков? Вы можете спросить.


Оказывается, в Protobuf 3 нет простых способов сделать это!


В Protobuf 2 есть обязательный параметр, предотвращающий удаление поля. Этот параметр был удален в Protobuf 3, поскольку он предотвращает рефакторинг для удаления полей. Если мы забудем обновить каждую службу, чтобы удалить это больше не используемое обязательное поле, особенно в компании с несколькими командами, работающими вместе, сообщения будут непреднамеренно удалены. Лучше не требовать этого заранее. (подробнее)


В Protobuf 3 был интерфейс jsonpb.JSONPBMarshaler. Мы можем просто реализовать этот интерфейс для всех перечислений, чтобы возвращать ошибку при виде нулевого значения. Но опять удалили! Как протокол, мы должны максимально минимизировать настройку. В противном случае эту настройку придется реализовать и поддерживать на всех разных языках в разных командах!


Итак, как проверить нулевое значение в полях перечисления?


Нам нужно добраться до пакета отражения. В интерфейсе protoreflect.Message есть метод Range() для перебора каждого заполненного поля. Мы можем использовать этот метод, чтобы убедиться, что нет полей enum с нулями… О, подождите. Он перебирает только заполненные поля. Таким образом, он не обнаружит нулевое значение в перечислении!


Но функция protojson.Marshal() может по-прежнему выдавать незаполненные поля с опцией EmitUnpopulated. Как он это реализует? Погрузитесь в encoding/protojson, там есть фрагмент кода для перебора незаполненных полей (source). Давайте украдем:


```иди


// unpopulatedFieldRanger оборачивает protoreflect.Message и изменяет его Range


// метод для дополнительного перебора незаполненных полей.


введите unpopulatedFieldRanger struct{ pref.Message }


func (m unpopulatedFieldRanger) Range (f func (pref.FieldDescriptor, pref.Value) bool) {


fds := m.Descriptor().Поля()


для я := 0; я < fds.Len(); я++ {


fd := fds.Get(i)


если m.Has(fd) || fd.ContainingOneof() != ноль {


continue // игнорировать заполненные поля и поля внутри oneof


v := m.Get(fd)


isProto2Scalar := fd.Syntax() == pref.Proto2 && fd.Default().IsValid()


isSingularMessage := fd.Cardinality() != pref.Repeated && fd.Message() != nil


если isProto2Scalar || isSingularMessage {


v = pref.Value{} // использовать недопустимое значение для выдачи null


если !f(fd, v) {


вернуть


m.Message.Range(f)


Приведенный выше код перебирает дополнительные поля, перебирая protoreflect.Message.Descriptor().Fields(). Поля внутри полей oneof пропускаются. Незаполненные сингулярные поля «сообщения» устанавливаются как «недействительные» (думайте об этом как «пустые» в сгенерированном JSON) перед отправкой во входную функцию.


Еще немного кода для написания, например реализация метода перемещения для перебора всех различных типов Protobuf: сообщения, массива (повторяется), динамического Struct и, конечно же, enum. Но это решаемо. И теперь я могу отдохнуть.


Спасибо за прочтение! Если у вас есть лучший способ сделать это, сообщите мне, подключившись к Twitter 👋


Также опубликовано в мой блог.



Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE