diff --git a/apps/frontend/app/providers.tsx b/apps/frontend/app/providers.tsx index 1c5efc3..a659759 100644 --- a/apps/frontend/app/providers.tsx +++ b/apps/frontend/app/providers.tsx @@ -9,12 +9,12 @@ import { AppToastProvider } from '@/components/id/toast-provider'; export function Providers({ children }: { children: React.ReactNode }) { return ( - - - + + + {children} - - + + ); } diff --git a/apps/frontend/lib/api.ts b/apps/frontend/lib/api.ts index 9efc54e..ca53ac5 100644 --- a/apps/frontend/lib/api.ts +++ b/apps/frontend/lib/api.ts @@ -680,8 +680,6 @@ export function buildSystemSettingPayload(setting: Pick { let gatewayReadyResolved = false; let gatewayReadyPromise: Promise | null = null; +let stableGatewayPromise: Promise | null = null; let gatewayReadyAt = 0; let activeApiRequests = 0; const apiRequestWaiters: Array<() => void> = []; @@ -703,11 +702,13 @@ const apiRequestWaiters: Array<() => void> = []; export function resetApiGatewayWarmup() { gatewayReadyResolved = false; gatewayReadyPromise = null; + stableGatewayPromise = null; } export function invalidateApiGatewayReady() { gatewayReadyResolved = false; gatewayReadyAt = 0; + stableGatewayPromise = null; if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent(API_NOT_READY_EVENT)); } @@ -745,26 +746,34 @@ export function subscribeApiNotReady(listener: () => void): () => void { /** Несколько подряд успешных /health через nginx — защита от «один OK, потом 502». */ export async function waitForStableGateway(stableProbes = 3): Promise { if (typeof window === 'undefined') return; + if (gatewayReadyResolved && stableProbes <= 1) return; + if (stableGatewayPromise) return stableGatewayPromise; - gatewayReadyResolved = false; - gatewayReadyAt = 0; + stableGatewayPromise = (async () => { + gatewayReadyResolved = false; + gatewayReadyAt = 0; - let streak = 0; - for (let attempt = 0; attempt < GATEWAY_STABLE_MAX_ROUNDS && streak < stableProbes; attempt += 1) { - if (await probeGatewayAlive()) { - streak += 1; - if (streak >= stableProbes) { - markApiGatewayReady(); - return; + let streak = 0; + for (let attempt = 0; attempt < GATEWAY_STABLE_MAX_ROUNDS && streak < stableProbes; attempt += 1) { + if (await probeGatewayAlive()) { + streak += 1; + if (streak >= stableProbes) { + markApiGatewayReady(); + return; + } + await sleep(GATEWAY_STABLE_PROBE_INTERVAL_MS); + } else { + streak = 0; + await sleep(Math.min(900 + attempt * 300, 2800)); } - await sleep(GATEWAY_STABLE_PROBE_INTERVAL_MS); - } else { - streak = 0; - await sleep(Math.min(900 + attempt * 300, 2800)); } - } - throw new ApiError('Сервер API временно недоступен. Попробуйте обновить страницу.', 503, 'GATEWAY_UNAVAILABLE'); + throw new ApiError('Сервер API временно недоступен. Попробуйте обновить страницу.', 503, 'GATEWAY_UNAVAILABLE'); + })().finally(() => { + stableGatewayPromise = null; + }); + + return stableGatewayPromise; } async function acquireApiSlot(): Promise { @@ -799,23 +808,7 @@ async function probeGatewayAlive(): Promise { } } -async function waitForGatewayAlive(): Promise { - if (gatewayReadyResolved) return; - - for (let attempt = 0; attempt < GATEWAY_PROBE_MAX_ATTEMPTS; attempt += 1) { - if (await probeGatewayAlive()) { - markApiGatewayReady(); - return; - } - if (attempt + 1 < GATEWAY_PROBE_MAX_ATTEMPTS) { - await sleep(Math.min(GATEWAY_PROBE_BASE_DELAY_MS + attempt * 280, 2800)); - } - } - - throw new ApiError('Сервер API временно недоступен. Попробуйте обновить страницу.', 503, 'GATEWAY_UNAVAILABLE'); -} - -/** Один общий прогрев: все apiFetch ждут успешный /health, без раннего «fail-open». */ +/** Один общий прогрев: все apiFetch ждут стабильный /health через nginx. */ export async function ensureApiGatewayReady(force = false): Promise { if (typeof window === 'undefined') return; if (gatewayReadyResolved && !force) return; @@ -823,9 +816,12 @@ export async function ensureApiGatewayReady(force = false): Promise { if (force) { gatewayReadyResolved = false; + gatewayReadyAt = 0; + stableGatewayPromise = null; } - gatewayReadyPromise = waitForGatewayAlive().finally(() => { + const probes = force ? 2 : 1; + gatewayReadyPromise = waitForStableGateway(probes).finally(() => { gatewayReadyPromise = null; }); @@ -848,7 +844,7 @@ async function fetchWithGatewayRetry(url: string, init: RequestInit): Promise/dev/null | grep -q '\"status\"' || exit 1", + ] + interval: 15s timeout: 5s - retries: 3 - - # После recreate api-gateway nginx держит stale DNS upstream → 502. Watcher - # делает nginx -s reload когда gateway снова поднялся (~55 с на Nest bootstrap). - nginx-gateway-sync: - profiles: ["proxy"] - image: docker:27-cli - container_name: lendry-id-nginx-gateway-sync - restart: unless-stopped - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - ./infra/nginx-gateway-sync/entrypoint.sh:/entrypoint.sh:ro - depends_on: - nginx: - condition: service_started - entrypoint: ["/bin/sh", "/entrypoint.sh"] + retries: 5 + start_period: 30s volumes: postgres_data: diff --git a/infra/nginx-gateway-sync/entrypoint.sh b/infra/nginx-gateway-sync/entrypoint.sh deleted file mode 100644 index ee2ca8d..0000000 --- a/infra/nginx-gateway-sync/entrypoint.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/sh -set -eu - -NGINX_CONTAINER="${NGINX_CONTAINER:-lendry-id-nginx}" -GATEWAY_CONTAINER="${GATEWAY_CONTAINER:-lendry-id-api-gateway}" -BOOT_WAIT_SEC="${BOOT_WAIT_SEC:-55}" -GATEWAY_HEALTH_URL="http://${GATEWAY_CONTAINER}:3000/health" - -reload_nginx() { - if docker exec "$NGINX_CONTAINER" nginx -s reload 2>/dev/null; then - echo "[nginx-gateway-sync] nginx reload OK" - else - echo "[nginx-gateway-sync] nginx reload failed — пробуем restart" - docker restart "$NGINX_CONTAINER" >/dev/null 2>&1 || true - fi -} - -verify_gateway() { - if docker exec "$NGINX_CONTAINER" wget -qO- --timeout=5 "$GATEWAY_HEALTH_URL" 2>/dev/null \ - | grep -q '"status"'; then - echo "[nginx-gateway-sync] nginx → ${GATEWAY_CONTAINER}:3000/health OK" - else - echo "[nginx-gateway-sync] предупреждение: nginx пока не достучался до ${GATEWAY_HEALTH_URL}" - fi -} - -on_gateway_start() { - echo "[nginx-gateway-sync] ${GATEWAY_CONTAINER} перезапущен, ждём Nest (${BOOT_WAIT_SEC}s)..." - sleep "$BOOT_WAIT_SEC" - reload_nginx - sleep 2 - verify_gateway -} - -echo "[nginx-gateway-sync] слежение за ${GATEWAY_CONTAINER} (boot wait ${BOOT_WAIT_SEC}s)..." - -# Первый запуск стека: gateway уже healthy, но nginx мог стартовать раньше — синхронизируем. -( - sleep "$BOOT_WAIT_SEC" - reload_nginx - sleep 2 - verify_gateway -) & - -docker events \ - --filter "container=${GATEWAY_CONTAINER}" \ - --filter event=start \ - --format '{{.Time}}' | while read -r _; do - on_gateway_start -done diff --git a/install.sh b/install.sh index 1fbf4ec..0a3cfb7 100644 --- a/install.sh +++ b/install.sh @@ -10,7 +10,7 @@ set -euo pipefail -SCRIPT_VERSION="2.4.2" +SCRIPT_VERSION="2.5.0" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT_DIR" @@ -426,30 +426,27 @@ recreate_docker_nginx() { compose_uses_proxy_profile || return 0 build_compose_stack_cmd log "Пересоздание контейнера lendry-id-nginx (подхват конфигов из ./nginx/conf.d)..." - docker_cmd "${COMPOSE_STACK_CMD[@]}" up -d --force-recreate nginx nginx-gateway-sync + docker_cmd "${COMPOSE_STACK_CMD[@]}" up -d --force-recreate nginx wait_for_service "nginx" 60 } -ensure_nginx_gateway_sync() { - compose_uses_proxy_profile || return 0 - build_compose_stack_cmd - log "Запуск nginx-gateway-sync (авто-reload Nginx после пересборки api-gateway)..." - docker_cmd "${COMPOSE_STACK_CMD[@]}" up -d nginx-gateway-sync -} - post_deploy_proxy_verify() { local strict="${1:-0}" compose_uses_proxy_profile || return 0 - wait_for_service "nginx" 30 - ensure_nginx_gateway_sync + wait_for_service "nginx" 60 wait_for_api_grpc_ready 90 || { [[ "$strict" -eq 1 ]] && fail "api-gateway → sso-core gRPC не готов (/health/ready). Проверьте: docker compose logs sso-core api-gateway" warn "api-gateway → sso-core gRPC не готов (/health/ready)" } - sync_nginx_after_api_gateway_restart + log "Проверка Docker Nginx → api-gateway (DNS + /idp-api)..." + reload_docker_nginx + sleep 2 verify_nginx_reaches_api_gateway 24 5 || { - [[ "$strict" -eq 1 ]] && fail "Docker Nginx не достучался до api-gateway:3000 — см. docker compose logs nginx api-gateway" - warn "Docker Nginx → api-gateway: проверка не прошла" + reload_docker_nginx + verify_nginx_reaches_api_gateway 12 5 || { + [[ "$strict" -eq 1 ]] && fail "Docker Nginx не достучался до api-gateway:3000 — см. docker compose logs nginx api-gateway" + warn "Docker Nginx → api-gateway: проверка не прошла" + } } verify_nginx_reaches_media_ws || true if [[ "$(env_get USE_NGINX_SSL false)" == "true" ]]; then @@ -460,6 +457,32 @@ post_deploy_proxy_verify() { fi } +# Поэтапный запуск: nginx стартует ПОСЛЕ healthy api-gateway — без 502 на первом запросе. +deploy_phased_stack() { + log "Фаза 1/5: PostgreSQL, Redis, RabbitMQ, MinIO, LDAP..." + compose_build_and_up postgres redis rabbitmq minio ldap-auth + wait_for_service "ldap-auth" 90 + + log "Фаза 2/5: sso-core..." + SKIP_DOCKER_PRUNE=1 + compose_build_and_up sso-core + wait_for_service "sso-core" 120 + + log "Фаза 3/5: api-gateway..." + compose_build_and_up api-gateway + wait_for_service "api-gateway" 90 + wait_for_api_grpc_ready 90 || warn "gRPC api-gateway → sso-core не готов — API может отдавать 502" + + log "Фаза 4/5: frontend, docs, media-ws..." + compose_build_and_up frontend docs media-ws + wait_for_service "frontend" 90 + wait_for_service "docs" 60 + + log "Фаза 5/5: Docker Nginx (после готовности backend)..." + compose_build_and_up nginx + post_deploy_proxy_verify 1 +} + prepare_compose_stack() { resolve_nginx_mode_and_ports write_compose_override @@ -2050,10 +2073,6 @@ nginx_upstream_var() { printf 'idp_up_%s' "$(printf '%s' "$1" | tr -c 'A-Za-z0-9' '_')" } -nginx_api_uses_static_ip() { - return 1 -} - # proxy_pass к КОРНЮ апстрима ($request_uri сохраняет путь и query). # docker: resolver + переменная (перерезолв DNS после recreate api-gateway). # host: статический proxy_pass на 127.0.0.1. @@ -2904,16 +2923,18 @@ deploy_stack() { verify_postgres_auth log "Сборка и запуск контейнеров (10–20 минут при первом запуске)..." - compose_build_and_up - recreate_host_port_services - ensure_host_upstreams - - wait_for_service "sso-core" 120 - wait_for_service "api-gateway" 90 - wait_for_api_grpc_ready 60 || warn "gRPC api-gateway → sso-core не готов — API может отдавать 502" - wait_for_service "frontend" 60 - wait_for_service "docs" 60 - post_deploy_proxy_verify 1 + if compose_uses_proxy_profile; then + deploy_phased_stack + else + compose_build_and_up + recreate_host_port_services + ensure_host_upstreams + wait_for_service "sso-core" 120 + wait_for_service "api-gateway" 90 + wait_for_api_grpc_ready 60 || warn "gRPC api-gateway → sso-core не готов — API может отдавать 502" + wait_for_service "frontend" 60 + wait_for_service "docs" 60 + fi } save_install_state() { @@ -3343,6 +3364,7 @@ docker_fresh_purge() { build_compose_stack_cmd log "Остановка стека и удаление volumes (PostgreSQL, Redis, MinIO, RabbitMQ)..." + docker_cmd rm -f lendry-id-nginx-gateway-sync 2>/dev/null || true docker_cmd "${COMPOSE_STACK_CMD[@]}" down -v --remove-orphans 2>/dev/null || true docker_cmd compose --env-file .env down -v --remove-orphans 2>/dev/null || true docker_cmd compose --env-file .env --profile proxy down -v --remove-orphans 2>/dev/null || true @@ -3609,7 +3631,6 @@ action_fix_all_errors() { esac apply_intranet_selfsigned_runtime recreate_docker_nginx - ensure_nginx_gateway_sync sync_nginx_after_api_gateway_restart verify_nginx_reaches_api_gateway 24 5 || compose_build_and_up api-gateway sync_nginx_after_api_gateway_restart