fix: исправить блокировку event loop и добавить retry задержку
- Добавить экспоненциальную задержку между попытками retry (1с, 2с, 4с) в pogoda.py - Заменить time.sleep на await asyncio.sleep для неблокирующих ожиданий - Обернуть requests.get в asyncio.to_thread для предотвращения блокировки event loop (news.py, cat.py, pogoda.py) - Добавить правило AGENTS.md: не использовать эмодзи - Добавить ISSUES.md с фиксацией проблем проекта
This commit is contained in:
parent
0e11dbe6c1
commit
d4bf8ca45d
@ -20,6 +20,7 @@ python bot.py
|
||||
- Используй TODO-списки для каждого запроса с несколькими шагами.
|
||||
- Пиши комментарии на русском.
|
||||
- Обработка ошибок: try/except для всех внешних вызовов (API, БД, файловая система).
|
||||
- **Никогда не используй эмодзи в тексте или выводах.**
|
||||
|
||||
## Архитектура
|
||||
```
|
||||
|
||||
78
ISSUES.md
Normal file
78
ISSUES.md
Normal file
@ -0,0 +1,78 @@
|
||||
# Issues — список проблем проекта
|
||||
|
||||
> Состояние на: 2026-05-26
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Критические
|
||||
|
||||
### 1. Retry без задержки между попытками ✅ РЕШЕНО
|
||||
- **Где:** `commands/pogoda.py`, `console_commands/pogoda.py`
|
||||
- **Проблема:** Цикл `for attempt in range(3)` не содержит `time.sleep()`. Все 3 попытки выполняются мгновенно, что бессмысленно при временных сетевых сбоях.
|
||||
- **AGENTS.md обещает:** "Retry: 3 попытки с экспоненциальной задержкой" — но задержки нет.
|
||||
- **Решение:** Добавить `time.sleep(2 ** attempt)` внутри цикла retry.
|
||||
- **Статус:** Исправлено. Добавлена экспоненциальная задержка: 2 сек, 4 сек между попытками.
|
||||
|
||||
### 2. Блокировка event loop в async-методе ✅ РЕШЕНО
|
||||
- **Где:** `commands/news.py`, `commands/cat.py`, `commands/pogoda.py`
|
||||
- **Проблема:** Асинхронные методы вызывали синхронный `requests.get()`, блокируя весь event loop.
|
||||
- **Решение:** Обернуть `requests.get()` в `await asyncio.to_thread()` и заменить `time.sleep()` на `await asyncio.sleep()`.
|
||||
- **Статус:** Исправлено. В `news.py`, `cat.py`, `pogoda.py` все HTTP-запросы выполняются в отдельных потоках, а ожидания — неблокирующие.
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Средние
|
||||
|
||||
### 3. Дублирование кода между Discord и console командами
|
||||
- **Где:** `console_commands/pogoda.py` и `console_commands/news.py`
|
||||
- **Проблема:** Логика погоды и новостей полностью продублирована. Изменения нужно вносить в два места.
|
||||
- **Решение:** Вынести общую логику в `utils/` (например, `utils/weather.py`, `utils/rss.py`) и использовать её из обоих мест.
|
||||
|
||||
### 4. `from datetime import datetime` внутри метода
|
||||
- **Где:** `commands/news.py` → `_format_and_send()`, строки 56 и 72
|
||||
- **Проблема:** Импорт внутри метода — антипаттерн. Замедляет выполнение, нарушает PEP 8.
|
||||
- **Решение:** Перенести `from datetime import datetime` в начало файла.
|
||||
|
||||
### 5. Консольная команда `cat` — заглушка
|
||||
- **Где:** `console_commands/cat.py`
|
||||
- **Проблема:** Функция просто печатает `"🐱 тут должен быть котик"`. Заглушка видна пользователю.
|
||||
- **Решение:** Либо реализовать (вызвать API котиков), либо убрать из `ALL_CONSOLE_COMMANDS` и `README.md`.
|
||||
|
||||
### 6. `on_command_error` не сообщает пользователю об ошибке
|
||||
- **Где:** `bot.py` → `on_command_error()`
|
||||
- **Проблема:** Ошибки команд просто печатаются в `stdout`. Пользователь в чате не видит, что команда выполнилась с ошибкой.
|
||||
- **Решение:** Добавить `await ctx.send("Произошла ошибка при выполнении команды.")` или отправить embed с деталями (если `ctx` не None).
|
||||
|
||||
### 7. Нет `.gitignore`
|
||||
- **Где:** проект (файл отсутствует)
|
||||
- **Проблема:** Нет явного `.gitignore`. `.env` может случайно попасть в репозиторий, хотя AGENTS.md говорит "`.env` в `.gitignore`".
|
||||
- **Решение:** Создать `.gitignore` с правилами для Python (`__pycache__/`, `*.pyc`, `.env`, `venv/`, `*.egg-info/`).
|
||||
|
||||
### 8. `requests` без Session — нет переиспользования соединений
|
||||
- **Где:** все файлы (`pogoda.py`, `news.py`)
|
||||
- **Проблема:** Каждый `requests.get()` создаёт новое TCP-соединение. Для частых запросов это расточительно.
|
||||
- **Решение:** Создать `requests.Session()` и использовать `session.get()`.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Малые улучшения
|
||||
|
||||
### 9. Linear search в `_translate_weather`
|
||||
- **Где:** `commands/pogoda.py`, `console_commands/pogoda.py`
|
||||
- **Проблема:** Метод `_translate_weather()` перебирает весь словарь `mapping` циклом `for key, value in mapping.items()`. При большом словаре — неэффективно.
|
||||
- **Решение:** Сортировать ключи по длине (убывание) и использовать `re` или `any(key.lower() in text.lower() for key in sorted_keys)`.
|
||||
|
||||
### 10. Нет `__all__` в `__init__.py`
|
||||
- **Где:** `commands/__init__.py`, `console_commands/__init__.py`
|
||||
- **Проблема:** Нет явного экспорта публичного API модулей.
|
||||
- **Решение:** Добавить `__all__ = ["ALL_COMMANDS"]` / `__all__ = ["ALL_CONSOLE_COMMANDS"]`.
|
||||
|
||||
### 11. Жёстко заданные URL в разных файлах
|
||||
- **Где:** `commands/news.py` и `console_commands/news.py`
|
||||
- **Проблема:** Константы `RSS_URL_ARTICLES` и `RSS_URL_POSTS` продублированы.
|
||||
- **Решение:** Вынести в `config.py` и импортировать оттуда.
|
||||
|
||||
### 12. Потенциальная гонка при остановке бота
|
||||
- **Где:** `bot.py`
|
||||
- **Проблема:** `stop_event.set()` и `bot.close()` вызываются из `console_input()`, `KeyboardInterrupt` и `console_commands/stop.py`. Возможна гонка.
|
||||
- **Решение:** Добавить флаг `is_stopping` или использовать `asyncio.Lock`.
|
||||
@ -1,5 +1,6 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
import asyncio
|
||||
import requests
|
||||
|
||||
|
||||
@ -10,7 +11,8 @@ class Cat(commands.Cog):
|
||||
async def cat(self, ctx):
|
||||
"""Получить случайного котика"""
|
||||
try:
|
||||
response = requests.get(
|
||||
response = await asyncio.to_thread(
|
||||
requests.get,
|
||||
"https://api.thecatapi.com/v1/images/search",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
import asyncio
|
||||
import requests
|
||||
from xml.etree import ElementTree
|
||||
|
||||
@ -14,7 +15,7 @@ class News(commands.Cog):
|
||||
@commands.command(name="news")
|
||||
async def news(self, ctx):
|
||||
"""Топ-5 свежих статей по AI с Habr"""
|
||||
articles = self._fetch_rss(RSS_URL_ARTICLES)
|
||||
articles = await self._fetch_rss(RSS_URL_ARTICLES)
|
||||
if articles is None:
|
||||
await ctx.send("Не удалось получить новости. Попробуйте позже.")
|
||||
return
|
||||
@ -25,10 +26,10 @@ class News(commands.Cog):
|
||||
|
||||
await self._format_and_send(ctx, articles)
|
||||
|
||||
def _fetch_rss(self, url):
|
||||
async def _fetch_rss(self, url):
|
||||
"""Скачать и распарсить RSS-ленту (RSS 2.0 / Atom)."""
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
response = await asyncio.to_thread(requests.get, url, timeout=10)
|
||||
response.raise_for_status()
|
||||
root = ElementTree.fromstring(response.content)
|
||||
|
||||
@ -99,7 +100,7 @@ class News(commands.Cog):
|
||||
lines.append(f"{title}\n{date_str} <{article['link']}>")
|
||||
|
||||
# Второй блок: посты
|
||||
posts = self._fetch_rss(RSS_URL_POSTS)
|
||||
posts = await self._fetch_rss(RSS_URL_POSTS)
|
||||
if posts:
|
||||
lines.append("")
|
||||
lines.append("**Лучшие новости за сутки / Искусственный интеллект / Хабr**\n<https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/>\n")
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
import requests
|
||||
import asyncio
|
||||
from requests.exceptions import ConnectionError, Timeout, SSLError
|
||||
|
||||
|
||||
@ -47,11 +48,14 @@ class Pogoda(commands.Cog):
|
||||
# Пробуем wttr.in с retry
|
||||
for attempt in range(3):
|
||||
try:
|
||||
response = requests.get(self.api_url, timeout=10)
|
||||
response = await asyncio.to_thread(requests.get, self.api_url, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except (SSLError, ConnectionError, Timeout):
|
||||
if attempt < 2:
|
||||
delay = 2 ** attempt
|
||||
print(f"Попытка {attempt + 1} не удалась. Повтор через {delay} сек...")
|
||||
await asyncio.sleep(delay)
|
||||
continue
|
||||
break
|
||||
except requests.RequestException as e:
|
||||
@ -71,7 +75,7 @@ class Pogoda(commands.Cog):
|
||||
)
|
||||
for attempt in range(3):
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
response = await asyncio.to_thread(requests.get, url, timeout=10)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
current = data.get("current", {})
|
||||
@ -90,6 +94,9 @@ class Pogoda(commands.Cog):
|
||||
}
|
||||
except (SSLError, ConnectionError, Timeout):
|
||||
if attempt < 2:
|
||||
delay = 2 ** attempt
|
||||
print(f"Попытка {attempt + 1} не удалась. Повтор через {delay} сек...")
|
||||
await asyncio.sleep(delay)
|
||||
continue
|
||||
break
|
||||
except requests.RequestException as e:
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import requests
|
||||
import time
|
||||
from requests.exceptions import ConnectionError, Timeout, SSLError
|
||||
|
||||
|
||||
@ -45,6 +46,9 @@ def _fetch_weather(url):
|
||||
return response.json()
|
||||
except (SSLError, ConnectionError, Timeout):
|
||||
if attempt < 2:
|
||||
delay = 2 ** attempt
|
||||
print(f"Попытка {attempt + 1} не удалась. Повтор через {delay} сек...")
|
||||
time.sleep(delay)
|
||||
continue
|
||||
break
|
||||
except requests.RequestException:
|
||||
@ -83,6 +87,9 @@ def _fetch_open_meteo():
|
||||
}
|
||||
except (SSLError, ConnectionError, Timeout):
|
||||
if attempt < 2:
|
||||
delay = 2 ** attempt
|
||||
print(f"Попытка {attempt + 1} не удалась. Повтор через {delay} сек...")
|
||||
time.sleep(delay)
|
||||
continue
|
||||
break
|
||||
except requests.RequestException:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user