непокрытые секции доставки видны в статусе разбора
- половина потока (50 доставок из 104) не несёт metrics вовсе и до сих пор числилась parsed: ретеншен, поверив статусу, срезал бы тела stateOfMind, которых в экспорте Apple нет - разбор перечисляет верхнеуровневые ключи data, непокрытые проглатываются декодированием: тело 40 МиБ из непокрытой секции удерживает 0 МиБ - статус partial и колонка delivery.uncovered_sections; миграция переводит прежние parsed в pending — им верить нельзя - витрина не изменилась: отпечаток совпал с прогоном до изменения
This commit is contained in:
+46
-8
@@ -82,6 +82,12 @@ type Stats struct {
|
||||
LayerMismatch bool
|
||||
Collisions []store.Collision
|
||||
IncomparableAt []store.Collision
|
||||
// Uncovered — верхнеуровневые ключи `data`, которых разбор не покрывает.
|
||||
// Это ответ на вопрос «что останется потерянным, если тело удалить»:
|
||||
// для stateOfMind он необратим — в экспорте Apple этой секции нет.
|
||||
Uncovered []string
|
||||
// UncoveredDropped — сколько имён отброшено границей списка.
|
||||
UncoveredDropped int
|
||||
}
|
||||
|
||||
// Fold разбирает тело доставки и раскладывает точки по часовым объектам.
|
||||
@@ -103,7 +109,7 @@ func (s *Service) Fold(ctx context.Context, deliveryID string) (Stats, error) {
|
||||
|
||||
body, err := s.readBody(d.RawPath)
|
||||
if err != nil {
|
||||
s.fail(ctx, deliveryID, err)
|
||||
s.fail(ctx, deliveryID, err, nil)
|
||||
return stats, err
|
||||
}
|
||||
|
||||
@@ -118,10 +124,15 @@ func (s *Service) Fold(ctx context.Context, deliveryID string) (Stats, error) {
|
||||
FallbackLayer: hae.Layer(fallback),
|
||||
})
|
||||
if err != nil {
|
||||
s.fail(ctx, deliveryID, err)
|
||||
// Список непокрытых секций переживает отказ: доставка, у которой не
|
||||
// определился слой, обязана остаться записью о том, что в теле есть
|
||||
// невосстановимая секция.
|
||||
s.fail(ctx, deliveryID, err, parsed.Uncovered)
|
||||
return stats, err
|
||||
}
|
||||
|
||||
stats.Uncovered = parsed.Uncovered
|
||||
stats.UncoveredDropped = parsed.UncoveredDropped
|
||||
stats.Metrics = parsed.Metrics
|
||||
stats.Points = len(parsed.Points)
|
||||
stats.SkippedNoTime = parsed.SkippedNoTime
|
||||
@@ -132,7 +143,7 @@ func (s *Service) Fold(ctx context.Context, deliveryID string) (Stats, error) {
|
||||
|
||||
merge, err := s.store.MergePoints(ctx, toIncoming(parsed.Points), deliveryID)
|
||||
if err != nil {
|
||||
s.fail(ctx, deliveryID, err)
|
||||
s.fail(ctx, deliveryID, err, parsed.Uncovered)
|
||||
return stats, err
|
||||
}
|
||||
|
||||
@@ -146,7 +157,20 @@ func (s *Service) Fold(ctx context.Context, deliveryID string) (Stats, error) {
|
||||
stats.Collisions = merge.Collisions
|
||||
stats.IncomparableAt = merge.IncomparableAt
|
||||
|
||||
if err := s.finish(ctx, deliveryID, store.ParseDone, int64(stats.Points), stats.Layer); err != nil {
|
||||
// Источник истины — список; статус производен от него и от факта отказа.
|
||||
// Приоритет назван явно, иначе два будущих читателя (ретеншен и /stats)
|
||||
// разойдутся: один спросит parse_status, другой — непустоту списка.
|
||||
status := store.ParseDone
|
||||
if len(parsed.Uncovered) > 0 {
|
||||
status = store.ParsePartial
|
||||
}
|
||||
out := store.ParseOutcome{
|
||||
Status: status,
|
||||
Points: int64(stats.Points),
|
||||
Layer: stats.Layer,
|
||||
Uncovered: parsed.Uncovered,
|
||||
}
|
||||
if err := s.finish(ctx, deliveryID, out); err != nil {
|
||||
s.log.ErrorContext(ctx, "delivery fold failed", "error", err, "delivery_id", deliveryID)
|
||||
return stats, err
|
||||
}
|
||||
@@ -186,6 +210,11 @@ func (s *Service) logResult(ctx context.Context, deliveryID string, st Stats) {
|
||||
"skipped_bad_end", st.SkippedBadEnd,
|
||||
"layer", st.Layer,
|
||||
"layer_mismatch", st.LayerMismatch,
|
||||
// Структурным []string, а не склейкой: JSON-кодировщик slog экранирует
|
||||
// управляющие символы, поэтому имя секции из чужого тела не разрывает
|
||||
// построчный разбор логов. Содержимого секций здесь нет.
|
||||
"uncovered", st.Uncovered,
|
||||
"uncovered_dropped", st.UncoveredDropped,
|
||||
}
|
||||
if len(st.Collisions) > 0 {
|
||||
attrs = append(attrs, "collisions", formatCollisions(st.Collisions))
|
||||
@@ -200,6 +229,10 @@ func (s *Service) logResult(ctx context.Context, deliveryID string, st Stats) {
|
||||
allSkipped := st.Points == 0 && skipped > 0
|
||||
|
||||
switch {
|
||||
case st.UncoveredDropped > 0:
|
||||
// Не частичный разбор, а тело, не похожее на HAE: секций у HAE восемь,
|
||||
// а границу выбило больше тридцати двух.
|
||||
s.log.WarnContext(ctx, "delivery folded, uncovered section list truncated", attrs...)
|
||||
case st.Incomparable > 0:
|
||||
// Выше перезаписей намеренно: несравнимый набор полей — событие реже и
|
||||
// информативнее, на живом потоке не случавшееся ни разу. Признаки при
|
||||
@@ -239,11 +272,11 @@ func formatCollisions(cs []store.Collision) string {
|
||||
const keepLayer = ""
|
||||
|
||||
// finish записывает исход разбора на контексте, переживающем отмену исходного.
|
||||
func (s *Service) finish(ctx context.Context, deliveryID, status string, points int64, layer string) error {
|
||||
func (s *Service) finish(ctx context.Context, deliveryID string, out store.ParseOutcome) error {
|
||||
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), finishTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := s.store.FinishParse(ctx, deliveryID, status, points, layer); err != nil {
|
||||
if err := s.store.FinishParse(ctx, deliveryID, out); err != nil {
|
||||
return fmt.Errorf("запись исхода разбора: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -251,7 +284,7 @@ func (s *Service) finish(ctx context.Context, deliveryID, status string, points
|
||||
|
||||
// fail отмечает доставку неразобранной. Тело остаётся в архиве, и её подберёт
|
||||
// пересборка — приём при этом не затрагивается: сохранили значит приняли.
|
||||
func (s *Service) fail(ctx context.Context, deliveryID string, cause error) {
|
||||
func (s *Service) fail(ctx context.Context, deliveryID string, cause error, uncovered []string) {
|
||||
level := slog.LevelError
|
||||
switch {
|
||||
case errors.Is(cause, hae.ErrLayerUnknown):
|
||||
@@ -269,7 +302,12 @@ func (s *Service) fail(ctx context.Context, deliveryID string, cause error) {
|
||||
// Слой НЕ затирается: доставка могла свернуться успешно раньше, и пустая
|
||||
// строка здесь оборвала бы цепочку наследования, то есть изменила бы
|
||||
// результат пересборки журнала.
|
||||
if err := s.finish(ctx, deliveryID, store.ParseFailed, 0, keepLayer); err != nil {
|
||||
out := store.ParseOutcome{
|
||||
Status: store.ParseFailed,
|
||||
Layer: keepLayer,
|
||||
Uncovered: uncovered,
|
||||
}
|
||||
if err := s.finish(ctx, deliveryID, out); err != nil {
|
||||
s.log.ErrorContext(ctx, "delivery parse status not recorded", "error", err, "delivery_id", deliveryID)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user