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
eb53a5ba81
commit
bf166735a6
@ -20,6 +20,7 @@ python bot.py
|
|||||||
- Используй TODO-списки для каждого запроса с несколькими шагами.
|
- Используй TODO-списки для каждого запроса с несколькими шагами.
|
||||||
- Пиши комментарии на русском.
|
- Пиши комментарии на русском.
|
||||||
- Обработка ошибок: try/except для всех внешних вызовов (API, БД, файловая система).
|
- Обработка ошибок: 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
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
import asyncio
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
@ -10,7 +11,8 @@ class Cat(commands.Cog):
|
|||||||
async def cat(self, ctx):
|
async def cat(self, ctx):
|
||||||
"""Получить случайного котика"""
|
"""Получить случайного котика"""
|
||||||
try:
|
try:
|
||||||
response = requests.get(
|
response = await asyncio.to_thread(
|
||||||
|
requests.get,
|
||||||
"https://api.thecatapi.com/v1/images/search",
|
"https://api.thecatapi.com/v1/images/search",
|
||||||
timeout=10
|
timeout=10
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
import asyncio
|
||||||
import requests
|
import requests
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ class News(commands.Cog):
|
|||||||
@commands.command(name="news")
|
@commands.command(name="news")
|
||||||
async def news(self, ctx):
|
async def news(self, ctx):
|
||||||
"""Топ-5 свежих статей по AI с Habr"""
|
"""Топ-5 свежих статей по AI с Habr"""
|
||||||
articles = self._fetch_rss(RSS_URL_ARTICLES)
|
articles = await self._fetch_rss(RSS_URL_ARTICLES)
|
||||||
if articles is None:
|
if articles is None:
|
||||||
await ctx.send("Не удалось получить новости. Попробуйте позже.")
|
await ctx.send("Не удалось получить новости. Попробуйте позже.")
|
||||||
return
|
return
|
||||||
@ -25,10 +26,10 @@ class News(commands.Cog):
|
|||||||
|
|
||||||
await self._format_and_send(ctx, articles)
|
await self._format_and_send(ctx, articles)
|
||||||
|
|
||||||
def _fetch_rss(self, url):
|
async def _fetch_rss(self, url):
|
||||||
"""Скачать и распарсить RSS-ленту (RSS 2.0 / Atom)."""
|
"""Скачать и распарсить RSS-ленту (RSS 2.0 / Atom)."""
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, timeout=10)
|
response = await asyncio.to_thread(requests.get, url, timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
root = ElementTree.fromstring(response.content)
|
root = ElementTree.fromstring(response.content)
|
||||||
|
|
||||||
@ -99,7 +100,7 @@ class News(commands.Cog):
|
|||||||
lines.append(f"{title}\n{date_str} <{article['link']}>")
|
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:
|
if posts:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("**Лучшие новости за сутки / Искусственный интеллект / Хабr**\n<https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/>\n")
|
lines.append("**Лучшие новости за сутки / Искусственный интеллект / Хабr**\n<https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/>\n")
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
import requests
|
import requests
|
||||||
|
import asyncio
|
||||||
from requests.exceptions import ConnectionError, Timeout, SSLError
|
from requests.exceptions import ConnectionError, Timeout, SSLError
|
||||||
|
|
||||||
|
|
||||||
@ -47,11 +48,14 @@ class Pogoda(commands.Cog):
|
|||||||
# Пробуем wttr.in с retry
|
# Пробуем wttr.in с retry
|
||||||
for attempt in range(3):
|
for attempt in range(3):
|
||||||
try:
|
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()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
except (SSLError, ConnectionError, Timeout):
|
except (SSLError, ConnectionError, Timeout):
|
||||||
if attempt < 2:
|
if attempt < 2:
|
||||||
|
delay = 2 ** attempt
|
||||||
|
print(f"Попытка {attempt + 1} не удалась. Повтор через {delay} сек...")
|
||||||
|
await asyncio.sleep(delay)
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
@ -71,7 +75,7 @@ class Pogoda(commands.Cog):
|
|||||||
)
|
)
|
||||||
for attempt in range(3):
|
for attempt in range(3):
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, timeout=10)
|
response = await asyncio.to_thread(requests.get, url, timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
current = data.get("current", {})
|
current = data.get("current", {})
|
||||||
@ -90,6 +94,9 @@ class Pogoda(commands.Cog):
|
|||||||
}
|
}
|
||||||
except (SSLError, ConnectionError, Timeout):
|
except (SSLError, ConnectionError, Timeout):
|
||||||
if attempt < 2:
|
if attempt < 2:
|
||||||
|
delay = 2 ** attempt
|
||||||
|
print(f"Попытка {attempt + 1} не удалась. Повтор через {delay} сек...")
|
||||||
|
await asyncio.sleep(delay)
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import requests
|
import requests
|
||||||
|
import time
|
||||||
from requests.exceptions import ConnectionError, Timeout, SSLError
|
from requests.exceptions import ConnectionError, Timeout, SSLError
|
||||||
|
|
||||||
|
|
||||||
@ -45,6 +46,9 @@ def _fetch_weather(url):
|
|||||||
return response.json()
|
return response.json()
|
||||||
except (SSLError, ConnectionError, Timeout):
|
except (SSLError, ConnectionError, Timeout):
|
||||||
if attempt < 2:
|
if attempt < 2:
|
||||||
|
delay = 2 ** attempt
|
||||||
|
print(f"Попытка {attempt + 1} не удалась. Повтор через {delay} сек...")
|
||||||
|
time.sleep(delay)
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
@ -83,6 +87,9 @@ def _fetch_open_meteo():
|
|||||||
}
|
}
|
||||||
except (SSLError, ConnectionError, Timeout):
|
except (SSLError, ConnectionError, Timeout):
|
||||||
if attempt < 2:
|
if attempt < 2:
|
||||||
|
delay = 2 ** attempt
|
||||||
|
print(f"Попытка {attempt + 1} не удалась. Повтор через {delay} сек...")
|
||||||
|
time.sleep(delay)
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
except requests.RequestException:
|
except requests.RequestException:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user