Обновить
8K+
1

Python, Web & Telegram Bots, Linux, C/C++

0,3
Рейтинг
Отправить сообщение

Что изменилось в task_struct между двумя ядрами? Написал diff для BTF

bpftool умеет показывать BTF-структуры, но сравнивать их между двумя ядрами уже приходится самому. Можно, конечно, открыть два дампа и искать отличия глазами. Только когда в них тысячи строк, а идентификаторы типов отличаются, занятие быстро надоедает.

Особенно если интересует одна конкретная структура и хочется понять, поменялся ли её размер, сместилось ли поле или появилось что-то новое.

Решил написать для этого небольшую утилиту - btf-layout-diff

Что она делает?

Это CLI-инструмент на C++20, который читает raw-дампы bpftool и сравнивает layout именованных STRUCT и UNION

Смотрит на размер структуры, поля, их битовые смещения и размеры bitfield. При этом не учитывает BTF type_id: идентификаторы могут отличаться между дампами, даже если сама структура не изменилась.

Получить дамп можно так:

bpftool btf dump file /sys/kernel/btf/vmlinux format raw > kernel.raw

Снимаем его на двух Linux-хостах, переносим файлы на одну машину и запускаем:

./build/btf-layout-diff kernel-old.raw kernel-new.raw --type task_struct

Для примера в репозитории есть две тестовые фикстуры. В одной task_struct занимает 24 байта, в другой - 32. Заодно добавлено поле new_field.

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

~ STRUCT task_struct  size: 24 -> 32 bytes  + new_field at 192 bits

Никакого настоящего изменения task_struct между версиями Linux здесь нет - оба дампа синтетические. Они нужны, чтобы пример можно было запустить без двух разных ядер.

А как насчёт CO-RE?

Собственно, из-за CO-RE мне и стало интересно сравнивать BTF.

Когда BPF-программа работает на одном ядре, но возникают проблемы на другом, хочется посмотреть, какие типы и смещения отличаются. CO-RE как раз использует BTF для адаптации обращений к структурам, но это не значит, что любые изменения layout автоматически ломают программу.

И наоборот: одинаковый layout ещё не гарантирует, что программа успешно загрузится.

Поэтому btf-layout-diff не проверяет совместимость CO-RE. Он не анализирует релокации из ELF, не отслеживает рекурсивно типы полей и ничего не знает о решениях verifier.

Задача гораздо проще: показать разницу между описаниями структур, чтобы не искать её вручную.

Что проверил?

Я добавил тесты на изменение размеров структур, смещений полей и bitfield. Отдельно проверил случай, когда меняются только type_id: ложных отличий в отчёте быть не должно.

Есть проверки неполной записи структуры и неоднозначных имён.

Локально собирал проект на macOS. В GitHub Actions сборка и тесты проходят на Linux и macOS.

Запустить можно из репозитория: sairex53/btf-layout-diff.

Пока следующий шаг — сравнить BTF двух реальных ядер, взять BPF-программу, которая обращается к изменившейся структуре, и посмотреть, как с этим работают её CO-RE-релокации.

Сейчас инструмент решает только первую часть задачи. Но для неё мне и хотелось иметь отдельный diff, а не разбирать два больших вывода bpftool вручную.

Теги:
+1
Комментарии0

Пять уровней форматирования - и aiogram перестаёт отвечать

Наткнулся на issue #1925 в aiogram: после получения сообщения с rich_message бот перестал отвечать. Внутри JSON была цепочка форматирования:

spoiler → subscript → bold → italic → underline

На aiogram 3.31.0 вызов Update.model_validate(...) загружал одно ядро на 100% и блокировал event loop. Автор issue пишет, что бот простоял так около часа.

Захотелось разобраться, как несколько уровней форматирования вообще могли положить обработку обновлений.

Что происходило при валидации

За вложенный текст отвечает RichTextUnion - рекурсивный union из строк, списков и 26 моделей форматирования. У каждой модели есть поле text, которое снова может содержать RichTextUnion.

RichTextUnion = (    str    | list["RichTextUnion"]    | RichTextBold    | RichTextItalic    # ... ещё 24 модели
)

Поле type у моделей было, но для выбора ветки union оно не использовалось. Поэтому Pydantic пробовал разные варианты. При проверке неподходящей модели он мог снова перейти к вложенному text, а там - очередной union с теми же вариантами.

То есть проблема не в огромном размере JSON. Валидатор многократно проходил по вложенной структуре, и с каждым новым уровнем количество проверок быстро росло.

