postmerge2
All checks were successful
Deploy on push / deploy (push) Successful in 7s

This commit is contained in:
Untone 2025-06-03 01:24:49 +03:00
commit 36ea07b8fc
3 changed files with 60 additions and 17 deletions

View File

@ -25,6 +25,26 @@
- **Отладка**: Добавлены debug команды для диагностики проблем Python установки
- **Надежность**: Стабильная работа CI/CD пайплайна на Gitea
### Оптимизация документации
- **docs/README.md**: Применение принципа DRY к документации:
- **Сокращение на 60%**: с 198 до ~80 строк без потери информации
- **Устранение дублирований**: убраны повторы разделов и оглавлений
- **Улучшенная структура**: Быстрый старт → Документация → Возможности → API
- **Эмодзи навигация**: улучшенная читаемость и UX
- **Унифицированный стиль**: consistent formatting для ссылок и описаний
- **docs/nginx-optimization.md**: Удален избыточный файл - достаточно краткого описания в features.md
- **Принцип единого источника истины**: каждая информация указана в одном месте
### Исправления кода
- **Ruff linter**: Исправлены все ошибки соответствия современным стандартам Python:
- **pathlib.Path**: Заменены устаревшие `os.path.join()`, `os.path.dirname()`, `os.path.exists()` на современные Path методы
- **Path операции**: `os.unlink()``Path.unlink()`, `open()``Path.open()`
- **asyncio.create_task**: Добавлено сохранение ссылки на background task для корректного управления
- **Код соответствует**: Современным стандартам Python 3.11+ и best practices
- **Убрана проверка типов**: Упрощен CI/CD пайплайн - оставлен только deploy без type-check
## [0.5.3] - 2025-06-02
## 🐛 Исправления

8
cache/precache.py vendored
View File

@ -76,6 +76,7 @@ async def precache_topics_followers(topic_id: int, session) -> None:
async def precache_data() -> None:
logger.info("precaching...")
logger.debug("Entering precache_data")
try:
# Список паттернов ключей, которые нужно сохранить при FLUSHDB
preserve_patterns = [
@ -116,6 +117,7 @@ async def precache_data() -> None:
continue
await redis.execute("FLUSHDB")
logger.debug("Redis database flushed")
logger.info("redis: FLUSHDB")
# Восстанавливаем все сохранённые ключи
@ -150,17 +152,22 @@ async def precache_data() -> None:
logger.error(f"Ошибка при восстановлении ключа {key}: {e}")
continue
logger.info("Beginning topic precache phase")
with local_session() as session:
# topics
q = select(Topic).where(Topic.community == 1)
topics = get_with_stat(q)
logger.info(f"Found {len(topics)} topics to precache")
for topic in topics:
topic_dict = topic.dict() if hasattr(topic, "dict") else topic
logger.debug(f"Precaching topic id={topic_dict.get('id')}")
await cache_topic(topic_dict)
logger.debug(f"Cached topic id={topic_dict.get('id')}")
await asyncio.gather(
precache_topics_followers(topic_dict["id"], session),
precache_topics_authors(topic_dict["id"], session),
)
logger.debug(f"Finished precaching followers and authors for topic id={topic_dict.get('id')}")
logger.info(f"{len(topics)} topics and their followings precached")
# authors
@ -177,6 +184,7 @@ async def precache_data() -> None:
precache_authors_followers(author_id, session),
precache_authors_follows(author_id, session),
)
logger.debug(f"Finished precaching followers and follows for author id={author_id}")
else:
logger.error(f"fail caching {author}")
logger.info(f"{len(authors)} authors and their followings precached")

49
main.py
View File

