# Что безопасно повторять

HTTP-статус и слово `retry` сами по себе не отвечают на главный вопрос: не
создаст ли повтор записи дубль. Для решения нужны две отдельные характеристики —
успел ли целевой вызов метода уйти в Битрикс24 и является исходный запрос чтением
или записью.

`Retry-After` указывает, когда допустима следующая попытка с точки зрения нагрузки.
Он не доказывает, что предыдущая запись не применилась.

## Термины

### Целевой вызов

Целевой вызов — бизнес-метод Битрикс24, который вы хотели выполнить, например
`crm.deal.get` или `crm.deal.add`. Служебное обновление OAuth-токена целевым
вызовом не считается.

Для `BH_APP_TIMEOUT` запрос мог дойти до приложения, а мог и не дойти: шлюз знает
только то, что отдал кадр запроса в туннель, а не то, что приложение его приняло —
агент мог не суметь подключиться к порту приложения. Шлюз лишь перестал ждать ответ
по собственному таймауту, а не потому что приложение отказало. Поэтому исход
бизнес-операции по одному коду неизвестен: если запрос дошёл, приложение могло
успеть вызвать Битрикс24 до и после того, как шлюз прекратил ждать.

### Чтение и запись

Чтение не меняет данные: его повтор обычно безопасен с точки зрения дублей.
Запись создаёт, изменяет, удаляет или отправляет данные, поэтому для неё нужен
отдельный вывод. Определяйте тип операции по смыслу метода, а не по HTTP-глаголу:
POST может выполнять поиск, а методы `*.add`, `*.update`, `*.delete` и `*.send`
меняют состояние.

### Неизвестный исход

«Неизвестно» означает, что по коду нельзя доказать, успел ли целевой вызов создать
эффект. Отсутствие успешного ответа не равно отсутствию эффекта.

## Правило принятия решения

- **Повтор безопасен** — дождитесь указанного условия и повторяйте с ограничением
  числа попыток.
- **Сначала перечитать** — найдите эффект операции по устойчивому бизнес-ключу или
  идентификатору. Повторяйте запись только если можете доказать, что эффекта нет.
- **Не повторять автоматически** — исправьте причину либо передайте случай человеку.
  После изменения входных данных это новая осознанная попытка, а не retry того же
  запроса.

Если надёжно проверить эффект записи невозможно, «сначала перечитать» означает
«не повторять автоматически». `X-Request-Id` нужен для диагностики и не является
ключом идемпотентности.

## Матрица кодов

