Дозакрыты находки ревью по слиянию сущностей

- Правило покрытия получило второй разряд (условный, как у точек), запрет
  вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и
  ряд из null больше не затирают маршрут. Победитель внутри доставки стал
  функцией множества версий — общим помощником с точками, — а провенанс
  поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину
  к прежнему содержимому.
- Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной
  записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма
  считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает
  на старте; текст ошибки разбора не несёт значений из тела.
- Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты:
  безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя
  был квадратичен по числу присланных версий одного ключа.
This commit is contained in:
av
2026-08-02 16:38:18 +03:00
parent 51a5272c96
commit 8331328134
52 changed files with 4921 additions and 481 deletions
+17 -2
View File
@@ -14,6 +14,7 @@ import (
"time"
"git.vakhrushev.me/av/healthlog/internal/archive"
"git.vakhrushev.me/av/healthlog/internal/hae"
"git.vakhrushev.me/av/healthlog/internal/ident"
"git.vakhrushev.me/av/healthlog/internal/store"
)
@@ -158,7 +159,17 @@ func (s *Service) Accept(ctx context.Context, body []byte, meta Meta) (Result, e
if err != nil {
// Тело уже на диске — данные не потеряны, но учёта нет. Такое тело
// подберёт пересборка (`healthlog reindex`), заведя запись заново.
s.log.ErrorContext(ctx, "delivery failed", "error", err, "delivery_id", res.DeliveryID, "raw_path", rawPath)
//
// Занятость базы называется отдельно. Уровень от этого не меняется:
// тело осиротело в любом случае, и вернуть его в журнал может только
// пересборка. Но лечится занятость не тем, чем сбой диска или испорченная
// база, — это конкуренция за запись, и она будет повторяться. Признак
// снимается с доменной ошибки, а не с предиката «обстоятельства вообще»:
// тот включает ещё и отмену снаружи, а здесь она невозможна по
// построению — учёт ведётся на контексте, переживающем обрыв соединения.
s.log.ErrorContext(ctx, "delivery failed", "error", err,
"delivery_id", res.DeliveryID, "raw_path", rawPath,
"db_busy", errors.Is(err, store.ErrBusy))
return Result{}, fmt.Errorf("record delivery: %w", err)
}
@@ -227,7 +238,11 @@ func checkEnvelope(body []byte) error {
var env envelope
if err := json.Unmarshal(body, &env); err != nil {
return fmt.Errorf("%w: %v", ErrMalformed, err) //nolint:errorlint // причину наружу не раскрываем, она уходит в лог
// Причина обрезается тем же пределом, что и в разборе: UnmarshalTypeError
// кладёт в текст ЛИТЕРАЛ значения, и тело из миллиона цифр давало
// мегабайт содержимого доставки в логе. Инвариант «тела только на DEBUG
// и с обрезкой» относится и к DEBUG.
return fmt.Errorf("%w: %s", ErrMalformed, hae.ClipCause(err))
}
if len(env.Data) == 0 {
return fmt.Errorf("%w: нет объекта data", ErrMalformed)