Skip to content

v1.11.13

Choose a tag to compare

@github-actions github-actions released this 03 Sep 15:50
· 39 commits to main since this release
68be146

Relio v1.11.13 — 정리 작업이 조용히 멈춰 있던 문제

만료된 Personal Key 정리, 세션·로그인 상태·멱등성 키 삭제, 예측 스냅샷, 인텔리전스 분석을 담당하는 1분 주기 유지보수 작업이 실행되다 말고 조용히 멈추던 문제를 고쳤습니다. 오류도 로그도 남지 않고 그냥 아무 일도 일어나지 않는 형태라 눈치채기 어려웠습니다.

1. 원인

1-1. 세션 Advisory Lock 은 그 잠금을 건 커넥션의 것

여러 Relio 컨테이너가 한 PostgreSQL 을 공유할 때 유지보수를 한 대만 돌리려고 pg_try_advisory_lock 을 씁니다. 그런데 이 잠금은 커넥션(세션) 소유입니다. 기존 코드는 잠금을 커넥션 풀에 대고 걸었습니다.

if err := r.DB.QueryRow(ctx, `SELECT pg_try_advisory_lock(733541122020269)`).Scan(&locked); err != nil || !locked {
    return
}
defer r.DB.Exec(context.Background(), `SELECT pg_advisory_unlock(733541122020269)`)

r.DB 는 풀이므로 각 구문마다 커넥션을 새로 빌립니다. 잠금은 커넥션 A 의 세션에 걸리는데, 그 뒤의 정리 구문과 마지막 해제 구문은 풀이 그때 내주는 아무 커넥션에서나 실행됩니다. HTTP 요청이 같은 풀을 함께 쓰는 실제 운영 환경에서는 다른 커넥션이 나오는 일이 흔합니다.

1-2. 해제가 빗나가면 잠금은 계속 남는다

자기가 갖고 있지 않은 잠금을 푸는 pg_advisory_unlock 은 오류가 아니라 경고와 함께 false 를 돌려줍니다. 반환값은 버려졌으므로 아무도 알아채지 못했고, 잠금은 커넥션 A 가 풀에서 재활용되거나 닫힐 때까지 그대로 남았습니다.

그 다음 1분 뒤 실행부터는 이렇게 갈렸습니다.

이번 회차가 받은 커넥션 결과
커넥션 A 같은 세션이라 잠금이 다시 걸리고(중첩) 정리가 실행됨
그 외 커넥션 false — 다른 인스턴스가 잠갔다고 판단하고 즉시 종료

즉 정리 작업 전체가 "풀이 마침 그 커넥션을 내줬을 때만" 도는 상태가 됩니다. 컨테이너를 한 대만 띄워도 마찬가지입니다.

1-3. 멈춘 작업들

한 회차가 건너뛰어질 때 함께 건너뛰어지는 것들입니다.

  • 회수 유예가 끝난 Personal Key 를 REVOKED 로 만료
  • 만료 시각이 지난 Personal Key 를 EXPIRED 로 전환
  • 만료된 세션, OIDC 로그인 상태, 멱등성 키 삭제
  • 예측 스냅샷 적재
  • 인텔리전스 분석(Signal·Risk·Recommendation 재생성)

2. 수정

Migrate 가 이미 쓰고 있던 방식대로, 커넥션 하나를 명시적으로 빌려 그 위에서 잠금 → 작업 → 해제를 모두 실행합니다.

conn, err := r.DB.Acquire(ctx)
if err != nil {
    if ctx.Err() == nil {
        r.Log.Error("acquire maintenance connection", "error", err)
    }
    return
}
defer conn.Release()
r.maintain(ctx, conn)

해제는 회차의 마지막에 같은 커넥션에서 실행되므로 잠금이 남지 않고, 다음 회차는 어떤 커넥션을 받든 정상적으로 잠금을 얻습니다.

잠금 질의 자체가 실패한 경우도 분리했습니다. 기존에는 오류와 "다른 인스턴스가 갖고 있음" 이 한 조건으로 묶여 둘 다 조용히 종료됐지만, 이제 오류만 로그로 남깁니다. 잠금을 못 얻는 것은 정상적인 결과이므로 그대로 조용히 넘어갑니다.

3. 회귀 방지

internal/job 에는 테스트가 없었습니다. 이번에 유지보수 회차가 실행되는 커넥션을 대역으로 세워 구문 순서를 확인하는 테스트를 넣었습니다.

  • 잠금을 얻으면 첫 구문이 pg_try_advisory_lock, 마지막 구문이 pg_advisory_unlock 이고 그 사이에 정리·스냅샷·분석이 모두 들어 있는지 — 해제가 스냅샷·분석보다 먼저 나오면 두 인스턴스가 동시에 분석을 돌릴 수 있으므로 순서까지 확인합니다.
  • 잠금을 얻지 못하면 잠금 시도 외에는 아무 구문도 실행하지 않는지.
  • 잠금 질의가 실패해도 마찬가지로 즉시 멈추는지.
  • internal/ 전체를 훑어 세션 Advisory Lock 구문이 풀(DB.Exec·DB.Query 등) 위에서 실행되면 실패시키는 가드. 같은 실수가 다른 패키지에서 다시 나오는 것을 막습니다.

4. 적용

마이그레이션은 없습니다. 재시작하면 첫 회차부터 정리와 분석이 매분 정상적으로 실행되며, 그동안 밀려 있던 만료 키·세션 정리와 인텔리전스 재계산이 순차적으로 따라잡습니다. 남아 있던 잠금은 기존 컨테이너가 종료되면서 함께 풀립니다.

오프라인 이미지

항목
Asset relio-v1.11.13.tar.gz
Docker Image relio:v1.11.13
SHA-256 47a1449d967e465663d2e8f09f436ae5b7e580cd8aa9a217353747b05eb457bd
압축 크기 36M
Architecture linux/amd64

이 Release가 직접 제공하는 Asset은 위 Docker Image tar.gz 하나뿐입니다.

오프라인 서버에서 로드

gunzip -c relio-v1.11.13.tar.gz | docker load
docker image inspect relio:v1.11.13

실행

# 최초 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.13

Relio 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 재검증을 통과합니다.