Из-за временных ограничений на территории РФ наблюдаются проблемы с оплатой. Если платёж не проходит, оставьте запрос в службу поддержки.Служба поддержки работает 24/7 — мы всегда на связи по вопросам хостинга и серверов.Открыт прием заявок на аренду выделенных серверов и размещение оборудования в дата-центре.Напоминаем: рекомендуем включить резервное копирование для дополнительной защиты данных.Доступна новая линейка VPS/VDS с NVMe-дисками и увеличенной производительностью.Технические работы на части серверов завершены. Все сервисы работают в штатном режиме.
Статья9 мин чтенияПросмотры1

Каждый API уложился в тайм-аут, а заказ всё равно ждёт: проверяем общий deadline

Детерминированная модель показывает, почему локальные тайм-ауты не ограничивают всю цепочку, как общий deadline отдаёт следующему вызову только остаток времени и почему повтор расходует тот же бюджет.

Комментарии 0

Три механизма последовательно расходуют фиолетовый световой поток до жёлтой точки у последнего затвора
В этой статье

Локальный тайм-аут каждого API не ограничивает время всей операции. Если каталог, доставка и программа лояльности последовательно получают по 100 мс, цепочка теоретически может ждать 300 мс — ещё до пауз и повторов. Общий deadline задаёт один крайний момент завершения и перед каждым следующим шагом уменьшает допустимое ожидание.

В учебной модели три вызова занимают по 70 мс. Каждый укладывается в локальные 100 мс, но вместе они требуют 210 мс при бюджете ответа 160 мс. С общим deadline третий шаг получает только оставшиеся 20 мс. Отдельный сценарий показывает, что тайм-аут первой попытки, пауза и повтор тоже расходуют тот же бюджет.

Чем deadline отличается от трёх тайм-аутов

Рекомендации Microsoft по временным сбоям предлагают учитывать общий период операции: время попыток и интервалы между повторами должны укладываться в заданный предел. Там же отдельно советуют задавать тайм-аут каждому исходящему вызову до добавления повторов.

В gRPC deadline и отмена задают момент, после которого вызов считается просроченным, и могут передаваться дочерним вызовам. Это пример конкретного протокола. Для обычного HTTP API тот же принцип придётся реализовать средствами используемого клиента и фреймворка.

Условия безопасной модели

  • Среда: CPython 3.12.14, только стандартная библиотека.
  • Сеть, файлы, база данных, рабочий магазин и реальные таймеры не используются.
  • VirtualClock двигается только по заданным целым миллисекундам, поэтому результат детерминирован.
  • Локальный тайм-аут одного вызова равен 100 мс, общий deadline — 160 мс.
  • В сценарии повтора первая попытка получает 60 мс, пауза занимает 20 мс, а повтор — 50 мс.
  • Модель ничего не записывает и не удаляет; после процесса временных данных нет.

Полный воспроизводимый расчёт

from __future__ import annotations from dataclasses import dataclass @dataclass class VirtualClock: now_ms: int = 0 def spend(self, milliseconds: int) -> None: if milliseconds < 0: raise ValueError("milliseconds must be non-negative") self.now_ms += milliseconds @dataclass(frozen=True) class CallResult: name: str status: str spent_ms: int remaining_ms: int | None def call_with_local_timeout( clock: VirtualClock, *, name: str, duration_ms: int, timeout_ms: int, ) -> CallResult: spent = min(duration_ms, timeout_ms) clock.spend(spent) status = "ok" if duration_ms <= timeout_ms else "timeout" return CallResult(name, status, spent, None) def call_with_deadline( clock: VirtualClock, *, name: str, duration_ms: int, per_call_timeout_ms: int, deadline_ms: int, ) -> CallResult: if duration_ms < 0 or per_call_timeout_ms <= 0: raise ValueError("duration must be non-negative and timeout must be positive") remaining = max(0, deadline_ms - clock.now_ms) if remaining == 0: return CallResult(name, "deadline_exceeded", 0, 0) allowed = min(per_call_timeout_ms, remaining) spent = min(duration_ms, allowed) clock.spend(spent) if duration_ms <= allowed: status = "ok" elif remaining <= per_call_timeout_ms: status = "deadline_exceeded" else: status = "timeout" return CallResult( name, status, spent, max(0, deadline_ms - clock.now_ms), ) def scenario_local_only() -> list[CallResult]: clock = VirtualClock() results = [ call_with_local_timeout( clock, name=name, duration_ms=70, timeout_ms=100, ) for name in ("catalog", "delivery", "loyalty") ] assert [item.status for item in results] == ["ok", "ok", "ok"] assert clock.now_ms == 210 print(f"local_only: statuses=ok,ok,ok total_ms={clock.now_ms}") return results def scenario_shared_deadline() -> list[CallResult]: clock = VirtualClock() results = [ call_with_deadline( clock, name=name, duration_ms=70, per_call_timeout_ms=100, deadline_ms=160, ) for name in ("catalog", "delivery", "loyalty") ] assert [item.status for item in results] == [ "ok", "ok", "deadline_exceeded", ] assert [item.spent_ms for item in results] == [70, 70, 20] assert clock.now_ms == 160 print( "shared_deadline: " "catalog=ok delivery=ok loyalty=deadline_exceeded " f"loyalty_spent_ms={results[-1].spent_ms} total_ms={clock.now_ms}" ) return results def scenario_retry_consumes_budget() -> list[CallResult]: clock = VirtualClock() results = [ call_with_deadline( clock, name="supplier_attempt_1", duration_ms=80, per_call_timeout_ms=60, deadline_ms=160, ) ] assert results[-1].status == "timeout" backoff_ms = 20 remaining_before_backoff = 160 - clock.now_ms if backoff_ms > remaining_before_backoff: raise AssertionError("test setup has no room for backoff") clock.spend(backoff_ms) results.append( call_with_deadline( clock, name="supplier_attempt_2", duration_ms=50, per_call_timeout_ms=60, deadline_ms=160, ) ) results.append( call_with_deadline( clock, name="audit", duration_ms=50, per_call_timeout_ms=60, deadline_ms=160, ) ) assert [item.status for item in results] == [ "timeout", "ok", "deadline_exceeded", ] assert [item.spent_ms for item in results] == [60, 50, 30] assert clock.now_ms == 160 print( "retry_budget: " "attempt_1=timeout backoff_ms=20 attempt_2=ok " "audit=deadline_exceeded audit_spent_ms=30 total_ms=160" ) return results def main() -> None: scenario_local_only() scenario_shared_deadline() scenario_retry_consumes_budget() if __name__ == "__main__": main()

