v0.17.1 출시 후보의 이관·동시 저장 수리 (2026-09-07)
- 지시: “0.17.1 배포할 수 있게 후속작업좀 진행해줘”. 추적 #1324, 릴리스 #1303.
- 기준: Production
v0.17.0(478ddf12), 초기 조사 main07c04f37. 최종 PR·SHA·원격 CI·staging 근거는 #1324/#1303에 기록한다. - 설계 정본: 투영 rollout, 통계 세대 계약 §6, 원본 불변.
- 변경: 기존 174 migration 불변, 신규
20260913010100_workout_generation_atomic_receipt.sql1개(함수 4개 재발행, 위험 low). 배포/CI workflow·정본 SQL·schema·배포 manifest는 자동 랜딩 큐로 통합한다. - 검증: 실제 고정 이미지의 populated CLI 이관·중단/재개, 두 DB 연결 회귀, 빈 DB replay·pgTAP·전체 CI. 사용자 사실·통계 수치 정책·권한·5초 잠금 예산은 유지한다.
1. 배경
출시 검토 당시 CASE-039가 실패했으나 Phase 1 보완 #1322가 현재 RPC 처리가 끝난 뒤 탭을 닫도록 이미 수정했다. 이 변경을 중복 구현하지 않고 최종 릴리스 전체 CI로 확인한다. 승인된 운영·공개 안내 #1298도 최신 main과 통합한다.
남은 실제 결함은 첫 반복수 투영 이관의 잠금 5,017ms와 동시 enqueue 중 정상 저장 실패였다. 후자는 Phase 1에서 재현해 D02로 인계된 P0 결함이며, 이 출시에서는 확인된 경합만 선행 수리한다. 전체 D02나 Phase 2 완료를 뜻하지 않는다.
2. 문제
첫 투영 migration은 새 열과 전체 UPDATE를 같은 트랜잭션에서 수행한다. 파생값만 채우는데도 세부 세트마다 부모 기록 버전까지 갱신해 강한 테이블 잠금이 길어졌다. 후속 SQL을 추가하는 것만으로 이미 적용된 첫 파일의 실행 비용을 줄일 수 없다.
저장 함수는 잠금 전에 전역 세대 N을 읽은 뒤 자기 enqueue 결과가 N+1인지 검사했다. 기다리는 동안 cron이나 다른 저장이 N+1을 만들면 자기 정상 결과 N+2를 중복 실행으로 오판해 전체 저장이 실패했다. 같은 source_ref를 다른 mutation ID로 동시에 생성할 때도 두 번째 영수증이 최초 기록의 세대 대신 잠금 전 0을 반환했다.
3. 개선
원본 SQL을 유지하고 기존 lift_guild.suppress_session_revision=on을 첫 projection의 별도 CLI 연결에만 전달한다. 파일의 원자적 실행·장부 기록은 Supabase CLI가 담당한다. 고정 v0.17.0 기준과 배포 HEAD의 신규 파일·hash manifest를 만들고, 실제 pending이 명시 목록의 정확한 suffix인지 검사한다. 후속 migration은 설정 없는 새 연결로 실행한다. staging-applied·Production release diff·원격 계약 검사는 유지하며 manifest와 결과를 Actions artifact로 남긴다.
enqueue의 기존 UUID 반환 API는 유지하고 내부 core가 기존 잠금 안에서 배정한 job ID와 세대를 반환한다. 저장·삭제는 자기 세대를 영수증에 담고 transaction-local 사용자별 enqueue 횟수로 완료 1회·계획 0회를 검사한다. 실패 시 원본·자식·dirty·세대·영수증·계수가 함께 rollback된다. source_ref 재생은 확정 영수증의 세대를 재사용한다.
4. 적용 과정과 계약
저장 앞에 owner queue 잠금을 추가하는 좁은 대안은 실제 인입의 calendar dirty 행 잠금과 교착을 일으켰다. 최종 구현은 기존 잠금 순서를 유지한다. 추가 enqueue 주입은 계속 거부하고 동일 revision 동시 수정은 한 개만 성공한다. core 권한은 service_role만이며 앱 RPC의 신원·반환 계약을 바꾸지 않는다.
실제 CLI 2.113.0의 JSON 옵션은 --output-format json이다. 첫 UPDATE에서 startup 설정이 유지됨을 관측했고 실패주입 1회 뒤 정상 실측 1회로 검증했다. 빈 replay 전용 snapshot 검사를 populated DB에 적용하면 fixture 때문에 거부하므로, populated DB는 public DDL/원천을 비교하고 빈 replay는 별도 sandbox에서 검사한다.
5. 적용 결과
| 검사 | 결과 |
|---|---|
| 실제 CLI 4년 이관 | 1,043세션·12,516세트·원본 33,127행. projection 3,125ms·잠금 1,498ms(5초 이내), readers 2,462ms·잠금 0 |
| 보존·공존 | 23표 digest·ID/간선/source·무게 투영·session revision 보존, 반복수 기대값 일치, owner probe 오류 0/65, 공존 break 0, SQL 원천 차이 0 |
| 중단·재개 | 실제 CLI UPDATE 직전 오류 시 열/원본/장부 전체 rollback 및 임시 파일 정리, 재개 성공, 이미 적용됐으면 phase 0 |
| 정상 쓰기 | 별도 owner RPC revision 1→2, 동일 mutation 재생 유지, 새 연결에 suppression 없음 |
| 동시 저장 | 실제 두 연결 9/9. 완료/계획/삭제·다른 저장·cron·source_ref·revision 충돌·rollback·과잉 enqueue·잠금 순서 |
| 기존 DB·통계 | 175 migration replay·snapshot·sql:check, pgTAP 109파일/1,899 assert, D01 oracle·결정성·1년 260세션/15표 기준 5/5 |
| 자동 검증 | 실제 동시 저장 검사를 local full CI와 원격 migration-smoke에 연결하고 DB가 없으면 해당 단계는 실패시킨다. 최종 통합 실행·SHA는 #1324/#1303에 추가 기록 |
4년 실측은 초기 main의 기존 두 migration을 대상으로 한 합성 데이터 증거다. 신규 D02 포함 고정 HEAD의 suffix 적용과 전체 CI를 릴리스 전에 별도로 확인한다. 과거 5,017ms 실패를 지우거나 동일 환경의 엄밀한 전후 벤치마크라고 주장하지 않는다.
6. 향상된 점과 남은 범위
정상 동시 저장이 다른 작업의 세대 증가 때문에 거절되지 않으며, 첫 이관의 실제 잠금 예산과 원본 보존을 확인할 수 있다. 이미 적용된 파일은 변경하지 않고 실행 범위·설정·재시도 대상이 고정된다.
이 작업은 main/staging 출시 준비다. Production은 릴리스 PR의 오너 승인과 기존 배포 경로로 수행한다. 독립 landing-page 프로젝트의 코드는 포함돼 있지만 앱 배포만으로 그 프로젝트의 공개 호스팅이 완료되는 것은 아니다. 운영 안내 #1298은 기존 법률문서 v1~v4를 유지하며 미확정 v5를 활성화하지 않는다.