過去の区間を再実行する — 二重に数えないように
한국어 원문으로 표시합니다.
목표
구간을 받아 과거를 다시 돌리는 실행기 runner.py 를 만든다. 정기 실행과 같은 코드로 파티션 단위 치환을 하고, 구간 예약으로 정기 실행과의 충돌을 막고, 이미 내보낸 구간은 봉인해 정정으로 남기고, 누적 집계가 백필 때문에 두 배가 되는 것을 직접 재현한 뒤 고친다.
왜 중요한가
백필은 지난 구간을 다시 돌리는 일이지만, 어려운 부분은 계산이 아니라 조율이다. 백필이 도는 동안 정기 실행도 돈다. 둘이 같은 날짜를 동시에 쓰면 어느 쪽이 이겼는지 아무도 모른다. 같은 코스의 멱등 실습은 같은 행을 두 번 넣어도 결과가 같아지게 만드는 일이었다. 여기는 행이 아니라 구간이다. 파티션 표가 멱등해도 누적 표가 멱등하지 않으면 백필 한 번에 숫자가 부풀어 오른다. 더하는 누적은 백필을 만나면 반드시 틀리고, 파생시키는 누적은 몇 번을 돌려도 같은 값을 낸다. 그리고 되돌아가면 안 되는 자리가 있다. 이미 보낸 보고서와 이미 나간 알림은 자료가 아니라 사건이다. 그 구간은 봉인하고, 새 값이 나오면 덮어쓰는 대신 정정으로 따로 기록해야 "그때 우리가 무엇을 보냈는가" 와 "지금 무엇이 맞는가" 를 둘 다 말할 수 있다. 채점기는 여러분의 문구를 믿지 않는다. 임시 디렉터리에 채점기가 만든 파티션을 차려 놓고 환경 변수로 그 저장소를 가리킨 뒤 여러분의 실행기를 실제로 실행하고, sqlite 표를 직접 열어 대조한다. 날짜와 금액은 실행마다 바뀝니다.
단계
- /root/backfill/gen_events.py 를 만들어 실행해 /root/backfill/events 아래
dt=2026-02-01.jsonl부터dt=2026-02-14.jsonl까지 열나흘치 파티션을 만드세요. - /root/backfill/runner.py 에
init와run <시작일> <끝일> <주인>을 만들어 구간의 파티션을 세어daily에 넣게 하세요. run이 파티션을 통째로 치환하게 고치고, 통째로 돌린 답과 하루씩 돌린 답과 재실행 결과를 /root/backfill/split.json 에 적으세요.rollup-add <시작일> <끝일>과rollup을 더하고, 백필 뒤에 두 방식이 얼마나 벌어지는지를 /root/backfill/double.json 에 적으세요.claim <시작일> <끝일> <주인>과release <시작일> <끝일> <주인>을 더해 구간을 예약하게 하세요.run이 남이 잡고 있는 파티션을 건너뛰고skipped로 보고하게 하세요. 건너뛴 것이 있으면 종료 코드는 5 입니다.seal <시작일> <끝일>과amend <날짜> <사유>를 더하세요. 봉인된 파티션은run이 건드리지 않고, 늦게 온 전표는corrections에 정정으로 남습니다.- /root/backfill/backfill_report.json 과 /root/backfill/backfill_report.md 로 한 장을 남기세요.
참고
- 실행 계약:
python3 /root/backfill/runner.py <명령> .... 답은 JSON 한 덩어리로 표준출력에 냅니다. 종료 코드는 0 성공, 2 모르는 명령이거나 인자 수가 다름, 3 상태 저장소 없음, 4 예약이 거절됨, 5 건너뛴 구간이 있음, 6 봉인되지 않은 구간에amend를 부름 입니다. - 상태 저장소 경로는 환경 변수
BACKFILL_DB로, 원본 파티션 폴더는BACKFILL_EVENTS로 바꿀 수 있어야 합니다. 기본값은 각각/root/backfill/state.db와/root/backfill/events입니다. 채점기가 이 두 변수로 자기 저장소를 가리킵니다. - 파티션 파일 이름은
dt=YYYY-MM-DD.jsonl이고, 줄 모양은{"order_id": 문자열, "dt": 날짜, "amount": 정수, "status": "paid" 또는 "refund"}입니다. 환불 줄의 amount 는 음수입니다. 한 파티션의orders는 줄 수,amount는 amount 의 합입니다. - 표는 다섯입니다.
daily(dt, orders, amount, owner)·rollup(metric, value)·claims(dt, owner)·seals(dt)·corrections(dt, orders_before, amount_before, orders_after, amount_after, reason).daily에는 기본 키를 걸지 않습니다 — 치환을 직접 구현해 보는 것이 이 실습의 요점입니다. run응답:{"owner": 문자열, "done": [날짜...], "skipped": [[날짜, 사유]...], "orders": 정수, "amount": 정수}.orders와amount는done에 든 파티션만 더한 값입니다. 사유는claimed_by:<주인>·sealed·no_data셋입니다. 원본 파일이 없는 날짜(no_data)는 종료 코드를 5 로 만들지 않습니다.claim응답:{"owner": 문자열, "claimed": [날짜...], "denied": [[날짜, 주인]...]}. 자기가 이미 잡은 날짜는claimed에 그대로 들어갑니다.release응답:{"owner": 문자열, "released": [날짜...]}이고 자기 예약만 풉니다.seal응답:{"sealed": [날짜...]}.amend응답:{"dt": 날짜, "orders_before": 정수, "amount_before": 정수, "orders_after": 정수, "amount_after": 정수, "reason": 문자열}.amend는daily를 고치지 않고corrections에 한 줄을 더합니다.rollup-add응답과rollup응답은{"mode": "add" 또는 "derive", "order_total": 정수, "amount_total": 정수}입니다.rollup-add는 구간 합계를 기존 값에 더하고,rollup은daily전체에서 다시 계산해 덮어씁니다.- 성능으로 판정하지 않습니다. 백필이 얼마나 걸렸는지는 재지 말고, 무엇을 어떤 순서로 건드렸는지만 남기세요.
- 공식 문서: python sqlite3 · SQLite Transaction · SQLite UPSERT · Airflow Dag Runs
- 흔한 실수: 백필용 코드를 따로 만들기, 구간 전체를 한 트랜잭션에 넣기, 남의 구간을 조용히 덮어쓰기, 누적을 더하는 방식으로 두기, 이미 내보낸 구간을 덮어쓰고 그때 무엇을 보냈는지 잃기.
- 상태 저장소를 눈으로 보려면
sqlite3 /root/backfill/state.db 'SELECT * FROM daily ORDER BY dt'를 쓰세요.
열나흘치 파티션 만들기
/root/backfill/gen_events.py 를 만들어 실행해 /root/backfill/events 아래 dt=2026-02-01.jsonl 부터 dt=2026-02-14.jsonl 까지 만드세요. 하루가 파일 하나이고 환불 줄의 amount 는 음수입니다.
날짜는 datetime.date.fromisoformat 과 timedelta 로 늘리면 됩니다. 하루마다 줄 수를 다르게 두면 나중에 파티션별 합계가 서로 구별됩니다. 환불 줄을 몇 개 섞어 금액이 단순한 누적이 아니게 만드세요.
구간을 받아 돌리는 실행기 만들기
/root/backfill/runner.py 에 init 와 run <시작일> <끝일> <주인> 을 만드세요. run 은 구간의 파티션마다 줄 수와 금액을 세어 daily 에 넣고, 원본 파일이 없는 날짜는 skipped 에 no_data 로 남깁니다.
정기 실행은 구간이 하루인 경우일 뿐입니다. 그러니 함수는 하나만 만드세요. 표는 init 에서 CREATE TABLE IF NOT EXISTS 로 만들고, daily 에는 기본 키를 걸지 않습니다. no_data 는 종료 코드를 5 로 만들지 않습니다.
파티션 단위로 치환하기
run 이 한 파티션을 다시 돌릴 때 그 파티션의 옛 결과를 지우고 넣게 고치세요. 그다음 통째로 돌린 답과 하루씩 돌린 답과 재실행 결과를 /root/backfill/split.json 에 whole·by_day·rerun_changed 로 적고, /root/backfill/state.db 의 daily 도 날짜마다 한 줄이 되게 다시 채우세요.
치환은 그 날짜의 행을 지우고 다시 넣는 것입니다. 통째로 돌린 답과 하루씩 돌린 답을 견주려면 저장소를 두 번 새로 만들어야 하는데, BACKFILL_DB 환경 변수로 임시 저장소를 가리키면 본 저장소를 건드리지 않고 비교할 수 있습니다.
누적 집계가 두 배가 되는 것 재현하기
rollup-add <시작일> <끝일> 과 rollup 을 더하세요. 그다음 임시 저장소에서 전 구간을 돌려 누적에 더하고, 일부 구간을 백필한 뒤 그 구간을 다시 더해, 파생시킨 값과 얼마나 벌어지는지를 /root/backfill/double.json 에 backfill_range·true_total·add_after_backfill·gap 으로 적으세요.
rollup-add 는 구간 합계를 기존 값에 더하고, rollup 은 daily 전체를 다시 세어 덮어씁니다. 벌어진 폭은 정확히 백필 구간의 금액 합계와 같아야 합니다 — 그 구간이 누적에 두 번 들어갔기 때문입니다.
구간을 먼저 예약하기
claim <시작일> <끝일> <주인> 과 release <시작일> <끝일> <주인> 을 더하세요. 남이 잡고 있는 날짜는 denied 에 주인과 함께 담고 종료 코드는 4 입니다. release 는 자기 예약만 풉니다.
claims 표의 기본 키를 날짜로 두면 한 날짜를 두 주인이 잡을 수 없습니다. 자기가 이미 잡은 날짜를 다시 잡는 것은 거절이 아니라 성공으로 두세요 — 재시도가 실패로 보이면 아무도 재시도하지 않습니다.
남의 구간은 건너뛰고 보고하기
run 이 남이 잡고 있는 파티션을 건드리지 않고 skipped 에 claimed_by:<주인> 으로 남기게 하세요. 건너뛴 것이 있으면 종료 코드는 5 입니다. 자기가 잡은 파티션은 그대로 돌립니다.
기다리지도 덮어쓰지도 않는 것이 요점입니다. 건너뛰고 말하면 호출한 쪽이 다시 부를지 사람을 부를지 정할 수 있습니다. 예약 목록은 구간을 돌기 전에 한 번만 읽으면 됩니다.
이미 내보낸 구간은 정정으로 남기기
seal <시작일> <끝일> 과 amend <날짜> <사유> 를 더하세요. 봉인된 파티션은 run 이 sealed 로 건너뜁니다. 그다음 2026-02-02 파티션에 늦게 온 전표 한 줄을 더하고 그 날짜를 봉인한 뒤 amend 로 정정을 남기세요. 정정의 before 와 after 는 달라야 합니다.
amend 는 daily 를 고치지 않습니다. 원본에서 다시 센 값과 daily 에 남아 있는 값을 나란히 corrections 에 적을 뿐입니다. 봉인되지 않은 날짜에 amend 를 부르면 종료 코드 6 으로 거절하세요 — 그런 날짜는 그냥 run 하면 됩니다.
재실행 한 판을 한 장으로 보고하기
/root/backfill/backfill_report.json 에 partitions·orders·amount·sealed·corrections·rollup·double_gap 을 적고, /root/backfill/backfill_report.md 에 ## 무엇을 다시 돌렸나 ## 정기 실행과 어떻게 부딪혔나 ## 누적 집계는 왜 두 배가 되나 ## 되돌리면 안 되는 자리 네 절로 쓰세요.
partitions·orders·amount 는 daily 표에서, sealed 는 seals 에서, corrections 는 줄 수로, rollup 은 파생시킨 누적에서 읽습니다. double_gap 은 4단계에서 잰 값을 그대로 씁니다. 보고서에는 벌어진 금액을 숫자로 적으세요.