This commit is contained in:
commit
36ea07b8fc
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -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
8
cache/precache.py
vendored
|
@ -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
49
main.py
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user