При использовании asyncio + PySide6 + Telethon загрузка списка чатов и тредов занимает ~30 минут — где и как искать причину этой задержки?

Делаю десктопное приложение на Python (PySide6 + Telethon + SQLite) для парсинга Telegram‑чатов. Проект в открытом доступе: <br/> github.com/Nynchezyabka/RozittaParser <br/> <br/> Честно: я не программист — проект возник из вайбкодинга: сначала был консольный скрипт, затем он разросся в GUI‑приложение. Буду очень благодарна за объяснения простым языком — я понимаю, что делает программа, но не всегда понимаю, почему возникают ошибки и как их исправить. Ситуация сейчас срочная. <br/> <br/> Столкнулась с двумя проблемами и не понимаю — это баг в моём коде или ограничение Telegram API: <br/> <br/> **Проблема 1: список чатов загружается ~30 минут** <br/> - Использую GetDialogs через Telethon. <br/> - Тот же код в отдельном консольном скрипте работает значительно быстрее. <br/> - Подозреваю, что проблема связана с тем, как asyncio event loop взаимодействует с потоком PySide6, но не уверена. <br/> <br/> Пример из логов (скачивание обычной группы без медиа, только текст): <br/> 16:23:38 — начало (total_est=47998 сообщений) <br/> 16:24:28 — 100 сообщений <br/> 16:27:39 — 200 сообщений <br/> ... <br/> 20:07:53 — 12400 сообщений (всё ещё идёт) <br/> <br/> То есть примерно 100 сообщений в среднем за 2–5 минут при отсутствии медиа. Это нормальная скорость для Telethon в GUI‑приложении, или что‑то явно не так? Какие основные причины такой задержки и где искать проблему (event loop, потоки, или настройка Telethon)? <br/> <br/> **Проблема 2: "database is locked"** <br/> - Использую SQLite с включённым WAL‑режимом. <br/> - Сделала thread‑local соединения и настроила batch insert (по идее). <br/> - Тем не менее периодически получаю ошибку "database is locked" при одновременной работе нескольких воркеров (Parse → STT → Export). <br/> <br/> Что это может значить в моём случае и какие практические шаги стоит предпринять, чтобы избежать блокировок при параллельной записи/чтении? Буду очень признательна за простыми словами — куда копать и какие изменения пробовать.
  • 16 марта 2026 г. 18:01
Ответы на вопрос 2
Коротко — похоже, дело не в «магии Telethon», а в архитектуре приложения: в GUI‑версии у вас что‑то блокирует поток/loop (или делает очень много мелких сетевых/дисковых операций), а параллельные записи в SQLite конфликтуют. Ниже — где копать и конкретные практические шаги, простым языком.

1) Как искать причину медленной загрузки (100 сообщений за несколько минут)
- Сравните поведение «консольный скрипт» vs «GUI»:
  - Запустите GUI‑версию, но временно отключите всё, что не относится к получению сообщений (STT, экспорт, запись в БД). Если скорость станет как в консоли — виноваты другие части (IO/CPU/блокировки).
  - Запустите GUI, но отключите запись в БД (или перенаправьте её в /dev/null). Если сильно ускорится — проблема в дисковых операциях/транзакциях.
- Логируйте время между сетевыми RPC вызовами Telethon:
  - Включите logging для telethon (logging.getLogger("telethon").setLevel(logging.DEBUG)) и смотрите, сколько времени уходит на каждый запрос (get_history/get_messages/get_dialogs).
  - Добавьте свои тайм‑метки перед и после await client.get_messages(...) / client.iter_messages(...). Это покажет, где именно тормозит — сеть/Telethon или ваша обработка каждого сообщения.
- Частые виновники замедления в GUI:
  - Вызываете синхронный (блокирующий) код в основном потоке/в asyncio loop — например долгие операции записи в SQLite, STT, синхронные обработчики для каждого сообщения.
  - Неправильная интеграция asyncio + Qt: несколько event loop’ов, вызовы asyncio.run внутри уже запущенного loop, или Telethon запущен в другом потоке, но вы вызываете coroutine неправильно (частые context switches / блокировки).
  - Вы выполняете сетевые вызовы по одному для каждого сообщения (например, get_entity внутри цикла), вместо пакетной загрузки/итерирования.
  - Ограничения Telegram / flood: если Telethon выдаёт FloodWaitError — это видно в логах. Если таких ошибок нет, то телеграм не вынуждает такие задержки.

Практические исправления (предпочтительно в порядке проб):
- Не блокируйте asyncio loop длительными операциями:
  - Вся тяжелая работа (STT, экспорт, крупные записи в БД) — в отдельном потоке/процессе или в executor (loop.run_in_executor) либо в worker‑очереди.
