Skip to content

순차 통계 체크포인트와 변경 구간 재생 (D07 #1417)

현재 상태: release/v0.18.0 반영 완료, 운영 미반영. 앱 PR #1522 · e2833dee, 검증 head c21b4acc. 분석·계획, Phase 1 증거.

계약

오래 기록한 사용자가 오늘 세션을 추가하면, PR 최고값·근력 신뢰 상태·최대반복수·점수의 이전 참조 상태를 검증한 체크포인트에서 읽고 그 뒤 관측을 재생한다. 과거 날짜 수정은 수정된 날보다 엄격히 앞선 체크포인트가 필요하다. 같은 날의 중간 상태는 재사용하지 않는다.

user_stats_sequential_days는 사용자·종목·완료 일자를 키로 갖는다. families 안의 각 계산(pr, strength, max_rep, score)은 정책 지문, D05 입력 형식, 수동 기록과 카탈로그의 지문, 원본 경계, 순차 상태, 상태 검증값, 확정 기준 세대를 각각 보존한다. 한 계산을 갱신하는 동안 다른 계산의 이전 시작 상태와 세대를 덮어쓰지 않는다. 원본 경계는 정규화 관측의 논리 ID와 내용으로 확인하며 과거 세트 전체의 해시를 다시 계산하지 않는다.

stats_sequential_checkpoint_read_v1{replay_from, checkpoint_on, state, reused, reason}을 반환한다. 없음·손상·정책·입력 형식·source·세대·경계 불일치는 reused=false, replay_from=null이며 호출자가 같은 정책의 전체 계산을 수행한다. 유효하면 checkpoint_on+1부터 재생한다. 영향 날짜까지 간격이 있어도 그 사이를 누락하지 않는다.

체크포인트 기록은 적용 세대를 올리지 않는다. 기존 claim/compute/publish의 계산 격리·완전 발행을 사용한다. 계산 실패나 미발행 상태의 체크포인트는 확정 시작 상태로 재사용할 수 없다. 계산을 모두 마친 뒤 유지된 prefix의 세대도 재인증하지만, 전역 applied는 기존 발행이 성공해야 바뀐다.

애플리케이션 역할은 체크포인트를 직접 쓰지 못하며, 현재 처리 중인 작업 또는 이미 확정된 유지보수 세대만 기록할 수 있다. 이 표에는 사용자 입력 원본이 없으므로 원본 보호 트리거 생성용 user-fact-columns 목록에는 넣지 않는다. 모든 열과 소유권·파생 등급은 DB 객체 장부에, 작성자는 통계 투영 장부에 등록한다. 계산 종류별 상태가 scalar이거나 필수 객체·숫자 형식이 손상돼도 오류로 끝내지 않고 full fallback한다.

수치 정책은 e1RM lift-guild.e1rm@3.0.0, curve nuzzo-2024-general-inverse@1.0.0, 최대반복수 barbelic.max-reps@2.0.0, #1401 점수 정책을 유지한다. UUID를 마지막 동점 키로 사용하는 기존 의미도 유지한다. 서로 다른 UUID로 새로 적재한 데이터의 출처 차이를 계산 정책 변경으로 감추지 않는다.

순차 상태와 점수 관측

Phase 3 구현은 다음 상태를 복원한다. 날짜가 없는 수동 기록은 전체 계산의 baseline이며 수동 기록 변경은 source 지문 불일치로 전체 계산을 요구한다.

계산일자 완료 상태구간 계산
PR반복수별 최고값 maxima이전 이벤트의 출처를 보존하고, 다음 후보의 엄격한 최고값 상승만 발행. 원본의 정확한 무게·제목은 suffix 세트 ID로만 조회
근력마지막 accepted 상태·마지막 신뢰 근거와 recent_1rm같은 날의 세션 순서·일별 투표를 유지. full fallback이면 세트 목적도 같은 범위부터 복원
최대반복수recent_max, 누적 한계 limit_repssuffix 관측만 다시 작성하고 세션 전/후 값을 구분
점수무게/반복수 × 실측/추정의 마지막 참조 4종각 세션 시작 전에 참조를 고정. 같은 시각의 수동 기록은 뒤에 정렬하고 일자 끝 상태는 다음 날에만 사용

점수 상태의 parts는 그날의 세부 종목별 점수와 null을 저장한다. stats_set_score_cached_parts_v1이 이를 행으로 읽으며, 조회는 점수가 있는 부분만 기존 순서로 합쳐 shared set마다 평균을 소수 한 자리로 반환한다. 종목 필터는 대표 종목이 아니라 실제로 점수에 포함된 구성 종목의 겹침을 사용한다.

Phase 3의 계산 종류별 체크포인트 표와 세트별 점수 표는 발행 행 수를 늘려 660세션·7,920세트 검사에서 시간 초과를 일으켰다. Phase 4는 두 파생 표를 일자별 표로 대체하는 forward migration을 사용한다. 캐시만 비워지며 원본과 기존 공개 통계는 보존한다. 캐시가 없으면 다음 계산이 full로 복원하고, 그 전 점수 조회는 동일 공식의 canonical 입력 경로를 사용한다. 일자별 방식의 첫 대용량 검사는 발행 3,431ms·재발행 2,759ms, 동시 CRUD·취소·재시도·full 수렴을 통과했다. 로컬 단일 측정이며 최종 통합 성능은 별도 검증한다.

계산 도중 점수 관측이 완성되면 같은 트랜잭션의 후속 집계가 이를 읽는다. 일반 조회는 확정 세대·체크포인트 유효성·정규화 관측의 포함 여부를 확인한다. 아직 파생 관측이 없거나 유효하지 않으면 canonical 입력을 같은 참조 선택·점수 공식에 넣는다. 별도 구 정책 엔진은 유지하지 않는다.

기존 3인자 PR/최대반복수 함수는 전체 재계산을 요청하는 유지보수 호환 진입점이다. 새 worker는 날짜를 받는 from_v1 계산을 사용한다. 점수 projection 조회 진입점도 같은 복합 평균 읽기로 위임한다. 호환 진입점의 소유자는 D07이며, D11에서 유지보수·D06 호출자 이행이 확인된 뒤 제거할 수 있다. 테스트의 변경 전 SQL은 실행 경로가 아니라 독립 oracle이다.

D06 입력

D06의 refresh_user_period_calendar_projection_v1(user_id, generation, scope, derived)에 전달할 derived v1은 version, user_id, generation, complete, mode, replay_from, exercise_ids, changes, fallback_reason이다. complete=true는 순차 계산이 끝났다는 뜻이며 applied generation을 뜻하지 않는다.

mode=listedchanges{exercise_id, old_date, new_date, metric_families} 배열이다. 해당 날짜·계산의 결과가 처음 생기면 old_date=null, 모두 없어지면 new_date=null이며 둘 다 null인 항목은 만들지 않는다. 빈 배열은 계산이 완료됐고 추가 파생 영향이 없다는 뜻이다. 복합 종목은 대표 종목뿐 아니라 영향을 받은 구성 종목을 보존한다. D06이 D04 old/new 사실과 합치고 일·주·월·분기·연도 bucket으로 확장한다. Phase 4는 D06 PR #1518(5b16a162)을 합쳐 최상위 refresh에서 실제 DTO를 전달하며, D07 함수의 기간 표 보정 UPDATE와 기간 집계 호출을 제거했다.

refresh_user_sequential_projections_v1은 각 계산의 검증된 시작점을 먼저 읽고, 그 범위의 파생 값·출처를 전후 비교한다. 값이 그대로면 날짜를 추가하지 않는다. 공유 점수가 바뀌면 요청 종목 밖의 구성 종목도 포함한다. 계산 시점에 원본 경계가 달라져 미리 확보한 범위보다 넓게 재생했다면 mode=full로 전달한다. 원본 날짜 이동·삭제는 D04의 old/new 사실이 담당하며 파생 변화와 합쳐 처리한다.

검증 기준

  • 변경 전 a1d462a8 SQL을 테스트 전용 oracle로 보존한다. 동일 canonical ID·시각·원본에서 기존 full과 새 결과의 수치·출처·정렬·null/신뢰 상태를 대조한다. 기대값을 새 구현에서 생성하지 않는다.
  • 날짜 이동·삭제·동일일 다세션·수동 baseline·실패 0회와 1회 이상·복합·보조·인입을 검증한다.
  • 정책·source·generation·경계·상태 손상은 full fallback, 부분 계산 실패는 applied 불변, 다음 유효 작업은 full과 같은 결과로 수렴해야 한다.
  • 최신 append의 D07 raw prefix 읽기와 최대반복수 관측 재작성을 측정한다. 기존 publisher가 사용자 출력 전체를 복사·교체하는 비용은 별도다.

Phase 1 기준선은 260세션·3,120세트에서 오늘 풀업 4세트 저장 시 max-rep 관측 DELETE 240 / INSERT 244 / UPDATE 488, 유지보수 worker 454ms다. 한 번의 로컬 실측이며 운영 p95나 성능 개선율이 아니다.

Phase 3 첫 측정은 같은 프로필에서 DELETE 0 / INSERT 4 / UPDATE 8로 줄었으나 전체 유지보수 시간은 1,165ms로 증가했다. 큰 비용의 기존 훈련 기간·리포트 집계는 D06 교체 범위다. 현재 수치를 최종 성능 개선으로 표시하지 않으며 D06 통합 후 다시 측정한다. 단계별 진단에서 PR은 새 원본 4행, 점수 재생은 원본 0행을 읽었다. 141개 pgTAP 파일·2,770개 단언과 별도 D07 oracle 2건(각 저장 단계 및 손상 복구 대조)이 통과했다.

최종 통합 검증 (2026-09-10)

D06 연결 후 동일 프로필의 오늘 반복수 유지보수 계산은 454→455ms, 관측 DELETE/INSERT/UPDATE는 240/244/488→0/4/8이다. 무게 추가는 466→423ms, 과거 무게 수정은 1,527→2,326ms로 증가했다. 일자 상태 검증·세대 재인증·실제 영향 비교를 포함한 로컬 단일 측정이며 모든 작업의 속도 개선으로 일반화하지 않는다.

최종 독립 oracle·실제 D06 소비 3건, D06 행동 10건, D10 포함 최종 pgTAP 143파일/2,824단언을 통과했다. 캐시 1,976행을 포함한 660세션·7,920세트의 격리 계산·동시 저장·발행·취소·full 수렴도 통과했다. 숫자·측정 조건·최종 Precheck와 release 통합 상태, 재현 가능한 JSON은 업데이트 기록에 보존한다.