Система ограничений

Система ограничений предоставляет комплексный механизм управления частотой запросов, временем выполнения операций и адаптивными задержками.

Обзор

Система состоит из нескольких компонентов, работающих вместе, чтобы предотвратить превышение лимитов API и обеспечить стабильную работу.

┌─────────────────────────────────────────────┐
RestrictionManager│  (Coordinator of all types of restrictions) │
└─────┬──────────────┬──────────────┬─────────┘
      │              │              │
      ▼              ▼              ▼
┌──────────┐  ┌────────────┐  ┌─────────────┐
Rate     │  │ Operating  │  │ AdaptiveLimiter  │  │ Limiter    │  │ Delayer└──────────┘  └────────────┘  └─────────────┘

Интерфейсы и типы

ILimiter (базовый интерфейс)

interface ILimiter {
  getTitle(): string
  setConfig(config: any): Promise<void>
  setLogger(logger: LoggerInterface): void
  getLogger(): LoggerInterface
  canProceed(requestId: string, method: string, params?: any): Promise<boolean>
  waitIfNeeded(requestId: string, method: string, params?: any): Promise<number>
  updateStats(requestId: string, method: string, data: any): Promise<void>
  reset(): Promise<void>
  getStats(): Record<string, any>
}

RestrictionParams

interface RestrictionParams {
  rateLimit?: RateLimitConfig      // Настройки rate limiting
  operatingLimit?: OperatingLimitConfig  // Настройки лимита времени операций
  adaptiveConfig?: AdaptiveConfig  // Настройки адаптивной задержки
  maxRetries?: number              // Максимальное число повторов запроса (по умолчанию: 3). Кроме `batch`-запросов
  retryDelay?: number              // Базовая задержка между повторами в мс (по умолчанию: 1000)
  retryOnNetworkError?: boolean    // Повтор при `NETWORK_ERROR` / `REQUEST_TIMEOUT` (HTTP 408) (по умолчанию: true).
                                   // Установите `false` для неидемпотентных вызовов — см. «Неидемпотентные вызовы» ниже.
  hardErrorCodes?: string[]        // Доп. коды для немедленного выброса (объединяются со встроенным hard-списком).
  softErrorCodes?: string[]        // Доп. коды для возврата как AjaxResult с ошибкой (объединяются со встроенным soft-списком).
}

Компоненты системы

1. RateLimiter (ограничение частоты)

Назначение: реализует алгоритм «Leaky Bucket» для ограничения частоты запросов.

RateLimiter — конфигурация

interface RateLimitConfig {
  burstLimit: number    // Ёмкость бакета (максимальное число одновременных запросов)
  drainRate: number     // Скорость утечки (запросов в секунду)
  adaptiveEnabled: boolean  // Включить адаптивное управление
}

RateLimiter — принцип работы

  1. Токеновая система: каждый запрос потребляет один токен
  2. Автоматическое пополнение: токены пополняются со скоростью drainRate
  3. Адаптивное управление:
  • При частых ошибках лимиты автоматически снижаются на 20%
  • При стабильной работе лимиты постепенно восстанавливаются
  • Минимальные значения: drainRate = 0.5, burstLimit = 5

2. OperatingLimiter (ограничение времени операций)

Назначение: контролирует суммарное время выполнения операций в скользящем окне.

OperatingLimiter — конфигурация

interface OperatingLimitConfig {
  windowMs: number      // Период времени в миллисекундах (по умолчанию: 600000 = 10 минут)
  limitMs: number       // Максимальное время выполнения в миллисекундах (по умолчанию: 480000 = 480 секунд)
  heavyPercent: number  // Порог для уведомлений о тяжёлых запросах (%)
}

OperatingLimiter — принцип работы

  1. Скользящее окно: отслеживает время выполнения за последние 10 минут
  2. Учёт по методам: статистика ведётся отдельно для каждого метода
  3. Буфер безопасности: в расчётах используется limitMs - 5000 (буфер 5 секунд)
  4. Блокировка: при достижении лимита блокирует выполнение до сброса статистики

