- Правило покрытия получило второй разряд (условный, как у точек), запрет вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и ряд из null больше не затирают маршрут. Победитель внутри доставки стал функцией множества версий — общим помощником с точками, — а провенанс поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину к прежнему содержимому. - Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает на старте; текст ошибки разбора не несёт значений из тела. - Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты: безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя был квадратичен по числу присланных версий одного ключа.
78 lines
4.6 KiB
Go
78 lines
4.6 KiB
Go
package store
|
||
|
||
import "context"
|
||
|
||
// pickBest выбирает победителя среди кандидатов на одни координаты.
|
||
//
|
||
// **Победитель — функция МНОЖЕСТВА кандидатов, а не порядка их поступления.**
|
||
// Попарная свёртка этого не даёт: полнота (или покрытие) — частичный порядок,
|
||
// тай-брейк — тотальный, и вместе они образуют нетранзитивное отношение победы,
|
||
// то есть цикл. При цикле повторная свёртка одной и той же доставки меняет
|
||
// содержимое витрины, и она перестаёт быть свёрткой журнала. Проверено дважды:
|
||
// сперва на точках, где нетранзитивность нашлась перебором троек, потом на
|
||
// сущностях, где ту же ошибку повторили молча.
|
||
//
|
||
// Отсюда и общий помощник вместо второй рукописной копии: механизм один,
|
||
// отношения разные. `architecture.md` уже обещает смену тай-брейка точек, когда
|
||
// род метрики будет измерен, — то есть правка одного экземпляра при живом
|
||
// втором запланирована заранее, и расхождение правил слияния ломает детерминизм
|
||
// свёртки молча.
|
||
//
|
||
// dominates(a, b) обязан быть СТРОГИМ превосходством: a не хуже b и b не не
|
||
// хуже a. Иначе взаимно покрывающие друг друга кандидаты выбьют друг друга, и
|
||
// множество непревзойдённых окажется пустым.
|
||
//
|
||
// less обязан быть ТОТАЛЬНЫМ строгим порядком: при неединственном минимуме
|
||
// победителем оказывается просто первый в срезе, то есть порядок элементов на
|
||
// проводе, а он у HAE нестабилен.
|
||
//
|
||
// Отбор непревзойдённых КВАДРАТИЧЕН по числу кандидатов, и это названо вслух,
|
||
// потому что число кандидатов приходит из чужого тела. Отсюда `ctx`: цикл, чья
|
||
// стоимость определяется размером входа, обязан видеть отмену. Без него тело с
|
||
// двадцатью тысячами версий одного ключа занимало бы единственного воркера
|
||
// свёртки дольше, чем длится его же дедлайн, — то есть дедлайн, заведённый
|
||
// ровно против такого случая, не значил бы ничего. Замерено: n=4000 — 3.9 с,
|
||
// n=8000 — вчетверо больше.
|
||
//
|
||
// Вызывающий обязан сокращать множество до входа сюда: совпавших кандидатов
|
||
// схлопывать, а число различных — ограничивать. Помощник этого не делает
|
||
// сам — что считать «тем же» кандидатом, знает только он.
|
||
//
|
||
// Возвращает индекс победителя и индексы непревзойдённых — вторые нужны тем,
|
||
// кто считает несравнимость среди них. Пустой срез кандидатов — нарушенный
|
||
// инвариант вызывающего: оба сегодняшних вызова собирают множество из карты и
|
||
// пустого дать не могут.
|
||
func pickBest[T any](ctx context.Context, cands []T, dominates func(a, b T) bool, less func(a, b T) bool) (winner int, maximal []int, err error) {
|
||
if len(cands) == 0 {
|
||
return 0, nil, errNoCandidates
|
||
}
|
||
|
||
maximal = make([]int, 0, len(cands))
|
||
for i := range cands {
|
||
if err := ctx.Err(); err != nil {
|
||
return 0, nil, err
|
||
}
|
||
beaten := false
|
||
for j := range cands {
|
||
if i == j {
|
||
continue
|
||
}
|
||
if dominates(cands[j], cands[i]) {
|
||
beaten = true
|
||
break
|
||
}
|
||
}
|
||
if !beaten {
|
||
maximal = append(maximal, i)
|
||
}
|
||
}
|
||
|
||
winner = maximal[0]
|
||
for _, i := range maximal[1:] {
|
||
if less(cands[i], cands[winner]) {
|
||
winner = i
|
||
}
|
||
}
|
||
return winner, maximal, nil
|
||
}
|