diff --git a/MANAGEMENT_RUNBOOK.md b/MANAGEMENT_RUNBOOK.md new file mode 100644 index 0000000..8c807a6 --- /dev/null +++ b/MANAGEMENT_RUNBOOK.md @@ -0,0 +1,175 @@ +# MANAGEMENT_RUNBOOK.md + +Единый runbook по management-командам проекта. + +Документ отвечает на 3 вопроса: +- что запускать; +- когда запускать; +- как безопасно откатываться/повторять запуск. + +## Каталог команд + +1. `generate_sitemaps` — оффлайн генерация sitemap-файлов. +2ю `regenerate_seria_prerender` — оффлайн пересборка pre-render шаблонов для `catalog_seria_info`. + +## Общие правила запуска + +- Запускать команды из корня репозитория. +- Для локального/CI запуска использовать `poetry`. +- Не запускать тяжелые операции через HTTP-эндпоинты `/service/*`. +- Перезапуск веб-сервера (`gunicorn`/`uWSGI`) делать отдельным шагом оркестрации, а не из кода Django. + +Базовый шаблон запуска: + +```bash +cd /Users/e-serg/PRJ/2022-oknardia +poetry run python oknardia/manage.py [args] +``` + +## 1) Команда `generate_sitemaps` + +Назначение: +- пересобрать `sitemap.xml` и chunk-файлы в `MEDIA_ROOT/_serv_sitemap`. + +Базовый запуск: + +```bash +cd /Users/e-serg/PRJ/2022-oknardia +poetry run python oknardia/manage.py generate_sitemaps +``` + +Запуск с параметрами: + +```bash +cd /Users/e-serg/PRJ/2022-oknardia +poetry run python oknardia/manage.py generate_sitemaps \ + --compare-min-depth 2 \ + --compare-max-depth 4 \ + --max-items 40000 \ + --max-file-size 5242880 \ + --max-files-qty 998 +``` + +Когда запускать: +- после деплоя; +- по расписанию (cron/systemd timer); +- после крупных изменений данных каталога/блога. + +### Важные замечания + +Чтобы `sitemap.xml` отдавал прокси-nginx напрямую из файловой системы, нужно, чтобы он физически лежал +в `MEDIA_ROOT/_serv_sitemap/sitemap.xml`. + +Допустимо, что файл доступен по двум URL (корневой и media), но в `robots.txt` должен быть указан один +канонический вариант `sitemap.xml` + +#### NGINX snippet (alias для корневого sitemap) + +```nginx +# Корневой sitemap.xml (для привычного для поисковиков URL) +location = /sitemap.xml { + alias /<путь-к-каталогку-с-докер-приложением>/media/_serv_sitemap/sitemap.xml; + default_type application/xml; + add_header Cache-Control "public, max-age=300"; +} +``` + +## 2) Команда `regenerate_seria_prerender` + +Назначение: +- пересобрать pre-render шаблоны для страниц серий (`catalog_seria_info`) в каталоге `seria_info/prepared/`. + +Проверка без записи файлов: + +```bash +cd /Users/e-serg/PRJ/2022-oknardia +poetry run python oknardia/manage.py regenerate_seria_prerender --dry-run +``` + +Пересборка только отсутствующих файлов: + +```bash +cd /Users/e-serg/PRJ/2022-oknardia +poetry run python oknardia/manage.py regenerate_seria_prerender +``` + +Принудительная пересборка всех root-серий: + +```bash +cd /Users/e-serg/PRJ/2022-oknardia +poetry run python oknardia/manage.py regenerate_seria_prerender --force +``` + +Выборочная пересборка: + +```bash +cd /Users/e-serg/PRJ/2022-oknardia +poetry run python oknardia/manage.py regenerate_seria_prerender --seria-id 843 --seria-id 2100 --force +``` + +Когда запускать: +- после обновления логики `catalog_seria_info`; +- после массового обновления данных серий/окон/квартир; +- после очистки `seria_info/prepared/`. + +## Оркестрация и reload веб-сервера + +Важно: +- reload веб-сервера не встроен в management-команды; +- это отдельная операция окружения. + +Пример для systemd + gunicorn: + +```bash +sudo systemctl reload gunicorn +``` + +Рекомендуемый batch-сценарий: + +```bash +cd /Users/e-serg/PRJ/2022-oknardia +poetry run python oknardia/manage.py regenerate_seria_prerender --force +poetry run python oknardia/manage.py generate_sitemaps +sudo systemctl reload gunicorn +``` + +## Cron/systemd timer (пример) + +Пример cron (раз в сутки в 03:20): + +```bash +20 3 * * * cd /Users/e-serg/PRJ/2022-oknardia && poetry run python oknardia/manage.py regenerate_seria_prerender --force && poetry run python oknardia/manage.py generate_sitemaps >> /var/log/oknardia-maintenance.log 2>&1 +``` + +Если нужен reload после batch, добавляй отдельной строкой/шагом оркестратора. + +## Диагностика + +Быстрая проверка конфигурации: + +```bash +cd /Users/e-serg/PRJ/2022-oknardia +poetry run python oknardia/manage.py check +``` + +Типовые причины проблем: +- нет прав записи в директории `templates/seria_info/prepared` или `MEDIA_ROOT/_serv_sitemap`; +- устаревшее виртуальное окружение / неустановленные зависимости; +- запуск не из того каталога. + +## План миграции `/service/*` -> management commands + +Текущее направление: +- все тяжелые и административные операции переносить из HTTP в management-команды; +- `/service/*` оставлять только как thin UI/мониторинг или убрать полностью. + +Кандидаты на перенос: +- действия из `service.py` (`/service/make_rating`, sitemap/служебные задачи и т.п.); +- любые операции, которые могут идти дольше обычного web-request. + +--- + +См. также: +- `SETUP.md` +- `README.md` + diff --git a/README.md b/README.md index 6c1118d..3e3686f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ * Рефакторинг `catalog_profile_manufacture` (`/catalog/profile/-`): упрощена валидация URL, убран дублирующий код маппинга для `PROFILES` и `MERCHANTS` через общие хелперы, стандартизирован хвост контекста (`LAST_VISIT`, `LOG_VISIT`, `ticks`) через `_append_visit_context`. * Рефакторинг `catalog_seria` (`/catalog/seria/`): raw SQL ⟶ ORM для списка корневых серий, подготовка данных упрощена, хвост контекста с визитами и `ticks` вынесен в общий helper внутри `catalog_series.py`. * Рефакторинг `catalog_seria_info` и связанных функций в `catalog_series.py`: raw SQL ⟶ ORM (`catalog_seria_info`, `seria_nav`, `seria_info_year`, `seria_info_geo_code`), снижена нагрузка на БД за счёт предвыборки и переиспользования агрегатов (`quantities_by_pair`, `offers_by_window`), добавлены безопасные fallback-значения для пустых выборок, включена потоковая обработка `iterator(chunk_size=500)` для гео-данных, обновлены комментарии и docstring под фактическую логику (таблица окон, pre-render light/heavy шаблонов, гео+статистика серии). -* +* Добавлена management-команда `regenerate_seria_prerender` для оффлайн-пересборки pre-render шаблонов `catalog_seria_info` (все или выбранные root-серии), с режимами `--dry-run` и `--force`; серверный reload (Gunicon? uWSGI или что там еще будет) должен быть вынесен из кода приложения в оркестрацию (cron/systemd/deploy step). * * * @@ -35,8 +35,7 @@ * [`AGENTS.md`](AGENTS.md) – контекст проекта для AI-ассистентов (архитектура, конвенции, рабочие сценарии). * [`SETUP.md`](SETUP.md) – пошаговая настройка окружения, запуск проекта и базовые команды разработки. -* Сервисные утилиты: - - [`SITEMAP_RUNBOOK.md`](SITEMAP_RUNBOOK.md) – sitemap (генерация, веса, cron, nginx) +* [`MANAGEMENT_RUNBOOK.md`](MANAGEMENT_RUNBOOK.md) – единый runbook по management-командам и batch-операциям. diff --git a/SETUP.md b/SETUP.md index 2f55f00..ee01922 100644 --- a/SETUP.md +++ b/SETUP.md @@ -233,6 +233,26 @@ python manage.py remove_stale_contenttypes # Удалить устаревши # Служебные python manage.py check # Проверить конфигурацию python manage.py check --deploy # Проверка для продакшена +python manage.py generate_sitemaps # Оффлайн генерация sitemap XML +python manage.py regenerate_seria_prerender --dry-run # Проверка пересборки pre-render шаблонов серий +python manage.py regenerate_seria_prerender --force # Принудительная пересборка pre-render шаблонов серий +``` + +### Пересборка pre-render шаблонов серий (рекомендуемый сценарий) + +Шаблоны для `catalog_seria_info` пересобираются оффлайн management-командой, без reload из кода Django. + +```bash +cd /path/to/project +poetry run python oknardia/manage.py regenerate_seria_prerender --force +# затем (опционально) один внешний reload процесса приложения, если это требуется вашей конфигурацией +# sudo systemctl reload gunicorn +``` + +Для выборочной пересборки используйте `--seria-id` несколько раз: + +```bash +poetry run python oknardia/manage.py regenerate_seria_prerender --seria-id 843 --seria-id 2100 --force ``` ## 📚 Дополнительные ресурсы @@ -240,7 +260,6 @@ python manage.py check --deploy # Проверка для продак - [Django документация](https://docs.djangoproject.com/en/stable/) - [AGENTS.md](./AGENTS.md) — архитектура и конвенции проекта - [README.md](./README.md) — основная информация о проекте -- [SECURITY_AUDIT_REPORT.md](./SECURITY_AUDIT_REPORT.md) — отчёт безопасности ## ❓ Решение проблем diff --git a/oknardia/web/catalog_series.py b/oknardia/web/catalog_series.py index e7e314e..9b64143 100644 --- a/oknardia/web/catalog_series.py +++ b/oknardia/web/catalog_series.py @@ -14,7 +14,7 @@ from oknardia.models import ( Building_Info, ) from web.report1 import get_last_all_user_visit_list, get_last_user_visit_cookies, get_last_user_visit_list -from web.add_func import get_flaps_for_big_pictures, touch_reload_wsgi +from web.add_func import get_flaps_for_big_pictures import time import os import math @@ -217,7 +217,6 @@ def catalog_seria_info( string_prerender = render_to_string("seria_info/all_seria_info_pre_light.html", to_template) with open(light_template_w_path, "w", encoding="utf-8") as file: file.write(string_prerender) - touch_reload_wsgi(light_template_w_path) else: to_template.update({"THIS_SERIA_NAME": q_seria.sName}) @@ -346,7 +345,7 @@ def seria_info_geo_code(seria_id: int | str = DEFAULT_SERIA_ID_FOR_CATALOG) -> d жилые/муниципальные/государственные площади, число жителей, квартир, лицевых счетов и диапазон показателя состояния домов. - :param seria_id: int | str -- id серии, для которой нужно получить данные + :param seria_id: int | str -- id серии, для которой нужно получить данные. :return: dict -- { "DATA4GEO": [...], "MUNICIPAL_M2": ..., diff --git a/oknardia/web/management/commands/regenerate_seria_prerender.py b/oknardia/web/management/commands/regenerate_seria_prerender.py new file mode 100644 index 0000000..741c572 --- /dev/null +++ b/oknardia/web/management/commands/regenerate_seria_prerender.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from pathlib import Path + +import pytils +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from django.db.models import F +from django.test import RequestFactory + +from oknardia.models import Seria_Info +from web import catalog_series + + +class Command(BaseCommand): + """Пересоздает pre-render шаблоны для страниц серий (/catalog/seria/.../all).""" + + help = "Пересоздает pre-render шаблоны catalog_seria_info для выбранных или всех корневых серий." + + def add_arguments(self, parser): + parser.add_argument( + "--seria-id", + type=int, + action="append", + default=[], + help="ID серии (можно передавать несколько раз). По умолчанию пересоздаются все корневые серии.", + ) + parser.add_argument( + "--force", + action="store_true", + help="Пересоздать даже если pre-render файл уже существует.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Только показать, что будет сделано, без генерации файлов.", + ) + + def handle(self, *args, **options): + seria_ids: list[int] = options["seria_id"] + force: bool = options["force"] + dry_run: bool = options["dry_run"] + + # Берем только корневые серии, потому что для них строятся канонические URL /all. + query = Seria_Info.objects.filter(id=F("kRoot_id")).only("id", "sName").order_by("id") + if seria_ids: + query = query.filter(id__in=seria_ids) + + targets = list(query) + if not targets: + raise CommandError("Не найдено подходящих корневых серий для пересоздания pre-render.") + + templates_root = Path(settings.TEMPLATES[0]["DIRS"][0]) + prepared_dir = templates_root / settings.PATH_FOR_SERIA_INFO_HTML_INCLUDE + prepared_dir.mkdir(parents=True, exist_ok=True) + + request_factory = RequestFactory() + created = 0 + planned = 0 + skipped = 0 + + for seria in targets: + target_file = prepared_dir / f"{seria.id}_id.html" + if target_file.exists() and not force: + skipped += 1 + self.stdout.write(f"SKIP {seria.id}: {target_file}") + continue + + if dry_run: + action = "REGEN" if target_file.exists() else "CREATE" + self.stdout.write(f"{action} {seria.id}: {target_file}") + planned += 1 + continue + + if target_file.exists(): + target_file.unlink() + + slug = pytils.translit.slugify(seria.sName) + request = request_factory.get(f"/catalog/seria/{slug}/all{seria.id}") + + # В команде принудительно включаем «production-mode» для вьюхи, + # чтобы она прошла тяжелую ветку и пересоздала pre-render файл. + old_debug = catalog_series.DEBUG + try: + catalog_series.DEBUG = False + response = catalog_series.catalog_seria_info(request, slug, seria.id) + finally: + catalog_series.DEBUG = old_debug + + if response.status_code != 200: + raise CommandError( + f"Серия {seria.id}: ожидался status=200, получен {response.status_code}." + ) + if not target_file.exists(): + raise CommandError(f"Серия {seria.id}: pre-render файл не создан: {target_file}") + + created += 1 + self.stdout.write(self.style.SUCCESS(f"OK {seria.id}: {target_file}")) + + if dry_run: + self.stdout.write( + self.style.SUCCESS( + f"DRY-RUN. Обработано: {len(targets)}. Будет создано/пересоздано: {planned}. Пропущено: {skipped}." + ) + ) + else: + self.stdout.write( + self.style.SUCCESS( + f"Готово. Обработано: {len(targets)}. Создано/пересоздано: {created}. Пропущено: {skipped}." + ) + ) +