- байтовый порядок канонических форм остался тай-брейком только внутри одной доставки: на живом корпусе он решал 98,8% спорных координат и системно хранил меньшее значение, из-за чего step_count терял род и verify:archive был красным - правило перестало быть коммутативным осознанно, поэтому порядок свёртки приведён к журнальному: проход воркера прекращается на отложенной доставке, а свёртка вне порядка журнала пишет WARN - заведены счётчики PointsHeld и PointsErased — удержание полнотой и единственное направление, в котором правило теряет содержание
175 lines
13 KiB
Go
175 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
// writeReport печатает итог пересборки человеку.
|
|
//
|
|
// Отдельной функцией с io.Writer, а не печатью в os.Stdout из недр: отчёт —
|
|
// новая поверхность вывода, и единственное, что защищает её от утечки данных о
|
|
// здоровье, — тест. Тест на глобальном os.Stdout был бы тестом на глобальном
|
|
// состоянии, то есть его бы не написали.
|
|
//
|
|
// Ни значений точек, ни имён метрик, ни имён устройств здесь нет и быть не
|
|
// может: содержимое витрины входит в отчёт только отпечатком, а он берёт его
|
|
// хешем.
|
|
func writeReport(w io.Writer, r report) {
|
|
p := func(format string, args ...any) {
|
|
_, _ = fmt.Fprintf(w, format+"\n", args...)
|
|
}
|
|
|
|
p("пересборка витрины из журнала")
|
|
p(" архив: тел %d, пропущено файлов %d, повторов идентификатора %d",
|
|
r.replay.Bodies, r.replay.SkippedFiles, r.replay.Duplicates)
|
|
p(" учёт: подобрано тел без записи %d, не удалось подобрать %d, записей без тела %d",
|
|
r.replay.Adopted, r.replay.AdoptFailed, r.replay.Orphans)
|
|
p(" свёрнуто: %d; отказов: слой не выведен %d, содержимое %d, прочее %d, отложено %d",
|
|
r.replay.Folded, r.replay.FailedLayer, r.replay.FailedMalformed, r.replay.FailedOther,
|
|
r.replay.Deferred)
|
|
// Удержанные версии сущностей печатаются ВСЕГДА, а не только при ненулевом
|
|
// значении: ноль здесь утверждение, а не отсутствие новостей. Отпечаток это
|
|
// правило не проверяет по построению — живой приём и пересборка пользуются
|
|
// одним правилом и одинаково сойдутся на одинаково удержанной версии, — так
|
|
// что счётчик и есть единственный способ увидеть, что правило слияния
|
|
// сущностей стало слишком строгим.
|
|
p(" слияние: частично разобрано %d, несравнимых наборов %d, удержано версий сущностей %d, версий одного ключа в одном теле %d",
|
|
r.replay.Partial, r.replay.Incomparable, r.replay.EntitiesHeld, r.replay.EntitiesDiverging)
|
|
// То же и по той же причине — про точки. Удержания говорят, спорит ли ещё
|
|
// правило полноты с журналом; потери — единственное направление, в котором
|
|
// тай-брейк «побеждает пришедшая» способен унести содержание, и человек,
|
|
// принимающий по этому отчёту необратимое решение о подмене базы, обязан
|
|
// видеть оба числа, а не выводить их из совпавшего отпечатка.
|
|
p(" точки: удержано полнотой %d, содержание унесено пришедшей %d",
|
|
r.replay.PointsHeld, r.replay.PointsErased)
|
|
|
|
if r.replay.Canceled {
|
|
// Ни отпечаток пересобранной витрины, ни число доставок после прогона при
|
|
// отмене не снимались. Печатать их сравнение значило бы выдать
|
|
// неизмеренное за измеренное — в единственном оракуле задачи.
|
|
p("")
|
|
p("прогон ОТМЕНЁН: сравнение не проводилось, файл назначения не создан")
|
|
return
|
|
}
|
|
|
|
// «Часть журнала не прочитана» — отдельное состояние, и оно обязано быть
|
|
// видно рядом с вердиктом отпечатков. Пропущенный симлинк на каталог уносит
|
|
// из прогона целый месяц одной строкой в счётчике, а вердикт «СОВПАЛИ»
|
|
// выдал бы сертификат воспроизводимости прогону, который этих тел не читал.
|
|
partialJournal := r.replay.SkippedFiles > 0 || r.replay.Orphans > 0 || r.replay.Duplicates > 0
|
|
// Нештатные отказы. Невыведенный слой сюда не входит: он есть в каждом
|
|
// журнале, и предупреждать о нём значило бы отправлять человека искать
|
|
// дефект там, где его нет. А вот «содержимое не разбирается» штатным не
|
|
// является: тело один раз уже прошло проверку формы на приёме.
|
|
//
|
|
// Отложенные доставки (занятая база, отмена) сюда входят: пересборка идёт в
|
|
// свежий файл при единственном писателе, и такая доставка в собранной
|
|
// витрине просто отсутствует — вместе с теми, кто наследовал от неё слой.
|
|
badFailures := r.replay.FailedOther > 0 || r.replay.FailedMalformed > 0 ||
|
|
r.replay.AdoptFailed > 0 || r.replay.Deferred > 0
|
|
|
|
if r.sourceMissing {
|
|
p(" объектов: %d", r.replay.Buckets)
|
|
p(" тренировок: %d", r.replay.Workouts)
|
|
p(" записей: %d", r.replay.Records)
|
|
p(" строк реестра категориальных значений: %d", r.replay.Categories)
|
|
p("")
|
|
p("рабочей базы %s нет — сверять не с чем, а заголовки доставок", r.dbPath)
|
|
p("не восстанавливаются: в архиве их нет.")
|
|
} else {
|
|
// «Было / стало» — единственное, по чему можно судить о НАПРАВЛЕНИИ
|
|
// расхождения. Отпечатки отвечают «да/нет», а решение о подмене
|
|
// необратимо; именно пара чисел 1737/1742 поймала прошлый дефект.
|
|
p(" объектов: было %d, стало %d", r.sourceBuckets, r.replay.Buckets)
|
|
p(" тренировок: было %d, стало %d", r.sourceWorkouts, r.replay.Workouts)
|
|
p(" записей: было %d, стало %d", r.sourceRecords, r.replay.Records)
|
|
p(" строк реестра категориальных значений: было %d, стало %d",
|
|
r.sourceCategories, r.replay.Categories)
|
|
p("")
|
|
p(" отпечаток рабочей: %s", r.sourcePrint)
|
|
p(" отпечаток пересобранной: %s", r.replay.Fingerprint)
|
|
switch {
|
|
case r.sourcePrint == r.replay.Fingerprint && !partialJournal:
|
|
p(" отпечатки СОВПАЛИ — состояние воспроизводимо")
|
|
case r.sourcePrint == r.replay.Fingerprint:
|
|
p(" отпечатки совпали, но сверка НЕПОЛНА: часть журнала не прочитана")
|
|
default:
|
|
p(" отпечатки РАЗОШЛИСЬ")
|
|
p(" ожидаемые причины: исправленный разбор; покрытая разбором новая")
|
|
p(" секция (её единиц хранения в рабочей базе нет по построению);")
|
|
p(" признак sealed не переносится (правила его выставления ещё нет)")
|
|
if r.sourceCategories < r.replay.Categories {
|
|
// Класс назван отдельно от факта расхождения: реестр появился
|
|
// вместе с бинарём, и у витрины, свёрнутой прежним, его нет по
|
|
// построению. Не назвав это, отчёт приучает человека
|
|
// игнорировать расхождение — то есть обесценивает оракул ровно
|
|
// там, где по нему принимается необратимое решение.
|
|
//
|
|
// Условие — НЕПОЛНОТА, а не пустота. Между выкаткой и прогоном
|
|
// проходят дни: воркер успевает набрать частые значения (фазы
|
|
// сна, контекст пульса) и не успевает редкие — имя тренировки,
|
|
// которая с тех пор не повторялась. Проверка «в рабочей базе
|
|
// реестра нет вовсе» такое состояние не ловила бы, и человек
|
|
// получил бы безадресное «разошлись» при совпавших числах
|
|
// объектов, тренировок и записей.
|
|
p(" РЕЕСТР НЕПОЛОН: строк категориальных значений в рабочей базе %d,",
|
|
r.sourceCategories)
|
|
p(" в пересобранной %d — реестр наполняется по мере свёртки, а целиком",
|
|
r.replay.Categories)
|
|
p(" его даёт только пересборка. Расхождение объясняется этим и лечится ею же")
|
|
}
|
|
if partialJournal {
|
|
p(" ВНИМАНИЕ: часть журнала не прочитана — расхождение может")
|
|
p(" объясняться этим, а не разбором")
|
|
}
|
|
}
|
|
}
|
|
|
|
if r.replay.Bodies == 0 || r.replay.Folded == 0 {
|
|
p("")
|
|
p("проигрывать было нечего: файл назначения не создан.")
|
|
p("проверьте storage.archive_dir и каталог запуска — пустая витрина")
|
|
p("совпадает по отпечатку с пустой витриной и выглядит идеальной сверкой")
|
|
return
|
|
}
|
|
|
|
if d := r.sourceAfter - r.sourceBefore; d != 0 {
|
|
// Доставки, приехавшие за время прогона, есть в рабочей базе и в архиве,
|
|
// но не в собранном файле. Подмена стёрла бы их учёт вместе с
|
|
// заголовками, восстановить которые неоткуда, — поэтому процедура здесь
|
|
// не печатается вовсе.
|
|
p("")
|
|
p("за время прогона в рабочую базу приехало доставок: %d.", d)
|
|
p("подменять этим файлом НЕЛЬЗЯ: учёта новых доставок в нём нет, а вместе")
|
|
p("с ним пропали бы их заголовки. Остановите сервис и пересоберите заново.")
|
|
return
|
|
}
|
|
|
|
p("")
|
|
if partialJournal {
|
|
p("ЧАСТЬ ЖУРНАЛА НЕ ПРОЧИТАНА: пропущено файлов %d, записей без тела %d,",
|
|
r.replay.SkippedFiles, r.replay.Orphans)
|
|
p("повторов идентификатора %d. Пересобранная витрина беднее рабочей на",
|
|
r.replay.Duplicates)
|
|
p("объекты этих доставок — и на объекты тех, кто наследовал от них слой.")
|
|
p("Проверьте каталог архива (симлинк на подкаталог обходом не читается)")
|
|
p("по DEBUG-строкам лога, прежде чем подменять базу.")
|
|
p("")
|
|
}
|
|
if badFailures {
|
|
p("отказы, которых быть не должно (%d прочих, %d по содержимому, %d при подборе, %d отложено) —",
|
|
r.replay.FailedOther, r.replay.FailedMalformed, r.replay.AdoptFailed, r.replay.Deferred)
|
|
p("разберитесь по логу, прежде чем подменять базу.")
|
|
p("")
|
|
}
|
|
p("собрано в %s", r.target)
|
|
p("подмена — вручную и при ОСТАНОВЛЕННОМ сервисе: он держит файл открытым,")
|
|
p("и переименование поверх живого процесса портит базу молча.")
|
|
p("")
|
|
p(" task down")
|
|
p(" mv %s %s", r.target, r.dbPath)
|
|
p(" rm -f %s-wal %s-shm", r.dbPath, r.dbPath)
|
|
p(" task up")
|
|
}
|