Дозакрыты находки ревью по слиянию сущностей
- Правило покрытия получило второй разряд (условный, как у точек), запрет вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и ряд из null больше не затирают маршрут. Победитель внутри доставки стал функцией множества версий — общим помощником с точками, — а провенанс поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину к прежнему содержимому. - Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает на старте; текст ошибки разбора не несёт значений из тела. - Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты: безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя был квадратичен по числу присланных версий одного ключа.
This commit is contained in:
+179
-41
@@ -41,7 +41,52 @@ const SignificantDigits = 12
|
||||
// числа округлены до SignificantDigits значащих цифр.
|
||||
//
|
||||
// Форма предназначена для сравнения и хеширования, а не для хранения.
|
||||
//
|
||||
// Возвращаемый срез принадлежит вызывающему целиком: буфер, в котором форма
|
||||
// собрана, наружу больше не показывается.
|
||||
func Form(raw []byte) ([]byte, error) {
|
||||
return form(raw)
|
||||
}
|
||||
|
||||
// Hash возвращает шестнадцатеричный SHA-256 канонической формы.
|
||||
//
|
||||
// Хеш — детектор изменений, а не ключ: совпал с сохранённым, значит писать
|
||||
// нечего. Именно это делает широкие проходы синхронизации дешёвыми — глубокий
|
||||
// проход переприсылает неделю, но почти все сравнения сходятся.
|
||||
func Hash(raw []byte) (string, error) {
|
||||
f, err := form(raw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hashOf(f), nil
|
||||
}
|
||||
|
||||
// FormAndHash отдаёт каноническую форму и её хеш ЗА ОДИН проход.
|
||||
//
|
||||
// Нужен тем, кому требуется и то, и другое: сущность хешируется ради
|
||||
// хеш-детектора и канонизируется ради сравнения полноты, и считать форму дважды
|
||||
// над теми же байтами значит платить дважды за самую дорогую операцию
|
||||
// хранилища (тело 40 МиБ даёт пик кучи 768 МиБ).
|
||||
//
|
||||
// Отдельной функции «хеш по готовой форме» здесь нет намеренно: она вводила бы
|
||||
// контракт очерёдности, в котором передача сырых байт вместо формы даёт
|
||||
// правдоподобный, но неверный хеш, а компилятор такую подмену не ловит.
|
||||
//
|
||||
// Обратной ошибки — «Form считает хеш и выбрасывает» — здесь тоже нет: общий
|
||||
// низ у трёх функций один и хеша не считает. Иначе каждая точка при каждом
|
||||
// слиянии платила бы SHA-256, который никто не смотрит: Form зовётся из SortKey
|
||||
// на каждый кандидат координаты, из HashAll на каждую точку часа и дважды на
|
||||
// каждое сравнение в Equal.
|
||||
func FormAndHash(raw []byte) ([]byte, string, error) {
|
||||
f, err := form(raw)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return f, hashOf(f), nil
|
||||
}
|
||||
|
||||
// form — общий низ: каноническая форма и ничего сверх неё.
|
||||
func form(raw []byte) ([]byte, error) {
|
||||
v, err := decode(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -54,18 +99,9 @@ func Form(raw []byte) ([]byte, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Hash возвращает шестнадцатеричный SHA-256 канонической формы.
|
||||
//
|
||||
// Хеш — детектор изменений, а не ключ: совпал с сохранённым, значит писать
|
||||
// нечего. Именно это делает широкие проходы синхронизации дешёвыми — глубокий
|
||||
// проход переприсылает неделю, но почти все сравнения сходятся.
|
||||
func Hash(raw []byte) (string, error) {
|
||||
form, err := Form(raw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
func hashOf(form []byte) string {
|
||||
sum := sha256.Sum256(form)
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// HashAll возвращает хеш канонической формы последовательности значений —
|
||||
@@ -246,8 +282,7 @@ func (f Fields) Relate(g Fields) Fullness {
|
||||
return FullnessEqual
|
||||
}
|
||||
|
||||
// Covers говорит, несёт ли f всё СОДЕРЖАНИЕ g: каждый содержательный ключ g
|
||||
// есть у f, и ни один верхнеуровневый массив не стал короче.
|
||||
// Covers говорит, несёт ли f всё СОДЕРЖАНИЕ g.
|
||||
//
|
||||
// Отдельно от Relate, и это не дубль. Relate гасит отношение включения до
|
||||
// FullnessEqual, когда значения общих содержательных ключей разошлись, — верно
|
||||
@@ -259,57 +294,160 @@ func (f Fields) Relate(g Fields) Fullness {
|
||||
// (95% её веса), а тест на фикстуре с неизменёнными значениями остался бы
|
||||
// зелёным.
|
||||
//
|
||||
// Длина верхнеуровневых массивов сравнивается потому, что усечённый маршрут
|
||||
// (три точки вместо 593) ключа не теряет. Досчёт ряды удлиняет, поэтому
|
||||
// укорачивание — законный признак «приехало меньше». Предел правила назван
|
||||
// вслух: сокращение ВНУТРИ элемента ряда (точка маршрута без altitude) не
|
||||
// ловится ничем, кроме сверки с телом в архиве.
|
||||
// Условий четыре, все по ВЕРХНЕМУ уровню:
|
||||
//
|
||||
// Длины считаются здесь, а не в Analyze: Analyze зовётся на каждый кандидат
|
||||
// слияния точек, и разбор heartbeatSeries на каждой точке стоил бы дороже
|
||||
// самого сравнения.
|
||||
// 1. каждый содержательный ключ g есть у f и содержателен;
|
||||
//
|
||||
// 2. если множества содержательных ключей СОВПАЛИ — каждый ключ g, даже
|
||||
// пустой, есть у f. Тот же второй разряд, что у Relate, и с тем же
|
||||
// условием: иначе ключ с пустым значением исчезает по жребию тай-брейка.
|
||||
//
|
||||
// Условность разряда проверена оракулом, а не выведена. Безусловный
|
||||
// вариант («строже — значит правильнее») оказался хуже: версия с
|
||||
// `totalEnergy: null` и без маршрута запирала законный досчёт навсегда —
|
||||
// приехавшая теряла пустой ключ, сохранённая теряла содержательный
|
||||
// `route`, и пара становилась несравнимой. Маршрут не доезжал НИКОГДА, и
|
||||
// пересборка проигрывала то же поражение. Второй разряд разрешает спор
|
||||
// равных, а не отменяет первый;
|
||||
//
|
||||
// 3. форма значения не вырождается: где у g объект — у f объект, где массив —
|
||||
// массив. Без этого «скелет» (каждый вложенный объект заменён числом)
|
||||
// признаётся равным настоящей тренировке и выигрывает тай-брейк журнала;
|
||||
//
|
||||
// 4. верхнеуровневый массив не теряет ни длины, ни СОДЕРЖАТЕЛЬНЫХ элементов:
|
||||
// усечённый маршрут (три точки вместо 593) ключа не теряет, а маршрут из
|
||||
// [null,null,null] не теряет и длины. Досчёт ряды удлиняет, поэтому и
|
||||
// укорачивание, и опустошение элементов — законные признаки «приехало
|
||||
// меньше».
|
||||
//
|
||||
// Условия 3 и 4 применяются к ключам, содержательным у g: у пустоты формы нет,
|
||||
// и требовать её сохранения значило бы отличать `[]` от `0` там, где ни то, ни
|
||||
// другое ничего не несёт.
|
||||
//
|
||||
// Содержательность элемента ряда — ТА ЖЕ пустота, что у поля (isEmpty): второй
|
||||
// словарь пустоты дал бы два ответа на один вопрос. Цена названа вслух: ряд
|
||||
// настоящих нулей ([0,0,0]) считается лишённым содержания, поэтому версия с ним
|
||||
// сохранённую не заместит. Ошибка направлена в безопасную сторону — правило
|
||||
// удерживает, а не затирает, и событие видно счётчиком; наблюдённые ряды HAE
|
||||
// состоят из объектов.
|
||||
//
|
||||
// Предел правила назван вслух и не закрывается: сокращение ВНУТРИ элемента ряда
|
||||
// (точка маршрута без altitude при непустом элементе и той же длине) не ловится
|
||||
// ничем, кроме сверки с телом в архиве. Поэлементная сверка содержимого
|
||||
// отвергнута ценой: она разворачивала бы каждый элемент маршрута в дерево
|
||||
// значений на каждое сравнение, а тело 40 МиБ уже даёт 768 МиБ пика.
|
||||
//
|
||||
// Поле `source` в множества не входит (см. Analyze) — исключение придумано для
|
||||
// точек, где оно измерено, и наследуется сущностью молча. Названо здесь потому,
|
||||
// что список исключений живёт в Analyze: правка ради точек изменит и правило
|
||||
// удержания сущностей, а ни один тест сущностей этого не заметит.
|
||||
//
|
||||
// Формы и длины считаются здесь, а не в Analyze: Analyze зовётся на каждый
|
||||
// кандидат слияния точек, и разбор heartbeatSeries на каждой точке стоил бы
|
||||
// дороже самого сравнения.
|
||||
func (f Fields) Covers(g Fields) bool {
|
||||
for k, gv := range g.full {
|
||||
fv, ok := f.full[k]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
gn, gok := arrayLen(gv)
|
||||
if !gok {
|
||||
continue
|
||||
if !shapeKept(fv, gv) {
|
||||
return false
|
||||
}
|
||||
fn, fok := arrayLen(fv)
|
||||
if !fok || fn < gn {
|
||||
}
|
||||
// Первый разряд пройден. Второй включается ТОЛЬКО при равенстве множеств
|
||||
// содержательных ключей: если f несёт содержание сверх g, спор уже решён в
|
||||
// её пользу, и пустой ключ его не отменяет.
|
||||
if len(f.full) != len(g.full) {
|
||||
return true
|
||||
}
|
||||
for k := range g.all {
|
||||
if _, ok := f.all[k]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// arrayLen возвращает число элементов верхнеуровневого массива. Второй возврат
|
||||
// — является ли значение массивом вообще.
|
||||
// shapeKept говорит, сохраняет ли значение fv форму и наполнение gv.
|
||||
func shapeKept(fv, gv json.RawMessage) bool {
|
||||
switch literalKind(gv) {
|
||||
case kindObject:
|
||||
return literalKind(fv) == kindObject
|
||||
case kindArray:
|
||||
// Третий возврат смотрится У ОБЕИХ сторон. Неразобравшийся массив у g
|
||||
// дал бы нули, то есть покрывался бы даже пустым `[]`. Из тела HAE это
|
||||
// недостижимо (значения приходят разобранным JSON), но сохранённая
|
||||
// версия приезжает сюда из `payload` базы, а вторым источником сущностей
|
||||
// планируется импорт родного экспорта Apple — там байты формирует другой
|
||||
// код.
|
||||
gTotal, gFull, gok := arrayShape(gv)
|
||||
fTotal, fFull, fok := arrayShape(fv)
|
||||
return gok && fok && fTotal >= gTotal && fFull >= gFull
|
||||
default:
|
||||
// Скаляр покрывается чем угодно: у f может быть и объект — это форма
|
||||
// богаче, а не беднее.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// literalKind — род значения по первому байту литерала, как это делает сам
|
||||
// сканер encoding/json. Материализовать значение ради рода незачем.
|
||||
type literalKindT int
|
||||
|
||||
const (
|
||||
kindScalar literalKindT = iota
|
||||
kindObject
|
||||
kindArray
|
||||
)
|
||||
|
||||
func literalKind(raw json.RawMessage) literalKindT {
|
||||
lit := bytes.TrimSpace(raw)
|
||||
if len(lit) == 0 {
|
||||
return kindScalar
|
||||
}
|
||||
switch lit[0] {
|
||||
case '{':
|
||||
return kindObject
|
||||
case '[':
|
||||
return kindArray
|
||||
default:
|
||||
return kindScalar
|
||||
}
|
||||
}
|
||||
|
||||
// arrayShape возвращает число элементов верхнеуровневого массива и число
|
||||
// СОДЕРЖАТЕЛЬНЫХ среди них. Третий возврат — является ли значение массивом.
|
||||
//
|
||||
// Элементы проглатываются в выбрасываемый RawMessage: считать нужно только
|
||||
// количество, а материализация маршрута в дерево значений стоила бы того же,
|
||||
// от чего отказался разбор тела.
|
||||
func arrayLen(raw json.RawMessage) (int, bool) {
|
||||
if len(bytes.TrimSpace(raw)) == 0 || bytes.TrimSpace(raw)[0] != '[' {
|
||||
return 0, false
|
||||
// Элементы проглатываются в выбрасываемый RawMessage: материализация маршрута в
|
||||
// дерево значений стоила бы того же, от чего отказался разбор тела. Проверка
|
||||
// пустоты идёт по литералу элемента и обхода не добавляет — он уже здесь был
|
||||
// ради счёта.
|
||||
func arrayShape(raw json.RawMessage) (total, contentful int, ok bool) {
|
||||
if literalKind(raw) != kindArray {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
if _, err := dec.Token(); err != nil { // открывающая скобка
|
||||
return 0, false
|
||||
return 0, 0, false
|
||||
}
|
||||
n := 0
|
||||
// Буфер объявлен НАД циклом: RawMessage.UnmarshalJSON делает
|
||||
// `append((*m)[0:0], data...)`, то есть переиспользует ёмкость. Объявление
|
||||
// внутри цикла обнуляло бы срез каждый виток и давало аллокацию на элемент —
|
||||
// маршрут в 593 точки стоил бы 593 аллокаций на каждую проверку покрытия,
|
||||
// притом что комментарий выше обещает обратное.
|
||||
var elem json.RawMessage
|
||||
for dec.More() {
|
||||
var skip json.RawMessage
|
||||
if err := dec.Decode(&skip); err != nil {
|
||||
return 0, false
|
||||
if err := dec.Decode(&elem); err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
total++
|
||||
if !isEmpty(elem) {
|
||||
contentful++
|
||||
}
|
||||
n++
|
||||
}
|
||||
return n, true
|
||||
return total, contentful, true
|
||||
}
|
||||
|
||||
// agreeOnShared говорит, совпадают ли значения ключей, содержательных у обеих
|
||||
|
||||
Reference in New Issue
Block a user