непокрытые секции доставки видны в статусе разбора

- половина потока (50 доставок из 104) не несёт metrics вовсе и до сих пор
  числилась parsed: ретеншен, поверив статусу, срезал бы тела stateOfMind,
  которых в экспорте Apple нет
- разбор перечисляет верхнеуровневые ключи data, непокрытые проглатываются
  декодированием: тело 40 МиБ из непокрытой секции удерживает 0 МиБ
- статус partial и колонка delivery.uncovered_sections; миграция переводит
  прежние parsed в pending — им верить нельзя
- витрина не изменилась: отпечаток совпал с прогоном до изменения
This commit is contained in:
av
2026-08-01 21:25:59 +03:00
parent 7a7594e3e7
commit 34e5109b6d
26 changed files with 1808 additions and 89 deletions
+76
View File
@@ -82,3 +82,79 @@ func largeBody(metrics, points int) []byte {
b.WriteString(`]}}`)
return []byte(b.String())
}
// Перечисление непокрытых секций не имеет права стать вторым способом удержать
// тело. Непокрытая секция ПРОГЛАТЫВАЕТСЯ декодированием в выбрасываемый
// RawMessage: копия одна, живёт до следующего члена, удерживается ноль. Разбор
// `data` в map[string]json.RawMessage дешевле по коду, но копирует байты всех
// секций и держит их до конца разбора — этот тест сторожит разницу.
func TestParseУдержаниеКучиНепокрытойСекции(t *testing.T) {
if testing.Short() {
t.Skip("измерение кучи: не для -short")
}
body := bodyWithUncovered(40 << 20)
t.Logf("тело %d МиБ, из них непокрытая секция почти всё", len(body)>>20)
var before, after runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&before)
// Слой единственной точке взяться неоткуда — наследуем, иначе разбор
// откажет раньше, чем дойдёт до замера.
res, err := hae.Parse(body, hae.Meta{FallbackLayer: hae.LayerMinute})
if err != nil {
t.Fatalf("разбор: %v", err)
}
runtime.GC()
runtime.ReadMemStats(&after)
retained := int64(after.HeapAlloc) - int64(before.HeapAlloc)
limit := int64(len(body)) * 4
t.Logf("непокрытых %v, удержано %d МиБ при теле %d МиБ",
res.Uncovered, retained>>20, len(body)>>20)
if retained > limit {
t.Errorf("удержано %d МиБ при теле %d МиБ — больше четырёх тел; "+
"похоже, секции удерживаются, а не проглатываются",
retained>>20, len(body)>>20)
}
runtime.KeepAlive(res)
runtime.KeepAlive(body)
}
// Тело из вложенных скобок обязано быть ОТВЕРГНУТО, а не съедено. Пропуск
// ручным счётом глубины по Token() этого не давал: делимитеры идут мимо
// сканера, ограничитель вложенности encoding/json не работает, и стек токенов
// растёт как O(глубины) — измерено 488 МиБ пика на теле 40 МиБ.
func TestParseВложенныеСкобкиОтвергаются(t *testing.T) {
t.Parallel()
const depth = 100_000
var b strings.Builder
b.WriteString(`{"data":{"deep":`)
b.WriteString(strings.Repeat("[", depth))
b.WriteString(strings.Repeat("]", depth))
b.WriteString(`}}`)
if _, err := hae.Parse([]byte(b.String()), hae.Meta{}); err == nil {
t.Error("тело из ста тысяч уровней вложенности принято — ограничитель вложенности не работает")
}
}
func bodyWithUncovered(size int) []byte {
var b strings.Builder
b.WriteString(`{"data":{"metrics":[{"name":"m","units":"count","data":[`)
b.WriteString(`{"date":"2025-06-05 10:00:00 +0300","qty":1}`)
b.WriteString(`]}],"stateOfMind":[`)
const entry = `{"id":"00000000-0000-0000-0000-000000000000","kind":"momentary_emotion","valence":0.5},`
for b.Len() < size {
b.WriteString(entry)
}
b.WriteString(`{"id":"tail"}]}}`)
return []byte(b.String())
}