**낡은 테스트**: test_handler_skips_record_on_negative_cache_hit 는 '캐시 히트면 이력을 남기지 않는다'를 검증했는데, 그 동작은 실측 버그였다 — 잡은 DONE 인데 price_history 에 새 행이 없어 이를 폴링하는 소비자(negodata 최저가 모달)가 결과를 영영 못 받고 로딩만 돌았다. 코드는 이미 '캐시 히트도 이 잡의 결과이므로 기록한다'로 고쳐져 있었고 테스트만 남아 있었다. → 현재 계약(not_found 스냅샷 1건 기록, 가격은 null)을 검증하도록 다시 씀. 전체 220 passed·0 failed. **오픈API 어댑터 제거**: shop.json 이 2026-07-31 종료돼 404 SE05 만 반환하고, 파이프라인은 naver_shop(크롤)로 옮겨 갔다. 되살릴 수 없는 코드를 남겨두면 다음 사람이 "키를 넣으면 되나" 하고 시간을 쓴다. - services/search/naver/ (adapter·transform) 삭제 - NaverConfig 모델·로더·설정 섹션 3개 파일에서 제거(죽은 키) - test_naver_transform 삭제, test_alerts 는 NaverAdapter 대신 스텁 사용 (검증 대상인 recent_stats/_note_result 는 베이스 SearchAdapter 계약이라 무관) **문서 정합화**: architecture(네이버 안티봇=WTM, 통과 3조건) · api(배송비가 이제 채워짐, 가격은 즉시판매가·쿠폰가 제외) · operations(kr_host·naver_ip_request_budget) · README 트리. source 이름 "naver" 는 그대로다 — price_history·by_mall·프론트 계약은 구현 교체와 무관하다.
95 lines
5.4 KiB
Python
95 lines
5.4 KiB
Python
import os
|
||
|
||
from config.config_loader import Configs
|
||
from config.config_models import (
|
||
WebServerConfig, LogConfig, MainDBConfig, NaverApiHubConfig, OpenAIConfig, DecodoConfig,
|
||
WorkerConfig, AlertConfig,
|
||
)
|
||
|
||
# ── 설정 소스는 config.local.toml 하나다(도메인 backend 와 동일 — 서버마다 그 서버의 값, 미커밋). ──
|
||
# 운영 전제: 항상 APP_ENV=local 로 띄운다 → config.local.toml (호스트 실행·pytest·docker 모두 동일).
|
||
# prod 서버도 그 서버의 config.local.toml 에 prod 값(시크릿·guard키·스케일)을 채우고 `docker compose up -d`.
|
||
# env 는 실행 입력과 '환경별로 바뀌는 접속점'만 담당한다:
|
||
# DB_HOST/PORT/… : DB 접속 override — config.local.toml 은 127.0.0.1(호스트 실행·pytest) 유지하고,
|
||
# 컨테이너에선 compose 가 DB_HOST=host.docker.internal 로 호스트 DB 를 가리킨다(_apply_db_env_override).
|
||
# PROCESS_COUNT : 실행 스크립트·부하벤치의 대화형 입력(uvicorn 워커 수 임시 override)
|
||
# WORKER_CONCURRENCY : 워커 실행 스크립트의 대화형 입력(worker_main 이 읽음)
|
||
# LPS_LIVE : 라이브 스모크 테스트 옵트인(설정이 아니라 실행 스위치)
|
||
# 시크릿(네이버·OpenAI·DECODO)은 env 로 넣지 않는다 — config.local.toml 에 두고 컨테이너엔 마운트한다.
|
||
APP_ENV = os.environ.get("APP_ENV", "local")
|
||
|
||
_config_dir = os.path.dirname(__file__)
|
||
_config_file = os.path.join(_config_dir, f"config.{APP_ENV}.toml")
|
||
|
||
if not os.path.exists(_config_file):
|
||
raise FileNotFoundError(
|
||
f"설정 파일이 없습니다: {_config_file} (APP_ENV={APP_ENV}). "
|
||
"config.local.toml 을 준비하세요(config.local.toml.example 복사). 컨테이너는 compose 가 이 파일을 마운트합니다."
|
||
)
|
||
|
||
configs = Configs(_config_file)
|
||
|
||
web_server_config: WebServerConfig = configs.get(WebServerConfig)
|
||
log_config: LogConfig = configs.get(LogConfig)
|
||
main_db_config: MainDBConfig = configs.get(MainDBConfig)
|
||
# 섹션이 없으면 기본값(빈/비활성)으로 동작.
|
||
naver_api_hub_config: NaverApiHubConfig = configs.get(NaverApiHubConfig) or NaverApiHubConfig()
|
||
openai_config: OpenAIConfig = configs.get(OpenAIConfig) or OpenAIConfig()
|
||
decodo_config: DecodoConfig = configs.get(DecodoConfig) or DecodoConfig()
|
||
worker_config: WorkerConfig = configs.get(WorkerConfig) or WorkerConfig()
|
||
alert_config: AlertConfig = configs.get(AlertConfig) or AlertConfig()
|
||
|
||
|
||
# DB 접속 env override — config.local.toml 의 접속 정보를 '환경별로 바뀌는 값만' 덮는다.
|
||
# 로컬(호스트 uvicorn·pytest)은 env 미설정 → toml(127.0.0.1) 그대로. 컨테이너는 compose 가
|
||
# DB_HOST=host.docker.internal 을 넣어 호스트 DB 를 가리킨다(도메인 backend 와 동일한 패턴).
|
||
# 값이 빈 문자열이면 override 하지 않는다 → 관리형 DB 는 LPS_DB_HOST= 로 비워 config.local.toml 의 호스트를 쓴다.
|
||
def _apply_db_env_override(cfg: MainDBConfig):
|
||
if os.environ.get("DB_HOST"):
|
||
cfg.write_host = cfg.read_host = os.environ["DB_HOST"]
|
||
if os.environ.get("DB_PORT"):
|
||
cfg.write_port = cfg.read_port = int(os.environ["DB_PORT"])
|
||
if os.environ.get("DB_USER"):
|
||
cfg.write_id = cfg.read_id = os.environ["DB_USER"]
|
||
if os.environ.get("DB_PASSWORD"):
|
||
cfg.write_pw = cfg.read_pw = os.environ["DB_PASSWORD"]
|
||
if os.environ.get("DB_NAME"):
|
||
cfg.name = os.environ["DB_NAME"]
|
||
# 커넥션 예산도 env 로 조정 가능(자동 산정 입력) — 전용/공유 PG 에 맞춰 서버별로.
|
||
if os.environ.get("DB_CONNECTION_BUDGET"):
|
||
cfg.connection_budget = int(os.environ["DB_CONNECTION_BUDGET"])
|
||
|
||
|
||
def _autosize_pool(cfg: MainDBConfig, process_count: int):
|
||
"""process_count 기반 커넥션 풀 자동 산정.
|
||
|
||
실제 동시 커넥션 = (pool_size + max_overflow) × 2엔진(R/W) × process_count.
|
||
이 값이 connection_budget 를 넘지 않도록 pool_size/max_overflow 를 역산한다.
|
||
budget<=0 이면 비활성(toml 의 pool_size/max_overflow 그대로 사용).
|
||
|
||
반환: 자동 산정을 수행했으면 (pool_size, max_overflow), 아니면 None.
|
||
"""
|
||
budget = cfg.connection_budget
|
||
if budget <= 0:
|
||
return None
|
||
pc = max(1, process_count)
|
||
# 워커·엔진당 커넥션 = 예산 / (2엔진 × process_count). 최소 1(pool_size>=1) 보장.
|
||
# (예산 준수가 최우선 — process_count 가 너무 크면 엔진당 1커넥션까지 줄여서라도 예산을 넘지 않는다)
|
||
per_engine = max(1, budget // (2 * pc))
|
||
# 정상(pool) 60% + 버스트(overflow) 40% 로 분할.
|
||
cfg.pool_size = max(1, round(per_engine * 0.6))
|
||
cfg.max_overflow = max(0, per_engine - cfg.pool_size)
|
||
return cfg.pool_size, cfg.max_overflow
|
||
|
||
|
||
# DB 접속점 override(호스트↔컨테이너) — 자동 산정(예산) 전에 적용해야 budget override 가 반영된다.
|
||
_apply_db_env_override(main_db_config)
|
||
|
||
# uvicorn 워커 수 — 실행 스크립트/부하벤치의 대화형 입력만 env 로 임시 override(설정은 toml 이 소스).
|
||
if os.environ.get("PROCESS_COUNT"):
|
||
web_server_config.process_count = int(os.environ["PROCESS_COUNT"])
|
||
|
||
# 커넥션 풀 자동 산정: process_count(위에서 확정) 기준으로 예산 안에 맞춘다.
|
||
# → 멀티워커 배포 시 풀 오버서브스크립션(→커넥션 고갈)을 config 가 스스로 방지.
|
||
_pool_autosized = _autosize_pool(main_db_config, web_server_config.process_count)
|