- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как «разобрано целиком» - каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка собрана из всего, от чего зависит ответ: версии витрины (`data_version` с закреплённого соединения плюс поколение — значение локально для соединения и не переживает переоткрытия), горизонта измерения и области действия ресурса. Версия снимается до и после сборки: снятая после пометила бы устаревший снимок свежим номером - предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший значение точки в сыром буфере записи лога
57 lines
4.0 KiB
Go
57 lines
4.0 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)
|
|
}
|
|
|
|
// ErrClosed — хранилище закрыто, и заводить новые соединения к нему поздно.
|
|
//
|
|
// Отдельная ошибка, а не общая: запрос версии витрины и остановка сервиса идут
|
|
// в разных горутинах, и запрос, успевший в это окно, обязан получить отказ, а
|
|
// не открыть базу заново. Соединение, открытое после закрытия пула, оставило бы
|
|
// последним себя — SQLite не сделал бы финальный чекпойнт, и рядом с базой
|
|
// остался бы неразобранный `-wal`.
|
|
var ErrClosed = errors.New("хранилище закрыто")
|