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) }