Приём отвечает 200 до свёртки, свёртку ведёт фоновый воркер
- Очередью служит сама таблица: доставка ждёт свёртки в статусе `pending`, канал несёт только бит «есть работа». Переполнять нечего, падение процесса очередь не теряет, а подбор `pending` при старте — обычный проход воркера, а не отдельный код. Классификация исхода общая с пересборкой журнала. - Исход разбора начал отражать доставку, а не обстоятельства: отмена и занятость базы статус не меняют (иначе конкуренция за базу выводила бы доставку из очереди навсегда), паника свёртки больше не валит процесс, а учёт доставки идёт через транзакцию с повторами. - Длинный бюджет ответа выдан маршруту приёма, а не всему серверу: `write_timeout` в Go покрывает и чтение тела, и общий подъём снял бы защиту с остальных маршрутов.
This commit is contained in:
@@ -54,6 +54,13 @@ type Delivery struct {
|
||||
}
|
||||
|
||||
// CreateDelivery записывает факт приёма пакета.
|
||||
//
|
||||
// Через ту же транзакцию с повторами, что и слияние точек, и это не симметрия
|
||||
// ради симметрии. Свёртка держит запись всю доставку целиком — измерено 11
|
||||
// секунд на 16 тысячах объектов, — а с фоновым воркером конкуренция за базу
|
||||
// стала штатной. Одиночный `Exec` пересиживал бы только `busy_timeout`, после
|
||||
// чего приём ответил бы `500` по доставке, тело которой уже на диске: доставка
|
||||
// исчезла бы из журнала, а телефон её не перешлёт.
|
||||
func (s *Store) CreateDelivery(ctx context.Context, d Delivery) error {
|
||||
const q = `
|
||||
INSERT INTO delivery (id, received_at, automation_name, automation_id,
|
||||
@@ -66,10 +73,13 @@ func (s *Store) CreateDelivery(ctx context.Context, d Delivery) error {
|
||||
headers = "{}"
|
||||
}
|
||||
|
||||
_, err := s.db.ExecContext(ctx, q,
|
||||
d.ID, FormatTime(d.ReceivedAt), d.AutomationName, d.AutomationID,
|
||||
d.Aggregation, d.Period, d.SessionID, d.Bytes, d.SHA256,
|
||||
d.RawPath, d.ParseStatus, d.Points, headers)
|
||||
err := s.inTx(ctx, func(tx *sql.Tx) error {
|
||||
_, err := tx.ExecContext(ctx, q,
|
||||
d.ID, FormatTime(d.ReceivedAt), d.AutomationName, d.AutomationID,
|
||||
d.Aggregation, d.Period, d.SessionID, d.Bytes, d.SHA256,
|
||||
d.RawPath, d.ParseStatus, d.Points, headers)
|
||||
return err //nolint:wrapcheck // обёртка одна, на выходе
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert delivery: %w", err)
|
||||
}
|
||||
@@ -148,6 +158,69 @@ func (s *Store) ListDeliveries(ctx context.Context) ([]Delivery, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// PendingDelivery — доставка, ожидающая свёртки. Она же курсор обхода: место в
|
||||
// журнале задаётся парой `(received_at, id)`, и вызывающему достаточно передать
|
||||
// обратно последнюю полученную строку.
|
||||
//
|
||||
// Метка приёма отдаётся не для порядка (его держит SQL), а для метки отставания:
|
||||
// «доставка ждала свёртки дольше N» считается от неё.
|
||||
type PendingDelivery struct {
|
||||
ID string
|
||||
ReceivedAt time.Time
|
||||
}
|
||||
|
||||
// PendingDeliveries возвращает неразобранные доставки в порядке журнала,
|
||||
// строго после курсора. Нулевой курсор означает «с начала».
|
||||
//
|
||||
// Курсор нужен не ради страниц, а ради завершимости обхода: доставка, у которой
|
||||
// не удалось записать даже исход разбора, остаётся `pending`, и выборка без
|
||||
// курсора выдавала бы её бесконечно.
|
||||
func (s *Store) PendingDeliveries(ctx context.Context, after PendingDelivery, limit int) ([]PendingDelivery, error) {
|
||||
// Сравнение кортежем, а не через OR: развёрнутая форма даёт SCAN по
|
||||
// индексу вместо SEARCH (проверено EXPLAIN QUERY PLAN). Тот же приём уже
|
||||
// применён в LastDerivedLayer.
|
||||
const q = `
|
||||
SELECT id, received_at FROM delivery
|
||||
WHERE parse_status = ? AND (received_at, id) > (?, ?)
|
||||
ORDER BY received_at, id LIMIT ?`
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, q, ParsePending, FormatTime(after.ReceivedAt), after.ID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("select pending deliveries: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var out []PendingDelivery
|
||||
for rows.Next() {
|
||||
var d PendingDelivery
|
||||
var receivedAt string
|
||||
if err := rows.Scan(&d.ID, &receivedAt); err != nil {
|
||||
return nil, fmt.Errorf("scan pending delivery: %w", err)
|
||||
}
|
||||
d.ReceivedAt, err = ParseTime(receivedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, d)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("select pending deliveries: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CountPendingDeliveries возвращает размер задолженности — сколько доставок
|
||||
// ждут свёртки. Нужен ровно одной строке лога при старте: сколько сервис должен
|
||||
// разобрать, прежде чем витрина станет полной.
|
||||
func (s *Store) CountPendingDeliveries(ctx context.Context) (int64, error) {
|
||||
var n int64
|
||||
if err := s.db.GetContext(ctx, &n,
|
||||
`SELECT count(*) FROM delivery WHERE parse_status = ?`, ParsePending); err != nil {
|
||||
return 0, fmt.Errorf("count pending deliveries: %w", err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// DeliveryStatus возвращает статус разбора доставки.
|
||||
func (s *Store) DeliveryStatus(ctx context.Context, id string) (string, error) {
|
||||
var status string
|
||||
|
||||
Reference in New Issue
Block a user