Skip to content

feat(telegram): calibrate the gate's history category from production logs (#1418) - #1441

Merged
axisrow merged 3 commits into
mainfrom
ao/tg_content_factory-92/1418-calibrate-history-gate
Sep 24, 2026
Merged

axisrow merged 3 commits into
mainfrom
ao/tg_content_factory-92/1418-calibrate-history-gate

Conversation

@axisrow

@axisrow axisrow commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Что

Калибровка Phase 2 (эпик #1331) по прод-логам: пул передаёт гейту
category_limits={"history": 24/30s} вместо дефолта пакета 0.1.0 (600/мин),
который никогда не срабатывал. Остальные категории — данных в логах нет,
остаются без изменений с пометкой «требует калибровки».

Данные (data/app.log, 2026-06-12 … 2026-09-01)

Полный разбор с методикой: tcf-1418-calib-report.md — приложен первым комментарием PR (в репо не хранится).

Сигнал Число
flood-wait строк / спарено с операцией 2734 / 2666 (97.5 %)
stream_messages (категория history) 209 флудов, медиана 14 с
тяжёлые ≥120 с 68 (все неспаренные), max 53123 с — бан 26.08
пик коллектора 92–115 сборов/мин на аккаунт — в 5× ниже 600/мин
нагрузка перед флудом 55–119 сборов/мин
warm_dialog_cache (dialogs 1/60) 2391 (июнь) → 55 (август) — Phase 1 работает
send / admin_action / channel_lifecycle флудов 0 — данных для калибровки нет

Аудит AC «warm_dialog_cache под гейтом на всех путях»: все 10 call-сайтов
src/ идут через единственный transport-метод backends.py:402; составные
теги (collect_channel_warm_dialog_cache, …) покрываются суффиксным правилом
endswith — регресс класса #1336 закрыт в пакете.

Калибровочная таблица

Категория Было (прод) Стало Обоснование
history 600/60 24/30 эмпирическая граница (30 req/~30 c, 31-й → FLOOD_WAIT_3) минус 20 % маржа; лог: 209 флудов при пиках 115/мин << 600/мин
dialogs 1/60 — #1330, подтверждено динамикой
dialog_sweep 12/60 — #1359
send 30/60 (+per-peer) — 0 флудов, требует калибровки
admin_action 10/60 — 0 флудов, требует калибровки
channel_lifecycle 3/300 — 0 флудов, требует калибровки
default 1000/60 — 3 единичных флуда, требует калибровки

Координация с пакетом

В локальной копии telethon-floodgate уже есть uncommitted HISTORY_SPEC = 24/30 (не выпущено). Прод с PyPI 0.1.0 живёт с 600/60 — оверрайд в доноре
закрывает прод сразу; после релиза 0.1.1 оверрайд снимается (записано в
комментарии константы).

Пропускная способность (AC)

Живой замер невозможен (без живых Telegram-вызовов) — оценка по логу:
медианные активные минуты (26–50/мин) ниже 48/мин — без изменений; пики
деферятся, каналы не теряются (инкрементальный min_id добирает следующим
проходом); дефер warm-prefetch сбор канала не останавливает (collection.py:491).

Тесты

Part of #1331
Closes #1418

🤖 Generated with Claude Code

axisrow and others added 2 commits September 24, 2026 15:35
… logs (#1418)

The released telethon-floodgate 0.1.0 default (600/min) never bound:
the production app.log shows 209 FLOOD_WAITs on messages.getHistory with
collector peaks of 115 channel fetches per minute, far below that guard.
Pass the empirically measured boundary (30 req/~30s, the 31st returned
FLOOD_WAIT_3, minus a 20% margin) as a category_limits override when the
pool constructs its gate. Categories with no flood signal in the logs
(send, admin_action, channel_lifecycle, default) keep the package
defaults and their needs-calibration note.

Regression tests: the pool ships the calibrated spec, and a peak burst
defers the 25th history call inside the 30s window (fake clock).

Part of #1331
Closes #1418

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
@axisrow

axisrow commented Sep 24, 2026

Copy link
Copy Markdown
Owner Author

#1418: калибровка rate_limit_gate по прод-логам — отчёт (Phase 2 эпика #1331)

Дата: 2026-09-24. Источник данных: data/app.log (основной чекаут,
10.4 MB, 2026-06-12 … 2026-09-01). Живой Telegram не использовался, код
пакета не менялся. Метод: read-only разбор лога + калибровка донора конфигом
пакета (сумма работ #1432/#1434 вынесла стек в PyPI telethon-floodgate).

1. Методика

Скрипт /tmp/tcf1418_calib.py + /tmp/tcf1418_deep.py (одноразовые, текст
алгоритма ниже):

  1. Строки Flood wait for <phone>: N seconds (reactive, pool_flood) — 2734 шт.
  2. Строки <op>: transient Flood wait Ns until ... UTC for <phone>
    (proactive, с тегом операции).
  3. Матчинг пар по (минута, телефон) ±1 минута: 2666 из 2734 спарены (97.5 %).
  4. Нагрузка коллектора: строки Collecting channel … account=<phone> →
    частота на (аккаунт, минута); контекст флуда — число Collecting-строк
    за 60 с до него.

2. Что показал лог

2.1 Распределение по операциям (2666 спаренных)

Операция Флудов Доля Категория gate
telegram_warm_dialog_cache 2446 91.7 % dialogs
telegram_stream_messages 209 7.8 % history
telegram_stream_dialogs 8 0.3 % dialog_sweep
telegram_invoke_request 3 0.1 % default
send / admin_action / channel_lifecycle 0 — —

2.2 Длительности

  • Спаренные (n=2666): медиана 29 с, p90 29 с, p99 30 с, max 30 с — это
    транс-пейсер Telethon при пагинации (27–30 с), «мягкое» замедление.
  • stream_messages (n=209): медиана 14 с, p90 25 с, max 27 с.
  • Все тяжёлые флуды (≥120 с) — в 68 неспаренных: медиана 120 с, p90 600 с,
    max 53123 с (бан 14.8 ч, +66...2247, 2026-08-26 21:00). По месяцам:
    2026-06 — 4, 2026-08 — 30, 2026-09 — 33. Значительная часть на
    тест-телефоне +70...0001 (600 с, серийные) — тестовый шум в прод-логе;
    реальные тяжёлые: 28.06 (+86...9509, 782/691 с), 26.08 (бан 53123 с).

2.3 Динамика (Phase 1 работает)

Операция июнь август
warm_dialog_cache 2391 55 (падение в 43×)
stream_messages 95 114 — но 40+74 из них 26–27.08, т.е. день бана и день после

Аккаунты: +66...2247 — 2482 (93 %), далее 75/64/45. stream_messages
кластеризуется слабо: 156 окон × 1 флуд, 25 × 2, 1 × 3.

2.4 Нагрузка коллектора (Collecting-строк на аккаунт-минуту)

Аккаунт минут медиана p95 max минут >48/мин
+66...2629 144 44 97 104 66
+66...2531 143 50 101 115 73
+86...9509 105 40 74 92 45
+66...2247 60 26 101 103 32

Перед stream_messages-флудом коллектор делал 55–119 сборов/мин (мода 59).
Вывод: пиковая реальная нагрузка 92–115/мин; старый guard history 600/мин
никогда не срабатывал
— медианные активные минуты 26–50/мин, пики в 5–8 раз
ниже лимита. 209 флудов — плата именно за это: гейта фактически не было.

2.5 Спецвопрос issue №4 (history 600/мин) — ответ

Лимит не «слишком высок», он не срабатывал вовсе: максимум наблюдаемой
нагрузки (115/мин) на 5× ниже 600/мин. Действующая граница Telegram
измерена прямо (калибровочный прогон пакета, см. §4): 30 запросов/~30 с,
31-й → FLOOD_WAIT_3. Лог-данные (медиана флуда 14 с при 95–119 сборах/мин)
с этой границей согласуются.

2.6 Спецвопрос issue №5 (warm_dialog_cache под гейтом?) — ответ

Все вызовы warm_dialog_cache в src/ идут через один transport-метод
TelegramTransportSession.warm_dialog_cache() (backends.py:402), который
сам резервирует слот гейта с тегом telegram_warm_dialog_cache. Call-сайтов
после декомпозиции #1046 — 10 (pool_dialogs ×5, collector_mixins ×3,
telegram_search, CLI ×2), каждый передаёт составной тег
(collect_channel_warm_dialog_cache, …), который матчится суффиксным
правилом endswith("_warm_dialog_cache") — регресс класса #1336 закрыт
в пакете тестом. Т.е. непокрытых путей нет; падение 2391→55 — эффект
связки dialogs 1/60 + персистентный dialog_cache-skip, а не утечки мимо
гейта. 85 % флудов июня — доисторическая эпоха до Phase 1 (#1330).

3. Калибровочная таблица

Прод-пакет = PyPI telethon-floodgate 0.1.0 (pin >=0.1.0,<0.2).

Категория Было (прод 0.1.0) Стало Обоснование
history 600/60 с — догадка, никогда не связывал 24/30 с Эмпирическая граница Telegram (30 req/30 с, 31-й → FLOOD_WAIT_3) минус 20 % маржа; лог: 209 флудов при пиковой нагрузке 115/мин << 600/мин
dialogs 1/60 с без изменений #1330; лог подтверждает: 2419→55 флудов warm после введения
dialog_sweep 12/60 с без изменений #1359; 8 флудов за 3 мес — режим штатный
send 30/60 с (+per-peer 1/1.0 с, 20/60) без изменений, требует калибровки 0 флудов — данных нет; per-peer-правки (1/1.1, 16/60) уже в локальном пакете по live-замеру
admin_action 10/60 с без изменений, требует калибровки 0 флудов — данных нет
channel_lifecycle 3/300 с без изменений, требует калибровки 0 флудов — данных нет
default 1000/60 с без изменений, требует калибровки 3 единичных флуда invoke_request за 3 мес — против catch-all-страховки данных нет

Дифф (минимальный): client_pool.py передаёт гейту
category_limits={"history": RateLimitSpec(24, 30.0)} — константа
HISTORY_CALIBRATED_SPEC + регресс-тесты (tests/test_rate_limit_gate.py):
пул применяет спеку; 25-й вызов в 30-секундном окне деферится до окна
(фейковые часы _Clock). Остальные категории — дефолты пакета, пометка
«требует калибровки» живёт в комментарии у конструирования гейта и в этой
таблице.

4. Координация с пакетом (важно)

В локальной копии ~/Projects/telethon-floodgate (main, uncommitted) уже
лежит ровно та же правка HISTORY_SPEC = 24/30 (комментарий «empirical
boundary») — на PyPI она не выпущена, поэтому прод с 0.1.0 живёт с
600/60. Донорский оверрайд закрывает прод уже сейчас; после релиза пакета
с этим дефолтом оверрайд можно снять (значения совпадут) — это записано в
комментарии константы.

5. Пропускная способность сбора (критерий AC)

Живой замер до/после невозможен (запрещены живые вызовы) — оценка по логу:

  • Медианные активные минуты (26–50 сборов/мин) — ниже 48/мин (24/30 с),
    дефера нет вообще, поведение не меняется.
  • Пики (p95 74–101, max 115) — дефер; канал откладывается, а не теряется:
    сбор инкрементальный (min_id), следующий проход добирает. Дефер warm-
    prefetch (collection.py:491) вообще не останавливает сбор канала.
  • Вместо пиков с флудами (каждый флуд = пауза 14–29 с + риск эскалации до
    120 с/бана) пики растягиваются гейтом до ~48 вызовов/30 с. Полных потерь
    пропускной способности нет; платформа-цена — задержка добора пиковых
    каналов до следующего прохода.

6. Связка с #1419 (рестарты)

Замер #1419 дополнительно показывает: 44 % стартов ловят флуд в первые 60 с
(~19× к базовой), рестарты 0.38/день кластеризуются, breaker в проде ни разу
не открывался (деплой после бан-инцидента). Данный PR калибровку лимитов
делает строже, что снижает и послемстартовый залп (gate-бюджеты после
рестарта пустые), но саму персистентность breaker/gate-состояний не решает —
это отдельное согласование (§8 отчёта #1419).

7. Проверки

  • ruff check src/telegram/client_pool.py tests/test_rate_limit_gate.py — чисто.
  • tests/test_rate_limit_gate.py — 13 passed (11 старых + 2 новых).
  • Полный сюит: параллельная (-m "not aiosqlite_serial" -n auto) и
    serial (-m aiosqlite_serial -n auto --dist=loadfile) части — см. PR.

8. Риски / follow-ups

  • «Пропускная способность не деградирует» подтверждена оценкой по логу, не
    живым замером — если понадобится живая проверка, делать по протоколу
    калибровщика пакета (scripts/calibrate_send_limits.py), burnable-аккаунт.
  • Релиз telethon-floodgate 0.1.1 с локальными правками владельца → снять
    донорский оверрайд.
  • Персистентность breaker-состояний (investigation: теряется ли проактивная защита от флуда при рестарте воркера — замер до фикса #1419 §8) — открытый вектор бан-масштаба
    при рестарт-циклах.

@axisrow axisrow left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Проверил дифф против origin/main (2 коммита: калибровка + перенос отчёта из репо). Что сверено: (1) семантика category_limits в telethon-floodgate 0.1.0 — specs.update мержит частичный словарь с дефолтами, переопределена только history, send (30/60) и остальные остаются пакетными (сверено с исходником установленного 0.1.0); (2) пин telethon-floodgate>=0.1.0,<0.2 совместим — RateLimitSpec и category_limits есть уже в 0.1.0, форма spec идентична (jitter_sec=0.0 у обоих), дрейфа нет; (3) единственная прод-точка конструирования гейта — client_pool.py:199, обходных путей нет; (4) tests/test_rate_limit_gate.py — 13 passed на фейковых часах, детерминированно. ВЕРДИКТ: ГОТОВО К МЕРЖУ (approve), блокеров нет. Одна содержательная находка вне диффа, не блокирующая (см. инлайн): калибровка впервые делает путь дефера живым в проде, но главный путь сбора не обрабатывает TelegramRateLimitedError — рекомендую ветку reschedule по образцу UsernameResolveRateLimitedError следом или в этот PR. Мелочь к описанию: «медианные активные минуты (26-50/мин) ниже 48/мин» — верх границы 50 кэп 48/мин превышает, деферы будут и в рутинной работе.

Comment thread src/telegram/client_pool.py
#1441)

With history calibrated to 24/30s (#1418) the gate legitimately binds on
peak collector minutes (median 50 fetches/min vs the 48/min cap), so
TelegramRateLimitedError now reaches the queue's main collection path.
Handle it like the neighbouring UsernameResolveRateLimitedError branch:
reschedule with run_after = now + retry_after + buffer and a pending
note, instead of FAILED + logger.exception on every peak.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@axisrow

axisrow commented Sep 24, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 1) — round eb580d1d-9a90-4cb1-9607-d15531152dbf

Reviewed locally (/review effort high + Codex companion effort high), no bots pinged. Head reviewed: 666c23bd1ee3894b8058cfd4f145096c3816806f, base: eab9046c (main).

Verdict Reviewer Finding Location
CLEAN claude Критических проблем не найдено; reschedule-ветка корректно зеркалирует соседнюю flood-wait обработку —
CLEAN codex Ship-blocker'ов нет; оверрайд и план его снятия привязаны к #1418; ветка очереди сохраняет одну pending-задачу и один delayed requeue; фейк-часовые тесты ловят неверные лимиты и поведение окна —

Фокус-проверки цикла (все пройдены, FIX'ов нет):

  1. Спека 24/30 — соответствует эмпирической границе (30 req/~30 c − 20%); план снятия оверрайда после релиза floodgate 0.1.1 закомментирован в коде у HISTORY_CALIBRATED_SPEC и продублирован в теле PR/отчёте (chore(telegram): калибровка лимитов rate_limit_gate по прод-логам (Phase 2 #1331) #1418 остаётся трекером).
  2. Дублирование ретраев — ветка механически совпадает с resolve-rate-limited обработкой: один reschedule + один _schedule_requeue_after_delay, _retried_tasks.discard на месте; регресс-тест фиксирует len(queue._delayed_requeues) == 1 — потерь и задвоений нет.
  3. Бесконечный цикл переносов — паттерн «вечный PENDING при постоянном дефере» идентичен существующим resolve/NoActive-веткам очереди (установленный дизайн, без счётчика попыток); дефер ограничен пиковой нагрузкой (медиана ниже кэпа).
  4. Мутационная краснота — проверено мутациями прод-кода: лимит 24→25 ловится test_pool_applies_production_history_calibration, буфер 5→0 ловится test_gate_rate_limit_keeps_task_pending (обе — красные до отката).

CI зелёный на head 666c23bd. Цикл завершён без FIX-вердиктов с первого раунда.

@axisrow axisrow left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ревью нового head 666c23b (дельта с прошлого ревью: один коммит fix(queue)). Прошлая находка закрыта корректно: (1) порядок веток в isinstance-цепочке верный — UsernameResolve-ветки раньше, новая ветка до NoActiveCollectionClientsError/ConnectionError; конфликтов иерархии нет (обе resolve-ошибки — прямые RuntimeError, TelegramRateLimitedError им не родня); (2) TelegramPeerRateLimitedError (подкласс, per-peer send) поглощается новой веткой — соответствует докстрингу пакета «every existing TelegramRateLimitedError handler absorbs it unchanged», семантика «операция недоступна сейчас» сохраняется; (3) reschedule_collection_task сбрасывает error/started_at/completed_at и ставит PENDING — ассерты теста валидны и реально отловят регрессию (без ветки статус был бы FAILED + error заполнен); (4) тест зеркалит существующий resolve-тест из того же файла (нижняя граница run_after без верхней — флейка не будет), TelegramRateLimitedError(+7001, history, 17.0) + буфер 5с = порог 22с — сходится. ВЕРДИКТ: ГОТОВ К МЕРЖУ (approve). Остаток прошлой находки — не блокирует: scheduler-путь collect_all_channels по-прежнему пишет gate-дефер в stats[errors] через generic except (collection.py:380) — цикл продолжается, данных потерь нет, добирается следующим проходом; только наблюдаемость. Можно follow-up: stats[deferred] += 1 по образцу UsernameResolveRateLimitedError рядом (371-379).

Comment thread src/collection_queue.py
@axisrow
axisrow merged commit aae6268 into main Sep 24, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(telegram): калибровка лимитов rate_limit_gate по прод-логам (Phase 2 #1331)

1 participant