OperatingLimiter — особенности

  • Для batch-запросов анализирует все вложенные методы
  • Автоматическая очистка устаревших данных (> windowMs + 10 секунд)
  • Логирование тяжёлых запросов при превышении heavyPercent

3. AdaptiveDelayer (адаптивные задержки)

Назначение: динамически рассчитывает задержки на основе опыта выполнения предыдущих запросов.

AdaptiveDelayer — конфигурация

interface AdaptiveConfig {
  enabled: boolean        // Включить адаптивные задержки
  thresholdPercent: number // Порог активации (% от operating limit)
  coefficient: number     // Множитель задержки (0.01 = 1% от оставшегося времени блокировки)
  maxDelay: number        // Максимальная задержка в миллисекундах
}

AdaptiveDelayer — алгоритм расчёта задержки

Если operating текущего метода > (limitMs × thresholdPercent / 100):
  Если operating_reset_at > текущее время:
    Задержка = (operating_reset_at - текущее время) × coefficient
  Иначе:
    Задержка = 7000 мс (значение по умолчанию)
  
  Итоговая задержка = min(рассчитанная, maxDelay)

AdaptiveDelayer — особенности

  • Для batch-запросов выбирает максимальную задержку среди всех методов
  • Не блокирует выполнение, только добавляет задержку
  • Использует статистику из OperatingLimiter

4. RestrictionManager (главный координатор)

Назначение: управляет всеми типами ограничений и обработкой ошибок.

Порядок применения ограничений

  1. Проверка Operating Limit: проверяет лимит времени операций
  2. Адаптивная задержка: при необходимости применяет адаптивную задержку
  3. Rate Limit: проверяет и применяет ограничение частоты (цикл для параллельных запросов)

Обработка ошибок

// Определение типа ошибки
#isRateLimitError(error): boolean          // 503 или QUERY_LIMIT_EXCEEDED
#isOperatingLimitError(error): boolean     // 429 или OPERATION_TIME_LIMIT
#isNonRetryableClientError(error): boolean // HTTP 4xx (кроме 429 / 408)
#isNeedThrowError(error): boolean          // Критические ошибки (повтор бессмыслен)

Стратегия повторов

  1. Для ошибок лимитов: экспоненциальная задержка с учётом номера попытки
  2. Для клиентских ошибок (HTTP 4xx, кроме 429 / 408): без повторов — ответ 4xx детерминирован, поэтому повтор не изменит исход. Ошибка выходит из цикла повторов на первой попытке; далее применяется обычная классификация hard/soft (выбрасывается как AjaxError или возвращается внутри AjaxResult для soft-кодов). 429 повторяется как rate/operating limit; 408 (таймаут запроса) остаётся временным.
  3. Для прочих ошибок: базовый backoff с jitter (±10%)
  4. Критические ошибки: без повторов

Конфигурации по умолчанию

Параметры по умолчанию для обычных тарифов (standard)

{
  rateLimit: {
    burstLimit: 50,      // 50 одновременных запросов
    drainRate: 2,        // 2 запроса в секунду
    adaptiveEnabled: true
  },
  operatingLimit: {
    windowMs: 600000,    // 10 минут
    limitMs: 480000,     // 480 секунд (8 минут)
    heavyPercent: 80     // Уведомление при 80% использования
  },
  adaptiveConfig: {
    enabled: true,
    thresholdPercent: 80, // Активация при 80% лимита
    coefficient: 0.01,    // 1% от оставшегося времени блокировки
    maxDelay: 7000        // Максимум 7 секунд задержки
  },
  maxRetries: 3,
  retryDelay: 1000,
  retryOnNetworkError: true
}

Параметры для тарифа Enterprise

{
  ...standard,
  rateLimit: {
    burstLimit: 250,     // 250 одновременных запросов
    drainRate: 5,        // 5 запросов в секунду
    adaptiveEnabled: true
  }
}

Параметры для массовой обработки данных

