• LINKS 1.0.8 — PostgreSQL 최초 기동 healthcheck race로 backup 복구 검증이 실패하던 결함 수정

    LINKS 1.0.8

    이 hotfix 릴리스는 이전 릴리스 1.0.7(3f6c504352) 위에 PostgreSQL 최초 기동 healthcheck 수정만 얹었습니다. 최종 소스는 9f4ada7c6c, 루트 패키지는 1.0.8, ARCA pin은 exact 0.1.18로 그대로입니다. 1.0.7에서 첫 sh backup.sh가 [6/8] Restoring into a fresh database and empty volumes에서 FATAL: database "links" does not exist로 실패할 수 있던 race를 고쳤습니다. schema는 바뀌지 않았습니다. DB 초기화와 migration이 필요 없습니다. backup 형식도 그대로입니다.

    업그레이드 주의: 1.0.7 → 1.0.8은 sh update.sh만 실행하고, 설치 뒤 sh backup.sh로 backup을 한 번 만들어 둡니다

    1. 대상·원인·수정: 공식 PostgreSQL image는 빈 data 디렉터리의 최초 기동에서 socket만 여는 임시 서버로 POSTGRES_DB를 만든 뒤 최종 서버로 다시 시작합니다. 기존 healthcheck(socket pg_isready)는 DB가 만들어지기 전의 임시 서버에서 이미 통과하므로 compose up --wait가 너무 일찍 돌아왔습니다. 그래서 backup.sh의 복구 검증(별도 project links-backup-verify의 빈 verify-postgres)이 느린 디스크에서 DB 생성 전에 접속해 [6/8]에서 실패할 수 있었습니다(BACKUP_EXIT=2, pin 해제, report.json 없음). 이제 root compose.yaml의 postgres와 ops/backup-verify.compose.yaml의 verify-postgres healthcheck가 TCP 127.0.0.1 pg_isready(임시 서버는 TCP를 열지 않음)와 대상 DB의 실제 쿼리(psql … select 1)를 함께 요구합니다. 같은 수정이 운영 postgres의 최초 기동(reset.sh 뒤 migration의 service_healthy 대기)에도 적용됩니다. healthcheck 설정이 바뀌므로 갱신 때 postgres container가 한 번 다시 만들어집니다(data는 그대로). 격리 gate에서 이전 container 종료부터 새 container healthy까지 약 5.3초(새 container 시작 → healthy 약 5.1초) 동안 PostgreSQL을 쓸 수 없었습니다. 실제 NAS의 중단 시간은 측정하지 않았습니다.
    2. 갱신 방법: 배포 디렉터리에서 sh update.sh만 실행합니다. schema는 바뀌지 않았고(infra/schema·infra/migrations·.env.example이 1.0.7과 같고 migration은 0개) reset.sh를 실행하지 않습니다. 배포 파일 중 compose.yaml과 ops/backup-verify.compose.yaml이 바뀌며 update.sh가 둘 다 받아 설치합니다. 1.0.8 설치 뒤 coordinator를 멈추고 sh backup.sh로 backup을 한 번 만들어 둡니다(운영 가이드 '운영 backup 명령'). 다음 1.0.9는 schema migration(게스트 공유 링크)을 포함하므로 그 전의 pre-update backup이 필요합니다. 1.0.7에서 실패한 backup 디렉터리(report.json 없음)는 복구에 쓰지 않습니다.
    3. 결과(R54 P3 수정과 NAS 조건 gate): 리뷰 R54의 P3(회귀 runner가 00 같은 인자로 검증 0회에도 PASS를 출력하던 문제)를 고쳤습니다. tests/run-postgres-init-race.sh가 반복 횟수와 지연을 선행 0 없는 양의 정수로만 받고 0·00·01·숫자 아님은 Docker 변경 전에 exit 2로 거부합니다. 릴리스 전에 격리 docker:24.0.2-dind(공식 Docker Compose v2.20.1, stager 1026:100, library 최상위 0:0 000 + 1026 ACL)에서 1.0.7 배포에 합성 VX Ace(rgss-wasm, ARCA 0.1.18)·MZ(web-static) ready build와 로그인 사용자의 세이브를 만든 뒤, 복구 검증 verify-postgres의 최초 기동에 15초+15초 init 지연을 넣었습니다. 1.0.7 backup.sh는 운영과 같이 [6/8]에서 database "links" does not exist로 실패했고(RED), 설치된 1.0.7 update.sh로 1.0.8에 갱신한 뒤 같은 지연에서 backup.sh가 8단계와 fresh restore drill을 모두 통과했습니다(아래 검증). Synology build(v2.20.1-6047-g6817716)와 실제 NAS 재현은 확인 전입니다.

    NAS 절차(1.0.7에서):

    cd /volume1/docker/links
    sh update.sh
    docker compose --env-file .env.production --profile runtime stop runtime-package-coordinator
    sh backup.sh
    docker compose --env-file .env.production --profile runtime up -d --no-build runtime-package-coordinator
    

    update.sh가 성공하면 단계 표시 6줄 뒤에 Updated to 1.0.8 (catalog 6f8cb1a5d406), runtime.package ready.가 출력됩니다. backup.sh는 [1/8]부터 [8/8]까지 출력한 뒤 Backup <id> PASS. Copy the whole directory off-host and keep it at least 30 days.로 끝나고, 실패하면 원인 한 줄을 출력하고 nonzero로 끝납니다. 복구 검증 단계([6/8])는 이제 새 PostgreSQL의 최초 init이 끝날 때까지 기다리므로 1.0.7보다 길어질 수 있습니다. backup 디렉터리 전체를 off-host로 복사해 둡니다.

    원본 game library는 읽기 전용으로 유지합니다. 이번 게시 작업에서 개발·운영 DB 초기화나 NAS 배포는 실행하지 않았습니다.

    수정 내용

    • compose.yaml postgres, ops/backup-verify.compose.yaml verify-postgres: healthcheck를 pg_isready -q -h 127.0.0.1 -p 5432 -U … -d …와 PGPASSWORD=… psql -h 127.0.0.1 … -v ON_ERROR_STOP=1 -qAt -c "select 1"로 바꿨습니다. 비밀번호는 container 환경 변수 참조($${POSTGRES_PASSWORD})로만 쓰며 렌더링된 설정에 값이 들어가지 않습니다. interval·timeout·retries·start_period, image pin, read_only·user·tmpfs·network는 그대로입니다.
    • tests/run-postgres-init-race.sh(신규 gate)와 tests/postgres-init-race/(test 전용 entrypoint·override): 같은 PostgreSQL image의 entrypoint를 그대로 쓰면서 DB 생성 전과 임시 서버 종료 전에 지연을 넣어 두 service를 빈 volume으로 기동하고, up --wait가 돌아온 즉시 TCP·socket 쿼리·init 완료를 요구합니다. 1.0.7 healthcheck는 FAIL(RED), 수정 뒤 PASS입니다. 반복 횟수·지연 인자 검증(R54 P3)을 함께 고쳤습니다.
    • 바꾸지 않은 것: backup.sh(ops/backup-registry.sh)와 restore 수락 규칙, backup 형식, schema·migration, application 코드, ARCA pin.

    문서

    • 운영 가이드 '운영 backup 명령': 1.0.7 이하 [6/8] 실패의 증상·원인·조치와 수정 배포 파일
    • 완료 기준선(운영 backup 행·검증), tests README(runner 색인 run-postgres-init-race.sh와 인자 규칙, test 전용 override, 변경별 검증 선택)

    알려진 제한(후속 과제)

    • 실제 NAS에서 1.0.7 [6/8] 실패의 재현과 1.0.8의 backup.sh·복구, 갱신 때 PostgreSQL 재생성의 실제 중단 시간은 NOT RUN입니다. 배포 뒤 운영자가 위 절차로 backup을 만들고 report.json의 executionStatus·restore·evidence를 확인합니다.
    • 1.0.9 예고: 관리자 공유 링크 게스트 재생(schema migration 포함), ARCA 0.1.19(모바일 고 DPR 배율), 게스트 catalog 차단. migration이 있으므로 1.0.9 갱신 전에 이 릴리스의 backup이 있어야 합니다.
    • 1.0.7과 같은 제한: 실제 Cloudflare 경유 artifact header, VX Ace 실제 browser acceptance, Synology 실제 환경의 갱신, PowerShell coordinator 전체 실행은 NOT RUN입니다. update.sh는 network가 빠진 container를 스스로 감지하지 않습니다.

    검증과 남은 범위

    • PASS: 소스 gate(1.0.7 base, 리뷰 R54 '병합 가능')
      • run-postgres-init-race.sh 10(init 15초+15초 지연): 10회 모두 두 service가 healthy가 된 즉시 사용 가능(20/20, healthy 33–37초). 1.0.7 healthcheck의 RED(healthy 3–6초 뒤 database "<name>" does not exist)는 수정 과제(T54)의 기록을 재사용했습니다.
      • R54 P3: 수정 전 00은 검증 0회로 PASS와 "rounds":00을 출력했습니다(정적 확인). 수정 뒤 반복 횟수 00·01·0·abc·007·1x와 지연 00·015·0·abc가 Docker 변경 없이 exit 2, 1·10은 정상 실행.
      • run-ops-backup.sh: Player lock 충돌 fail closed, 8단계, fresh restore drill, rgss-wasm build 복구, save history, evidence 3. run-backup-restore.sh phase-0 drill: 11개 항목 모두 PASS(rgss-wasm active generation과 web-static 포함).
      • static(Prettier·ESLint·typecheck·docs-static), 문서 검사, git diff --check, reset 기록기 30·deploy 기록기 93 synthetic case, Compose config 3조합 Watchtower label(20·20·32)과 restore drill 렌더링(2).
      • schema gate: infra/schema·infra/migrations 변경 없음이라 NOT RUN.
    • PASS: 릴리스 gate(Compose v2.20.1, NAS 조건)
      • 환경: 격리 docker:24.0.2-dind, host Compose는 공식 v2.20.1 linux-x86_64 binary(release의 게시 checksum과 일치). 합성 CA의 HTTPS release 서버가 Gitea를, 합성 TLS registry가 git.2ex.dev를 대신했고, registry에는 실제 1.0.7 image와 이 릴리스의 1.0.8 image를 넣었습니다. 배포 파일은 1.0.7 tag와 이 릴리스 커밋의 Git blob입니다.
      • 1.0.7 설치 → reset.sh → update.sh: 12개 service, network 불일치 0, 모두 Watchtower 제외, readiness PASS. 합성 VX Ace(rgss-wasm, ARCA 0.1.18) ready, MZ ready, 로그인 사용자 세이브 201·북마크 1.
      • 복구 검증 verify-postgres에만 test 전용 init 지연(15초+15초)을 Docker PATH shim으로 넣었습니다(배포 파일은 release와 byte 동일). 1.0.7 backup.sh: drill postgres가 3초 만에 healthy → [6/8] database "links" does not exist, exit 2, pin 해제, report 없음(RED, 운영 증상과 동일).
      • 1.0.8 게시 뒤 설치된 1.0.7 update.sh: exit 0(65초), Pending migrations 줄 0, Updated to 1.0.8 (…), runtime.package ready., 7개 파일이 1.0.8과 같고 .tmp 0. postgres container 재생성(ID 변경, 새 healthcheck 적용, 새 container 시작 → healthy 5.1초). 12개 service, network 불일치 0, unhealthy 0, 12/12 Watchtower 제외, readiness PASS, catalog·Player 1.0.8. schema current(migration 0), row count 불변, 같은 사용자의 세이브 ETag·bytes·Player save 파일 목록 동일, VX Ace 0.1.18 build ready·active 유지, MZ ready.
      • coordinator 정지 → 같은 지연에서 sh backup.sh(1.0.8): exit 0(46초). drill postgres는 두 지연과 init 완료까지 31초 기다린 뒤 healthy, [1/8]–[8/8], Backup <id> PASS, report.json executionStatus PASS, restore drill schema current·migration 0, runtimeBuilds 2(rgss-wasm + web-static), runtime snapshot 78 files, save 1 namespace·2 files, evidence 2 build, active pin 0, verify project 잔여 0.
      • coordinator 재기동(7초 뒤 새 ready 줄) → 재-update: exit 0(34초), 배포 파일 불변, postgres 재생성 없음, 12 service 불일치 0, readiness PASS.
      • PostgreSQL 재생성 시간(같은 배포에서 1.0.7 → 1.0.8 healthcheck 설정 전환을 live event로 측정): 이전 container 종료 → 새 container 시작 0.22초, 시작 → healthy 5.07초, 합계 약 5.3초.
    • PASS: 최종 1.0.8 소스. 릴리스 커밋 두 개(root version, 완료 기준선의 version 문구) 뒤 문서 검사, static, git diff --check를 다시 통과했습니다.
    • 게시 이미지: 같은 최종 소스에서 build했고 gate도 이 image로 실행했습니다. image 안에서 root version 1.0.8, ARCA pin·설치본 0.1.18, migration 0개를 확인했습니다.
    • NOT RUN: 실제 NAS 재현·backup·복구와 1.0.7 → 1.0.8 갱신, 브라우저 matrix(변경은 PostgreSQL healthcheck뿐), live provider.

    배포 이미지와 파일

    • Git annotated tag는 1.0.8이고 최종 소스는 9f4ada7c6c입니다(branch release-1.0.8, 1.0.7 base).
    • 단일 linux/amd64 이미지: git.2ex.dev/hanyul/links:1.0.8 = git.2ex.dev/hanyul/links:latest
    • 원격 digest: sha256:6f8cb1a5d406231f9851efd176ed3b5bc8cf41a993f542c8e6278ce6a3fa133a
    • release asset은 없습니다. update.sh가 tag 1.0.8의 7개 배포 파일을 HTTPS로 받습니다. 대응은 update.sh=ops/deploy-registry.sh, reset.sh=ops/reset-registry.sh, backup.sh=ops/backup-registry.sh이고, 나머지는 compose.yaml·infra/docker/Caddyfile·ops/backup-verify.compose.yaml·ops/runtime-package-coordinator.sh입니다.
    • 1.0.7 대비 바뀐 배포 파일은 compose.yaml과 ops/backup-verify.compose.yaml(healthcheck)뿐입니다.
    Downloads 2