Что исправили

В PR #1934 добавили callable discriminator. Он выбирает ветку сразу: строку по "str", список по "list", модель - по её полю type.

Обычного Field(discriminator="type") здесь недостаточно: строки и списки поля type не имеют.

По замерам автора PR, валидация проблемного обновления сократилась с 5,4 секунды до 0,3 мс. Исправление влили 11 октября.

Проверил отдельно от aiogram

Собрал небольшой стенд: 26 упрощённых рекурсивных моделей, два варианта union — обычный и с дискриминатором.

На Python 3.14.8 и Pydantic 2.12.5 для глубины 4 получил:

  • Обычный union - 195,061 мс

  • С дискриминатором - 0,004 мс

Разница почти в 49 тысяч раз. Для варианта с дискриминатором проверил и глубину 100: 0,073 мс. Медленный вариант ограничил глубиной 4 - дальше выполнение становилось слишком долгим.

Бенчмарк использует прогрев и медиану пяти запусков. Есть четыре теста на вложенность и выбор моделей; в CI они запускаются на Python 3.10–3.13.

git clone https://github.com/sairex53/aiogram-rich-text-validation-lab
cd aiogram-rich-text-validation-lab
python -m pip install -r requirements.txt
python -m unittest -v
python benchmark.py

Это не воспроизведение исходного update: стенд не импортирует aiogram и не использует JSON из issue. Я сравнивал именно поведение Pydantic на похожей структуре типов.

Похожую проблему с RichBlockUnion в aiogram уже исправляли в PR #1845. А рекурсивный RichTextUnion тогда остался без дискриминатора.

Теги:
0
Комментарии0

Оказывается у callback_data надо считать байты, а не символы

В Telegram ограничение для callback_data - 1–64 байта. В Python len() считает символы. Для ASCII разницы нет, но для emoji и других многобайтных символов она быстро становится заметной:

prefix = "🐍" * 10
len(prefix)                 # 10
len(prefix.encode("utf-8")) # 40

В aiogram-tool 1.2.3 я нашёл расчёт длины служебного идентификатора для LongCallbackData. Он вычитал len(prefix) из лимита 64, то есть при emoji оставлял себе лишнее место. Дальше библиотека генерировала ID, который уже вместе с префиксами не помещался в лимит aiogram.

Минимальное воспроизведение — класс с prefix="🐍" * 10. В регрессионном тесте _UniqueIDCallbackData.build(Data).pack() на исходном коде падает с ValueError: Resulted callback data is too long. То есть длинный payload библиотека умеет убирать в storage, но даже короткая ссылка на него может не поместиться, если пользовательский prefix не ASCII.

Исправление — считать UTF-8 байты для каждого поля, участвующего в расчёте:

separators_len = len(cls.__separator__.encode("utf-8")) * len(cls.model_fields)
unique_id_len = (    64    - len(UNIQUE_PREFIX.encode("utf-8"))    - len(callback_data.__prefix__.encode("utf-8"))    - separators_len
) // 2

Проверка не ограничивается сравнением длины: тест строит длинный callback, проверяет, что результат укладывается в 64 байта, а затем прогоняет его через LongCallbackQueryFilter и проверяет восстановленные данные.

На исходной версии новый тест падает на сборке служебного ID. После патча два теста файла test_unique_id.py проходят (2 passed). Тест, патч и инструкция по воспроизведению лежат в репозитории: тест, патч, инструкция. PR в upstream пока не открывал.

Проверял на Linux, Python 3.12, aiogram 3.29.1. Это unit-тесты упаковки и распаковки; реальный callback в Telegram не отправлял.

Теги:
+3
Комментарии0

Ошибка в on_error может остановить очередь сообщений бота

В очереди исходящих сообщений ошибка Telegram API - обычная ситуация: пользователь заблокировал бота, запрос оборвался, Telegram вернул временную ошибку. А что будет, если обработчик этой ошибки тоже выбросит исключение?

Я проверил этот сценарий в aiogram-dialog-yaml.

В очередь кладутся два сообщения. Первое падает при отправке. MessageQueue вызывает on_error, а тот, например, пишет в базу и сам падает из-за недоступной базы. Исключение выходит из worker-корутины. Второе сообщение остаётся в очереди, а queue.join() ждёт его бесконечно: рабочего, который мог бы его забрать, уже нет.

Это воспроизводится без токена и запросов к Telegram. Тест подменяет бота объектом, у которого отправка первого сообщения выбрасывает RuntimeError, и проверяет оба варианта on_error: обычную и async-функцию. На исходном коммите оба теста завершились TimeoutError на queue.join().