{
  ...standard,
  rateLimit: {
    burstLimit: 30,
    drainRate: 1,
    adaptiveEnabled: true
  },
  operatingLimit: {
    windowMs: 600_000,
    limitMs: 480_000,
    heavyPercent: 50 // Более высокий порог для уведомлений
  },
  adaptiveConfig: {
    enabled: true,
    thresholdPercent: 50, // Больше порог
    coefficient: 0.015, // Больше пауза
    maxDelay: 10_000 // Макс 10 секунд
  },
  maxRetries: 5 // Больше попыток
}

Параметры реального времени

{
  ...standard,
  adaptiveConfig: {
    enabled: false, // Выключено
    thresholdPercent: 100,
    coefficient: 0.001,
    maxDelay: 480_000
  },
  maxRetries: 1
}

Мониторинг и статистика

Получение статистики

import { ApiVersion } from '@bitrix24/b24jssdk'

// const $b24 = ...

const statsV2 = $b24.getHttpClient(ApiVersion.v2).getStats()
const statsV3 = $b24.getHttpClient(ApiVersion.v3).getStats()

Структура статистики:

{
  // Общая статистика
  retries: number,                 // Число попыток повтора
  consecutiveErrors: number,       // Последовательные ошибки
  limitHits: number,               // Попадания в лимит
  
  // Rate Limiter
  tokens: number,                  // Текущее число токенов
  burstLimit: number,              // Текущий burst limit
  drainRate: number,               // Текущий drain rate
  
  // Adaptive Delayer
  adaptiveDelays: number,          // Число применённых задержек
  totalAdaptiveDelay: number,      // Суммарное время задержек
  adaptiveDelayAvg: number,        // Средняя задержка
  
  // Operating Limiter
  heavyRequestCount: number,       // Число тяжёлых запросов
  operatingStats: {                // Статистика по методам (в секундах)
    [method: string]: number
  },
  
  // Ошибки по методам
  errorCounts: {
    [method: string]: number
  }
}

Сброс статистики

// Полный сброс всей статистики лимитера
import { ApiVersion } from '@bitrix24/b24jssdk'

// const $b24 = ...

await $b24.getHttpClient(ApiVersion.v2).reset()
await $b24.getHttpClient(ApiVersion.v3).reset()

Долгие запросы и неидемпотентные вызовы

SDK поставляется с 30-секундным таймаутом axios и повторяет неудавшиеся запросы до maxRetries раз. Оба значения по умолчанию неверны для долгих, неидемпотентных REST-методов — канонический пример crm.documentgenerator.document.add, который может рендерить шаблон 10‒60 секунд и является исходным репортом из issue #24.

Почему по умолчанию это ломается

Когда сервер отвечает дольше таймаута axios, разворачивается такая последовательность:

  1. Клиент открывает запрос, сервер начинает обработку.
  2. Axios срабатывает по таймауту, SDK видит REQUEST_TIMEOUT (code: ECONNABORTED).
  3. REQUEST_TIMEOUT трактуется как временная ошибка → SDK повторяет.
  4. Сервер, не зная, что клиент сдался, завершает первый запрос и сохраняет документ. Затем принимает повтор и сохраняет ещё один.
  5. После maxRetries попыток SDK выбрасывает исходную ошибку (её реальный код), и у вызывающего остаётся 2-3 дубликата в CRM.

Тот же риск касается каждого неидемпотентного метода: crm.deal.add, crm.contact.add, disk.folder.uploadfile, tasks.task.add, любого кастомного REST- endpoint, создающего состояние.

Используйте этот паттерн для любого вызова, создающего сущность, независимо от ожидаемой длительности:

import { ApiVersion, ParamsFactory } from '@bitrix24/b24jssdk'

// const $b24 = ...

// 1. Поднимите таймаут, чтобы клиент действительно ждал медленную операцию.
const clientAxios = $b24.getHttpClient(ApiVersion.v2).ajaxClient
clientAxios.defaults.timeout = 120_000 // по умолчанию 30_000

// 2. Отключите повторы на транспортных ошибках, чтобы клиентский таймаут никогда
//    не создавал дубликаты сущностей на сервере.
await $b24.setRestrictionManagerParams({
  ...ParamsFactory.getDefault(),
  retryOnNetworkError: false
})

