Обережно кодогенерація

yaroslavpodorvanov

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 цю помилку вже виправили.

Висновки:

Звісно хочеться використовувати оптимізовані бібліотеки, але стандартні краще протестовані та мають менше помилок.
Приклади доступні в репозиторії.

💖 💪 🙅 🚩
yaroslavpodorvanov
Yaroslav Podorvanov

Posted on September 21, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related