Дозакрыты находки ревью по слиянию сущностей
- Правило покрытия получило второй разряд (условный, как у точек), запрет вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и ряд из null больше не затирают маршрут. Победитель внутри доставки стал функцией множества версий — общим помощником с точками, — а провенанс поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину к прежнему содержимому. - Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает на старте; текст ошибки разбора не несёт значений из тела. - Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты: безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя был квадратичен по числу присланных версий одного ключа.
This commit is contained in:
@@ -97,7 +97,14 @@ func TestReplayЖивогоАрхива(t *testing.T) {
|
||||
if first.Records == 0 {
|
||||
t.Error("записей в витрине нет — секция stateOfMind не разбирается")
|
||||
}
|
||||
t.Logf("тренировок %d, записей %d", first.Workouts, first.Records)
|
||||
// Удержанные версии сущностей печатаются рядом с их числом. Число — это
|
||||
// «сколько лежит», а удержания — «сколько правило слияния не пустило», и
|
||||
// второе отпечатком не проверяется по построению: живой приём и пересборка
|
||||
// пользуются одним правилом и одинаково сойдутся на одинаково удержанной
|
||||
// версии. Печатается, а не утверждается: удержание — событие для разбора,
|
||||
// а не отказ сходимости.
|
||||
t.Logf("тренировок %d, записей %d, удержано версий сущностей %d",
|
||||
first.Workouts, first.Records, first.EntitiesHeld)
|
||||
|
||||
// Повторное проигрывание того же журнала даёт то же состояние: свёртка
|
||||
// детерминирована, и пересборка даёт то же, что живой приём.
|
||||
|
||||
@@ -32,6 +32,17 @@ type Outcome struct {
|
||||
// Incomparable — столкновений с несравнимыми наборами полей. На живом потоке
|
||||
// их не было ни разу, и на этом стоит отказ от объединения полей.
|
||||
Incomparable int
|
||||
// EntitiesHeld — версий сущностей, удержанных правилом «не теряем
|
||||
// содержания». Без него правило слияния сущностей проверить нечем:
|
||||
// сходимость отпечатка его не проверяет ПО ПОСТРОЕНИЮ — живой приём и
|
||||
// пересборка пользуются одним правилом и одинаково сойдутся на одинаково
|
||||
// удержанной версии. То есть слишком строгое правило (замораживающее
|
||||
// тренировку на старой версии) выглядело бы идеальной сходимостью.
|
||||
EntitiesHeld int
|
||||
// EntitiesDiverging — версии одного ключа, приехавшие в одном теле с разным
|
||||
// содержанием. Событие другого рода, чем удержание, и считается отдельно:
|
||||
// смешанное число не отвечало бы ни на один из двух вопросов.
|
||||
EntitiesDiverging int
|
||||
}
|
||||
|
||||
// Add накапливает исход одной доставки в общий.
|
||||
@@ -43,6 +54,8 @@ func (o *Outcome) Add(other Outcome) {
|
||||
o.FailedOther += other.FailedOther
|
||||
o.Partial += other.Partial
|
||||
o.Incomparable += other.Incomparable
|
||||
o.EntitiesHeld += other.EntitiesHeld
|
||||
o.EntitiesDiverging += other.EntitiesDiverging
|
||||
}
|
||||
|
||||
// classify раскладывает ошибку свёртки по классам исхода.
|
||||
@@ -52,7 +65,7 @@ func (o *Outcome) Add(other Outcome) {
|
||||
// проверяется она перебором классов, без базы и без архива.
|
||||
//
|
||||
// Неэкспортируемая намеренно: её результат содержит поля `Partial` и
|
||||
// `Incomparable`, которые дописывает только Play, — вторая публичная дверь
|
||||
// `Incomparable`, `EntitiesHeld` и `EntitiesDiverging`, которые дописывает только Play, — вторая публичная дверь
|
||||
// молча занижала бы именно тот счётчик, по которому принимается решение о
|
||||
// судьбе тела в архиве.
|
||||
func classify(err error) Outcome {
|
||||
@@ -105,6 +118,8 @@ func (p Player) Play(ctx context.Context, deliveryID string) (Outcome, error) {
|
||||
out.Partial++
|
||||
}
|
||||
out.Incomparable += st.Incomparable
|
||||
out.EntitiesHeld += st.EntitiesHeld
|
||||
out.EntitiesDiverging += st.EntitiesDiverging
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
|
||||
@@ -203,6 +203,8 @@ func Run(ctx context.Context, o Options) (Report, error) {
|
||||
"failed_other", rep.FailedOther,
|
||||
"partial", rep.Partial,
|
||||
"incomparable", rep.Incomparable,
|
||||
"entities_held", rep.EntitiesHeld,
|
||||
"entities_diverging", rep.EntitiesDiverging,
|
||||
"buckets", rep.Buckets,
|
||||
"workouts", rep.Workouts,
|
||||
"records", rep.Records)
|
||||
@@ -333,8 +335,11 @@ func stopOr(rep Report, err error) (Report, error) {
|
||||
// prepare оставляет от учётной записи ФАКТЫ ЖУРНАЛА и сбрасывает производные от
|
||||
// разбора поля.
|
||||
//
|
||||
// `parse_status`, `points`, `derived_layer` и `uncovered_sections` — результат
|
||||
// ПРЕДЫДУЩЕЙ свёртки, а не то, что приехало вместе с доставкой. Перенести их
|
||||
// `parse_status`, `points`, `derived_layer`, `uncovered_sections` и
|
||||
// `skipped_entities` — результат ПРЕДЫДУЩЕЙ свёртки, а не то, что приехало
|
||||
// вместе с доставкой. У последнего пустота означает «не измерялось», так что
|
||||
// перенос выдал бы измерение прежнего разбора за измерение текущего — а по нему
|
||||
// решают, можно ли удалить тело. Перенести их
|
||||
// значило бы сделать пересобранную витрину функцией прошлого прогона: доставка,
|
||||
// чей повторный разбор отказал (штатный исход, когда слой не выводится),
|
||||
// сохранила бы слой прежнего разбора — свёртка не затирает его намеренно, — и
|
||||
|
||||
@@ -625,3 +625,55 @@ func TestИмяТелаОбязаноБытьКаноническим(t *testing
|
||||
t.Errorf("повторов %d: настоящее тело вытеснено подложенным", rep.Duplicates)
|
||||
}
|
||||
}
|
||||
|
||||
// Число пропущенных сущностей — производное от разбора поле, и пересборка его
|
||||
// не переносит. Пустота у него значит «не измерялось», а перенесённое число
|
||||
// выдавало бы измерение ПРЕЖНЕГО разбора за измерение текущего — притом что по
|
||||
// нему принимается необратимое решение об удалении тела.
|
||||
func TestЧислоПропусковНеПереноситсяВПересобраннуюБазу(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
arch := openArchive(t, filepath.Join(dir, "raw"))
|
||||
src := openStore(t, filepath.Join(dir, "live.db"))
|
||||
ctx := context.Background()
|
||||
|
||||
items := journal(t, "minute.json")
|
||||
live(t, arch, src, items)
|
||||
|
||||
// Проставляем счётчик в исходной базе, как если бы его измерил прежний
|
||||
// разбор, и убираем тело: пересборке будет нечего пересчитывать.
|
||||
n := int64(7)
|
||||
for _, it := range items {
|
||||
err := src.FinishParse(ctx, it.id, store.ParseOutcome{
|
||||
Status: store.ParseDone,
|
||||
SkippedEntities: &n,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("простановка счётчика: %v", err)
|
||||
}
|
||||
}
|
||||
// Тело убираем: доставка становится записью без тела, пересборка её не
|
||||
// сворачивает — и производные поля обязаны начаться пустыми, а не приехать
|
||||
// из журнала.
|
||||
raw := filepath.Join(dir, "raw")
|
||||
if err := os.RemoveAll(raw); err != nil {
|
||||
t.Fatalf("удаление тел: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(raw, 0o755); err != nil {
|
||||
t.Fatalf("пересоздание каталога архива: %v", err)
|
||||
}
|
||||
|
||||
rep, dst := run(t, ctx, arch, src, filepath.Join(dir, "rebuild.db"))
|
||||
if rep.Orphans == 0 {
|
||||
t.Fatal("доставка не стала записью без тела — тест проверяет не то")
|
||||
}
|
||||
|
||||
got, err := dst.LastDelivery(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("чтение доставки: %v", err)
|
||||
}
|
||||
if got.SkippedEntities != nil {
|
||||
t.Errorf("пересборка перенесла счётчик прежнего разбора: %d", *got.SkippedEntities)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user