add: management-команда regenerate_seria_prerender для оффлайн пересборки pre-render шаблонов серий; обновлены SETUP.md, README.md, MANAGEMENT_RUNBOOK.md
This commit is contained in:
175
MANAGEMENT_RUNBOOK.md
Normal file
175
MANAGEMENT_RUNBOOK.md
Normal file
@@ -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 <command> [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`
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
* Рефакторинг `catalog_profile_manufacture` (`/catalog/profile/<id>-<manufacturer>`): упрощена валидация URL, убран дублирующий код маппинга для `PROFILES` и `MERCHANTS` через общие хелперы, стандартизирован хвост контекста (`LAST_VISIT`, `LOG_VISIT`, `ticks`) через `_append_visit_context`.
|
* Рефакторинг `catalog_profile_manufacture` (`/catalog/profile/<id>-<manufacturer>`): упрощена валидация 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` (`/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 шаблонов, гео+статистика серии).
|
* Рефакторинг `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-ассистентов (архитектура, конвенции, рабочие сценарии).
|
* [`AGENTS.md`](AGENTS.md) – контекст проекта для AI-ассистентов (архитектура, конвенции, рабочие сценарии).
|
||||||
* [`SETUP.md`](SETUP.md) – пошаговая настройка окружения, запуск проекта и базовые команды разработки.
|
* [`SETUP.md`](SETUP.md) – пошаговая настройка окружения, запуск проекта и базовые команды разработки.
|
||||||
* Сервисные утилиты:
|
* [`MANAGEMENT_RUNBOOK.md`](MANAGEMENT_RUNBOOK.md) – единый runbook по management-командам и batch-операциям.
|
||||||
- [`SITEMAP_RUNBOOK.md`](SITEMAP_RUNBOOK.md) – sitemap (генерация, веса, cron, nginx)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
21
SETUP.md
21
SETUP.md
@@ -233,6 +233,26 @@ python manage.py remove_stale_contenttypes # Удалить устаревши
|
|||||||
# Служебные
|
# Служебные
|
||||||
python manage.py check # Проверить конфигурацию
|
python manage.py check # Проверить конфигурацию
|
||||||
python manage.py check --deploy # Проверка для продакшена
|
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/)
|
- [Django документация](https://docs.djangoproject.com/en/stable/)
|
||||||
- [AGENTS.md](./AGENTS.md) — архитектура и конвенции проекта
|
- [AGENTS.md](./AGENTS.md) — архитектура и конвенции проекта
|
||||||
- [README.md](./README.md) — основная информация о проекте
|
- [README.md](./README.md) — основная информация о проекте
|
||||||
- [SECURITY_AUDIT_REPORT.md](./SECURITY_AUDIT_REPORT.md) — отчёт безопасности
|
|
||||||
|
|
||||||
## ❓ Решение проблем
|
## ❓ Решение проблем
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from oknardia.models import (
|
|||||||
Building_Info,
|
Building_Info,
|
||||||
)
|
)
|
||||||
from web.report1 import get_last_all_user_visit_list, get_last_user_visit_cookies, get_last_user_visit_list
|
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 time
|
||||||
import os
|
import os
|
||||||
import math
|
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)
|
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:
|
with open(light_template_w_path, "w", encoding="utf-8") as file:
|
||||||
file.write(string_prerender)
|
file.write(string_prerender)
|
||||||
touch_reload_wsgi(light_template_w_path)
|
|
||||||
else:
|
else:
|
||||||
to_template.update({"THIS_SERIA_NAME": q_seria.sName})
|
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 -- {
|
:return: dict -- {
|
||||||
"DATA4GEO": [...],
|
"DATA4GEO": [...],
|
||||||
"MUNICIPAL_M2": ...,
|
"MUNICIPAL_M2": ...,
|
||||||
|
|||||||
113
oknardia/web/management/commands/regenerate_seria_prerender.py
Normal file
113
oknardia/web/management/commands/regenerate_seria_prerender.py
Normal file
@@ -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<ID>)."""
|
||||||
|
|
||||||
|
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<ID>.
|
||||||
|
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}."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
Reference in New Issue
Block a user