Исправление небольшое: ошибку самого callback логируем и продолжаем цикл очереди

try:    result = self._on_error(task, exc)    if inspect.isawaitable(result):        await result
except Exception:    log.exception(        "on_error callback failed for %s(chat_id=%s)",        task.method,        task.chat_id,    )

Здесь важно ловить Exception, а не BaseException: отмена worker-задачи через asyncio.CancelledError должна по-прежнему доходить до asyncio. И это не retry: неудачное сообщение не отправляется повторно. Изменение только не даёт сбою в обработчике ошибок уронить единственного worker’а и оставить остальные сообщения без обработки.

После патча регрессионный тест проходит, полный набор тестов проекта на Python 3.12 — 21 passed. Тест, патч и инструкция по воспроизведению лежат в репозитории: тест, патч, инструкция. Pull request автору проекта пока не отправлял.

Проверить можно на том же коммите: добавить test_error_handler_failure.py в tests/, применить queue.patch и запустить PYTHONPATH=src python -m pytest -q.

Это локальная проверка очереди с fake bot, не интеграционный тест Telegram API. Для production retry, backoff и dead-letter очередь всё равно надо проектировать отдельно.

Теги:
0
Комментарии0

Где у новостного бота заканчивается дубль и начинается новая информация?

Хочу обсудить такую задачу: Python-бот собирает посты из 30 Telegram-каналов и делает дайджест. Повторов много, но пропустить уточнение хуже, чем оставить лишний абзац. Исходники и ссылки хранятся отдельно.

Синтетический пример:

  • А: «Сервис N выпустит версию 5.0 14 октября. Бесплатный лимит — 200 запросов».

  • Б: тот же анонс другими словами и без цифры про лимит.

  • В: «Выход версии 5.0 перенесли на 21 октября».

  • Г: автор поста А исправил 200 на 20, не создавая новую публикацию.

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

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

Для эксперимента задам ограничения: до 2000 постов в сутки, VPS с 2 vCPU и 4 ГБ RAM, не больше 20 обращений и 200 тысяч входных токенов внешней LLM в день. Можно обрабатывать пачками. В дайджесте у каждого факта должна остаться ссылка на конкретный пост и версия текста, из которой он взят.

Рассматриваю два варианта: дешёвая группировка по тексту с последующей проверкой изменений через LLM или извлечение фактов из всей пачки до удаления повторов. У первого риск потерять важное ещё на предварительном фильтре. У второго — потратить бюджет и получить ошибку уже при извлечении.

Как бы вы построили этот конвейер и небольшой проверочный набор? Особенно интересны случаи, на которых похожесть текстов обманывает. Какую ошибку допустили бы осознанно и чем измеряли бы, что новая версия бота стала лучше?

Теги:
-1
Комментарии0

В какой момент Telegram-бот имеет право сказать «задача отменена»?

Хочу разобрать мысленный эксперимент с Python + aiogram, PostgreSQL и несколькими воркерами. Бот собирает PDF. Пользователь может отменить работу и сразу запустить новую. Генерация долгая, сам файл отправляется через Telegram.

Неприятная гонка:

  1. Воркер закончил PDF, проверил в базе: задача активна, версия диалога совпадает.

  2. Начал отправлять документ. Сетевой запрос завис.

  3. Пользователь нажал «Отменить». Бот записал отмену и ответил «Отменено».

  4. Старый запрос завершился, PDF появился в чате уже после этого ответа.

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

Я вижу два разных обещания пользователю. Первое: после подтверждения отмены новые действия по этой задаче не запускаются, но уже начатая отправка ещё может завершиться. Второе: после подтверждения в чате вообще не появится старый результат.

Первое проще честно описать. Для второго пока не понимаю, где провести границу между состоянием в базе и доставкой сообщения. Удаление уже пришедшего документа тоже не убирает момент, когда пользователь его увидел.

Какой контракт вы бы выбрали для такого бота и как реализовали бы его при нескольких воркерах? Интересует конкретный порядок действий при отмене, отправке и перезапуске процесса. И тест, который воспроизводит именно эту гонку, а не просто проверяет флаг cancelled.

Теги:
-1
Комментарии0

Информация

В рейтинге
2 485-й
Откуда
Владивосток, Приморский край, Россия
Зарегистрирован
Активность

Специализация

Фулстек разработчик, Веб-разработчик
Стажёр
От 30 000 ₽
Python
Linux