// Теперь безопасно для неидемпотентных вызовов.
const result = await $b24.actions.v2.call.make({
  method: 'crm.documentgenerator.document.add',
  params: { templateId: 42, entityTypeId: 2, entityId: 6014, values: {} }
})
Одного шага недостаточно. Длинный таймаут без retryOnNetworkError: false всё равно создаёт дубликаты на нестабильной сети (сервер ответил, но ответ не дошёл). Флаг без таймаута срабатывает слишком рано на запросах, которые завершились бы за 35-40 секунд.

Точечное применение — переопределение на вызов

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

import { B24Hook, ParamsFactory } from '@bitrix24/b24jssdk'
import { ApiVersion } from '@bitrix24/b24jssdk'

const $b24Strict = B24Hook.fromWebhookUrl(hookUrl, {
  restrictionParams: {
    ...ParamsFactory.getDefault(),
    retryOnNetworkError: false
  }
})
$b24Strict.getHttpClient(ApiVersion.v2).ajaxClient.defaults.timeout = 120_000

await $b24Strict.actions.v2.call.make({ method: 'crm.deal.add', params: {/*…*/} })

Крайний случай — отключить все повторы

Если вы не можете позволить ни одного повтора ни при каких обстоятельствах (например, биллинговая операция), задайте maxRetries: 1:

import { ParamsFactory } from '@bitrix24/b24jssdk'

await $b24.setRestrictionManagerParams({
  ...ParamsFactory.getDefault(),
  maxRetries: 1
})

Это затрагивает каждый класс ошибок, а не только транспортные, поэтому используйте осторожно.

Настройка классификации ошибок

SDK группирует коды ошибок REST на три категории:

  1. Hard-ошибки — выбрасываются сразу как AjaxError. Без повтора. Используются для фатальных ситуаций (сбои авторизации, неверные аргументы, удалённые порталы).
  2. Soft-ошибки — возвращаются внутри AjaxResult как payload ошибки, не выбрасываются. Используются для кодов, которые вызывающий код обычно проверяет в рамках обычного потока управления (например, ENTITY_NOT_FOUND, ошибки валидации v3).
  3. Всё остальное — возможность повтора определяется HTTP-статусом. Клиентские ошибки (HTTP 4xx, кроме 429 и 408) никогда не повторяются — они детерминированы, поэтому SDK быстро завершается на первой попытке независимо от того, перечислен ли код ошибки. Временные состояния (5xx, 429, 408, сетевые ошибки) повторяются до maxRetries раз с backoff и jitter.

Встроенные списки покрывают стандартную REST-поверхность Bitrix24. Если ваше приложение использует кастомные REST-endpoint-ы (например, из локального приложения или обработчика placement), которые возвращают свои коды ошибок со статусом не 4xx, эти коды будут повторяться по умолчанию — что неверно для неидемпотентных бизнес-ошибок.

Расширьте классификацию через hardErrorCodes и softErrorCodes:

import { ParamsFactory } from '@bitrix24/b24jssdk'

// const $b24 = ...

await $b24.setRestrictionManagerParams({
  ...ParamsFactory.getDefault(),

  // Эти будут выброшены сразу вместо повтора:
  hardErrorCodes: [
    'DOCUMENT_GENERATOR_ALREADY_IN_QUEUE', // бизнес-код: не повторять
    'MY_APP_INVALID_PAYLOAD'               // кастомный REST: нужна правка на стороне вызывающего
  ],

  // Эти будут возвращены в AjaxResult вместо выброса:
  softErrorCodes: [
    'MY_APP_VALIDATION_FAILED'             // ожидается в рамках обычного потока
  ]
})
Объединение, не замена. Коды, заданные пользователем, добавляются к встроенным спискам. Вы можете только добавлять коды — встроенные (коды авторизации, INTERNAL_SERVER_ERROR, коды валидации v3 и т.д.) остаются на месте. Это предотвращает случайное отключение критичных защит, таких как обнаружение expired_token.

Встроенные списки доступны как статические поля для справки:

import { RestrictionManager } from '@bitrix24/b24jssdk'

