[ВОЛК] увеличение таймаутов на запись и в gunicorn
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-01-10 15:45:40 +03:00
parent c999898c58
commit 54ddc05058
2 changed files with 75 additions and 47 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ After=network.target
[Service] [Service]
User=root User=root
WorkingDirectory=/opt/testbot-git WorkingDirectory=/opt/testbot-git
ExecStart=/opt/testbot-git/venv/bin/gunicorn --bind 192.168.0.109:5006 --workers 4 --timeout=120 wsgi_edit:app ExecStart=/opt/testbot-git/venv/bin/gunicorn --bind 192.168.0.109:5006 --workers 4 --timeout=240 wsgi_edit:app
Restart=always Restart=always
[Install] [Install]
+74 -46
View File
@@ -155,10 +155,11 @@ async def publish_to_tg(vk_post_id):
# Создаем httpx клиент с отключенным HTTP/2 # Создаем httpx клиент с отключенным HTTP/2
# Используем более детальные таймауты: connect, read, write, pool # Используем более детальные таймауты: connect, read, write, pool
# Таймаут на запись увеличен для больших файлов (до 10 МБ)
timeout_config = httpx.Timeout( timeout_config = httpx.Timeout(
connect=10.0, # Таймаут на подключение connect=10.0, # Таймаут на подключение
read=60.0, # Таймаут на чтение (увеличен для больших изображений) read=120.0, # Таймаут на чтение (увеличен для больших изображений)
write=30.0, # Таймаут на запись write=120.0, # Таймаут на запись (увеличен для больших файлов)
pool=10.0 # Таймаут на получение соединения из пула pool=10.0 # Таймаут на получение соединения из пула
) )
httpx_client = httpx.AsyncClient( httpx_client = httpx.AsyncClient(
@@ -234,67 +235,94 @@ async def publish_to_tg(vk_post_id):
logger.info(f"Длина подписи: {len(text)} символов") logger.info(f"Длина подписи: {len(text)} символов")
# Скачиваем изображение # Скачиваем изображение
image_data = None
try: try:
image_data = await download_image(image_url, httpx_client) image_data = await download_image(image_url, httpx_client)
except Exception as download_error: except Exception as download_error:
logger.error(f"Ошибка при скачивании изображения для записи VK ID {vk_post_id}: {download_error}") logger.error(f"Ошибка при скачивании изображения для записи VK ID {vk_post_id}: {download_error}")
raise # Пробрасываем ошибку, чтобы перейти к fallback raise # Пробрасываем ошибку, чтобы перейти к fallback
# Логируем время начала запроса # Вычисляем динамический таймаут на запись в зависимости от размера файла
request_start_time = time.time() # Базовый таймаут 60 секунд + 10 секунд на каждый МБ (минимум 120 секунд)
logger.info(f"Начало запроса отправки фото в {datetime.now(timezone.utc).isoformat()}") file_size_mb = len(image_data) / (1024 * 1024)
write_timeout = max(120.0, 60.0 + (file_size_mb * 10.0))
logger.info(f"Размер файла: {file_size_mb:.2f} МБ, таймаут на запись: {write_timeout:.1f} секунд")
# Отправляем изображение как файл через multipart/form-data # Создаем клиент с увеличенным таймаутом на запись для больших файлов
# Создаем BytesIO объект для файла upload_timeout_config = httpx.Timeout(
image_file = io.BytesIO(image_data) connect=10.0,
files = { read=120.0,
'photo': ('image.jpg', image_file, 'image/jpeg') write=write_timeout, # Динамический таймаут на запись
} pool=10.0
data = { )
'chat_id': CHANNEL_ID, upload_client = httpx.AsyncClient(
'caption': text, http2=False,
'parse_mode': 'HTML', timeout=upload_timeout_config,
'disable_notification': str(PUBLISH_SILENTLY).lower() follow_redirects=True
} )
photo_response = await httpx_client.post(send_photo_url, files=files, data=data) try:
request_duration = time.time() - request_start_time # Логируем время начала запроса
logger.info(f"Запрос отправки фото завершен за {request_duration:.2f} секунд") request_start_time = time.time()
logger.info(f"Начало запроса отправки фото в {datetime.now(timezone.utc).isoformat()}")
if photo_response.status_code == 200:
photo_result = photo_response.json() # Отправляем изображение как файл через multipart/form-data
if photo_result.get('ok'): # Создаем BytesIO объект для файла
message_data = photo_result.get('result', {}) image_file = io.BytesIO(image_data)
message_id = message_data.get('message_id') files = {
message_has_image = True # Успешно отправили изображение 'photo': ('image.jpg', image_file, 'image/jpeg')
}
data = {
'chat_id': CHANNEL_ID,
'caption': text,
'parse_mode': 'HTML',
'disable_notification': str(PUBLISH_SILENTLY).lower()
}
photo_response = await upload_client.post(send_photo_url, files=files, data=data)
request_duration = time.time() - request_start_time
logger.info(f"Запрос отправки фото завершен за {request_duration:.2f} секунд")
if photo_response.status_code == 200:
photo_result = photo_response.json()
if photo_result.get('ok'):
message_data = photo_result.get('result', {})
message_id = message_data.get('message_id')
message_has_image = True # Успешно отправили изображение
else:
# Ошибка в ответе API - логируем полную информацию
log_telegram_error(photo_response, f"[Отправка фото для VK ID {vk_post_id}]")
error_code = photo_result.get('error_code')
if error_code == 429:
retry_after = photo_result.get('parameters', {}).get('retry_after', 60)
raise RetryAfterException(retry_after)
raise Exception(f"API error: {photo_result.get('description', 'Unknown error')}")
else: else:
# Ошибка в ответе API - логируем полную информацию
log_telegram_error(photo_response, f"[Отправка фото для VK ID {vk_post_id}]") log_telegram_error(photo_response, f"[Отправка фото для VK ID {vk_post_id}]")
error_code = photo_result.get('error_code') if photo_response.status_code == 429:
if error_code == 429: retry_after = 60
retry_after = photo_result.get('parameters', {}).get('retry_after', 60) try:
error_data = photo_response.json()
retry_after = error_data.get('parameters', {}).get('retry_after', 60)
except:
pass
raise RetryAfterException(retry_after) raise RetryAfterException(retry_after)
raise Exception(f"API error: {photo_result.get('description', 'Unknown error')}") raise Exception(f"HTTP {photo_response.status_code}: {photo_response.text}")
else: finally:
log_telegram_error(photo_response, f"[Отправка фото для VK ID {vk_post_id}]") await upload_client.aclose()
if photo_response.status_code == 429:
retry_after = 60
try:
error_data = photo_response.json()
retry_after = error_data.get('parameters', {}).get('retry_after', 60)
except:
pass
raise RetryAfterException(retry_after)
raise Exception(f"HTTP {photo_response.status_code}: {photo_response.text}")
except RetryAfterException: except RetryAfterException:
raise raise
except httpx.TimeoutException as e: except (httpx.TimeoutException, httpx.WriteTimeout, httpx.ReadTimeout) as e:
logger.error(f"ТАЙМАУТ при отправке изображения для записи VK ID {vk_post_id}") timeout_type = type(e).__name__
logger.error(f"Тип исключения: {type(e).__name__}") logger.error(f"ТАЙМАУТ ({timeout_type}) при отправке изображения для записи VK ID {vk_post_id}")
logger.error(f"Тип исключения: {timeout_type}")
logger.error(f"Сообщение об ошибке: {str(e)}") logger.error(f"Сообщение об ошибке: {str(e)}")
logger.error(f"Полная информация об исключении: {repr(e)}") logger.error(f"Полная информация об исключении: {repr(e)}")
if hasattr(e, 'request'): if hasattr(e, 'request'):
logger.error(f"Запрос, вызвавший таймаут: {e.request.method} {e.request.url if hasattr(e.request, 'url') else 'N/A'}") logger.error(f"Запрос, вызвавший таймаут: {e.request.method} {e.request.url if hasattr(e.request, 'url') else 'N/A'}")
# Для WriteTimeout логируем размер файла
if isinstance(e, httpx.WriteTimeout) and image_data is not None:
logger.error(f"Размер файла, вызвавшего таймаут записи: {len(image_data) / (1024 * 1024):.2f} МБ")
logger.error(f"Трассировка стека:\n{traceback.format_exc()}") logger.error(f"Трассировка стека:\n{traceback.format_exc()}")
raise # Пробрасываем таймаут наверх raise # Пробрасываем таймаут наверх
except Exception as e: except Exception as e: