통계 게시 예약 대기 단축 (2026-09-09)
- 기간: 2026-09-09. 오너: “바벨릭 #1465 진행해줘, 목표 릴리스는 17.7”.
- 랜딩: 미완료. 앱 목표
release/v0.17.7, 구현302d0fbfa, 검증 격리88c71a63a. Production 적용 없음. - 재개한 통합 후보: 최신 main
df394f77a를 합친 head6becc2843, treeceb756ce8bf722a16cdf0998cacd6941d5d2e565. 오너의 최소 CI 지시에 따라 자기 full·큐 요청을 추가하지 않고 HQ에 전달했다. - 설계서: #1465 분석·Phase 계획.
- 정본: 통계 worker 운영 계약.
- 도구: 전용 Supabase sandbox의 실제 저장/pg_cron 전후 측정 스크립트. 식별자를 제외한 측정 JSON. Production은 Management API
read_only: true로만 조회. - 게이트: pgTAP의 실제 예약 상태 6개, DB 통합의 재적용·설정 보존·로그 경계 2개. 기존 계산 격리·세대·원자 게시 검증 유지.
Phase 현황
| Phase | 내용 | 상태 |
|---|---|---|
| Phase 1 | 예약 변경·실행 로그 보관·회귀 | 구현 완료 302d0fbfa, 전체 check의 기존 문서 의존 검사 미해소 |
| Phase 2 | 실제 예약 실행 전후 측정·동치 검사 | 측정 완료. 재개 검증에서 기존 1년 기준 2표 차이의 원인을 입증하고 기준 갱신 |
| Phase 3 | 문서·릴리스 PR·사전 검증 | 최신 main 기반의 자기 커밋과 좁은 검사 결과를 HQ에 전달. 통합 후보 검증·병합 미완료 |
1. 배경
운동을 저장한 뒤 하루 요약·기록·PR이 늦게 채워졌다. v0.17.5는 저장과 통계 계산을 격리했고 앱은 게시 완료를 다시 확인하지만, 세 worker는 각각 5초 예약을 기다렸다. 이슈는 일반 저장의 게시를 3초 안팎으로 줄이는 것이다.
2. 문제 제기
예약 지연과 계산 비용은 서로 다르다
2026-09-09 18:24 KST Production 조회에서 세 예약이 실제로 5초였고, 30분간 각 352회 실행되었다. claim 평균은 8.16ms였으나 compute의 최대는 26.53초, publish 최대는 5.53초였다. 완료된 24시간 projection 18건의 compute 최대는 26.26초, publish 최대는 5.89초였다.
24시간 완료 job 중앙값 53.33초/p95 5,424.51초에는 장애 복구와 backfill이 섞여 있다. 이를 일반 저장의 전후 수치로 사용하지 않는다. 조회 시점에 pending/processing은 0건, 과거 failed는 42건이었다.
Production만 게시 예산이 6초다
#1452 기록의 운영 override를 실제 cron 명령에서 확인했다. 저장소·staging 기본인 3초로 덮어쓰면 큰 계정의 통계 게시가 다시 실패할 수 있다.
3. 해결 방안
| 안 | 판단과 근거 |
|---|---|
| 세 cron 주기만 각각 1초로 변경 | 채택. 별도 트랜잭션·기존 제한시간과 게시 병렬성을 보존한다. |
| 세 함수를 같은 SQL 함수로 묶기 | 기각. claim 선행 커밋과 단계별 실패 격리가 사라진다. |
| 단일 직렬 cron 또는 새 알림 실행기 | 기각. 긴 compute의 게시 지연 또는 추가 실행기 운영 비용이 생긴다. |
| 앱 대기·재시도 확대 | 기각. 서버 예약 대기를 줄이지 못한다. |
일반 저장의 예약 대기를 줄이고, 긴 계산까지 3초 완료라고 표현하지 않는다. 이는 이슈의 B안이며 제품 계산 정책·권한·사용자 원본을 변경하지 않는다.
4. 적용한 내용
코드는 작업 브랜치에 게시했다. 사전 검증이 통과하지 않았으므로 앱 릴리스 PR과 큐 요청은 하지 않았다.
Phase 1 — 예약과 로그 보관
신규 마이그레이션 20260914000000_stats_projection_cadence는 세 기존 job이 모두 있는지 확인하고 schedule만 바꾼다. 운영의 command와 일시정지 상태를 보존하며 재적용해도 job을 중복 생성하지 않는다.
추가한 로그 정리는 해당 세 worker의 7일 지난 완료 실행 기록을 10분마다 최대 5,000행 삭제한다. 한 번에 발생할 수 있는 새 실행 이력 1,800건보다 큰 배치로 과거 누적도 따라잡되 삭제량을 제한한다.
주요 결정과 그 근거
publish 6초 override는 그대로 둔다. 계산 함수·lease·attempt 한도·세대 검사·원자적 게시 동작을 수정하지 않는다. 실제 배포는 v0.17.7 승격 경로다.
작업 중 드러난 것
스냅샷 생성 전에 새 migration 지문을 반영한 상태에서 검사를 실행해 생성물 불일치가 발생했다. 생성기 재실행 뒤 SQL 계약 16개는 통과했다. 최종 npm run check는 3,402 pass / 3 fail / 29 조건부 skip이다. 실패 3건은 앱 안의 문서 장부·스냅샷 지문 일치를 요구하는 검사다. #1463/PR #1466의 분리 변경이 아직 목적 release에 들어오지 않았고, 모든 문서를 별도 repo로 옮긴다는 오너 지시에 따라 앱 문서 갱신으로 우회하지 않았다.
결정성 검사의 준비 코드에서 compute/publish 두 cron이 일시정지 대상에서 빠져 있었다. 두 예약과 연결·전송 중 미종료 실행까지 기다리도록 보완했고 복구·실패 경계 단위 테스트 7개가 통과했다. 실제 D01은 손계산 oracle, 재계산 동치·정책 불변식·고정 날짜 창 검사를 통과했지만 저장된 1년 기준의 기간 통계/달력 요약 두 해시가 달랐다. 이전 5초 조건에서 다시 실행해도 동일한 행 수·동일한 두 해시로 실패했다. 계산 SQL·정규화·기준 파일은 목적 release와 동일하며 저장된 기준은 9월 7일, #1432 계산 격리 수리는 9월 9일이다. 이번 주기 변경에서 생긴 회귀는 아니지만 과거 어느 열/값의 변경인지는 최초 검증 때 확정하지 못했다. 이때는 기준 파일을 덮어쓰지 않았으며 D01 전체 성공으로 기록하지 않았다. 아래 재개 검증에서 두 해시의 차이를 구체적으로 확인했다.
재개 검증 — D01 기준 차이 확인
main df394f77a를 자기 브랜치에 합쳤다. 충돌한 두 cron 검사 파일은 main의 다섯 예약 목록과 이번 변경의 미종료 실행 대기·다섯 상태 복구 검사를 함께 보존했다. cron 단위 7개와 SQL 정의 계약 16개를 좁게 실행해 통과했다. 제품 계산 함수는 main과 같다.
실제 1년 D01은 34.142초에 oracle·재계산 동치·불변식·고정 날짜 검사를 통과했고, 동일한 두 표의 저장 기준 해시 차이를 재현했다(4 pass / 1 fail). 같은 가상 사용자의 입력·투영을 유지한 rollback 트랜잭션에서 bf9064973의 옛 set_score_observations_v1을 적용해 기간·달력 점수 집계만 다시 만들었다. 기준 저장 이후 추가된 기간 통계의 report_distributions_v1 열을 비교 결과에서 제외하자 두 옛 해시가 모두 정확히 복원됐다. 트랜잭션 rollback 뒤 현재 투영 15표가 그대로인 것도 확인했다.
차이의 원인은 기존 #1432 2af83d02b의 점수 계산 변경과 #1398 0fef81328의 리포트 분포 열 추가다. 검증 근거 JSON에 변경된 열·행 수·복원한 해시·원상복구 결과를 남겼다. 6becc2843은 기대 해시 두 개와 기준 출처·날짜만 갱신하며 사용자 원본이나 별도 계산 정책을 바꾸지 않는다.
이미 캡처한 실제 재계산 전후 스냅샷을 새 기준과 대조해 15표 모두 일치함을 확인했다. 오너의 최소 CI 지시에 따라 같은 DB workload를 다시 적재하지 않았다. 갱신 후 D01 프로세스 전체를 새로 실행해 5 pass를 얻었다고 표현하지 않는다.
측정 하네스는 5초 실행의 마지막 집계에서 sending 상태를 실패로 오분류했다. 이 상태의 end_time은 null이며 DB 실패가 아니었다. fixtures 정리와 cron 복구를 확인한 뒤 분류를 수정하고 1초 조건을 독립 실행했다. 이미 완료된 5초 표본을 재사용했고 각 실행 시각·수정 이력을 JSON에 남겼다.
최종 DB 재생의 스냅샷 검사에서는 기준 release부터 있던 dblink 확장 한 줄이 재생 결과에 없어 실패했다. 테스트가 남긴 확장이 스냅샷에 들어간 기존 결함이며 #1460의 1dd16c921이 이를 수리한다. 이번 변경은 해당 확장 정의를 추가하거나 변경하지 않았고 다른 담당자의 브랜치·패치를 대신 병합하지 않았다. 이 실패도 현재 release의 통합 조건으로 남는다.
660세션/7,920세트 검사는 첫 실행에서 통과했지만, 최종 DB 검사에서는 재계산 뒤 게시가 3,028ms에 57014(3초 statement timeout)로 실패했다. 이 검사는 cron이 없는 별도 임시 DB에서 기존 worker를 직접 호출하므로 1초 예약 간격의 효과를 측정하는 검사가 아니다. 계산/게시 함수는 이번 변경에서 수정하지 않았지만, 제한시간 경계의 실패를 통과로 대체하지 않는다. 임시 DB는 정리했다. 다른 무거운 검사를 마친 뒤 같은 코드·3초 제한으로 해당 검사만 재실행한 결과는 통과했다(19:58 KST, 재게시 1,291ms). 동시 CRUD·재계산 동치·취소/lease 회전도 통과했고 임시 DB를 정리했다. 최종 DB 전체 실행의 실패 기록은 그대로 유지하며, 공유 로컬 환경에서 제한시간을 넘길 수 있는 변동성이 남아 있다.
문서 PR #1의 첫 원격 CI는 GitHub가 계정의 결제 실패 또는 지출 한도 확인을 요구해 job 실행 전에 차단됐다. 워크플로 단계는 시작하지 않았고 문서/관리자 검사는 skipped다. 로컬 문서 검사·빌드· 게시 자산 검증은 통과했지만 원격 CI 성공으로 기록하지 않는다.
5. 적용 결과
| 항목 | 결과 |
|---|---|
| 실제 예약 주기 | 5초 → 1초, 전용 sandbox 확인 |
| 신규 회귀 | DB 통합 2개·pgTAP 6개·검증 격리 단위 7개 통과 |
| 최종 DB 재생/pgTAP | 새 migration 재생 후 130파일/2,328 assert 통과 |
| 동시 저장 세대·영수증 | 13개 통과 |
| 스냅샷 재생 일치 | 기존 dblink 확장 잔여로 실패, #1460 선행 수리 |
| 큰 이력 회귀 | 첫 실행 통과, 최종 DB 검사에서 게시 3초 제한 실패, 단독 재실행은 게시 1.291초로 통과 |
| 일반 저장 18회: 요청→게시 p50 | 9.811초 → 1.894초 |
| 일반 저장 18회: 요청→게시 p95/최대 | 14.833초 → 2.359초, 84.10% 감소 |
| 일반 저장: commit→게시 p95 | 14.780초 → 2.238초 |
| 4명 동시 저장: p50/최대 | 14.907/24.947초 → 4.148/6.188초 |
| 빈 큐 약 30초: 실행 수 | 17회 → 87회, 실측 초당 실행 빈도 5.117배 |
| 빈 큐 약 30초: 누적 실행 시간 | 114.703ms → 1,192.932ms, 초당 비율 10.400배 |
| 빈 큐 작업/시도 생성 | 양쪽 모두 0건 |
| 원본 보존 | 44건의 저장 후→게시 후 원본 5표 digest 모두 일치 |
| D01 | 재현 4 pass / 1 fail → 두 과거 해시 복원으로 원인 입증 → 기준 갱신 후 같은 실측 스냅샷 15표 대조 통과 |
| 전체 check/full CI | check 3,402 pass / 3 fail / 29 skip, 큐 full 미실행 |
| 문서 검증 | 로컬 검사·빌드·31개 자산 통과, 원격 CI는 계정 결제/한도로 실행 전 차단 |
| Production | 변경 전 조회만 수행, 후보 미적용 |
각 조건은 4계정×12회 초기 이력(3종목×4세트), 한 계정의 생성/수정 18회, 4계정의 동시 저장 1회씩이다. authenticated 역할의 실제 save_session_v5를 호출하고 영수증의 요청 세대에 해당하는 작업의 DB 완료 시각을 비교했다. pg_cron이 실제로 처리했으며 worker를 직접 호출해 대기를 생략하지 않았다. 네트워크·브라우저 시간은 포함하지 않고, 18표본의 nearest-rank p95는 최대값과 같다.
빈 실행의 누적 runtime은 CPU 사용률이 아니다. WAL은 677,096→188,904bytes였으나 직전 부하·백그라운드 작업이 섞여 비용 감소의 근거로 쓰지 않는다. 저장 이력이 큰 운영 계정과 4명 동시 저장까지 3초를 보장하는 결과로 확대하지 않는다.
6. 이번 개선으로 향상된 것
계산이 짧은 저장이 다음 예약을 기다리는 시간을 줄인다. 이미 승인된 운영 제한시간을 보존하는 회귀와 실행 로그의 보관 범위·배치 경계 검사를 남긴다.
남은 것
아래 상태는 2026-09-09 22:30 KST 통합 재개 시점이다. 앞선 실패는 당시 실행 기록이며 현재 차단으로 자동 승계하지 않는다.
- 최신 main
df394f77a에는 #1460의dblink스냅샷 수리가 반영됐다. 목적 release의 최신 기반 반영은 #1483, 문서 분리는 #1466을 통해 통합한다. 조회 시 둘 다 OPEN이었다. - 최신 main을 자기 브랜치에 합쳤고 D01의 두 표 차이를 위 절차로 입증·해소했다. 통합 후보에서 필요한 생성물·마이그레이션 번호 조정은 자기 범위에서 처리한다.
- 문서 정본 최신 main
bb40643을 자기 브랜치에 반영하고 로컬 검사·빌드·31개 자산 검사를 통과했다. 최근 저장소의 원격 CI 성공을 확인했으므로 과거 결제 안내를 현재 오너 작업으로 반복하지 않는다. 자기 PR의 최종 head 결과는 별도로 확인한다. - #1465 담당 범위는 앱 작업 PR의 v0.17.7 release 통합과 관련 문서·검증까지다. release→staging→main 승격은 오너가 지정한 HQ가 맡는다.
- 후속 최소 CI 결정에 따라 HQ가 여러 작업을 모은 통합 후보를 준비한다. 자기 앱 PR·새 full·큐 요청은 만들지 않고 최신 main 기반의 head/tree와 검사 근거를 전달했다. 기존 동일 코드의 전체 검사·일반/동시 저장 측정·대용량 게시 검사를 반복하지 않았다.
대용량 게시 검사의 제한시간 변동성도 최종 CI에서 확인한다. 큰 계정의 실제 계산/게시 비용은 예약 간격만으로 없어지지 않는다.
오너 손이 필요한 것
현재 없음. 최초 CI의 계정 안내는 과거 실행 기록으로 남기고 최신 실행 결과로 판단한다.