- Интеграция Telethon + PySide6:
  - Самый простой и надёжный вариант: запускать Telethon в отдельном потоке со своим asyncio loop. Пример:
    - В потоке:
      loop = asyncio.new_event_loop()
      asyncio.set_event_loop(loop)
      client = TelegramClient(...)
      loop.run_until_complete(client.start())
      loop.run_until_complete(client.run_until_disconnected())
    - Из GUI, чтобы запустить корутину в этом loop: asyncio.run_coroutine_threadsafe(coro, client.loop)
  - Другой вариант — qasync: интегрирует asyncio loop в Qt event loop (позволяет запускать всё в одном потоке). Если вы не хотите множества потоков — qasync упрощает жизнь.
- Оптимизируйте запросы к Telegram:
  - Используйте итераторы клиент.iter_messages(..., limit=..., batch=True) или client.get_messages с большим limit, а не по одному message-> отдельный network call.
  - Собирайте сообщения пакетно и лишь потом отправляйте их на обработку/запись.
- Профилирование:
  - Временно упростите код: только загрузка сообщений и печать таймстемпов. Сколько времени идут RPC? Это покажет узкое место.

2) "database is locked" — что это значит и как лечить (простыми словами)
- Почему возникает:
  - SQLite блокирует базу, когда один connection держит write‑транзакцию, а другой пытается тоже писать. Даже при WAL, если транзакция пишущая длительная, другие писатели будут ждать и будут получать "database is locked".
  - Частая причина — множество маленьких транзакций (INSERT по одному) и длительные SELECT/закрытые курсоры, которые удерживают транзакцию.
  - Если вы используете один connection из нескольких потоков без корректной синхронизации — это тоже приводит к ошибке.
- Практические советы, что сделать:
  - Используйте отдельный «writer» — один поток (или процесс) с одним connection к SQLite, который принимает батчи на запись через очередь (queue). Работники (парсер, STT, экспорт) кладут данные в очередь — writer группирует и вставляет одним большим INSERT в одной транзакции.
  - Используйте транзакции правильно: открываем BEGIN, делаем много INSERT, COMMIT. Это быстрее и уменьшает вероятность блокировки.
  - Установите PRAGMA busy_timeout: например при создании соединения выполнить:
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA busy_timeout = 5000")  # миллисекунды
    conn.execute("PRAGMA synchronous = NORMAL")
    Это заставит sqlite ждать, а не выдавать сразу ошибку.
  - Каждый поток — своё sqlite3.Connection (thread‑local) или вообще не делить connection между потоками. Лучше: один dedicated writer, остальные используют очередь.
  - Если читающие операции держат транзакцию открытой (SELECT внутри транзакции), убедитесь, что они не делают этого слишком долго.
  - Для высокой параллельности подумайте о смене СУБД на серверную (Postgres), если приложение интенсивно пишет/читает параллельно — SQLite не про это.
  - Используйте batch insert (executemany) и подготовленные транзакции вместо множества одиночных INSERT.

Примеры архитектуры (очень простые паттерны)
- Writer queue (псевдокод):
  - В GUI/парсере:
    queue.put(batch_of_records)
  - Writer thread:
    conn = sqlite3.connect(dbfile)
    while True:
      batch = queue.get()
      conn.execute("BEGIN")
      conn.executemany("INSERT ...", batch)
      conn.commit()
- Offload блокирующих операций в executor:
  - await loop.run_in_executor(None, blocking_db_insert, data)
  - Но лучше централизовать записи через один поток.

Доп. рекомендации и чеклист для быстрой диагностики
- Включите debug‑логи Telethon и логируйте время между запросами.
- Отключите постобработку сообщений (STT/запись) и проверьте скорость загрузки — если быстро, проблема не в Telethon.
- Проверьте, не используете ли вы client.get_entity или другие сетевые вызовы внутри цикла на каждое сообщение — это замедлит всё.
- Убедитесь, что Telethon запускается в одном loop и вы не создаёте/останавливаете loop многократно.
- Если используете threadpool для STT — ограничьте число воркеров, не более cpu_count.
- Для «database is locked»: временно увеличьте busy_timeout и логируйте время выполнения транзакций; если блокировки продолжаются — реализуйте writer queue.

Если хотите, могу:
- Посмотреть конкретные места в вашем репозитории (укажите файлы/фрагменты), где вы запускаете Telethon и где делаете запись в БД, и подсказать конкретные правки.
- Привести пример полного minimal‑рефакторинга: Telethon в отдельном потоке + очередь на запись + run_coroutine_threadsafe пример.