console.log(RestrictionManager.BUILT_IN_HARD_ERROR_CODES)
console.log(RestrictionManager.BUILT_IN_SOFT_ERROR_CODES)

Глобальная область setRestrictionManagerParams в пределах экземпляра

setRestrictionManagerParams() мутирует весь экземпляр $b24 — он перенастраивает единственный RestrictionManager, общий для каждого запроса, идущего через этот экземпляр. Это не переопределение на отдельный вызов.

На сервере это грабли, когда один экземпляр $b24 разделяется между параллельными запросами (типовой паттерн: синглтон уровня модуля, переиспользуемый многими HTTP-обработчиками). Изменение параметров для одного запроса меняет их для всех запросов в полёте на этом экземпляре.

import { ParamsFactory } from '@bitrix24/b24jssdk'

// ⚠️ АНТИ-ПАТТЕРН: общий $b24, параллельные обработчики
// Запрос A — неидемпотентное создание, которое хочет ОТКЛЮЧИТЬ повторы:
await $b24.setRestrictionManagerParams({
  ...ParamsFactory.getDefault(),
  retryOnNetworkError: false
})
await $b24.actions.v2.call.make({
  method: 'crm.deal.add',
  params: { fields: { TITLE: 'Deal A' } }
})

// Запрос B — выполняется параллельно на ТОМ ЖЕ $b24 — теперь ТОЖЕ видит
// retryOnNetworkError: false, хотя никогда об этом не просил. А если
// Запрос B первым вызовет setRestrictionManagerParams, создание из
// Запроса A незаметно выполнится с параметрами B. Побеждает последний
// записавший — глобально.

Поскольку мутация глобальна и нет изоляции на уровне запроса, два параллельных потока createDeal могут затирать друг у друга конфигурацию rate-limit / повторов.

Безопасные паттерны

  1. Выделенный экземпляр $b24 для неидемпотентных / нестандартных потоков. Постройте отдельный экземпляр (или передайте restrictionParams при конструировании) для той ветки кода, которой нужно другое поведение, чтобы она никогда не разделяла состояние с общим пулом по умолчанию:
    import { B24Hook, ParamsFactory, ApiVersion } from '@bitrix24/b24jssdk'
    
    const $b24Strict = B24Hook.fromWebhookUrl(hookUrl, {
      restrictionParams: {
        ...ParamsFactory.getDefault(),
        retryOnNetworkError: false
      }
    })
    $b24Strict.getHttpClient(ApiVersion.v2).ajaxClient.defaults.timeout = 120_000
    
    await $b24Strict.actions.v2.call.make({
      method: 'crm.deal.add',
      params: { fields: { TITLE: 'Deal A' } }
    })
    
  2. Задавайте параметры ограничений один раз при старте, а не на каждый запрос. Считайте их конфигурацией экземпляра, а не переключателем на отдельный вызов.
  3. Идемпотентность на уровне приложения. Для неидемпотентных созданий, которые должны пережить повтор независимо от конфигурации ограничений, добавьте собственную защиту идемпотентности (ключ дедупликации / уникальное бизнес-поле, проверяемое перед вставкой), чтобы дублирующий вызов был no-op, даже если повтор проскочит.

Рекомендации по использованию

Конфигурация для разных сценариев:

import { ParamsFactory } from '@bitrix24/b24jssdk'

// const $b24 = ...

// Параметры по умолчанию
$b24.setRestrictionManagerParams( ParamsFactory.getDefault() )

// Пакетная обработка
$b24.setRestrictionManagerParams( ParamsFactory.getBatchProcessing() )

// Real-time
$b24.setRestrictionManagerParams( ParamsFactory.getRealtime() )

// По тарифному плану
$b24.setRestrictionManagerParams( ParamsFactory.fromTariffPlan('enterprise') )

// Динамическое изменение конфигурации
$b24.setRestrictionManagerParams({
  ...ParamsFactory.getDefault(),
  rateLimit: {
    burstLimit: 30,  // Временное снижение при проблемах
    drainRate: 1,
    adaptiveEnabled: true
  }
})