From c72e510fb0d0aa29dd2886c5b1d90d21687d225e Mon Sep 17 00:00:00 2001 From: PTah Date: Mon, 6 Jul 2026 12:00:37 +1000 Subject: [PATCH] =?UTF-8?q?fix:=20defer=20daily=20report=20push=20=D0=B8?= =?UTF-8?q?=20=D1=83=D0=B2=D0=B5=D0=BB=D0=B8=D1=87=D0=B8=D1=82=D1=8C=20poo?= =?UTF-8?q?l=20=D0=91=D0=94=20(0.4.12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FCM/каналы для report.daily.* после commit ingest (BackgroundTasks); SAC_DB_POOL_SIZE=15, SAC_DB_MAX_OVERFLOW=25 для штурма 09:00. Co-authored-by: Cursor --- backend/app/api/v1/events.py | 10 ++++++++-- backend/app/config.py | 2 ++ backend/app/database.py | 6 +++++- backend/app/services/notify_dispatch.py | 17 +++++++++++++++++ backend/app/version.py | 2 +- backend/tests/test_ingest.py | 2 +- deploy/env.native.example | 3 +++ 7 files changed, 37 insertions(+), 5 deletions(-) diff --git a/backend/app/api/v1/events.py b/backend/app/api/v1/events.py index 2f6ea78..b0b4734 100644 --- a/backend/app/api/v1/events.py +++ b/backend/app/api/v1/events.py @@ -2,7 +2,7 @@ from datetime import datetime, timezone from typing import Any -from fastapi import APIRouter, Body, Depends, HTTPException, Query +from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException, Query from fastapi.responses import JSONResponse from pydantic import BaseModel from sqlalchemy import func, select @@ -51,6 +51,7 @@ from app.services.notify_dispatch import ( notify_lifecycle, notify_problem, notify_rdg_connection, + schedule_notify_daily_report, ) router = APIRouter(prefix="/events", tags=["events"]) @@ -79,6 +80,7 @@ def _ingest_response(event: Event, *, created: bool) -> IngestResponse: @router.post("", response_model=IngestResponse) def post_event( + background_tasks: BackgroundTasks, payload: dict[str, Any] = Body(...), db: Session = Depends(get_db), _api_key: str = Depends(get_api_key_auth), @@ -96,11 +98,12 @@ def post_event( event, created = ingest_event(db, payload) problem = None problem_created = False + deferred_daily_report_id: int | None = None if created: problem, problem_created = maybe_create_problem(db, event) maybe_auto_disconnect_stuck_rdp_session(db, event) if event.type in DAILY_REPORT_EVENT_TYPES: - notify_daily_report(event, db=db) + deferred_daily_report_id = event.id elif event.type == LIFECYCLE_EVENT_TYPE: notify_lifecycle(event, db=db) elif event.type in AUTH_LOGIN_SUCCESS_TYPES: @@ -116,6 +119,9 @@ def post_event( logger.info("ingest duplicate event_id=%s", event.event_id) db.commit() + if deferred_daily_report_id is not None: + background_tasks.add_task(schedule_notify_daily_report, deferred_daily_report_id) + body = _ingest_response(event, created=created) if problem is not None: body.problem_id = problem.id diff --git a/backend/app/config.py b/backend/app/config.py index f416d77..94c57d4 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -34,6 +34,8 @@ class Settings(BaseSettings): model_config = _settings_config() database_url: str = "postgresql+psycopg2://sac:sac@localhost:5432/sac" + sac_db_pool_size: int = 15 + sac_db_max_overflow: int = 25 sac_public_url: str = "http://localhost:8000" jwt_secret: str = "change-me-in-production" jwt_algorithm: str = "HS256" diff --git a/backend/app/database.py b/backend/app/database.py index d109cc2..8bbd10c 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -6,7 +6,11 @@ from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker from app.config import get_settings settings = get_settings() -engine = create_engine(settings.database_url, pool_pre_ping=True) +_engine_kwargs: dict = {"pool_pre_ping": True} +if settings.database_url.startswith("postgresql"): + _engine_kwargs["pool_size"] = max(1, int(settings.sac_db_pool_size)) + _engine_kwargs["max_overflow"] = max(0, int(settings.sac_db_max_overflow)) +engine = create_engine(settings.database_url, **_engine_kwargs) SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False) diff --git a/backend/app/services/notify_dispatch.py b/backend/app/services/notify_dispatch.py index 06756ca..b1fb200 100644 --- a/backend/app/services/notify_dispatch.py +++ b/backend/app/services/notify_dispatch.py @@ -142,3 +142,20 @@ def notify_rdg_connection(event: Event, *, db: Session | None = None) -> None: if not should_notify_event(event, db): return _dispatch_lifecycle_channels(event, db=db, policy=policy) + + +def schedule_notify_daily_report(event_db_id: int) -> None: + """Отложенное оповещение по суточному отчёту (после commit ingest, вне горячего POST).""" + from app.database import SessionLocal + + db = SessionLocal() + try: + event = db.get(Event, event_db_id) + if event is None: + logger.warning("deferred daily report notify: event id=%s not found", event_db_id) + return + notify_daily_report(event, db=db) + except Exception: + logger.exception("deferred daily report notify failed event_db_id=%s", event_db_id) + finally: + db.close() diff --git a/backend/app/version.py b/backend/app/version.py index 664d6fd..4b34410 100644 --- a/backend/app/version.py +++ b/backend/app/version.py @@ -1,5 +1,5 @@ """Единый источник версии SAC (API, health, логи, OpenAPI).""" APP_NAME = "Security Alert Center" -APP_VERSION = "0.4.11" +APP_VERSION = "0.4.12" APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}" diff --git a/backend/tests/test_ingest.py b/backend/tests/test_ingest.py index d678b51..99dfed2 100644 --- a/backend/tests/test_ingest.py +++ b/backend/tests/test_ingest.py @@ -126,7 +126,7 @@ def test_ingest_daily_report_calls_notify_daily_report(client, auth_headers): "summary": "stats", "details": {"generated_by": "agent", "report_body": "line1"}, } - with patch.object(events_api, "notify_daily_report") as mock_daily: + with patch.object(events_api, "schedule_notify_daily_report") as mock_daily: with patch.object(events_api, "notify_event") as mock_event: r = client.post("/api/v1/events", json=payload, headers=auth_headers) assert r.status_code == 201 diff --git a/deploy/env.native.example b/deploy/env.native.example index b066bca..af87725 100644 --- a/deploy/env.native.example +++ b/deploy/env.native.example @@ -4,6 +4,9 @@ # systemd НЕ парсит этот файл — читает приложение (SAC_CONFIG_FILE). DATABASE_URL=postgresql+psycopg2://sac:CHANGE_ME_POSTGRES_PASSWORD@127.0.0.1:5432/sac +# SQLAlchemy pool (uvicorn workers × concurrent ingest). Было по умолчанию 5+10 — мало для штурма 09:00. +SAC_DB_POOL_SIZE=15 +SAC_DB_MAX_OVERFLOW=25 SAC_PUBLIC_URL=https://sac.kalinamall.ru JWT_SECRET=CHANGE_ME_openssl_rand_hex_32