Skip to content

feat(S15P11A705-96): dev 배포 게이트 3종 — /ready · GMS_BASE_URL fail-fast · 양방향 스모크 - #33

Merged
colosair merged 1 commit into
mainfrom
feat/S15P11A705-96-dev-deployment-gates
Jul 29, 2026
Merged

feat(S15P11A705-96): dev 배포 게이트 3종 — /ready · GMS_BASE_URL fail-fast · 양방향 스모크#33
colosair merged 1 commit into
mainfrom
feat/S15P11A705-96-dev-deployment-gates

Conversation

@colosair

Copy link
Copy Markdown
Member

요약

인프라(@tpals0409)가 dev 배포 activation 조건으로 요청한 게이트 3종을 구현합니다 — GET /ready 신설, GMS_BASE_URL 형식 startup fail-fast, embedding+judge 양방향 스모크 명령. 요청 원문은 #32 §2·§3이며 이 PR이 그 회신입니다.

셋은 같은 결함 하나를 서로 다른 시점에서 막습니다 — "틀린 설정으로도 서버가 정상 기동하고 첫 실사용 요청에서야 실패한다"(#32 리뷰 ①·②).

시점 게이트 잡는 것
기동 Settings._gms_base_url_shape GMS_BASE_URL/gmsapi/ 세그먼트 누락
배포 직후 python -m app.smoke.gms_roundtrip 인증 오류 · 네트워크 도달 실패 · 모델 미존재
운영 중 GET /ready 기동 후 끊긴 DB · 비어 있는 Preset 캐시

GET /health는 정적 {"status":"ok"} 그대로 둡니다 — liveness/startup 전용이라는 #32 합의사항이며, 회귀 테스트로 고정했습니다. DB 스키마·도메인 코드·의존성 변경 없습니다(현재 9개 유지).

Jira (필수)

관련 GitHub Issue (선택)

변경 사항

구현

  • app/api/probe.py (신규) — GET /ready. DB 커넥션 획득 후 SELECT 1 + Preset 캐시 ≥ 1건 → 200 {"status":"ready"} / 503 {"status":"not_ready"}. GMS를 호출하지 않고, 응답은 status 한 필드뿐입니다. 예외는 원인 무관하게 not_ready로 환원하되 로그에 예외 타입 이름만 남깁니다 — asyncpg 예외 메시지에 DSN이 섞여 들어올 수 있습니다.
  • app/core/config.pyGMS_PATH_SEGMENT 상수, SettingsError, _gms_base_url_shape model_validator. 세그먼트가 없으면 기동을 중단하며 에러 메시지에 값을 넣지 않습니다(아래 리뷰 포인트 1).
  • app/smoke/gms_roundtrip.py (신규) — python -m app.smoke.gms_roundtrip. embedding 1회 + judge 1회 실호출, 한쪽 실패해도 나머지를 실행하고 최종 exit 1. DB 접근 없음. 출력은 검사명과 ok/failed (예외타입)뿐이며 httpx·httpcore 로거를 차단합니다(httpx는 요청마다 INFO로 전체 URL을 남깁니다).
  • app/main.py — probe 라우터 등록(prefix 없음 → 무인증). /health 핸들러는 미변경, 용도를 못박는 주석만 추가.

테스트 (14건 추가, pytest -q 52 → 66 passed)

  • tests/test_api.py (+6) — /ready 200 · 503(캐시 0건) · 503(DB 끊김) · 무인증 · 값 미노출 · /health 불변 회귀. 픽스처를 _api_client(db, settings, preset_rows) 헬퍼로 추출해 캐시 적재 여부만 다른 api/ready_api 두 픽스처로 나눴습니다(기존 테스트 동작 불변).
  • tests/test_unit.py (+8) — GMS_BASE_URL 거부 2 · 수용 1 · 에러 메시지 값 미노출 1 · 스모크 집계 3 · 출력·종료 코드 1.

문서

  • docs/implements/2026-07-29-dev-deployment-gates.md (신규, I23) — 판단 근거·실측·인프라 전달 사항.
  • docs/implements/README.md · docs/WORKLOG.md — I23 등재. 번호는 main과 열린 PR(#27이 P44 선점)을 확인해 미사용 값 사용.
  • docs/spec/integration-tests.md · tests/README.md — 테스트 수 52→66 정합, 계층 표에 신규 축 추가, "외부 실호출을 CI에 넣지 않는다" 컨벤션 명문화.
  • README.md · .env.example — 프로브·스모크 표, GMS_BASE_URL 형식 요구 주석.

테스트 / 검증

  • ruff check . — exit 0
  • pytest -q66 passed, exit 0 (기존 52 → +14). 로컬 Docker + Testcontainers pgvector 0.8.1-pg16
  • python -m compileall app tools — exit 0
  • DB 변경 시 pgvector(PostgreSQL) 통합 확인 — DB 스키마 변경은 없으나, /ready 테스트는 실제 pgvector 커넥션 풀로 200/503 양 분기를 검증
  • 계약 변경 시 관련 문서 갱신 — Team-PinLog/docs static/05 변경 없음(엔드포인트 추가는 내부 프로브라 공용 계약 밖). 본 레포 docs/ 갱신 완료

실측 (수동, 로컬 실 GMS + 실 pgvector 27 preset)

항목 명령 결과
/ready 실기동 uvicorn app.main:app --port 8011curl /ready 200 {"status":"ready"}
/health 불변 curl /health 200 {"status":"ok"}
스모크 정상 python -m app.smoke.gms_roundtrip embedding: ok / judge: ok / exit 0
스모크 비대칭 실패 위 + PINLOG_JUDGE_MODEL=<미존재 모델> embedding: ok / judge: failed (TransientError) / exit 1
URL fail-fast GMS_BASE_URL=<세그먼트 없는 값> python -c "import app.main" SettingsError 기동 중단, 메시지에 값 없음

실행하지 않은 것 — 스모크의 실호출 자동 테스트. 외부 의존이라 _CHECKS를 스텁으로 교체해 집계·종료 코드·값 미노출 규약만 CI에서 검증하고, 실제 GMS 왕복은 위 수동 실측으로 대신했습니다. 실호출을 CI에 넣으면 GMS 가용성이 CI 성패에 들어옵니다. tests/README.md에 이 판단을 명문화했습니다.

배경

GMS_BASE_URL 하나를 두 클라이언트가 다르게 소비합니다 — 임베딩은 {URL}/embeddings를 그대로 붙이고, 판정은 URL.split("/gmsapi/")[0] + "/gmsapi"로 root를 파생합니다. 세그먼트가 빠지면 임베딩은 정상 동작하고 judge만 조용히 실패합니다. 기동 시 GMS로 요청을 보내지 않고 /health가 상수 응답이라, 지금까지는 이 상태로도 서버가 정상 기동했습니다.

형식 검증만으로는 부족합니다 — 인증 오류·도달 실패·모델 미존재는 형식이 맞아도 발생합니다. 그래서 스모크가 실호출로 나머지를 덮고, 인프라가 이 스모크 성공을 activation 조건으로 걸었습니다(#32 §3).

@tpals0409 님이 확인해 주신 대로 현재 live AI runtime Secret이 없어 기존 값과의 호환성 문제가 없으므로, 형식 검증을 유예 없이 startup fail-fast로 바로 넣었습니다(#32 코멘트 §2).

리뷰 포인트

  1. ValueError 대신 SettingsError(RuntimeError)를 쓴 이유 — pydantic은 ValueError/AssertionError만 가로채 ValidationError로 감싸는데, 그때 input_value를 메시지에 실어 넣습니다. pydantic 2.13.4 실측:

    방식 메시지에 실리는 것
    field_validator + ValueError input_value='https://…'endpoint 전체 노출
    model_validator(after) + ValueError input_value={'secret': 'SENTINEL-SECR…원시 입력 dict 앞부분 노출
    model_validator(after) + SettingsError 우리가 쓴 문장만

    기동 실패 메시지는 배포 파이프라인 로그에 남으므로 세 번째를 택했습니다. test_gms_base_url_error_carries_no_values가 이 성질을 고정합니다. 다만 pydantic 관례에서 벗어나는 선택이라 이견 있으면 말씀해 주세요.

  2. /ready가 Profile을 재조회하지 않는 것 — 캐시는 lifespan이 settings.embedding_profile로 조회한 행만 담으므로(main.py) 건수 ≥ 1이 곧 "현재 Profile 기준 ≥ 1건"입니다. 별도 쿼리는 같은 사실을 두 번 묻는 것이라 넣지 않았습니다. 계약 문구("현재 profile preset cache ≥ 1건")를 이렇게 읽은 것이 맞는지 확인 부탁드립니다.

  3. 스모크가 첫 실패에서 끊지 않는 것 — 이 명령이 존재하는 이유가 비대칭 장애라, 한 번 실행으로 어느 쪽이 죽었는지 알아야 한다고 판단했습니다. 종료 코드는 마지막에 한 번만 판정합니다.

  4. 검증 범위를 /gmsapi/ 세그먼트 하나로 한정한 것 — scheme·host 형식은 검사하지 않습니다. 그쪽 오류는 스모크가 실호출로 잡는 편이 확실하고, 검증을 늘리면 정상 값을 막을 위험만 커집니다.

미결 / 후속

  • _profile_consistency의 값 노출 — 기존 Profile 검증기는 여전히 ValueError 경로라, Profile 불일치로 기동이 깨지면 리뷰 포인트 1과 같은 방식으로 원시 입력 dict 앞부분이 로그에 남습니다. 이번 티켓 범위 밖이라 손대지 않았습니다. 별도 티켓 후보입니다.
  • /gmsapi/ 리터럴 이중화llm_client._rootconfig.GMS_PATH_SEGMENT가 각자 리터럴입니다. 통합은 client 계층 변경이라 범위 밖으로 뒀습니다.
  • 외부 API retry/error classificationS15P11A705-121. #32에서 dev 배포 blocker 제외로 합의됐습니다.
  • 인프라 전달 잔여 — 이 PR 병합 후 (1) immutable source SHA + image digest, (2) sealed-secrets cert로 생성한 strict-scope ai-owner-secrets encrypted artifact를 #32에 전달하겠습니다.

인프라 배선 요청 (@tpals0409)

  • readiness probe → GET /ready, startup·liveness probe → GET /health
  • activation 전 컨테이너에서 python -m app.smoke.gms_roundtrip 실행 → exit 0 확인
  • GMS_BASE_URL/gmsapi/ 세그먼트 포함 형식이어야 기동됩니다(AI owner가 Secret에 등록)

…· 양방향 스모크

인프라가 dev 배포 activation 조건으로 요청한 3종(ai#32 §2·§3).

- GET /ready — DB SELECT 1 + 현재 profile preset 캐시 ≥1건.
  200 {"status":"ready"} / 503 {"status":"not_ready"}. GMS 미호출,
  무인증(/internal/ 밖), 응답·로그에 값 미노출. /health는 정적 200 유지.
- Settings._gms_base_url_shape — GMS_BASE_URL의 /gmsapi/ 세그먼트 누락 시
  기동 중단. 세그먼트가 빠지면 임베딩만 살고 judge가 조용히 실패한다.
  pydantic ValueError 경로는 ValidationError가 input_value를 메시지에 실어
  credential 앞부분을 노출하므로 SettingsError(RuntimeError)를 쓴다.
- app.smoke.gms_roundtrip — embedding 1회 + judge 1회 실호출.
  한쪽 실패해도 나머지를 실행하고 최종 exit 1. DB 접근 없음.
  출력은 검사명과 ok/failed(예외 타입)뿐이며 httpx 로깅을 차단한다.

테스트 14건 추가(pytest 52 → 66 passed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@colosair
colosair requested a review from tpals0409 July 29, 2026 01:25
@colosair
colosair marked this pull request as ready for review July 29, 2026 01:30
@colosair
colosair merged commit 7e31398 into main Jul 29, 2026
2 checks passed
@colosair
colosair deleted the feat/S15P11A705-96-dev-deployment-gates branch July 29, 2026 01:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant