непокрытые секции доставки видны в статусе разбора
- половина потока (50 доставок из 104) не несёт metrics вовсе и до сих пор числилась parsed: ретеншен, поверив статусу, срезал бы тела stateOfMind, которых в экспорте Apple нет - разбор перечисляет верхнеуровневые ключи data, непокрытые проглатываются декодированием: тело 40 МиБ из непокрытой секции удерживает 0 МиБ - статус partial и колонка delivery.uncovered_sections; миграция переводит прежние parsed в pending — им верить нельзя - витрина не изменилась: отпечаток совпал с прогоном до изменения
This commit is contained in:
+104
@@ -0,0 +1,104 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Перечисление непокрытых секций доставки
|
||||
|
||||
Разбор SHALL перечислять верхнеуровневые ключи объекта `data` и возвращать
|
||||
вызывающему те из них, которые он не покрывает. Содержимое непокрытой секции
|
||||
MUST NOT удерживаться после того, как разбор прошёл мимо неё: тела доходят до
|
||||
42 МиБ, и удержание кучи здесь — часть контракта, а не деталь реализации.
|
||||
|
||||
Покрытым сегодня является ровно один ключ — `metrics`. Разбор и перечисление
|
||||
MUST ходить по одному объявленному множеству покрытых имён: состояние «секция
|
||||
разбирается, но числится непокрытой» невыразимо по построению.
|
||||
|
||||
Непокрытым ключ считается независимо от того, что лежит внутри: содержимое не
|
||||
интерпретируется, поэтому и о пустоте секции разбор честно ничего не знает.
|
||||
Измерено на живом архиве — пустых секций HAE не присылает ни разу (99 доставок).
|
||||
|
||||
Список SHALL быть каноничен: имена отсортированы, повторов нет. Порядок ключей в
|
||||
JSON от HAE нестабилен, а значение уезжает в базу и сравнивается между
|
||||
доставками.
|
||||
|
||||
Отсутствие непокрытых ключей и отсутствие секции `metrics` — разные события, и
|
||||
оба нормальны: половина потока состоит из доставок без метрик вовсе (48 из 99).
|
||||
|
||||
#### Scenario: Незнакомая секция попадает в список непокрытых
|
||||
|
||||
- **WHEN** тело содержит `data.workouts` наряду с `data.metrics`
|
||||
- **THEN** разбор возвращает `workouts` в списке непокрытых ключей
|
||||
- **AND** точки секции `metrics` разбираются как обычно
|
||||
|
||||
#### Scenario: Доставка без метрик разбирается и не теряется
|
||||
|
||||
- **WHEN** тело содержит только `data.stateOfMind`
|
||||
- **THEN** разбор завершается без ошибки, точек нет
|
||||
- **AND** `stateOfMind` возвращается в списке непокрытых ключей
|
||||
|
||||
#### Scenario: Доставка из одних метрик непокрытых ключей не даёт
|
||||
|
||||
- **WHEN** единственный ключ `data` — `metrics`
|
||||
- **THEN** список непокрытых ключей пуст
|
||||
|
||||
#### Scenario: Один и тот же набор секций даёт один и тот же список
|
||||
|
||||
- **WHEN** два тела несут те же секции в разном порядке, а одно из них
|
||||
повторяет непокрытый ключ дважды
|
||||
- **THEN** списки непокрытых ключей у них совпадают
|
||||
|
||||
#### Scenario: Содержимое непокрытой секции не удерживается в памяти
|
||||
|
||||
- **WHEN** тело в десятки мегабайт состоит преимущественно из непокрытой секции
|
||||
- **THEN** после разбора удержано не больше четырёх размеров тела — та же
|
||||
граница, что и для тела из метрик
|
||||
- **AND** содержимое непокрытой секции в результат разбора не попадает
|
||||
|
||||
### Requirement: Границы списка непокрытых секций
|
||||
|
||||
Список непокрытых ключей MUST быть ограничен — не больше 32 имён и не больше
|
||||
64 байт на имя: имена приходят из тела, которым отправитель управляет целиком.
|
||||
Срабатывание любой из границ MUST быть видно вызывающему — молчаливое усечение
|
||||
превратило бы список в уверенный, но неполный ответ на вопрос «что останется
|
||||
потерянным, если тело удалить».
|
||||
|
||||
Число имён сверх предела отдаётся счётчиком. Имя длиннее предела обрезается по
|
||||
границе рун, к обрезанному приписывается маркер `…` — сверх предела, а не внутри
|
||||
него. Обрезка не инъективна, поэтому обрезанное имя сравнению со словарём
|
||||
известных секций не подлежит.
|
||||
|
||||
Предел длины считается по байтам **декодированного** имени: escape-
|
||||
последовательности JSON к этому моменту уже разобраны.
|
||||
|
||||
#### Scenario: Ключей больше предела
|
||||
|
||||
- **WHEN** объект `data` содержит 40 непокрытых ключей
|
||||
- **THEN** список содержит 32 имени
|
||||
- **AND** число отброшенных имён отдано отдельным счётчиком
|
||||
|
||||
#### Scenario: Имя ключа длиннее предела
|
||||
|
||||
- **WHEN** непокрытый ключ длиннее 64 байт
|
||||
- **THEN** в списке лежит имя, обрезанное по границе рун, с маркером `…`
|
||||
|
||||
### Requirement: Отказ разбора остаётся всё или ничего
|
||||
|
||||
Разбор SHALL оставаться операцией «всё или ничего»: ошибка, встреченная
|
||||
**после** того, как секция `metrics` уже разобрана (обрезанное тело, мусор в
|
||||
следующем члене), MUST NOT оставлять точки в результате — доставка считается
|
||||
неразобранной целиком.
|
||||
|
||||
Иначе часть точек оказалась бы в витрине под статусом, по которому доставку
|
||||
никто не подберёт, и свёртка перестала бы быть детерминированной по журналу.
|
||||
|
||||
Повтор ключа `metrics` в одном объекте `data` SHALL давать объединение секций, а
|
||||
не победу последней: молча терять точки нельзя.
|
||||
|
||||
#### Scenario: Тело оборвано после секции метрик
|
||||
|
||||
- **WHEN** тело содержит целую секцию `metrics`, а следующий член `data`
|
||||
оборван
|
||||
- **THEN** разбор завершается ошибкой и точек не отдаёт
|
||||
|
||||
#### Scenario: Секция метрик встречается дважды
|
||||
|
||||
- **WHEN** объект `data` содержит два ключа `metrics`
|
||||
- **THEN** точки обеих секций попадают в результат
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Учёт частично разобранной доставки
|
||||
|
||||
Система SHALL отличать доставку, разобранную целиком, от доставки, в теле
|
||||
которой остались непокрытые разбором секции. Доставка с непустым списком
|
||||
непокрытых ключей MUST получать статус `partial`, а не `parsed`.
|
||||
|
||||
Статусы разбора:
|
||||
|
||||
```
|
||||
pending этим разбором ещё не смотрели
|
||||
parsed разобрано всё, что в теле было
|
||||
partial разобрано покрытое; в теле остались непокрытые секции
|
||||
failed разобрать не удалось, точек нет
|
||||
```
|
||||
|
||||
Источник истины — список непокрытых ключей; статус производен от него и от
|
||||
факта отказа, в порядке `failed` → `partial` → `parsed`. Приоритет назван явно,
|
||||
чтобы читатели (ретеншен, статистика) спрашивали статус, а не сравнивали список
|
||||
со строкой.
|
||||
|
||||
Список непокрытых ключей SHALL сохраняться рядом с доставкой — именами ключей,
|
||||
без содержимого секций. Он же ответ на вопрос «что останется потерянным, если
|
||||
тело удалить»: для `stateOfMind` доставки HAE единственный источник, в экспорте
|
||||
Apple его нет (находка 46). Поэтому список MUST сохраняться и при отказе
|
||||
разбора, если разбор успел его собрать: `failed` с непустым списком — законное
|
||||
состояние.
|
||||
|
||||
Запись списка MUST замещать прежнее значение целиком, включая замещение пустым:
|
||||
иначе доставка, все секции которой стали покрытыми, осталась бы `partial`
|
||||
навсегда.
|
||||
|
||||
Список — снимок покрытия **на момент свёртки**. Задача, которая начинает
|
||||
разбирать секцию, тем же изменением SHALL переводить `partial`-строки с этим
|
||||
ключом в `pending`; ретеншену позволено смотреть на `partial` только при
|
||||
соблюдении этого правила.
|
||||
|
||||
Статусы, поставленные разбором, который частичного исхода не различал, доверия
|
||||
не заслуживают: под `parsed` у них лежат и полностью разобранные доставки, и
|
||||
доставки без метрик вовсе. Такие строки MUST переводиться в `pending` — «этим
|
||||
разбором ещё не смотрели». Число точек у них до пересвёртки остаётся прежним: оно
|
||||
производно от объектов витрины, которые никуда не делись.
|
||||
|
||||
#### Scenario: Доставка с непокрытой секцией отмечается частичной
|
||||
|
||||
- **WHEN** разбор доставки вернул непустой список непокрытых ключей
|
||||
- **THEN** `parse_status` доставки равен `partial`
|
||||
- **AND** список непокрытых ключей сохранён вместе с доставкой
|
||||
- **AND** точки покрытой секции сохранены как обычно
|
||||
|
||||
#### Scenario: Доставка без непокрытых секций остаётся `parsed`
|
||||
|
||||
- **WHEN** разбор доставки не дал непокрытых ключей
|
||||
- **THEN** `parse_status` равен `parsed`
|
||||
- **AND** сохранённый список непокрытых ключей пуст
|
||||
|
||||
#### Scenario: Отказ разбора сильнее частичности
|
||||
|
||||
- **WHEN** разбор доставки завершился ошибкой
|
||||
- **THEN** `parse_status` равен `failed`
|
||||
- **AND** список непокрытых ключей сохранён, если разбор успел его собрать
|
||||
|
||||
#### Scenario: Пересвёртка после того, как секция стала покрытой
|
||||
|
||||
- **WHEN** доставка со статусом `partial` сворачивается повторно разбором,
|
||||
который эту секцию уже покрывает
|
||||
- **THEN** её статус становится `parsed`
|
||||
- **AND** сохранённый список непокрытых ключей пуст
|
||||
|
||||
#### Scenario: Строки прежнего разбора переводятся в неразобранные
|
||||
|
||||
- **WHEN** база содержит доставки со статусом `parsed`, свёрнутые до появления
|
||||
частичного статуса
|
||||
- **THEN** после миграции их статус равен `pending`
|
||||
- **AND** тела остаются в архиве, а повторная свёртка даёт то же состояние
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Значения точек не попадают в логи
|
||||
|
||||
Данные о здоровье чувствительнее токенов. Система MUST NOT писать значения
|
||||
точек и тела доставок в записи лога уровня выше `DEBUG`.
|
||||
|
||||
Непокрытые секции называются в логе **именами ключей**: имя секции — это форма
|
||||
пакета, а не измерение. Содержимое секции в лог не попадает ни при каком уровне
|
||||
выше `DEBUG`. Имена идут структурным атрибутом, а не склейкой в текст сообщения:
|
||||
кодировщик экранирует управляющие символы, и имя из чужого тела не разрывает
|
||||
построчный разбор логов.
|
||||
|
||||
Частичный разбор уровня записи не повышает: `partial` — установившееся состояние
|
||||
половины потока (48 доставок из 99), и постоянный `WARN` обесценил бы уровень.
|
||||
Повышает уровень другое — срабатывание границ списка: тело с сотнями секций или
|
||||
с именем длиннее предела на HAE не похоже вовсе.
|
||||
|
||||
#### Scenario: Разбор доставки логируется без значений
|
||||
|
||||
- **WHEN** доставка разобрана
|
||||
- **THEN** запись лога содержит счётчики (метрик, точек, объектов) и
|
||||
идентификатор доставки
|
||||
- **AND** не содержит ни значений точек, ни имён устройств
|
||||
|
||||
#### Scenario: Непокрытые секции названы именами ключей
|
||||
|
||||
- **WHEN** доставка содержит непокрытую секцию
|
||||
- **THEN** запись лога содержит имена непокрытых ключей отдельным атрибутом
|
||||
- **AND** не содержит ничего из содержимого этих секций
|
||||
- **AND** уровень записи из-за одной лишь частичности не повышается
|
||||
|
||||
#### Scenario: Границы списка сработали
|
||||
|
||||
- **WHEN** список непокрытых ключей усечён по числу имён или по длине имени
|
||||
- **THEN** запись лога имеет уровень `WARN`
|
||||
- **AND** содержит число отброшенных имён
|
||||
Reference in New Issue
Block a user