v1.11.10
Relio v1.11.10 — 분석이 많이 쌓인 계정에서 Signal·Risk·Insight·Recommendation 단건 조회가 "not found" 로 끝나던 문제
Signal, Risk, Insight, Recommendation은 목록에는 보이는데 항목을 열면 "not found"가 나고, 무시·수용·기각도 같은 이유로 실패할 수 있었습니다. 분석 결과가 가장 많이 쌓이는 바쁜 계정에서만 나타나던 문제입니다.
1. 원인
단건 조회 네 곳이 목록 질의를 대신 부르고 그 결과를 Go에서 훑었습니다.
items, err := s.ListSignals(ctx, p, SignalFilter{Status: "ALL", Limit: 200})
for _, item := range items {
if item.ID == id {
return item, nil
}
}
return Signal{}, errors.New("signal not found")목록은 최대 200건이고, 정렬 기준은 심각도(Signal, Risk)와 점수·우선순위(Insight, Recommendation)입니다. 즉 200건을 넘는 계정에서 그 뒤에 밀린 항목은 이 코드가 볼 수 없는 곳에 있었고, 존재하지 않는 것과 똑같이 취급되었습니다.
읽기만 막힌 것이 아닙니다. IgnoreSignal, AcceptRisk, AcceptRecommendation, DismissRecommendation은 모두 쓰기 전에 대상 레코드를 한 번 읽습니다. 그래서 조언과 위험을 처리하는 동작 전체가 같은 경계에서 함께 실패했습니다.
| 상황 | 수정 전 | 수정 후 |
|---|---|---|
| 정렬 상위 200건 안의 항목 조회 | 정상 | 정상 |
| 그 뒤에 밀린 항목 조회 | not found |
정상 |
| 그 뒤에 밀린 Signal 무시 / Risk 수용 | not found |
정상 |
| 그 뒤에 밀린 Recommendation 수용 / 기각 | not found |
정상 |
영향을 받던 경로는 GET·POST /api/v1/signals/{id}, /api/v1/risks/{id}, /api/v1/insights/{id}, /api/v1/recommendations/{id} 계열이며, 같은 Service를 쓰는 MCP Tool도 동일했습니다.
2. 수정
- 네 Filter(
SignalFilter,RiskFilter,InsightFilter,RecommendationFilter)에ID필드를 두고, 각 질의에 기존 필터와 같은 형태의 선택 조건($n='' OR x.id::text=$n)을 추가했습니다 - 단건 조회는 이제
ID를 채우고Limit: 1로 같은 질의를 부릅니다. 목록과 단건이 한 문장을 공유하므로, 가시성을 결정하는 것은 여전히 Data Scope 조인 하나뿐입니다. 권한이 넓어지지도 좁아지지도 않습니다 ID가 비어 있거나 공백뿐이면 조건은 걸리지 않습니다. 목록 동작은 이전과 완전히 동일합니다
3. 재발 방지
각 목록 질의를 (sql, args)를 돌려주는 순수 빌더로 분리해, DB 없이 조건과 인자를 검사할 수 있게 했습니다.
검증
- 단위 Test 3개 추가: 단건 질의 네 개가 실제로 id Placeholder를 걸고 인자와 대응하는지, 빈·공백 id는 목록을 필터링하지 않는지, 모든
$n이 인자와 1:1로 맞는지 확인하는 회귀 가드 - id 조건을 지우면 새 Test가 실제로 실패하는지 확인
- Go 전체 Test/Vet (
go test -race ./...,go vet ./...),gofmt - TypeScript Typecheck와 React Production Build
- 환경변수 계약(
check-env-contract.sh)과 외부 정적 Asset 차단(check-static-assets.sh)
업그레이드
DB Migration은 없습니다. API 계약과 화면도 그대로이며, 저장된 데이터에는 영향이 없습니다.
지금까지 목록 뒤쪽에 밀려 열리지 않던 항목이 열리고, 무시·수용·기각도 정상 처리됩니다. 이 문제로 처리하지 못하고 남겨 둔 조언과 위험이 있다면 이번 Release 이후 다시 처리할 수 있습니다.
오프라인 이미지
| 항목 | 값 |
|---|---|
| Asset | relio-v1.11.10.tar.gz |
| Docker Image | relio:v1.11.10 |
| SHA-256 | a76c1bb6760fbbdd5fa4f9c974609531686f3d893c50684a1ac19e57c2e6bc5f |
| 압축 크기 | 36M |
| Architecture | linux/amd64 |
이 Release가 직접 제공하는 Asset은 위 Docker Image tar.gz 하나뿐입니다.
오프라인 서버에서 로드
gunzip -c relio-v1.11.10.tar.gz | docker load
docker image inspect relio:v1.11.10실행
# 최초 1회만 생성하고 비밀번호 관리 도구에 보관하세요.
ENCRYPTION_KEY="$(openssl rand -hex 32)"
docker run -d \
--name relio \
-p 8080:8080 \
-e POSTGRES_DSN="postgres://relio:password@postgres:5432/relio" \
-e BOOTSTRAP_ADMIN="admin" \
-e BOOTSTRAP_ADMIN_PASSWORD="ChangeMe-Immediately" \
-e ENCRYPTION_KEY="$ENCRYPTION_KEY" \
-v relio-data:/var/lib/relio \
relio:v1.11.10Relio Application이 받는 환경변수는 필수 3개(POSTGRES_DSN, BOOTSTRAP_ADMIN, BOOTSTRAP_ADMIN_PASSWORD)와 선택 1개(ENCRYPTION_KEY)뿐입니다.
ENCRYPTION_KEY를 설정하면 Personal Key와 SSO Client Secret이 재기동, 이미지 교체, relio-data Volume 재생성 후에도 유지됩니다. 설정하지 않으면 자격증명을 여는 Key가 Volume 안에만 존재하므로 /var/lib/relio를 PostgreSQL과 같은 복구 시점으로 반드시 보존해야 합니다.
검증
Release 전에 Source Test, TypeScript Typecheck, Frontend Build, Go Vet, 환경변수 계약, 외부 정적 Asset 차단, 비 Root 실행, 완전 격리 Network 신규 설치 Smoke Test, 이전 Release 업그레이드 Test, Docker Save/Load 재검증을 통과합니다.