Compare commits
No commits in common. "2cc096a047e84aa19ced907a374a8f7cfcef4494" and "a659b18771691ff21e9dd43fafff7bb93818bfa7" have entirely different histories.
2cc096a047
...
a659b18771
5 changed files with 1 additions and 387 deletions
|
|
@ -98,24 +98,6 @@ jobs:
|
||||||
python3 scripts/check-compose-ambiguous-hosts.py --selftest
|
python3 scripts/check-compose-ambiguous-hosts.py --selftest
|
||||||
python3 scripts/check-compose-ambiguous-hosts.py
|
python3 scripts/check-compose-ambiguous-hosts.py
|
||||||
|
|
||||||
- name: "Guard: подмена фронта МЕРЫ без окна недоступности (#3274)"
|
|
||||||
# Тем же шагом-соседом и по той же причине: секунды на PR, падение
|
|
||||||
# блокирует merge.
|
|
||||||
#
|
|
||||||
# ЗАЧЕМ. Публичный лендинг лежал 30–90 с на КАЖДОМ деплое МЕРЫ —
|
|
||||||
# не потому, что подмена контейнера медленная (0,5 с), а потому, что
|
|
||||||
# `up -d` со списком сервисов делает create всех (старые контейнеры
|
|
||||||
# УДАЛЯЮТСЯ) и только потом start, дождавшись зависимостей. Лечение —
|
|
||||||
# две половинки в разных файлах: `frontend` вынесен из общей пачки в
|
|
||||||
# deploy-tradein.yml + ретрай подключения в caddy/sites/apps.caddy.
|
|
||||||
# Обе обратимы молча и незаметно (дописать frontend обратно в SERVICES
|
|
||||||
# «за компанию»; скопировать новый публичный путь с блока без импорта),
|
|
||||||
# а отказ виден только непрерывной пробой во время деплоя — то есть
|
|
||||||
# никогда, если её никто не запустил.
|
|
||||||
run: |
|
|
||||||
python3 scripts/check-frontend-swap-window.py --selftest
|
|
||||||
python3 scripts/check-frontend-swap-window.py
|
|
||||||
|
|
||||||
- name: "Guard: Caddyfile синтаксически валиден"
|
- name: "Guard: Caddyfile синтаксически валиден"
|
||||||
# Тем же шагом-соседом и по той же причине, что два гейта рядом: бежит
|
# Тем же шагом-соседом и по той же причине, что два гейта рядом: бежит
|
||||||
# на КАЖДОМ PR, стоит секунды, падение блокирует merge.
|
# на КАЖДОМ PR, стоит секунды, падение блокирует merge.
|
||||||
|
|
|
||||||
|
|
@ -1137,31 +1137,7 @@ jobs:
|
||||||
# tradein-mvp/backend/** включает app/tgbot_main.py), никакого
|
# tradein-mvp/backend/** включает app/tgbot_main.py), никакого
|
||||||
# in-flight state вроде scrape_runs → пересоздаётся безусловно вместе
|
# in-flight state вроде scrape_runs → пересоздаётся безусловно вместе
|
||||||
# с browser/backend/frontend, отдельного graceful-drain не требует.
|
# с browser/backend/frontend, отдельного graceful-drain не требует.
|
||||||
#
|
SERVICES="browser backend frontend tgbot"
|
||||||
# ── FRONTEND ЗДЕСЬ НЕТ. ЭТО И ЕСТЬ ЛЕЧЕНИЕ #3274 ─────────────────
|
|
||||||
# Публичный лендинг лежал 30–90 с на КАЖДОМ деплое. Причина не в
|
|
||||||
# том, что подмена контейнера медленная — она занимает полсекунды.
|
|
||||||
# Причина в том, что `docker compose up -d` со СПИСКОМ сервисов
|
|
||||||
# работает в две фазы: сначала create (старый контейнер каждого
|
|
||||||
# сервиса останавливается и УДАЛЯЕТСЯ — иначе занято container_name),
|
|
||||||
# и только потом start, в порядке зависимостей и с ожиданием их
|
|
||||||
# условий. Между фазами фронта уже нет, а нового ещё нет.
|
|
||||||
#
|
|
||||||
# Замер на проде (docker inspect, 10.09, оба деплоя МЕРЫ):
|
|
||||||
# пачка сервисов: tradein-backend создан 15:01:40 → запущен
|
|
||||||
# 15:02:10 = 30 с (и 503 на лендинге в
|
|
||||||
# 15:01:46/15:01:52/15:02:06 — ровно окно);
|
|
||||||
# ОДИН сервис: tradein-frontend создан 16:42:17.5 → запущен
|
|
||||||
# 16:42:18.0 = 0,5 с, 503 в логе нет вообще.
|
|
||||||
# Тот же двухфазный порядок воспроизведён на стенде по меткам
|
|
||||||
# .Created/.StartedAt: соседи создаются сразу, стартуют через 41 с.
|
|
||||||
#
|
|
||||||
# Поэтому фронт пересоздаётся ОТДЕЛЬНОЙ командой ниже, после этой
|
|
||||||
# пачки: в его графе один сервис, фазы create и start идут подряд.
|
|
||||||
# Остаток (~0,5 с) добирает ретрай подключения в Caddy — см.
|
|
||||||
# снипет (tradein_frontend_retry) в caddy/sites/apps.caddy.
|
|
||||||
# Гейт на обе половины: scripts/check-frontend-swap-window.py.
|
|
||||||
SERVICES="browser backend tgbot"
|
|
||||||
SCRAPER_STOP_TS=""
|
SCRAPER_STOP_TS=""
|
||||||
scraper_stale=""
|
scraper_stale=""
|
||||||
if [ "${SCRAPER_RECREATE:-true}" = "true" ]; then
|
if [ "${SCRAPER_RECREATE:-true}" = "true" ]; then
|
||||||
|
|
@ -1257,21 +1233,6 @@ jobs:
|
||||||
" || echo "WARNING: startup-reap query failed — orphaned runs (if any) fall back to the 6h zombie reaper"
|
" || echo "WARNING: startup-reap query failed — orphaned runs (if any) fall back to the 6h zombie reaper"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# (4b) Фронт — ОТДЕЛЬНОЙ командой, один сервис в графе (#3274).
|
|
||||||
# Обоснование и прод-замеры — у SERVICES выше. Здесь важен ПОРЯДОК:
|
|
||||||
# эта команда идёт ПОСЛЕ пачки (backend уже поднят — новый SSR сразу
|
|
||||||
# ходит в новый бэкенд) и ДО `caddy reload` ниже, чтобы reload, как
|
|
||||||
# и раньше, оставался последним касанием прокси.
|
|
||||||
#
|
|
||||||
# ЗАЧЕМ ЖДАТЬ ПОСЛЕ КОМАНДЫ. `up -d` возвращает управление, когда
|
|
||||||
# контейнер ЗАПУЩЕН, а не когда Next начал слушать (на проде между
|
|
||||||
# ними ~0,1 с, см. journald «Ready in 110ms», но это не гарантия).
|
|
||||||
# Полноценная проверка фронта — health-check ниже по файлу, он же
|
|
||||||
# валит деплой при неудаче; здесь только короткая пауза, чтобы
|
|
||||||
# ретрай Caddy (2 с) не пришёлся на ещё не слушающий порт.
|
|
||||||
docker compose -p gendesign-tradein $COMPOSE_FILES up -d --no-deps frontend
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
# (5) `docker restart tradein-backend` БОЛЬШЕ НЕ НУЖЕН (issue #2216).
|
# (5) `docker restart tradein-backend` БОЛЬШЕ НЕ НУЖЕН (issue #2216).
|
||||||
# История (PR #493 / deploy 1156): backend раньше поднимался ПЕРЕД
|
# История (PR #493 / deploy 1156): backend раньше поднимался ПЕРЕД
|
||||||
# миграциями, его lifespan-hook (ensure_fdw_user_mapping) падал с
|
# миграциями, его lifespan-hook (ensure_fdw_user_mapping) падал с
|
||||||
|
|
|
||||||
|
|
@ -42,40 +42,6 @@
|
||||||
# код.
|
# код.
|
||||||
# ═══════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
# ── (tradein_frontend_retry) — подмена контейнера фронта без 502 (#3274) ─────
|
|
||||||
#
|
|
||||||
# ЧТО ЭТО. Ретрай ПОДКЛЮЧЕНИЯ к tradein-frontend: пока идёт подмена
|
|
||||||
# контейнера, Caddy не отдаёт ошибку сразу, а до 2 с переспрашивает апстрим с
|
|
||||||
# шагом 100 мс. Импортируется ВНУТРЬ каждого `reverse_proxy tradein-frontend`
|
|
||||||
# в этом файле (9 блоков: gendsgn.ru/trade-in/* и все публичные пути
|
|
||||||
# meraocenka.ru).
|
|
||||||
#
|
|
||||||
# ПОЧЕМУ ЭТО НЕ ТО, ЧТО ОТВЕРГНУТО В #3274. Там `lb_try_duration` отвергнут
|
|
||||||
# для окна 30–90 с — держать посетителя минуту в ожидании хуже честной
|
|
||||||
# ошибки, инструмент рассчитан на разрыв в сотни миллисекунд. Правка в
|
|
||||||
# deploy-tradein.yml (frontend вынесен из общего `up -d`) СНАЧАЛА сводит окно
|
|
||||||
# к этим сотням миллисекунд, и только после этого ретрай становится
|
|
||||||
# применим. Числа, на которых это стоит (замеры 11.09):
|
|
||||||
# - прод, ОДИН сервис в `up -d`: контейнер создан 16:42:17.5 → запущен
|
|
||||||
# 16:42:18.0 — 0,5 с;
|
|
||||||
# - прод, ПАЧКА сервисов в одном `up -d`: создан 15:01:40 → запущен
|
|
||||||
# 15:02:10 — 30 с (фаза start ждёт готовности зависимостей, а старый
|
|
||||||
# контейнер снесён ещё в фазе create);
|
|
||||||
# - стенд (реальный образ фронта + Caddy 2), одиночная подмена: без ретрая
|
|
||||||
# 1 × 502, с ретраем 241/241 × 200, один запрос подождал 0,55 с.
|
|
||||||
#
|
|
||||||
# ПОТОЛОК. 2 с — это ЦЕНА ОЖИДАНИЯ при настоящей аварии: если фронт лежит
|
|
||||||
# долго, каждый запрос сначала висит 2 с и лишь потом получает 503-заглушку
|
|
||||||
# (../deploy-window.caddy.snippet). Поэтому не «побольше на всякий случай»:
|
|
||||||
# запас над измеренными 0,5 с четырёхкратный, дальше растёт только вред.
|
|
||||||
# Бэкенду (tradein-backend) этот приём НАМЕРЕННО не дан: его старт — единицы
|
|
||||||
# секунд (uvicorn + подключение к БД), там 2 с не хватит, а больше — уже тот
|
|
||||||
# самый вред. Его окно закрывается отдельно, не этой правкой.
|
|
||||||
(tradein_frontend_retry) {
|
|
||||||
lb_try_duration 2s
|
|
||||||
lb_try_interval 100ms
|
|
||||||
}
|
|
||||||
|
|
||||||
# Caddy config.
|
# Caddy config.
|
||||||
#
|
#
|
||||||
# - gendsgn.ru — main production site, auto-TLS via Let's Encrypt.
|
# - gendsgn.ru — main production site, auto-TLS via Let's Encrypt.
|
||||||
|
|
@ -171,7 +137,6 @@ gendsgn.ru {
|
||||||
@uipreview path /trade-in/ui-preview/* /trade-in/_next/static/*
|
@uipreview path /trade-in/ui-preview/* /trade-in/_next/static/*
|
||||||
handle @uipreview {
|
handle @uipreview {
|
||||||
reverse_proxy tradein-frontend:3000 {
|
reverse_proxy tradein-frontend:3000 {
|
||||||
import tradein_frontend_retry
|
|
||||||
# #2558 review: тот же периметр-scrub, что и у /trade-in/api/* и
|
# #2558 review: тот же периметр-scrub, что и у /trade-in/api/* и
|
||||||
# @tradein ниже — этот блок тоже теперь ДО basic_auth, клиент
|
# @tradein ниже — этот блок тоже теперь ДО basic_auth, клиент
|
||||||
# мог бы прислать свой X-Authenticated-User. Сейчас инертно
|
# мог бы прислать свой X-Authenticated-User. Сейчас инертно
|
||||||
|
|
@ -275,7 +240,6 @@ gendsgn.ru {
|
||||||
handle @tradein {
|
handle @tradein {
|
||||||
# Next.js basePath=/trade-in — фронт сам ждёт префикса в URL
|
# Next.js basePath=/trade-in — фронт сам ждёт префикса в URL
|
||||||
reverse_proxy tradein-frontend:3000 {
|
reverse_proxy tradein-frontend:3000 {
|
||||||
import tradein_frontend_retry
|
|
||||||
# См. комментарий над /trade-in/api/* выше — та же логика (явное
|
# См. комментарий над /trade-in/api/* выше — та же логика (явное
|
||||||
# удаление вместо Set с пустым {http.auth.user.id}), и по той же
|
# удаление вместо Set с пустым {http.auth.user.id}), и по той же
|
||||||
# причине здесь больше нет инжекта X-Internal-Auth-Secret
|
# причине здесь больше нет инжекта X-Internal-Auth-Secret
|
||||||
|
|
@ -391,7 +355,6 @@ meraocenka.ru {
|
||||||
handle / {
|
handle / {
|
||||||
rewrite * /trade-in/mera-public
|
rewrite * /trade-in/mera-public
|
||||||
reverse_proxy tradein-frontend:3000 {
|
reverse_proxy tradein-frontend:3000 {
|
||||||
import tradein_frontend_retry
|
|
||||||
# Тот же периметр-скраб, что у @uipreview (:87) и @tradein ниже.
|
# Тот же периметр-скраб, что у @uipreview (:87) и @tradein ниже.
|
||||||
# Этот блок вообще не под basic_auth, поэтому анонимный клиент
|
# Этот блок вообще не под basic_auth, поэтому анонимный клиент
|
||||||
# тем более может прислать свой X-Authenticated-User. Сейчас
|
# тем более может прислать свой X-Authenticated-User. Сейчас
|
||||||
|
|
@ -442,7 +405,6 @@ meraocenka.ru {
|
||||||
handle @meraPages {
|
handle @meraPages {
|
||||||
rewrite * /trade-in/mera-public{path}
|
rewrite * /trade-in/mera-public{path}
|
||||||
reverse_proxy tradein-frontend:3000 {
|
reverse_proxy tradein-frontend:3000 {
|
||||||
import tradein_frontend_retry
|
|
||||||
header_up -X-Authenticated-User
|
header_up -X-Authenticated-User
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -609,7 +571,6 @@ meraocenka.ru {
|
||||||
respond @foreignRouteChunk 404
|
respond @foreignRouteChunk 404
|
||||||
|
|
||||||
reverse_proxy tradein-frontend:3000 {
|
reverse_proxy tradein-frontend:3000 {
|
||||||
import tradein_frontend_retry
|
|
||||||
header_up -X-Authenticated-User
|
header_up -X-Authenticated-User
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -620,7 +581,6 @@ meraocenka.ru {
|
||||||
handle /favicon.ico {
|
handle /favicon.ico {
|
||||||
rewrite * /trade-in/favicon.ico
|
rewrite * /trade-in/favicon.ico
|
||||||
reverse_proxy tradein-frontend:3000 {
|
reverse_proxy tradein-frontend:3000 {
|
||||||
import tradein_frontend_retry
|
|
||||||
header_up -X-Authenticated-User
|
header_up -X-Authenticated-User
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -634,7 +594,6 @@ meraocenka.ru {
|
||||||
handle /robots.txt {
|
handle /robots.txt {
|
||||||
rewrite * /trade-in/robots.txt
|
rewrite * /trade-in/robots.txt
|
||||||
reverse_proxy tradein-frontend:3000 {
|
reverse_proxy tradein-frontend:3000 {
|
||||||
import tradein_frontend_retry
|
|
||||||
header_up -X-Authenticated-User
|
header_up -X-Authenticated-User
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -654,7 +613,6 @@ meraocenka.ru {
|
||||||
handle /og-mera.png {
|
handle /og-mera.png {
|
||||||
rewrite * /trade-in/og-mera.png
|
rewrite * /trade-in/og-mera.png
|
||||||
reverse_proxy tradein-frontend:3000 {
|
reverse_proxy tradein-frontend:3000 {
|
||||||
import tradein_frontend_retry
|
|
||||||
header_up -X-Authenticated-User
|
header_up -X-Authenticated-User
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -666,7 +624,6 @@ meraocenka.ru {
|
||||||
handle /logo-mera.png {
|
handle /logo-mera.png {
|
||||||
rewrite * /trade-in/logo-mera.png
|
rewrite * /trade-in/logo-mera.png
|
||||||
reverse_proxy tradein-frontend:3000 {
|
reverse_proxy tradein-frontend:3000 {
|
||||||
import tradein_frontend_retry
|
|
||||||
header_up -X-Authenticated-User
|
header_up -X-Authenticated-User
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Гейт: подмена tradein-frontend не возвращает окно недоступности (#3274).
|
|
||||||
|
|
||||||
ПОЧЕМУ. Публичный лендинг meraocenka.ru лежал 30–90 с на КАЖДОМ деплое МЕРЫ.
|
|
||||||
Причина — НЕ медленная подмена контейнера (она стоит полсекунды), а то, что
|
|
||||||
`docker compose up -d` со СПИСКОМ сервисов работает в две фазы: сначала create
|
|
||||||
(старый контейнер каждого сервиса останавливается и удаляется — иначе занято
|
|
||||||
`container_name`), потом start, в порядке зависимостей и с ожиданием их
|
|
||||||
условий. Всё, что между фазами, фронт лежит.
|
|
||||||
|
|
||||||
Замер на проде 10.09 (docker inspect .Created/.StartedAt, два деплоя подряд):
|
|
||||||
|
|
||||||
пачка сервисов: tradein-backend создан 15:01:40 → запущен 15:02:10 (30 с)
|
|
||||||
один сервис: tradein-frontend создан 16:42:17.5 → запущен 16:42:18.0 (0,5 с)
|
|
||||||
|
|
||||||
В логе Caddy у первого деплоя три 503 на лендинге (15:01:46, 15:01:52,
|
|
||||||
15:02:06), у второго — ни одного.
|
|
||||||
|
|
||||||
ЧТО ДЕРЖИТ РЕШЕНИЕ, И ПОЧЕМУ ЭТО ГЕЙТ, А НЕ КОММЕНТАРИЙ. Половинки лежат в
|
|
||||||
разных файлах и обе невидимо обратимы:
|
|
||||||
|
|
||||||
1) deploy-tradein.yml — `frontend` ВЫНЕСЕН из общего `up -d $SERVICES` в
|
|
||||||
свою команду. Достаточно дописать его обратно в SERVICES «за компанию»,
|
|
||||||
и окно вернётся целиком, молча: деплой останется зелёным.
|
|
||||||
2) caddy/sites/apps.caddy — каждый `reverse_proxy tradein-frontend:3000`
|
|
||||||
импортирует (tradein_frontend_retry) (lb_try_duration), который добирает
|
|
||||||
оставшиеся ~0,5 с. Новый публичный путь копируют с соседнего блока — и
|
|
||||||
если копируют блок БЕЗ импорта, именно этот путь снова отдаёт 502.
|
|
||||||
|
|
||||||
Оба отказа не видны ни по статусу джобы, ни по глазам: их видно только
|
|
||||||
непрерывной пробой во время деплоя (scripts/probe-deploy-window.sh).
|
|
||||||
|
|
||||||
ЧЕГО НЕ ЛОВИТ. Это разбор текста, а не исполнение: гейт не проверяет, что
|
|
||||||
команда реально отработала и что Caddy реально ретраит (это проверяется
|
|
||||||
пробой на живом деплое). Не смотрит на сервисы, кроме frontend, — бэкенду
|
|
||||||
ретрай намеренно не дан (его старт длиннее, чем разумное ожидание клиента).
|
|
||||||
|
|
||||||
Запуск: python3 scripts/check-frontend-swap-window.py [--selftest]
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
|
||||||
WORKFLOW = REPO / ".forgejo" / "workflows" / "deploy-tradein.yml"
|
|
||||||
APPS_CADDY = REPO / "caddy" / "sites" / "apps.caddy"
|
|
||||||
|
|
||||||
SNIPPET_NAME = "tradein_frontend_retry"
|
|
||||||
FRONTEND_UPSTREAM = "reverse_proxy tradein-frontend:3000 {"
|
|
||||||
|
|
||||||
# `SERVICES="browser backend tgbot"` / `SERVICES="$SERVICES scraper"`.
|
|
||||||
_SERVICES_RE = re.compile(r'^\s*SERVICES=(["\']?)(.*?)\1\s*$', re.M)
|
|
||||||
# Одиночная команда подмены фронта.
|
|
||||||
_FRONTEND_UP_RE = re.compile(r"up -d --no-deps frontend\s*$", re.M)
|
|
||||||
|
|
||||||
|
|
||||||
def strip_comments(text: str) -> str:
|
|
||||||
"""Убирает строки-комментарии (shell/YAML/Caddy — везде `#`).
|
|
||||||
|
|
||||||
Обязательно: разбор дефекта живёт в комментарии рядом с правкой и содержит
|
|
||||||
его же формулировку. Без этого гейт спорил бы с собственным объяснением.
|
|
||||||
"""
|
|
||||||
return "\n".join(ln for ln in text.splitlines() if not ln.lstrip().startswith("#"))
|
|
||||||
|
|
||||||
|
|
||||||
def check_workflow(text: str) -> list[str]:
|
|
||||||
body = strip_comments(text)
|
|
||||||
errors: list[str] = []
|
|
||||||
|
|
||||||
for m in _SERVICES_RE.finditer(body):
|
|
||||||
value = m.group(2)
|
|
||||||
if re.search(r"(^|\s)frontend(\s|$)", value):
|
|
||||||
errors.append(
|
|
||||||
f'SERVICES={value!r} снова содержит `frontend`: он вернётся в общий '
|
|
||||||
f"`up -d` со списком сервисов, а это и есть окно 30–90 с (#3274). "
|
|
||||||
f"Фронт подменяется отдельной командой `up -d --no-deps frontend`."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not _FRONTEND_UP_RE.search(body):
|
|
||||||
errors.append(
|
|
||||||
"в deploy-tradein.yml нет отдельной команды `up -d --no-deps frontend` — "
|
|
||||||
"подмена фронта либо пропала, либо снова уехала в общую пачку (#3274)."
|
|
||||||
)
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def check_caddy(text: str) -> list[str]:
|
|
||||||
errors: list[str] = []
|
|
||||||
lines = text.splitlines()
|
|
||||||
body = strip_comments(text)
|
|
||||||
|
|
||||||
if f"({SNIPPET_NAME}) {{" not in body:
|
|
||||||
errors.append(
|
|
||||||
f"снипет ({SNIPPET_NAME}) не объявлен в apps.caddy — импортировать нечего."
|
|
||||||
)
|
|
||||||
elif "lb_try_duration" not in body:
|
|
||||||
errors.append(
|
|
||||||
f"снипет ({SNIPPET_NAME}) есть, но без `lb_try_duration` — он больше "
|
|
||||||
"ничего не добирает, оставшиеся ~0,5 с подмены снова видны как 502."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Блок апстрима фронта: от строки `reverse_proxy tradein-frontend:3000 {`
|
|
||||||
# до закрывающей скобки на её же отступе. Импорт должен быть внутри.
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if line.strip().startswith("#") or FRONTEND_UPSTREAM not in line:
|
|
||||||
continue
|
|
||||||
indent = len(line) - len(line.lstrip())
|
|
||||||
block: list[str] = []
|
|
||||||
for nxt in lines[i + 1 :]:
|
|
||||||
if nxt.strip() == "}" and (len(nxt) - len(nxt.lstrip())) == indent:
|
|
||||||
break
|
|
||||||
block.append(nxt)
|
|
||||||
if not any(f"import {SNIPPET_NAME}" in b for b in block if not b.strip().startswith("#")):
|
|
||||||
errors.append(
|
|
||||||
f"apps.caddy:{i + 1} — `reverse_proxy tradein-frontend:3000` без "
|
|
||||||
f"`import {SNIPPET_NAME}`: на этом пути подмена контейнера снова "
|
|
||||||
f"видна посетителю как 502 (#3274)."
|
|
||||||
)
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def selftest() -> None:
|
|
||||||
good_wf = 'SERVICES="browser backend tgbot"\n docker compose up -d --no-deps frontend\n'
|
|
||||||
assert check_workflow(good_wf) == [], check_workflow(good_wf)
|
|
||||||
|
|
||||||
bad_wf = 'SERVICES="browser backend frontend tgbot"\n docker compose up -d --no-deps frontend\n'
|
|
||||||
assert any("SERVICES" in e for e in check_workflow(bad_wf)), "не поймал frontend в SERVICES"
|
|
||||||
|
|
||||||
missing_wf = 'SERVICES="browser backend tgbot"\n docker compose up -d --no-deps $SERVICES\n'
|
|
||||||
assert any("отдельной команды" in e for e in check_workflow(missing_wf)), (
|
|
||||||
"не поймал пропажу отдельной команды"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Комментарий с той же формулировкой не должен ронять гейт.
|
|
||||||
commented = '# SERVICES="browser backend frontend tgbot" # так было до #3274\n' + good_wf
|
|
||||||
assert check_workflow(commented) == [], "гейт спорит с собственным комментарием"
|
|
||||||
|
|
||||||
good_caddy = (
|
|
||||||
"(tradein_frontend_retry) {\n lb_try_duration 2s\n}\n"
|
|
||||||
"handle {\n reverse_proxy tradein-frontend:3000 {\n"
|
|
||||||
" import tradein_frontend_retry\n header_up -X-Y\n }\n}\n"
|
|
||||||
)
|
|
||||||
assert check_caddy(good_caddy) == [], check_caddy(good_caddy)
|
|
||||||
|
|
||||||
bad_caddy = (
|
|
||||||
"(tradein_frontend_retry) {\n lb_try_duration 2s\n}\n"
|
|
||||||
"handle {\n reverse_proxy tradein-frontend:3000 {\n"
|
|
||||||
" header_up -X-Y\n }\n}\n"
|
|
||||||
)
|
|
||||||
assert any("без `import" in e for e in check_caddy(bad_caddy)), "не поймал блок без импорта"
|
|
||||||
|
|
||||||
no_snippet = (
|
|
||||||
"handle {\n reverse_proxy tradein-frontend:3000 {\n"
|
|
||||||
" import tradein_frontend_retry\n }\n}\n"
|
|
||||||
)
|
|
||||||
assert any("не объявлен" in e for e in check_caddy(no_snippet)), "не поймал пропажу снипета"
|
|
||||||
|
|
||||||
print("SELFTEST OK")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
if "--selftest" in sys.argv:
|
|
||||||
selftest()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
errors = check_workflow(WORKFLOW.read_text(encoding="utf-8"))
|
|
||||||
errors += check_caddy(APPS_CADDY.read_text(encoding="utf-8"))
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
print("Гейт #3274 (окно подмены фронта) НЕ ПРОЙДЕН:\n")
|
|
||||||
for e in errors:
|
|
||||||
print(f" ✗ {e}")
|
|
||||||
print(
|
|
||||||
"\nЧем проверять эффект на живом деплое: scripts/probe-deploy-window.sh "
|
|
||||||
"(запускать НА хосте прода, см. шапку скрипта)."
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print("Гейт #3274: фронт подменяется отдельной командой, все 9 апстримов с ретраем — OK")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# Проба окна недоступности при деплое (#3274).
|
|
||||||
#
|
|
||||||
# ЗАЧЕМ. Статус джобы деплоя зелёный независимо от того, видел ли посетитель
|
|
||||||
# ошибку: подмена контейнера происходит ВНУТРИ успешного прогона. Единственный
|
|
||||||
# честный замер — непрерывный опрос публичного адреса во время деплоя и подсчёт
|
|
||||||
# САМОЙ ДЛИННОЙ СЕРИИ подряд идущих не-200. Приёмка #3274 сформулирована именно
|
|
||||||
# так (комментарий от 05.09): «серия не-2xx/000 на / в окне мержа < 2 с».
|
|
||||||
#
|
|
||||||
# ГДЕ ЗАПУСКАТЬ — НА САМОМ ХОСТЕ (ssh poincare), не с ноутбука. Боевой периметр
|
|
||||||
# отбивает частые серии с одного внешнего адреса: замер 11.09 дал 30–50 %
|
|
||||||
# `000` с внешнего IP при интервале 0 и ноль при 2 с, а с самого хоста — 0/10
|
|
||||||
# без пауз. Иначе защита периметра читается как простой сервиса.
|
|
||||||
#
|
|
||||||
# ПОЧЕМУ 000 СЧИТАЕТСЯ ОТДЕЛЬНО. curl отдаёт `000`, когда HTTP-ответа не было
|
|
||||||
# вообще (TCP/TLS не встал, таймаут). Это может быть и простой (Caddy
|
|
||||||
# пересоздан), и отбой периметра — сваливать его в одну кучу с 502/503 нельзя,
|
|
||||||
# иначе своя же защита попадёт в числа простоя.
|
|
||||||
#
|
|
||||||
# Запуск:
|
|
||||||
# scripts/probe-deploy-window.sh [URL] [ИНТЕРВАЛ_С] [ДЛИТЕЛЬНОСТЬ_С] [ФАЙЛ]
|
|
||||||
# scripts/probe-deploy-window.sh --summary <ФАЙЛ> # пересчитать сводку
|
|
||||||
# scripts/probe-deploy-window.sh --selftest # проверка счётчика серий
|
|
||||||
#
|
|
||||||
# Пример (окно деплоя, 10 минут с шагом 200 мс):
|
|
||||||
# ssh poincare 'nohup /opt/gendesign/scripts/probe-deploy-window.sh \
|
|
||||||
# https://meraocenka.ru/ 0.2 600 /tmp/probe-mera.tsv >/dev/null 2>&1 &'
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
# Сводка по TSV (epoch<TAB>ISO-время<TAB>код<TAB>секунды): сколько чего, и главное — самая
|
|
||||||
# длинная серия подряд идущих не-200 в СЕКУНДАХ (по меткам времени самих
|
|
||||||
# образцов, а не «число образцов × интервал»: curl с таймаутом растягивает шаг,
|
|
||||||
# и умножение занизило бы реальную длину окна).
|
|
||||||
summarize() {
|
|
||||||
awk -F'\t' '
|
|
||||||
# Серия закрывается первым 200. Длина в секундах считается ТОЛЬКО в END:
|
|
||||||
# средний шаг известен лишь после прохода, а внутри цикла он ещё 0 — из-за
|
|
||||||
# этого первая версия занижала окно ровно на один шаг (поймал --selftest).
|
|
||||||
{ total++; code[$3]++
|
|
||||||
if ($3 == "200") {
|
|
||||||
if (streak > max_n) { max_n = streak; max_first = first_bad_ts; max_last = last_bad_ts; max_at = first_bad_at }
|
|
||||||
streak = 0
|
|
||||||
} else {
|
|
||||||
if (!streak) { first_bad_ts = $1; first_bad_at = $2 }
|
|
||||||
streak++; last_bad_ts = $1
|
|
||||||
}
|
|
||||||
if (prev_ts && $1 - prev_ts < 60) { gaps += $1 - prev_ts; gapn++ }
|
|
||||||
prev_ts = $1
|
|
||||||
}
|
|
||||||
END {
|
|
||||||
step = (gapn ? gaps / gapn : 0)
|
|
||||||
if (streak > max_n) { max_n = streak; max_first = first_bad_ts; max_last = last_bad_ts; max_at = first_bad_at }
|
|
||||||
printf "образцов: %d, шаг ~%.2f с\n", total, step
|
|
||||||
for (c in code) printf " %s: %d\n", c, code[c]
|
|
||||||
printf "максимальная серия не-200: %d образцов ≈ %.1f с (начало %s)\n", \
|
|
||||||
max_n, (max_n ? max_last - max_first + step : 0), (max_at ? max_at : "-")
|
|
||||||
}
|
|
||||||
' "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "${1:-}" = "--summary" ]; then
|
|
||||||
summarize "$2"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${1:-}" = "--selftest" ]; then
|
|
||||||
tmp=$(mktemp)
|
|
||||||
# 10 образцов с шагом 1 с: три подряд не-200 (2-я…4-я секунды) → серия ≈ 3 с.
|
|
||||||
printf '1000\tT0\t200\t0.01\n1001\tT1\t502\t0.01\n1002\tT2\t000\t5.00\n1003\tT3\t503\t0.01\n1004\tT4\t200\t0.01\n1005\tT5\t200\t0.01\n1006\tT6\t502\t0.01\n1007\tT7\t200\t0.01\n' >"$tmp"
|
|
||||||
out=$(summarize "$tmp")
|
|
||||||
rm -f "$tmp"
|
|
||||||
echo "$out"
|
|
||||||
echo "$out" | grep -q "максимальная серия не-200: 3 образцов ≈ 3.0 с" || {
|
|
||||||
echo "SELFTEST FAILED: серия посчитана неверно" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
echo "$out" | grep -q " 000: 1" || { echo "SELFTEST FAILED: 000 не выделен отдельно" >&2; exit 1; }
|
|
||||||
echo "SELFTEST OK"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
URL=${1:-https://meraocenka.ru/}
|
|
||||||
INTERVAL=${2:-1}
|
|
||||||
DURATION=${3:-900}
|
|
||||||
OUT=${4:-/tmp/probe-deploy-window.tsv}
|
|
||||||
|
|
||||||
: >"$OUT"
|
|
||||||
echo "проба: $URL каждые ${INTERVAL}с в течение ${DURATION}с → $OUT"
|
|
||||||
end=$(( $(date +%s) + DURATION ))
|
|
||||||
while [ "$(date +%s)" -lt "$end" ]; do
|
|
||||||
# --max-time 5: зависший запрос не должен растягивать шаг пробы на минуты.
|
|
||||||
# -o /dev/null: тело не нужно, важны код и время.
|
|
||||||
line=$(curl -sS -o /dev/null --max-time 5 -w '%{http_code}\t%{time_total}' "$URL" 2>/dev/null || echo "000 0")
|
|
||||||
printf '%s\t%s\t%s\n' "$(date +%s.%N)" "$(date -u +%H:%M:%SZ)" "$line" >>"$OUT"
|
|
||||||
sleep "$INTERVAL"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "── сводка ─────────────────────────────────────────"
|
|
||||||
summarize "$OUT"
|
|
||||||
Loading…
Add table
Reference in a new issue