@ -1,7 +1,8 @@
import asyncio
import os
from importlib import import_module
from os.path import exists, join
from pathlib import Path
from typing import Any, AsyncGenerator
from ariadne import load_schema_from_path, make_executable_schema
from ariadne.asgi import GraphQL
@ -9,7 +10,7 @@ from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.responses import JSONResponse, Response
from starlette.routing import Mount, Route
from starlette.staticfiles import StaticFiles
@ -18,7 +19,6 @@ from auth.middleware import AuthMiddleware, auth_middleware
from auth.oauth import oauth_callback, oauth_login
from cache.precache import precache_data
from cache.revalidator import revalidation_manager
from services.exception import ExceptionHandlerMiddleware
from services.redis import redis
from services.schema import create_all_tables, resolvers
from services.search import check_search_service, initialize_search_index_background, search_service
@ -27,8 +27,8 @@ from settings import DEV_SERVER_PID_FILE_NAME
from utils.logger import root_logger as logger
DEVMODE = os.getenv("DOKKU_APP_TYPE", "false").lower() == "false"
DIST_DIR = join(os.path.dirname(__file__), "dist") # Директория для собранных файлов
INDEX_HTML = join(os.path.dirname(__file__), "index.html")
DIST_DIR = Path(__file__).parent / "dist" # Директория для собранных файлов
INDEX_HTML = Path(__file__).parent / "index.html"
# Импортируем резолверы ПЕРЕД созданием схемы
import_module("resolvers")
@ -38,8 +38,6 @@ schema = make_executable_schema(load_schema_from_path("schema/"), list(resolvers
# Создаем middleware с правильным порядком
middleware = [
# Начинаем с обработки ошибок
Middleware(ExceptionHandlerMiddleware),
# CORS должен быть перед другими middleware для корректной обработки preflight-запросов
Middleware(
CORSMiddleware,
@ -66,7 +64,7 @@ graphql_app = GraphQL(schema, debug=DEVMODE, http_handler=EnhancedGraphQLHTTPHan
# Оборачиваем GraphQL-обработчик для лучшей обработки ошибок
async def graphql_handler(request: Request):
async def graphql_handler(request: Request) -> Response:
"""
Обработчик GraphQL запросов с поддержкой middleware и обработкой ошибок.
@ -121,8 +119,9 @@ async def shutdown() -> None:
# Удаляем PID-файл, если он существует
from settings import DEV_SERVER_PID_FILE_NAME
if exists(DEV_SERVER_PID_FILE_NAME):
os.unlink(DEV_SERVER_PID_FILE_NAME)
pid_file = Path(DEV_SERVER_PID_FILE_NAME)
if pid_file.exists():
pid_file.unlink()
async def dev_start() -> None:
@ -137,11 +136,11 @@ async def dev_start() -> None:
Используется только при запуске сервера с флагом "dev".
"""
try:
pid_path = DEV_SERVER_PID_FILE_NAME
pid_path = Path(DEV_SERVER_PID_FILE_NAME)
# Если PID-файл уже существует, проверяем, не запущен ли уже сервер с этим PID
if exists(pid_path):
if pid_path.exists():
try:
with open(pid_path, encoding="utf-8") as f:
with pid_path.open(encoding="utf-8") as f:
old_pid = int(f.read().strip())
# Проверяем, существует ли процесс с таким PID
@ -154,7 +153,7 @@ async def dev_start() -> None:
print("[warning] Invalid PID file found, recreating")
# Создаем или перезаписываем PID-файл
with open(pid_path, "w", encoding="utf-8") as f:
with pid_path.open("w", encoding="utf-8") as f:
f.write(str(os.getpid()))
print(f"[main] process started in DEV mode with PID {os.getpid()}")
except Exception as e:
@ -163,7 +162,11 @@ async def dev_start() -> None:
print(f"[warning] Error during DEV mode initialization: {e!s}")
async def lifespan(_app):
# Глобальная переменная для background tasks
background_tasks = []
async def lifespan(_app: Any) -> AsyncGenerator[None, None]:
"""
Функция жизненного цикла приложения.
@ -198,11 +201,23 @@ async def lifespan(_app):
await asyncio.sleep(10) # 10-second delay to let the system stabilize
# Start search indexing as a background task with lower priority
asyncio.create_task(initialize_search_index_background())
search_task = asyncio.create_task(initialize_search_index_background())
background_tasks.append(search_task)
# Не ждем завершения задачи, позволяем ей выполняться в фоне
yield
finally:
print("[lifespan] Shutting down application services")
# Отменяем все background tasks
for task in background_tasks:
if not task.done():
task.cancel()
# Ждем завершения отмены tasks
if background_tasks:
await asyncio.gather(*background_tasks, return_exceptions=True)
tasks = [redis.disconnect(), ViewedStorage.stop(), revalidation_manager.stop()]
await asyncio.gather(*tasks, return_exceptions=True)
print("[lifespan] Shutdown complete")
@ -215,7 +230,7 @@ app = Starlette(
# OAuth маршруты
Route("/oauth/{provider}", oauth_login, methods=["GET"]),
Route("/oauth/{provider}/callback", oauth_callback, methods=["GET"]),
Mount("/", app=StaticFiles(directory=DIST_DIR, html=True)),
Mount("/", app=StaticFiles(directory=str(DIST_DIR), html=True)),
],
lifespan=lifespan,
middleware=middleware, # Явно указываем список middleware