хранилище описано как свёртка по журналу событий
- экспорт Apple это снапшот всей истории, доставки после его даты — события поверх; состояние пересобирается как import(экспорт) + replay(доставки) - отсюда ретеншен архива меняется с произвольных 14 дней на «до следующего проверенного экспорта» (~2 ГБ за квартал, измерено), а свёртка обязана быть детерминированной — воспроизведение строго по received_at - названы границы модели: stateOfMind в экспорт не попадает вовсе, а верхние слои за периоды с удалёнными доставками не воскресают и досчитываться не должны — каталог обязан говорить это честно
This commit is contained in:
+60
-12
@@ -15,11 +15,12 @@ healthlog принимает выгрузки Apple Health из приложен
|
||||
в каком их прислал HAE — без переименований, пересчётов и отбрасывания
|
||||
незнакомых полей. Поэтому хранилище само по себе является полной копией
|
||||
данных, а не производной выжимкой.
|
||||
- **Сырой архив — страховка разбора, а не вечный склад.** Тело запроса ложится
|
||||
на диск до разбора и живёт ограниченный срок (по умолчанию две недели):
|
||||
этого хватает, чтобы пережить ошибку в нашем разборе и пересобрать
|
||||
хранилище (`healthlog reindex`). Дальше источником истины остаются часовые
|
||||
объекты — прямое следствие пункта выше, см. «Хранилище».
|
||||
- **Хранилище — свёртка по журналу, а не единственная копия.** Экспорт Apple
|
||||
это снапшот всей истории, доставки HAE после его даты — события поверх
|
||||
снапшота. Состояние всегда пересобираемо: `import(экспорт) + replay(доставки)`.
|
||||
Отсюда срок жизни сырого архива — до следующего проверенного экспорта, а не
|
||||
произвольные две недели. Исключение названо вслух: `stateOfMind` в экспорт не
|
||||
попадает, см. «Хранилище».
|
||||
- **Сохранили — значит приняли.** Код ответа отражает доставку, а не разбор
|
||||
(см. «Приём»).
|
||||
- **Ничего не теряем молча.** Идентичность — устойчивые координаты
|
||||
@@ -236,16 +237,63 @@ HRV); у накопительных — только `date`. Поэтому то
|
||||
|
||||
## Хранилище
|
||||
|
||||
### Сырой архив — короткая страховка
|
||||
### Сырой архив и восстановление состояния
|
||||
|
||||
`raw/ГГГГ/ММ/ДД/<ulid>.json.gz` — тело запроса как пришло, не редактируется.
|
||||
Живёт **ограниченный срок** (`storage.raw_retention`, по умолчанию 14 дней),
|
||||
после чего удаляется.
|
||||
|
||||
Смысл срока: архив нужен, чтобы пережить ошибку в **нашем** разборе и
|
||||
пересобрать хранилище (`healthlog reindex`). Двух недель на это заведомо
|
||||
хватает. Вечно хранить его незачем — часовые объекты держат те же точки
|
||||
дословно, так что архив дублировал бы данные, а не страховал их.
|
||||
Два источника вместе образуют **полный журнал событий**, а хранилище —
|
||||
свёртку по нему:
|
||||
|
||||
```
|
||||
состояние = import(последний проверенный экспорт) ← снапшот всей истории
|
||||
+ replay(доставки после его даты) ← хвост событий
|
||||
```
|
||||
|
||||
Экспорт Apple — не просто «источник истины для нижнего слоя», а снапшот: он
|
||||
содержит всю историю целиком (3.6 млн записей с 2019 года). Доставки HAE после
|
||||
его даты — события поверх снапшота. Значит любое повреждение хранилища,
|
||||
включая ошибку в нашем разборе любой давности, лечится пересборкой, а не
|
||||
восстановлением из бекапа.
|
||||
|
||||
Отсюда три следствия, каждое из которых меняет реализацию.
|
||||
|
||||
**Срок жизни архива определяется циклом экспорта, а не календарём.** Прежние
|
||||
14 дней были произвольным числом. Правильное правило: доставки хранятся **до
|
||||
следующего проверенного экспорта**, иначе в журнале появится дыра между концом
|
||||
ретеншена и датой снапшота. Цена измерена: поток даёт ~23 МБ архива в сутки,
|
||||
то есть ~2 ГБ за квартал между экспортами. Это дёшево за возможность
|
||||
пересобрать что угодно.
|
||||
|
||||
**Свёртка обязана быть детерминированной.** Проигрывание должно давать то же
|
||||
состояние, что и приём в реальном времени. Слияние «выигрывает более полная
|
||||
точка» коммутативно и порядка не требует; но когда две одинаково полные точки
|
||||
несут разные значения, исход решает порядок — поэтому воспроизведение идёт
|
||||
строго по `received_at`, а не по порядку файлов в каталоге.
|
||||
|
||||
**`reindex` и `import` — одна операция, а не две.** Восстановление это импорт
|
||||
снапшота плюс проигрывание хвоста; отдельной «пересборки из архива» не
|
||||
существует, она просто вырожденный случай с пустым снапшотом.
|
||||
|
||||
#### Что не восстанавливается, и это сказано вслух
|
||||
|
||||
Модель почти полна, но не полностью — умолчать об этом опаснее, чем признать.
|
||||
|
||||
**`stateOfMind` в экспорте отсутствует вовсе.** Проверено на свежем экспорте:
|
||||
ни одного типа со словом `StateOfMind` (есть только `MindfulSession` — это
|
||||
минуты осознанности, другое). Состояние разума живёт **только** в доставках
|
||||
HAE. Значит для него доставки не хвост журнала, а единственный источник: либо
|
||||
они не удаляются никогда, либо его история держится на самих сохранённых
|
||||
записях и восстановлению не подлежит.
|
||||
|
||||
**Верхние слои за периоды с удалёнными доставками.** После проигрывания
|
||||
снапшота у старого периода будет только слой `sample`; `minute` и `hour` за
|
||||
него не воскреснут. Посчитать их вниз из `sample` технически можно — и нельзя
|
||||
по инварианту: это была бы **наша** агрегация под видом присланной.
|
||||
|
||||
Поэтому правило: **восстановление не обязано быть побайтным, оно обязано быть
|
||||
честным.** Каталог разрезов показывает, какие слои есть за какой период; после
|
||||
пересборки старый период честно объявляет один слой вместо трёх, а не
|
||||
притворяется, что ничего не изменилось.
|
||||
|
||||
### Устаревание нижнего слоя
|
||||
|
||||
|
||||
Reference in New Issue
Block a user