| Код | Целевой вызов дошёл до Битрикс24 | Чтение | Запись | Условие и пояснение |
|---|---|---|---|---|
| `QUEUE_OVERFLOW` | Нет | Повтор безопасен | Повтор безопасен | Выдержать `Retry-After`, снизить параллелизм, использовать backoff со случайной добавкой. Очередь отказала до запуска вызова. |
| `QUEUE_TIMEOUT` | Нет | Повтор безопасен | Повтор безопасен | Выдержать `Retry-After`. Вызов удалён из очереди до начала исполнения. |
| `RATE_LIMITED` | Неизвестно | Повтор безопасен | Повтор безопасен | Выдержать `Retry-After`. Код объединяет локальные ограничения до вызова и отказ Битрикс24 по частоте без применения операции. |
| `OPERATION_TIME_LIMIT` | Неизвестно | Повтор безопасен | Сначала перечитать | Строго выдержать `Retry-After`. Первый отказ приходит от уже вызванного метода Битрикс24, последующие могут возникать локально до отправки. |
| `TIMEOUT_QUARANTINE` | Нет | Повтор безопасен | Повтор безопасен | Не повторять раньше полного `Retry-After`: досрочные запросы мешают восстановлению. Текущий вызов остановлен локально до отправки. |
| `BITRIX_TIMEOUT` | Да | Повтор безопасен | Сначала перечитать | Дождаться рекомендуемой паузы. Платформа прекратила ждать ответ, но обработка записи в Битрикс24 могла продолжиться. |
| `BITRIX_ERROR` | Да | Повтор безопасен после исправления причины | Не повторять автоматически | Исправить причину по `b24Code`, `message` и полям ошибки. Простая пауза не меняет результат; для сомнительной записи дополнительно проверить состояние. |
| `BITRIX_UNAVAILABLE` | Неизвестно | Повтор безопасен | Сначала перечитать | Использовать ограниченный экспоненциальный backoff. Код покрывает upstream 5xx и сетевые сбои; отсутствие ответа не доказывает отсутствие эффекта. |
| `POOL_EXHAUSTED` | Неизвестно | Повтор безопасен | Сначала перечитать | Выдержать `Retry-After`. Общий обработчик ошибки БД может сработать на разных этапах обработки запроса. |
| `DB_TRANSIENT` | Неизвестно | Повтор безопасен | Сначала перечитать | Выдержать `Retry-After`. Откат локальной транзакции БД не откатывает уже совершённый вызов Битрикс24. |
| `SERVICE_UNAVAILABLE` | Неизвестно | Повтор безопасен | Сначала перечитать | Выдержать `Retry-After`. Граница платформы не знает, успел ли backend завершить внешний эффект; в частности, 504 не останавливает upstream. |
| `TOKEN_REFRESH_FAILED` | Нет | Повтор безопасен после восстановления авторизации | Повтор безопасен после восстановления авторизации | Целевой метод не отправлен. Один backoff без ремонта авторизации бесполезен. |
| `ERROR_LOOP_DETECTED` | Нет | Повтор безопасен после устранения причины | Не повторять автоматически | Текущий вызов остановлен circuit breaker до отправки. Сначала исправить стабильную причину; редкие probe-вызовы могут уйти дальше. |
| `BH_APP_STARTING` | Неизвестно | Повтор безопасен | Сначала перечитать | Кадр от приложения не пришёл вовсе, либо ответил сам агент, а не приложение — оба случая говорят об отсутствии кадра от приложения, но не доказывают, что код приложения его не увидел: соединение могло оборваться уже во время начатой обработки. Подождать `Retry-After`. |
| `BH_APP_TIMEOUT` | Неизвестно | Повтор безопасен | Сначала перечитать | Шлюз прекращает ждать ответ по одному из двух окон (30 секунд без единого кадра либо начатый ответ, замолчавший дольше 15 секунд) — не потому что приложение отказало. Запрос мог быть доставлен приложению и выполниться, а мог и не дойти: шлюз знает только то, что отдал кадр в туннель. `Retry-After` в ответе нет: пауза не делает повтор безопасным. |
| `LARGE_BODY_BACKEND_BUSY` | Нет | Повтор безопасен | Повтор безопасен | Выдержать `Retry-After`. Admission-гейт отказал до route handler и целевого вызова. |

## Почему некоторые коды неоднозначны

### `RATE_LIMITED`

Доставка неизвестна, потому что один код используют и локальные ограничения, и
Битрикс24. Повтор записи после паузы всё же безопасен: известные источники либо не
отправляют вызов, либо однозначно отказывают по частоте без применения операции.
Это не означает, что любой ответ HTTP 429 всегда возникает до отправки.

### `OPERATION_TIME_LIMIT`

Первый отказ с этим кодом возвращает уже вызванный метод Битрикс24. После него
Вайбкод запоминает паузу и может отбивать следующие запросы до отправки. Поэтому
для записи применяется более консервативное действие — сначала проверить эффект.

### `TIMEOUT_QUARANTINE`

Безопасен текущий запрос, остановленный локальной паузой. Операция, чей более
ранний таймаут включил карантин, могла иметь неизвестный исход — к ней это обещание
не относится.

### `DB_TRANSIENT`

Откат транзакции в базе Вайбкод не является распределённым откатом Битрикс24. Если
внешний вызов уже завершился, локальный rollback не отменит его эффект.

### `BH_APP_STARTING`

Кадр от приложения не пришёл вовсе, либо ответил сам агент, а не приложение. Оба
случая говорят об отсутствии кадра от приложения, но не доказывают, что код
приложения его не увидел: соединение могло оборваться уже во время начатой
обработки. Поэтому запись требует сначала перечитать состояние, а не повторяется
вслепую — в отличие от `BH_APP_TIMEOUT` ниже, у этого кода есть `Retry-After`.

