Разбираем баг в smolagents: выражение 10**10**8 вешает процесс из-за GIL. Узнайте, как мы нашли и исправили проблему с помощью оценки битовой длины.
Представьте: вы запускаете агента на smolagents, он вычисляет 10 ** 10 ** 8 — и процесс зависает навсегда. Ни ошибки, ни таймаута, ни события в Sentry. Просто тишина. Это реальный баг из issue #2473, который мы нашли и исправили. В этой статье я покажу, почему таймаут не сработал, как мы это починили и какой урок извлекли.
smolagents использует поточный таймаут: рабочий поток выполняет код, главный поток ждёт в future.result(timeout=2). Обычно это работает, потому что CPython переключает потоки между инструкциями байт-кода. Но 10 ** 10 ** 8 — не обычное выражение. CPython вычисляет большие целые числа (**, <<, *) одним C-вызовом, который удерживает GIL от начала до конца. Нет границы байт-кода — нет переключения потоков — нет таймаута. Результат содержит около 400 миллионов бит, вычисление занимает минуты или часы.
Мы запустили код с таймаутом 2 секунды и faulthandler на 20 секунд. Таймаут не сработал. Процесс завис до внешнего убийства. Вот стек потоков:
Thread 0x16d1f3000 (worker):
File "local_python_executor.py", line 753 in evaluate_binop # вычисление степени
Thread 0x1f00d5e80 (main):
File "threading.py", line 999 in start
File "concurrent/futures/thread.py", line 180 in submit # никогда не вернулсяГлавный поток застрял внутри ThreadPoolExecutor.submit и даже не дошёл до future.result. Таймер 2 секунды никогда не запускался. Существующая защита MAX_OPERATIONS (10 миллионов AST-операций) не помогает — это всего несколько узлов AST, вся стоимость внутри одного.
Этот баг — противоположность шумным сбоям. Мы направили Sentry на симуляцию агента с непропатченной версией (1.26.0) — и получили пустой проект. Ни событий, ни транзакций. Процесс завис, SDK не успел отправить данные. Для таких багов нужен супервизор. Мы добавили небольшой процесс-наблюдатель, который даёт рабочему потоку 25 секунд, затем убивает его и отправляет отчёт в Sentry с дампом стека:
watchdog: starting worker (before) with 25s budget
worker: smolagents 1.26.0
worker: step 1 executing 'result = 10 ** 10 ** 8'...
watchdog: worker FROZE, killed it, reporting to SentryВ результате в Sentry появился issue с полной историей: окружение before, обработанное супервизором, с замороженным фреймом evaluate_binop в прикреплённом стеке. Анализ Seer от Sentry независимо пришёл к тому же выводу: сигналы и прерывания потоков требуют GIL, а одна C-операция с большими целыми никогда его не отпускает.
Прервать вычисление из Python невозможно. Но можно оценить ущерб за O(1). Перед выполнением **, << или * над целыми числами исполнитель теперь оценивает битовую длину результата:
if op == "**":
estimated_bits = left.bit_length() * right # верхняя граница
elif op == "<<":
estimated_bits = left.bit_length() + right
elif op == "*":
estimated_bits = left.bit_length() + right.bit_length()Если оценка превышает 1 миллион бит (около 300 тысяч цифр, всё ещё щедро), выбрасывается информативный InterpreterError:
Операция '**' дала бы целое число около 400000000 бит, что превышает
максимум в 1000000 бит. Используйте меньшие операнды или
pow(base, exp, mod) для модульного возведения.Это сообщение важно: агент передаёт его модели, и модель может на него отреагировать — использовать pow(base, exp, mod), который быстрый и легитимный. Агент восстанавливается на следующем шаге вместо зависания хоста.
После патча защита отклоняет выражение за 0.0 секунд, ошибка попадает в Sentry как нормальный issue, и второй шаг выполняется успешно:
watchdog: starting worker (after) with 25s budget
worker: smolagents 1.27.0.dev0
worker: step 1 error surfaced to the model: InterpreterError: ...
worker: step 2 executing '2 + 2'...
worker: step 2 ok
worker: DONE
watchdog: worker exited with code 0Через минуту после открытия PR ревьюер OpenAI Codex нашёл реальную дыру: моя проверка type(x) is int пропускала bool и подклассы int. True << 10**9 и class BigInt(int) всё ещё доходили до C-вызовов. Исправил на isinstance, добавил регрессионные тесты. ИИ нашёл баг, ИИ исправил, ИИ проверил — я только направлял.
**, <<, цепочки *, все составные формы, pow(a, b), варианты с bool и подклассами int*=, pow(7, 2**64, 97), float pow, 1 ** 10**9Тесты на заблокированные паттерны зависают навсегда на непропатченной версии — я проверял честно, с резервным убийством.
Самые страшные баги — не те, что будят вас в 3 часа ночи, а те, что гарантируют, что вас никогда не разбудят. Инструментируйте тишину. Добавьте супервизор для зависаний, оценивайте битовую длину перед опасными операциями и используйте Sentry для мониторинга отсутствия событий. Ваш будущий «я» скажет спасибо.
Ссылки: Issue #2473, PR #2551.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →