непокрытые секции доставки видны в статусе разбора
- половина потока (50 доставок из 104) не несёт metrics вовсе и до сих пор числилась parsed: ретеншен, поверив статусу, срезал бы тела stateOfMind, которых в экспорте Apple нет - разбор перечисляет верхнеуровневые ключи data, непокрытые проглатываются декодированием: тело 40 МиБ из непокрытой секции удерживает 0 МиБ - статус partial и колонка delivery.uncovered_sections; миграция переводит прежние parsed в pending — им верить нельзя - витрина не изменилась: отпечаток совпал с прогоном до изменения
This commit is contained in:
@@ -258,3 +258,106 @@ Export шлёт под одним именем, чтобы одно имя оз
|
||||
- **THEN** ответ на приём остаётся `200`
|
||||
- **AND** исход виден в `delivery.parse_status` и в записи лога
|
||||
|
||||
### 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** точки обеих секций попадают в результат
|
||||
|
||||
|
||||
Reference in New Issue
Block a user