Скажете, какие файлы/функции вызывают Telethon/запись в БД — и я укажу точные изменения.
<a href="https://github.com/eminsk/RozittaParser" rel="nofollow">https://github.com/eminsk/RozittaParser</a> <br/> все правки то что считал сделал там если какие вопросы пиши уже в моем репозитарии уже <br/> если какие будут вопросы. <br/> <br/> Резузультаты теста <br/> uv run pytest <br/> ============================= test session starts ============================= <br/> platform win32 -- Python 3.14.3, pytest-9.0.2, pluggy-1.6.0 <br/> rootdir: C:\proekts\RozittaParser <br/> configfile: pyproject.toml <br/> collected 86 items <br/> tests\test_core\test_database.py ..................                      [ 20%] <br/> tests\test_core\test_merger.py ............                              [ 34%] <br/> tests\test_core\test_utils.py ..............................             [ 69%] <br/> tests\test_features\test_export.py ...........                           [ 82%] <br/> <br/> Я разобрал код, и у тебя тут не одна причина, а несколько конкретных узких мест. <br/> Главное: это не “просто Telethon медленный”. В проекте есть реальные места, которые сами создают тормоза и database is locked. <br/> Что у тебя тормозит на самом деле <br/> - Список чатов тормозит не столько из-за get_dialogs(), сколько из-за доп. запросов после него: в features/chats/chats_api.py:183 список диалогов грузится один раз, но потом в features/chats/chats_api.py:229 для каждого канала идет GetFullChannelRequest, чтобы проверить linked group. Если каналов много, это уже пачка сетевых запросов. <br/> - Топики форума могут резко тормозить, если прямой API не срабатывает и код уходит в fallback-скан сообщений: features/chats/chats_api.py:381. Это уже не “получить список веток”, а “пройтись по сообщениям и угадать ветки”. <br/> - Текстовый парсинг у тебя явно ненормально медленный. Самая сильная причина, которую я вижу: очень агрессивное логирование. В features/parser/api.py:826 есть debug-лог практически на каждое сообщение, а в core/logger.py:124 и core/logger.py:197 файл-лог включен на DEBUG вообще для всего приложения. На больших чатах это легко убивает скорость. <br/> - В features/parser/api.py:530 ты вызываешь iter_messages(...) без wait_time. У Telethon на больших историях это часто добавляет внутренние паузы между запросами. Это не объясняет 4 часа целиком, но добавляет тормоз. <br/> - Еще один баг: по документации ты рассчитываешь на batch insert, но фактически во время основного прохода строки копятся в памяти и пишутся в БД только в самом конце: features/parser/api.py:387, features/parser/api.py:416. То есть во время “долгого парсинга текста” тормозит не SQLite, а сеть/логирование/итерация. <br/> Откуда берется database is locked <br/> Тут у тебя, похоже, смешиваются две разные блокировки. <br/> - Telethon session — это тоже SQLite. Если одновременно живы два TelegramClient на одном .session, получаешь тот же database is locked. <br/> - Архивная БД сообщений — отдельная SQLite, и она тоже может ловить lock. <br/> Самый опасный кусок у тебя тут: ui/main_window.py:1590. <br/> - Перед стартом ParseWorker окно ждет ChatsWorker и TopicsWorker только 30_000 мс. <br/> - Если загрузка чатов реально висит дольше, ты все равно потом стартуешь парсер: ui/main_window.py:1606. <br/> - Это значит, что ChatsWorker еще может держать Telethon session, а ParseWorker уже открывает новый TelegramClient. <br/> Это очень похоже на главный источник твоего периодического database is locked. <br/> То есть проблема не “SQLite плохой”, а “два клиента пересеклись по времени на одном session-файле”. <br/> Что я считаю корнем проблемы по приоритету <br/> 1. Слишком подробный DEBUG-лог в файл на больших объемах — core/logger.py:124, core/logger.py:197, features/parser/api.py:826. <br/> 2. Старт ParseWorker, пока ChatsWorker/TopicsWorker еще не закончили — ui/main_window.py:1590. <br/> 3. Дорогие дополнительные запросы для linked group после get_dialogs() — features/chats/chats_api.py:229. <br/> 4. Возможный fallback-скан топиков вместо прямого API — features/chats/chats_api.py:381. <br/> 5. Batch insert реализован не так, как ожидается: запись откладывается до конца — features/parser/api.py:416. <br/> Почему твои цифры выглядят подозрительно <br/> - 48k текстовых сообщений за почти 4 часа — это слишком медленно. <br/> - Даже если Telegram иногда душит историю, это не должно быть настолько плохо на чистом тексте. <br/> - Значит, почти наверняка тормозит не только Telegram API, а еще и твоя обвязка вокруг него. <br/> Что я бы исправлял первым делом <br/> - Временно понизить файловый лог с DEBUG до INFO и убрать per-message debug-лог. <br/> - Не запускать ParseWorker, если ChatsWorker или TopicsWorker еще живы. Не “подождать 30 секунд”, а именно не запускать вообще до полного завершения. <br/> - В iter_messages(...) явно задать wait_time=0 или маленькое значение и заново замерить. <br/> - Кэшировать linked-group данные, чтобы не делать GetFullChannelRequest для каждого канала каждый раз. <br/> - Писать batch в БД периодически по ходу парсинга, а не только в конце. <br/> - Добавить retry на lock в insert_messages_batch() и upsert_messages_batch() в core/database.py:351 и core/database.py:402.
Похожие вопросы