Сохраните код в новом пустом каталоге как deadline_budget_lab.py. Затем выполните команду:

python3 deadline_budget_lab.py

Два последовательных запуска завершились с кодом 0 и дали одинаковый вывод:

local_only: statuses=ok,ok,ok total_ms=210 shared_deadline: catalog=ok delivery=ok loyalty=deadline_exceeded loyalty_spent_ms=20 total_ms=160 retry_budget: attempt_1=timeout backoff_ms=20 attempt_2=ok audit=deadline_exceeded audit_spent_ms=30 total_ms=160

SHA-256 проверенного файла: e83a0ea001903e4b630a255c65947406ea93e251d61255ad4a4f2b10c7e20baf.

Что подтвердил расчёт

  • Без общего deadline три исправных вызова завершаются за 210 мс: локальных ошибок нет, но бюджет ответа 160 мс уже нарушен.
  • С deadline первые два шага занимают 140 мс, поэтому третьему остаётся 20 мс вместо локальных 100 мс.
  • Первая попытка останавливается по локальному timeout через 60 мс: до общего deadline ещё 100 мс, поэтому это не deadline_exceeded. Пауза 20 мс и повтор 50 мс расходуют тот же бюджет и оставляют следующему шагу только 30 мс. Этот последний шаг уже останавливается по общему deadline.
  • Нулевой остаток означает немедленную остановку нового вызова, а не ещё один полный локальный тайм-аут.
Три временные шкалы вызовов API с общей границей 160 миллисекунд и отдельно показанной паузой между попытками
Учебная модель, шкала в миллисекундах. Строка 1: A, B и C по 70 мс дают 210 мс при отдельных тайм-аутах 100 мс. Строка 2: общий deadline 160 мс оставляет C только 20 мс после A и B. Строка 3: первая попытка A1 расходует 60 мс и завершается по локальному timeout; Δ — пауза 20 мс, A2 — повтор 50 мс, B получает последние 30 мс. Пунктир отмечает общий deadline, а не локальный тайм-аут первой попытки.

Числа учебные и не описывают производительность конкретного магазина. Модель проверяет арифметику бюджета и ветвление клиента; она не измеряет DNS, установление TLS, пул соединений, серверную очередь или отмену уже отправленного HTTP-запроса.

Повтор должен спросить разрешение у остатка времени

Проверять только число попыток мало. Перед паузой и каждой новой попыткой клиенту нужен остаток общего времени. Если рекомендуемая пауза больше остатка, новый вызов не стоит начинать: он заранее не может уложиться в договор. Серверный Retry-After тоже не продлевает deadline автоматически; для фоновой операции можно сохранить работу и продолжить позже по отдельному контракту.

Отмена ожидания на стороне клиента не доказывает, что внешняя система прекратила обработку. Изменяющие операции всё равно требуют идемпотентности или последующей сверки состояния. Иначе быстрый отказ интерфейса может скрывать медленное, но уже принятое изменение.

Как перенести механизм в интеграцию магазина

  • Зафиксируйте общий бюджет для бизнес-операции: например, расчёт оформления заказа, обновление цены или фоновый импорт. Не смешивайте интерактивный ответ и многочасовую задачу.
  • Перед каждым внешним вызовом вычисляйте остаток по монотонным часам и передавайте минимум из локального тайм-аута и остатка.
  • Включайте в бюджет соединение, чтение ответа, паузы, повторы и собственную обработку между вызовами.
  • Определите, какой результат можно пропустить или отложить. Цена доставки и необязательный бонус имеют разные последствия.
  • Сохраняйте идентификатор операции, число попыток, израсходованное и оставшееся время без токенов и персональных данных.
  • Для изменяющих вызовов проверяйте повтор после тайм-аута и отказ внешней системы; новый deadline не заменяет идемпотентность.

Для пользовательского модуля 1С-Битрикс: Управление сайтом это проектный приём, а не обещание штатной настройки. До реализации нужно зафиксировать редакцию и версию продукта, направление обмена, место выполнения кода, товар или торговое предложение, поля цены и остатка, права технического пользователя и реальные ограничения каждого API.

Общий deadline отвечает на вопрос «сколько времени осталось у всей операции». Circuit breaker решает соседнюю задачу: когда временно перестать обращаться к зависимому сервису после серии технических отказов.

Критерий приёмки

На тестовой среде цепочка должна завершаться не позже общего deadline с учётом заданного допуска измерения. После исчерпания бюджета новые вызовы и повторы не стартуют, уже начатые ожидания получают отмену по возможностям клиента, а неоднозначные изменяющие операции сверяются по устойчивому идентификатору. Если журнал показывает только отдельные тайм-ауты и не показывает общий расход, система пока умеет считать деревья, но теряет лес.

Обсуждение 0

Делись опытом и задавай вопросы. Комментарии без ссылок появляются после проверки редактором.

Пока никто не написал. Начни обсуждение.