непокрытые секции доставки видны в статусе разбора
- половина потока (50 доставок из 104) не несёт metrics вовсе и до сих пор числилась parsed: ретеншен, поверив статусу, срезал бы тела stateOfMind, которых в экспорте Apple нет - разбор перечисляет верхнеуровневые ключи data, непокрытые проглатываются декодированием: тело 40 МиБ из непокрытой секции удерживает 0 МиБ - статус partial и колонка delivery.uncovered_sections; миграция переводит прежние parsed в pending — им верить нельзя - витрина не изменилась: отпечаток совпал с прогоном до изменения
This commit is contained in:
@@ -3,6 +3,7 @@ package store
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
@@ -11,10 +12,18 @@ import (
|
||||
// Статусы разбора доставки. Код ответа приёма от них не зависит: сохранили —
|
||||
// значит приняли.
|
||||
const (
|
||||
// ParsePending — тело сохранено, разбора ещё не было.
|
||||
// ParsePending — этим разбором тело ещё не смотрели.
|
||||
//
|
||||
// Смысл именно такой, а не «тела ещё не касались»: миграция 00005 перевела
|
||||
// сюда доставки, разобранные кодом, который не различал частичный разбор.
|
||||
// Статус консервативный — ретеншен не трогает pending никогда.
|
||||
ParsePending = "pending"
|
||||
// ParseDone — тело разобрано, точки разложены по объектам.
|
||||
// ParseDone — разобрано всё, что в теле было.
|
||||
ParseDone = "parsed"
|
||||
// ParsePartial — разобрано покрытое, но в теле остались секции, которых
|
||||
// разбор не покрывает. Не отклонение, а установившееся состояние половины
|
||||
// потока: 48 доставок из 99 несут только workouts или stateOfMind.
|
||||
ParsePartial = "partial"
|
||||
// ParseFailed — разобрать не удалось. Тело лежит в архиве, доставку
|
||||
// подберёт пересборка.
|
||||
ParseFailed = "failed"
|
||||
@@ -38,6 +47,10 @@ type Delivery struct {
|
||||
// значений), уже без секретов. Именованные поля выше дублируют часть из
|
||||
// них: по ним ходят запросы, а Headers хранит всё остальное на будущее.
|
||||
Headers string
|
||||
// UncoveredSections — секции тела, которых разбор не покрыл, JSON-массивом
|
||||
// имён. Ответ на вопрос «что останется потерянным, если тело удалить»:
|
||||
// ретеншен обязан спрашивать его прежде, чем срезать тело.
|
||||
UncoveredSections string
|
||||
}
|
||||
|
||||
// CreateDelivery записывает факт приёма пакета.
|
||||
@@ -69,7 +82,7 @@ func (s *Store) LastDelivery(ctx context.Context) (Delivery, error) {
|
||||
const q = `
|
||||
SELECT id, received_at, automation_name, automation_id, aggregation,
|
||||
period, session_id, bytes, sha256, raw_path, parse_status,
|
||||
points, headers
|
||||
points, headers, uncovered_sections
|
||||
FROM delivery ORDER BY received_at DESC, id DESC LIMIT 1`
|
||||
|
||||
var d Delivery
|
||||
@@ -77,7 +90,7 @@ func (s *Store) LastDelivery(ctx context.Context) (Delivery, error) {
|
||||
err := s.db.QueryRowxContext(ctx, q).Scan(
|
||||
&d.ID, &receivedAt, &d.AutomationName, &d.AutomationID, &d.Aggregation,
|
||||
&d.Period, &d.SessionID, &d.Bytes, &d.SHA256, &d.RawPath,
|
||||
&d.ParseStatus, &d.Points, &d.Headers)
|
||||
&d.ParseStatus, &d.Points, &d.Headers, &d.UncoveredSections)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return Delivery{}, ErrNotFound
|
||||
}
|
||||
@@ -101,14 +114,40 @@ func (s *Store) LastDelivery(ctx context.Context) (Delivery, error) {
|
||||
// Пустой layer означает «не трогать»: у неудачной свёртки слоя нет, а
|
||||
// затирание оборвало бы цепочку наследования — в том числе у доставки, которая
|
||||
// раньше свернулась успешно, — и результат пересборки журнала изменился бы.
|
||||
func (s *Store) FinishParse(ctx context.Context, id, status string, points int64, layer string) error {
|
||||
// ParseOutcome — исход разбора доставки. Структурой, а не растущим списком
|
||||
// позиционных параметров: у FinishParse их было уже четыре, и пятый неизбежно
|
||||
// перепутали бы местами с четвёртым.
|
||||
type ParseOutcome struct {
|
||||
Status string
|
||||
Points int64
|
||||
// Layer пустой означает «не трогать» — см. FinishParse.
|
||||
Layer string
|
||||
// Uncovered замещает прежнее значение ЦЕЛИКОМ, включая замещение пустым:
|
||||
// у слоя пустота — отсутствие знания, у списка — знание об отсутствии.
|
||||
// Пересвёртка доставки, чья секция стала покрытой, обязана список очистить.
|
||||
Uncovered []string
|
||||
}
|
||||
|
||||
func (s *Store) FinishParse(ctx context.Context, id string, out ParseOutcome) error {
|
||||
const q = `
|
||||
UPDATE delivery
|
||||
SET parse_status = ?, points = ?,
|
||||
derived_layer = CASE WHEN ? = '' THEN derived_layer ELSE ? END
|
||||
derived_layer = CASE WHEN ? = '' THEN derived_layer ELSE ? END,
|
||||
uncovered_sections = ?
|
||||
WHERE id = ?`
|
||||
|
||||
res, err := s.db.ExecContext(ctx, q, status, points, layer, layer, id)
|
||||
// Ровно одно представление пустоты — `[]`: nil-срез Go сериализуется как
|
||||
// null, и в колонке появилось бы второе значение с тем же смыслом.
|
||||
sections := out.Uncovered
|
||||
if sections == nil {
|
||||
sections = []string{}
|
||||
}
|
||||
encoded, err := json.Marshal(sections)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode uncovered sections: %w", err)
|
||||
}
|
||||
|
||||
res, err := s.db.ExecContext(ctx, q, out.Status, out.Points, out.Layer, out.Layer, string(encoded), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update parse status: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user