fix: Phase 5 진단 핫픽스 + Phase 6 run_sql 안전 가드
진단 보고서(plans/...phase5-사용자체크리스트.md) 기반 7건 코드 이슈
수정 + Phase 6 잔여 항목 중 최우선인 run_sql 가드 구현.
핫픽스:
- nl2sql_worker.py: _list_drawings 파싱 버그(문자열 분리) HIGH
- nl2sql_worker.py: 5개 async 함수 blocking DB 연결 → to_thread MED
- ExperionDbContext.cs: KB DDL의 {} 문자가 String.Format placeholder로
오인되어 부팅 실패 → 별도 NpgsqlCommand 사용 HIGH
- KbIngestWorker: 단일 청크 임베딩 실패 시 전체 abort → 부분 인덱싱 LOW
- KbAuthService: 초기 비번 로그 평문 → 마스킹 + 콘솔 분리 출력 LOW
- KbQdrantClient: new HttpClient → IHttpClientFactory LOW
- OllamaController: plant_context.md 매 요청 파일 읽기 → mtime 캐시 LOW
Phase 6 — run_sql 가드:
- _validate_sql 강화: \b 단어 경계로 updated_at 오탐 제거, WITH 허용,
TRUNCATE/COPY 추가, 다중 세미콜론 차단
- _apply_sql_guards: LIMIT 미지정 시 SELECT * FROM (...) _capped LIMIT 1000
- _execute_sql_internal: 매 호출 SET statement_timeout = 30000
- SQL_MAX_ROWS / SQL_STATEMENT_TIMEOUT_MS 환경변수화
- 응답 JSON에 row_limit 필드 추가
- nl2sql_worker.py의 _run_sql / _query_with_nl에도 동일 적용
기타:
- .gitignore: storage/ 추가 (KB 업로드 원본 디렉토리)
- opencode.json: 모델 항목을 실제 서빙 모델(Qwen3.6-27B-FP8 / 256K)로 동기화
검증:
- dotnet build: 경고 0건, 에러 0건
- python3 -m py_compile: OK
- _apply_sql_guards / _validate_sql 스모크 테스트 통과
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -139,4 +139,150 @@
|
||||
- run_sql 안전장치 (자동 LIMIT, statement_timeout)도 Phase 6
|
||||
|
||||
---
|
||||
막힘이 있는 단계가 있으면 어디서 멈췄는지 알려주시면 함께 보겠습니다.
|
||||
막힘이 있는 단계가 있으면 어디서 멈췄는지 알려주시면 함께 보겠습니다.
|
||||
|
||||
1. KbStorageService.SaveAsync — SHA256 TransformBlock 사용 (MED)
|
||||
문제: SHA256.TransformBlock() / TransformFinalBlock()은 .NET 8에서 Deprecated되었으며, 스트리밍 입력에서 단일 패스에서 SHA256 계산 시 버퍼 경계 처리가 정확하지 않을 수 있다. 특히 input.ReadAsync(buffer.AsMemory(0, buffer.Length))로 읽은 데이터를 TransformBlock(buffer, 0, read, null, 0)에 넘기는데, TransformBlock은 내부 버퍼링을 하며 마지막 블록에서 TransformFinalBlock을 호출해야 하는데, 여기서는 빈 배열을 전달한다. 이 자체는 동작하지만 SHA256.Hash가 TransformFinalBlock 호출 후에만 유효하므로 로직상 문제는 없다. 하지만 SHA256.Create()의 인스턴스가 using 처리되지 않아 리소스 누수 가능성이 있다.
|
||||
|
||||
근거: KbStorageService.cs:36 — using var sha = SHA256.Create(); → 실제로는 using이 적용되어 있음.
|
||||
|
||||
교차 검증: 실제 코드를 다시 확인하니 using var sha = SHA256.Create();로 되어 있고 TransformFinalBlock(Array.Empty<byte>(), 0, 0) 호출 후 sha.Hash를 참조함. Q1 통과 — 이미 올바르게 구현됨. 보고서에서 제거.
|
||||
|
||||
2. KbIngestWorker — FOR UPDATE SKIP LOCKED 미사용 (MED)
|
||||
문제: 플랜 3.7절에서 "FOR UPDATE SKIP LOCKED 큐 폴링(2초)"으로 명시되어 있으나, 실제 구현은 WHERE j.FinishedAt == null && j.Stage == "parse" && j.Attempts < _maxAttempts + OrderBy(j => j.EnqueuedAt).FirstOrDefaultAsync()로 단순 SELECT 후 업데이트 패턴이다. 동시에 여러 인스턴스가 실행되면 같은 잡을 중복으로 처리할 수 있다.
|
||||
|
||||
근거: KbIngestWorker.cs:74-77
|
||||
|
||||
교차 검증:
|
||||
|
||||
Q2: 다른 레이어에서 처리되는가? → Program.cs에서 KbIngestWorker는 단일 AddHostedService로 등록됨. 단일 인스턴스만 실행되므로 동시성 문제는 발생하지 않음.
|
||||
Q3: 의도적 설계인가? → 플랜에는 SKIP LOCKED가 명시되었으나, 단일 인스턴스 아키텍처에서는 불필요함.
|
||||
Q2/Q3 통과 — 보고서에서 제거.
|
||||
|
||||
3. KbQdrantClient — HttpClient 직접 생성 (LOW)
|
||||
문제: KbQdrantClient 생성자에서 new HttpClient를 직접 생성하고 있다. .NET best practice는 IHttpClientFactory를 사용하는 것이다. 현재 Singleton으로 등록되므로 단일 인스턴스에서는 문제가 없으나, using 처리되지 않아Dispose 누수 가능성이 있다.
|
||||
|
||||
근거: KbQdrantClient.cs:21 — new HttpClient { BaseAddress = new Uri(baseUrl), Timeout = TimeSpan.FromSeconds(30) }
|
||||
|
||||
교차 검증:
|
||||
|
||||
Q2: KbQdrantClient가 Singleton으로 등록되어 수명주기 전체에서 하나의 HttpClient를 사용하므로 소켓 고갈 문제는 없음.
|
||||
Q4: 실제 장애 시나리오? → 앱 종료 시 HttpClient가 명시적으로 Dispose되지 않지만, 프로세스 종료 시 OS가 정리하므로 실제 영향 없음.
|
||||
Q2/Q4 통과 → LOW 강등
|
||||
|
||||
3. KbQdrantClient — HttpClient 직접 생성 (LOW)
|
||||
문제: KbQdrantClient.cs:21 — new HttpClient 직접 생성. IHttpClientFactory 미사용.
|
||||
|
||||
영향: Singleton 등록이므로 단일 인스턴스에서는 소켓 고갈 없음. 하지만 명시적 Dispose가 없어 이론적 리소스 누수. 프로세스 종료 시 정리됨.
|
||||
|
||||
수정: Program.cs에서 AddHttpClient("KbQdrant") 팩토리 등록 후 KbQdrantClient에 주입
|
||||
|
||||
4. nl2sql_worker.py — 비동기 함수 내 blocking DB 연결 (MED)
|
||||
문제: _run_sql, _query_pv_history, _get_tag_metadata, _list_drawings, _query_with_nl 함수들이 async def로 선언되어 있으나, 내부에서 _get_db_connection()을 asyncio.to_thread 없이 직접 호출한다. psycopg3의 connect()는 blocking TCP 호출이며, 이벤트 루프를 블로킹한다.
|
||||
|
||||
근거: nl2sql_worker.py:208-210 — conn = _get_db_connection() (async 함수 내에서 blocking)
|
||||
|
||||
교차 검증:
|
||||
|
||||
Q1: 이미 수정된 문제인가? → server.py의 _get_db_connection()은 asyncio.to_thread로 감싸져 있으나, nl2sql_worker.py는 별도 워커로 독립 구현되어 있고 여기서는 감싸지 않음.
|
||||
Q2: 다른 레이어에서 처리되는가? → 이 워커는 FastAPI + uvicorn으로 독립 실행되며, /execute 엔드포인트가 HTTP 요청을 처리한다. 이벤트 루프 블로킹은 동시 HTTP 처리 성능에 영향을 줄 수 있음.
|
||||
Q4: 실제 시나리오? → 여러 HTTP 요청이 동시 도착 시 DB 연결 대기 중 이벤트 루프 블로킹으로 다른 요청 지연.
|
||||
Q1/Q2/Q4 통과 — 유지
|
||||
|
||||
4. nl2sql_worker.py — 비동기 함수 내 blocking DB 연결 (MED)
|
||||
문제: async def 함수 내에서 psycopg.connect()를 asyncio.to_thread 없이 직접 호출하여 이벤트 루프 블로킹.
|
||||
|
||||
근거: nl2sql_worker.py:210 (_run_sql), 238 (_query_pv_history), 270 (_get_tag_metadata), 303 (_list_drawings), 346 (_query_with_nl)
|
||||
|
||||
영향: 동시 HTTP 요청이 여러 개 들어올 때 DB 연결 대기 중 이벤트 루프 블로킹 → 응답 지연
|
||||
|
||||
수정: 각 함수에서 conn = await asyncio.to_thread(_get_db_connection)으로 변경
|
||||
|
||||
5. nl2sql_worker.py:_list_drawings — 데이터 파싱 버그 (HIGH)
|
||||
문제: cur.fetchall() 결과가 (name,) 튜플인데, dict(zip(columns, row[0]))로 처리하고 있다. row[0]는 문자열(name 컬럼 값)이며, 문자열은 이터러블이므로 각 문자가 개별 값으로 분리되어 {"name": "f"} 같은 잘못된 결과가 생성된다.
|
||||
|
||||
근거: nl2sql_worker.py:327 — data = [dict(zip(columns, row[0])) for row in rows]
|
||||
|
||||
교차 검증:
|
||||
|
||||
Q1: 이미 수정된 문제인가? → 확인함. 아직 수정되지 않음.
|
||||
Q4: 실제 시나리오? → list_drawings 도구 호출 시 반환된 names 배열이 각 문자로 분리된 객체가 됨 → 프론트엔드에서 렌더링 불가.
|
||||
Q1/Q4 통과 — 유지
|
||||
|
||||
5. nl2sql_worker.py:_list_drawings — 데이터 파싱 버그 (HIGH)
|
||||
문제: row[0]가 문자열인데 dict(zip(columns, row[0]))로 전달 → 문자열이 문자 단위 이터레이션되어 잘못된 dict 생성.
|
||||
|
||||
근거: nl2sql_worker.py:327
|
||||
|
||||
영향: list_drawings 도구 호출 시 names 필드가 {"name": "f"} 같은 잘못된 데이터 반환
|
||||
|
||||
수정: dict(zip(columns, row)) 또는 단순히 [row[0] for row in rows]로 변경
|
||||
|
||||
6. OllamaController — plant_context.md 매 요청마다 파일 읽기 (LOW)
|
||||
문제: LoadPlantContext()는 매 호출마다 File.ReadAllText()를 수행한다. ComposeSystemPrompt()는 모든 채팅 요청마다 호출되며, 이는 불필요한 디스크 I/O를 발생시킨다.
|
||||
|
||||
근거: OllamaController.cs:76-89 — LoadPlantContext(), 호출 위치 cs:147
|
||||
|
||||
교차 검증:
|
||||
|
||||
Q3: 의도적 설계인가? → 파일 변경을 실시간으로 반영하기 위한 것으로 보이나, plant_context.md는 운영 중 자주 변경되지 않음.
|
||||
Q4: 실제 장애 시나리오? → 파일 읽기는 빠르지만 고빈도 채팅 시 누적 오버헤드. 캐싱으로 개선 가능하나 동작에는 영향 없음.
|
||||
Q3/Q4 → LOW 강등
|
||||
|
||||
6. OllamaController — plant_context.md 매 요청 파일 읽기 (LOW)
|
||||
문제: LoadPlantContext()가 ComposeSystemPrompt() 호출마다 File.ReadAllText() 실행.
|
||||
|
||||
근거: OllamaController.cs:76-89
|
||||
|
||||
영향: 고빈도 채팅 시 불필요한 디스크 I/O. 동작에는 영향 없음.
|
||||
|
||||
수정: Lazy<string> 또는 MemoryCache로 캐싱 (파일 변경 감지 옵션)
|
||||
|
||||
7. KbAuthService.EnsureCredentialAsync — 자동 생성 비밀번호 로깅 (LOW)
|
||||
문제: 자동 생성된 초기 비밀번호가 _logger.LogWarning()로 평문 로깅된다. 로그 파일이 적절히 보호되지 않은 환경에서는 비밀번호 노출 위험이 있다.
|
||||
|
||||
근거: KbAuthService.cs:64 — _logger.LogWarning("[Kb] 관리자 초기 비밀번호 자동 생성: {Pw} ← 즉시 변경하세요", initial)
|
||||
|
||||
교차 검증:
|
||||
|
||||
Q3: 의도적 설계인가? → 환경변수가 설정되지 않은 경우 사용자에게 비밀번호를 전달할 방법이 로그밖에 없으므로 의도적임.
|
||||
Q4: 실제 시나리오? → 로그 파일이 여러 사용자에게 노출되는 환경에서만 문제.
|
||||
Q3/Q4 → LOW 강등
|
||||
|
||||
7. KbAuthService — 자동 생성 비밀번호 평문 로깅 (LOW)
|
||||
문제: 자동 생성 비밀번호가 로그에 평문으로 기록됨.
|
||||
|
||||
근거: KbAuthService.cs:64
|
||||
|
||||
영향: 로그 파일 접근 권한이 느슨한 환경에서 비밀번호 노출. 한 번만 기록되며 즉시 변경 요구.
|
||||
|
||||
수정: 부분 마스킹({Pw[:4]}****) 또는 콘솔 출력 전용
|
||||
|
||||
8. KbIngestWorker — 임베딩 실패 시 전체 실패 (LOW)
|
||||
문제: 단일 청크의 임베딩 실패(vec == null) 시 전체 문서 인덱싱이 실패한다. 부분 실패 처리가 없어 일부 청크만 문제가 있어도 전체가 재시도된다.
|
||||
|
||||
근거: KbIngestWorker.cs:123 — if (vec == null) throw new Exception("임베딩 실패(부분)")
|
||||
|
||||
교차 검증:
|
||||
|
||||
Q3: 의도적 설계인가? → 임베딩 모델(Ollama)이 응답하지 않는 상태에서는 부분 인덱싱보다 전체 실패가 더 안전한 선택.
|
||||
Q4: 재시도 로직이 attempts 기반이므로 3회 시도 후 failed 상태로 전환됨.
|
||||
Q3/Q4 → LOW 강등
|
||||
|
||||
8. KbIngestWorker — 단일 청크 임베딩 실패 시 전체 실패 (LOW)
|
||||
문제: 청크 1개 임베딩 실패 시 전체 문서 인덱싱 abort.
|
||||
|
||||
근거: KbIngestWorker.cs:123
|
||||
|
||||
영향: 대용량 문서에서 일부 청크만 문제 있어도 전체 재시도. 3회 시도 후 failed 전환.
|
||||
|
||||
수정: 실패 청크 제외하고 나머지 인덱싱 진행 (선택적)
|
||||
|
||||
요약
|
||||
# 심각도 항목 상태
|
||||
5 🔴 HIGH nl2sql_worker.py:_list_drawings 데이터 파싱 버그 수정 필요
|
||||
4 🟠 MED nl2sql_worker.py blocking DB 연결 (5개 함수) 수정 필요
|
||||
3 🟡 LOW KbQdrantClient HttpClient 직접 생성 개선 권장
|
||||
6 🟡 LOW OllamaController plant_context.md 매 요청 파일 읽기 개선 권장
|
||||
7 🟡 LOW 자동 생성 비밀번호 평문 로깅 개선 권장
|
||||
8 🟡 LOW 단일 청크 임베딩 실패 시 전체 실패 개선 권장
|
||||
Reference in New Issue
Block a user