- Правило покрытия получило второй разряд (условный, как у точек), запрет вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и ряд из null больше не затирают маршрут. Победитель внутри доставки стал функцией множества версий — общим помощником с точками, — а провенанс поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину к прежнему содержимому. - Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает на старте; текст ошибки разбора не несёт значений из тела. - Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты: безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя был квадратичен по числу присланных версий одного ключа.
48 lines
3.2 KiB
Go
48 lines
3.2 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
)
|
|
|
|
// ErrNotFound — записи нет. Граничную ошибку драйвера (sql.ErrNoRows)
|
|
// транслируем в доменную здесь же, у источника, чтобы выше по коду не торчал
|
|
// database/sql.
|
|
var ErrNotFound = errors.New("запись не найдена")
|
|
|
|
// errNoCandidates — выбор победителя позван на пустом множестве. Нарушенный
|
|
// инвариант вызывающего, а не свойство данных: множество собирается из карты и
|
|
// пустым быть не может. Ошибкой, а не паникой, потому что путь проходит внутри
|
|
// свёртки принятой доставки — отказ обязан быть диагностируемым, а не «index
|
|
// out of range» в стеке фоновой горутины.
|
|
var errNoCandidates = errors.New("выбор победителя на пустом множестве версий")
|
|
|
|
// ErrBusy — база занята, и повторы транзакции этого не пересидели.
|
|
//
|
|
// Доменная ошибка, а не код драйвера: на неё ветвится свёртка. Отказ по
|
|
// занятости не является свойством доставки — работа просто не сделана, и
|
|
// доставка обязана остаться в очереди. Без этого различения конкуренция за
|
|
// базу выводила бы доставку из очереди навсегда.
|
|
var ErrBusy = errors.New("база занята")
|
|
|
|
// Transient отвечает, вызван ли отказ ОБСТОЯТЕЛЬСТВАМИ, а не данными.
|
|
//
|
|
// Ровно два случая: работу прекратили снаружи и база оказалась занята дольше,
|
|
// чем длятся повторы транзакции. Оба означают «не сделано», а не «не выходит»,
|
|
// поэтому работа обязана остаться к повторению.
|
|
//
|
|
// Определение живёт здесь, в одном месте, и его читают двое: тот, кто пишет
|
|
// исход разбора доставки, и тот, кто классифицирует этот исход в счётчики. Две
|
|
// копии правила разошлись бы, и доставка одновременно осталась бы в очереди и
|
|
// числилась отказавшей.
|
|
//
|
|
// Дедлайн самой операции сюда НЕ входит: не уложившаяся в бюджет работа не
|
|
// уложится в него и в следующий раз, а бесконечный повтор заведомо
|
|
// безнадёжного — это очередь, которая не движется.
|
|
func Transient(err error) bool {
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
return false
|
|
}
|
|
return errors.Is(err, context.Canceled) || errors.Is(err, ErrBusy)
|
|
}
|