### `BH_APP_TIMEOUT`

Этот ответ формируется, когда шлюз перестаёт ждать ответ приложения по одному
из двух окон: 30 секунд без единого кадра, либо начатый ответ (заголовки уже
пришли) замолчал дольше 15 секунд. Ни то ни другое не означает, что запрос
гарантированно дошёл до приложения — шлюз знает только то, что отдал кадр в
туннель, а не то, что приложение его приняло. Если запрос всё же дошёл,
приложение могло успеть вызвать Битрикс24, поэтому исход операции по коду
`BH_APP_TIMEOUT` неизвестен, а `Retry-After` в ответе нет: пауза перед повтором
не делает повтор безопасным.

## Алгоритм для чтения

1. Распознайте код ответа.
2. Для ошибки авторизации, бизнес-ошибки или circuit breaker сначала устраните
   причину.
3. Для временного отказа выдержите `Retry-After`; если его нет, примените
   ограниченный экспоненциальный backoff со случайной добавкой.
4. Повторите чтение не более заданного клиентом числа раз.
5. При устойчивом отказе остановитесь и передайте поддержке время и
   `X-Request-Id`, если он был в ответе.

## Алгоритм для записи

1. Не используйте единый список retryable-кодов для чтения и записи.
2. Если ячейка «Запись» разрешает повтор, дождитесь указанного условия и
   повторяйте с ограничением числа попыток.
3. Если требуется перечитать состояние, найдите именно бизнес-эффект исходной
   операции по уникальному признаку.
4. Если эффект уже есть, считайте исходную запись успешной и не повторяйте её.
5. Если эффект достоверно отсутствует, выполните новую попытку.
6. Если проверка ненадёжна, остановите автоматический повтор и разрешите случай
   вручную.

Обычный GET без уникального критерия не доказывает отсутствие дубля. Если метод
имеет собственный серверный ключ идемпотентности, можно опираться на контракт
этого метода; эта страница сама такой гарантии не вводит.

## Пример классификатора

Классификатор принимает вид операции явно. Он не пытается вывести его из HTTP-
глагола и не реализует универсальный `readBack`: проверка эффекта зависит от
конкретной сущности.

```javascript
const VERIFY_WRITE_STATE = new Set([
  'OPERATION_TIME_LIMIT', 'BITRIX_TIMEOUT', 'BITRIX_UNAVAILABLE',
  'POOL_EXHAUSTED', 'DB_TRANSIENT', 'SERVICE_UNAVAILABLE', 'BH_APP_STARTING',
  'BH_APP_TIMEOUT',
]);

const FIX_BEFORE_RETRY = new Set([
  'BITRIX_ERROR', 'TOKEN_REFRESH_FAILED', 'ERROR_LOOP_DETECTED',
]);

const SAFE_RETRY = new Set([
  'QUEUE_OVERFLOW', 'QUEUE_TIMEOUT', 'RATE_LIMITED',
  'TIMEOUT_QUARANTINE', 'LARGE_BODY_BACKEND_BUSY',
]);

function classifyRetry(code, operationKind) {
  if (FIX_BEFORE_RETRY.has(code)) return 'fix_before_retry';
  if (operationKind === 'write' && VERIFY_WRITE_STATE.has(code)) {
    return 'state_verification_required';
  }
  if (SAFE_RETRY.has(code) || operationKind === 'read') return 'retry_with_limit';
  return 'state_verification_required';
}
```

Неизвестный классификатору код обрабатывайте консервативно: чтение можно повторить
с ограниченным backoff, запись — только после проверки состояния. Ошибка сети без
JSON-конверта платформы имеет тот же неизвестный исход для записи.

В batch решение принимается отдельно для каждого элемента по его коду и семантике
подзапроса. HTTP 200 у общего конверта не означает успех всех элементов.

## Смотрите также

- [Коды ошибок](/docs/errors)
- [Лимиты, очереди и паузы](/docs/errors/limits)
- [Битрикс24 и платформа](/docs/errors/platform)
- [Повторы и обработка ошибок в коде](/docs/errors/handling)
- [Batch](/docs/batch)
- [Среда выполнения приложения](/docs/infra/app-runtime)
