# 🐞 Снизить цену слияния на широкой доставке

- **Тип:** fix
- **Категория:** Ядро
- **Зачем:** 63 МБ на одной координате держат транзакцию 5.15 с при busy_timeout 5 с — соседние доставки уходят в failed
- **Теги:** goal:limits-and-load

Вынуто ревью кода задачи `pravilo-sliyaniya-tochek` (профиль `deep`, враждебный
проход и независимая реализация — независимо друг от друга).

Двигает строку «Завершения» цели: «Занятость базы не выводит доставку из очереди».

## Оракул: измерено

Тело 63 МБ (в запросе ~200 КБ gzip — предел приёма 64 МиБ), 119 точек на ОДНОЙ
координате:

```
транзакция держалась 5.149 с, аллоцировано 6108 МБ, пик кучи 189 МБ
```

При `busy_timeout` 5000 мс и `_txlock=immediate` параллельная доставка
получает `SQLITE_BUSY`, `inTx` повторяет её до пяти раз (каждый повтор —
полный пересчёт слияния), и на исчерпании попыток доставка уходит в
`parse_status=failed`. Тело при этом в архиве остаётся, но пересборки, которая
его подберёт, ещё нет.

Отдельный вклад, измеренный бенчмарком на часовом объекте нижнего слоя
(3 600 точек):

```
HashAll на объект   9.46 мс   6.7 МБ аллокаций
Form одной точки    2.2 мкс
```

`hashPoints` пересчитывает каноническую форму ВСЕХ точек часа на каждое
слияние, включая неизменившиеся. Утверждение `docs/architecture.md` о
дешевизне глубокого прохода («4400 сравнений хеша, почти все сойдутся»)
стоимости самих сравнений не учитывает: чтобы сравнить хеш, его надо посчитать.

Часть цены задачей `pravilo-sliyaniya-tochek` уже снята: `isEmpty` больше не
материализует значение (было 410 мс на точку 16.5 МБ), а разбор точки
переиспользуется через `canon.Fields`. Осталось структурное.

## Что делать

1. Держать хеш каждой точки в `payload` рядом с координатами (`storedPoint`
   уже есть) и пересчитывать форму только для новых и выигравших столкновение
   — стоимость станет пропорциональна дельте, а не объёму часа.
2. Считать `canon.Form` точки один раз на столкновение и переиспользовать в
   `Equal`, `Relate` и порядке вместо трёх независимых разборов.
3. Проверять `ctx.Err()` в цикле по точкам: сейчас дедлайн свёртки неисполним,
   прервать слияние нечем.
4. Оценить потолок числа кандидатов на одной координате: выбор победителя
   квадратичен по ним, и 119 точек на одну метку — вход, который приём
   принимает.

## Связано

- Разнесение ответа приёма и свёртки **сделано** (архив change
  `2026-08-02-otvet-i-svyortka`): воркер убрал влияние на время ответа, но не на
  блокировку записи — длинная транзакция слияния держит её по-прежнему. Заодно
  оттуда взято главное смягчение: занятость базы больше не выводит доставку из
  очереди, она остаётся `pending` и пересворачивается. Оракул окна —
  `task verify:busy`.
- Пересборка (`healthlog reindex`) подбирает доставки, ушедшие в `failed` по
  другим причинам.
