Yaroslav Podorvanov
Posted on September 21, 2020
Раніше вже писав про збільшення швидкодії та зменшення використання пам'яті після використання кодогенерації і ця історія має продовження, а саме розбір помилок.
Protobuf, перша помилка яка показала себе через пару тижнів після змін в коді
В проекті ми використовуємо офіційну бібліотеку Protobuf github.com/protocolbuffers/protobuf, яка підчас серіалізації використовує рефлексію і будує слайс байтів через append.
А потім я дізнався про "Protocol Buffers for Go with Gadgets" github.com/gogo/protobuf, бібліотеку-fork яка генерує додатковий код щоб прибрати рефексію підчас серіалізації і вже записує в слайс байтів по індексу бо так швидше.
Коли змінював одну бібліотеку на іншу то важливим вважав що стало працювати швидше і написані раніше тести пройшли успішно.
І все б було гаразд але в проекті існувала латка яка через пару тижнів після заміни перезапустила мікросервіс через паніку:
panic: runtime error: index out of range
Латка виглядала приблизно так:
import (
"github.com/golang/protobuf/proto"
google "gitlab.com/go-yp/go-warning-codegeneration/models/protos/google/advertisement"
)
func example() {
var popup = &google.Popup{
Id: uuid(),
Viewed: true,
Clicked: false,
}
// some deep nested function
go func() {
var content, err = proto.Marshal(popup)
if err != nil {
// log error
return
}
// store to database
store(content)
}()
// some delay with other actions
// @temporary hack
go func() {
popup.Clicked = true
var content, err = proto.Marshal(popup)
if err != nil {
// log error
return
}
// store to database again
store(content)
}()
}
І зі стандартною бібліотекою латка працювала без паніки для мікросервісу який працює постійно:
import (
"github.com/golang/protobuf/proto"
google "gitlab.com/go-yp/go-warning-codegeneration/models/protos/google/advertisement"
"testing"
)
const (
n = 1000000
)
func TestGoogleProtoMarshal(t *testing.T) {
for i := 0; i < n; i++ {
var popup = &google.Popup{
Id: uint32(i),
Viewed: true,
Clicked: false,
}
// some deep nested function
go func() {
_, _ = proto.Marshal(popup)
}()
// @temporary hack
go func() {
popup.Clicked = true
_, _ = proto.Marshal(popup)
}()
}
}
А от з github.com/gogo/protobuf при аналогічному тесті вже видає паніку.
Якщо розглянути згенерований код:
func (m *Popup) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *Popup) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
//...
if m.Clicked {
i--
dAtA[i] = 1
i--
dAtA[i] = 0x18
}
//...
return len(dAtA) - i, nil
}
то стає зрозуміло що розрахунок ємності слайсу відбувався за умов m.Clicked = false, а серіалізація за умов m.Clicked = true і таким чином отримав паніку "index out of range".
Звісно латку ми виправили і стало працювати навіть краще.
JSON, помилка у vendor бібліотеці
Бібліотека easyjson теж для серіалізації працює через додатковий код замість використання рефлексії.
Але після внесення в easyjson одної з оптимізацій, час від часу почали отримувати зламаний JSON, ось приклад тесту який покаже помилку.
package tests
import (
"github.com/stretchr/testify/require"
"gitlab.com/go-yp/go-warning-codegeneration/models/jsons/easy"
"testing"
)
const (
// language=JSON
popupWithUnicodeContent = `{
"title": "Some title with symbol \u201Dt",
"description": "Any description"
}`
// language=JSON
popupContent = `{
"title": "Some title",
"description": "Any description"
}`
)
func TestEasyjsonUnmarshalJSON(t *testing.T) {
content := make([]byte, 0, 1024)
content = append(content[:0], popupWithUnicodeContent...)
var popup easy.Popup
unmarshalErr := popup.UnmarshalJSON(content)
require.NoError(t, unmarshalErr)
var expected = easy.Popup{
Title: "Some title with symbol \u201Dt",
Description: "Any description",
}
require.Equal(t, expected, popup)
content = append(content[:0], popupContent...)
/**
Failed:
expected: easy.Popup{Title:"Some title with symbol ”t", Description:"Any description"}
actual : easy.Popup{Title:"Some title with symbol ”t", Description:" }y description"}
*/
require.Equal(t, expected, popup)
}
В easyjson цю помилку вже виправили.
Висновки:
Звісно хочеться використовувати оптимізовані бібліотеки, але стандартні краще протестовані та мають менше помилок.
Приклади доступні в репозиторії.
Posted on September 21, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.