Дозакрыты находки ревью по слиянию сущностей
- Правило покрытия получило второй разряд (условный, как у точек), запрет вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и ряд из null больше не затирают маршрут. Победитель внутри доставки стал функцией множества версий — общим помощником с точками, — а провенанс поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину к прежнему содержимому. - Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает на старте; текст ошибки разбора не несёт значений из тела. - Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты: безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя был квадратичен по числу присланных версий одного ключа.
This commit is contained in:
+77
-6
@@ -220,7 +220,7 @@ func Parse(body []byte, meta Meta) (res Result, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
res = Result{}
|
||||
err = fmt.Errorf("%w: паника разбора: %v", ErrMalformed, r)
|
||||
err = fmt.Errorf("%w: паника разбора: %s", ErrMalformed, clip(fmt.Sprint(r)))
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -422,7 +422,7 @@ func decodeEnvelope(body []byte) (envelope, error) {
|
||||
var env envelope
|
||||
|
||||
fail := func(e error) (envelope, error) {
|
||||
return envelope{}, fmt.Errorf("%w: %v", ErrMalformed, e) //nolint:errorlint // причина уходит в лог, наружу не раскрывается
|
||||
return envelope{}, fmt.Errorf("%w: %s", ErrMalformed, ClipCause(e))
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(bytes.NewReader(body))
|
||||
@@ -430,6 +430,7 @@ func decodeEnvelope(body []byte) (envelope, error) {
|
||||
// Верхний уровень тела: интересует только data. Прочие ключи конверта в
|
||||
// список не идут — иначе в одном списке смешались бы имена секций и мусор
|
||||
// конверта, а форму `{"data": …}` проверяет приём.
|
||||
at := dec.InputOffset()
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return fail(err)
|
||||
@@ -441,7 +442,7 @@ func decodeEnvelope(body []byte) (envelope, error) {
|
||||
return envelope{}, nil
|
||||
}
|
||||
if d, ok := tok.(json.Delim); !ok || d != '{' {
|
||||
return fail(fmt.Errorf("ожидался объект, встречено %v", tok))
|
||||
return fail(fmt.Errorf("ожидался объект, встречено %s", tokenDesc(tok, at)))
|
||||
}
|
||||
seen := make(map[string]struct{})
|
||||
for dec.More() {
|
||||
@@ -486,13 +487,14 @@ type envelope struct {
|
||||
// decodeData разбирает объект data, дописывая в конверт покрытые секции и
|
||||
// имена непокрытых.
|
||||
func decodeData(dec *json.Decoder, seen map[string]struct{}, env *envelope) error {
|
||||
at := dec.InputOffset()
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// data не объект — прежнее поведение: ошибка ровно там, где была.
|
||||
if d, ok := tok.(json.Delim); !ok || d != '{' {
|
||||
return fmt.Errorf("data: ожидался объект, встречено %v", tok)
|
||||
return fmt.Errorf("data: ожидался объект, встречено %s", tokenDesc(tok, at))
|
||||
}
|
||||
|
||||
for dec.More() {
|
||||
@@ -544,17 +546,85 @@ func decodeSection(dec *json.Decoder) ([]json.RawMessage, error) {
|
||||
// memberName читает имя члена объекта. Token() отдаёт имя уже после разбора
|
||||
// escape-последовательностей, поэтому границы считаются по декодированному.
|
||||
func memberName(dec *json.Decoder) (string, error) {
|
||||
at := dec.InputOffset()
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name, ok := tok.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("ожидалось имя члена, встречено %v", tok)
|
||||
return "", fmt.Errorf("ожидалось имя члена, встречено %s", tokenDesc(tok, at))
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// maxCauseLen — предел длины чужой причины в тексте нашей ошибки.
|
||||
//
|
||||
// Сообщения самого разбора значений не несут (см. tokenDesc), но ошибка может
|
||||
// прийти и из encoding/json, а его UnmarshalTypeError кладёт в текст ЛИТЕРАЛ
|
||||
// значения: тело из миллиона цифр давало текст ошибки в мегабайт, и он уезжал
|
||||
// атрибутом `error` выше DEBUG. Предел держится здесь, на границе, а не у
|
||||
// логирующего: обрезка живёт в другом месте и о новой ошибке разбора не узнает.
|
||||
const maxCauseLen = 200
|
||||
|
||||
// ClipCause переводит чужую ошибку в ограниченную по длине строку.
|
||||
//
|
||||
// Экспортировано ради приёма: он проверяет форму конверта тем же
|
||||
// encoding/json и обязан держать тот же предел — иначе инвариант обходится
|
||||
// через соседний пакет.
|
||||
func ClipCause(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return clip(err.Error())
|
||||
}
|
||||
|
||||
func clip(s string) string {
|
||||
if len(s) <= maxCauseLen {
|
||||
return s
|
||||
}
|
||||
// По границе рун: обрезка посреди многобайтовой руны даёт мусор в логе.
|
||||
cut := maxCauseLen
|
||||
for cut > 0 && !utf8.RuneStart(s[cut]) {
|
||||
cut--
|
||||
}
|
||||
return s[:cut] + "…"
|
||||
}
|
||||
|
||||
// tokenDesc описывает встреченный токен БЕЗ его значения: род и смещение начала
|
||||
// во входе.
|
||||
//
|
||||
// Значение из тела в сообщение не попадает никогда. Инвариант «тела запросов
|
||||
// только на DEBUG и с обрезкой» обходится одним `%v`: строка в 8 МиБ на месте
|
||||
// ожидаемого объекта давала текст ошибки в 8 МиБ, и он уезжал атрибутом `error`
|
||||
// на уровень WARN — то есть содержимое доставки оказывалось в логе целиком.
|
||||
// Предел держит само сообщение, а не обрезка на стороне логирующего: обрезка
|
||||
// живёт в другом месте и о новой ошибке разбора не узнает.
|
||||
//
|
||||
// Род называется словарём JSON, а не именем типа языка: `json.Delim` не говорит
|
||||
// ничего о том, какая скобка встретилась. Сам делимитер печатается значением —
|
||||
// он из фиксированного набора и содержимого не раскрывает.
|
||||
//
|
||||
// Смещение берётся ДО чтения токена: InputOffset() отдаёт позицию конца
|
||||
// последнего возвращённого токена, и снятое после оно указывало бы на конец
|
||||
// виновного значения — то есть на восемь мегабайт дальше начала проблемы.
|
||||
func tokenDesc(tok json.Token, at int64) string {
|
||||
kind := "?"
|
||||
switch v := tok.(type) {
|
||||
case json.Delim:
|
||||
kind = fmt.Sprintf("%q", string(v))
|
||||
case string:
|
||||
kind = "string"
|
||||
case json.Number, float64:
|
||||
kind = "number"
|
||||
case bool:
|
||||
kind = "bool"
|
||||
case nil:
|
||||
kind = "null"
|
||||
}
|
||||
return fmt.Sprintf("%s на смещении %d", kind, at)
|
||||
}
|
||||
|
||||
// swallow проглатывает значение целиком, ничего не удерживая.
|
||||
func swallow(dec *json.Decoder) error {
|
||||
var skip json.RawMessage
|
||||
@@ -562,12 +632,13 @@ func swallow(dec *json.Decoder) error {
|
||||
}
|
||||
|
||||
func expectDelim(dec *json.Decoder, want json.Delim) error {
|
||||
at := dec.InputOffset()
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d, ok := tok.(json.Delim); !ok || d != want {
|
||||
return fmt.Errorf("ожидалось %q, встречено %v", want, tok)
|
||||
return fmt.Errorf("ожидалось %q, встречено %s", want, tokenDesc(tok, at))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user