LabHub
배우기 러닝패스 코스

データパイプライン

過去の区間を再実行する — 二重に数えないように

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

구간을 받아 과거를 다시 돌리는 실행기 runner.py 를 만든다. 정기 실행과 같은 코드로 파티션 단위 치환을 하고, 구간 예약으로 정기 실행과의 충돌을 막고, 이미 내보낸 구간은 봉인해 정정으로 남기고, 누적 집계가 백필 때문에 두 배가 되는 것을 직접 재현한 뒤 고친다.

왜 중요한가

백필은 지난 구간을 다시 돌리는 일이지만, 어려운 부분은 계산이 아니라 조율이다. 백필이 도는 동안 정기 실행도 돈다. 둘이 같은 날짜를 동시에 쓰면 어느 쪽이 이겼는지 아무도 모른다. 같은 코스의 멱등 실습은 같은 행을 두 번 넣어도 결과가 같아지게 만드는 일이었다. 여기는 행이 아니라 구간이다. 파티션 표가 멱등해도 누적 표가 멱등하지 않으면 백필 한 번에 숫자가 부풀어 오른다. 더하는 누적은 백필을 만나면 반드시 틀리고, 파생시키는 누적은 몇 번을 돌려도 같은 값을 낸다. 그리고 되돌아가면 안 되는 자리가 있다. 이미 보낸 보고서와 이미 나간 알림은 자료가 아니라 사건이다. 그 구간은 봉인하고, 새 값이 나오면 덮어쓰는 대신 정정으로 따로 기록해야 "그때 우리가 무엇을 보냈는가" 와 "지금 무엇이 맞는가" 를 둘 다 말할 수 있다. 채점기는 여러분의 문구를 믿지 않는다. 임시 디렉터리에 채점기가 만든 파티션을 차려 놓고 환경 변수로 그 저장소를 가리킨 뒤 여러분의 실행기를 실제로 실행하고, sqlite 표를 직접 열어 대조한다. 날짜와 금액은 실행마다 바뀝니다.

단계

  1. /root/backfill/gen_events.py 를 만들어 실행해 /root/backfill/events 아래 dt=2026-02-01.jsonl 부터 dt=2026-02-14.jsonl 까지 열나흘치 파티션을 만드세요.
  2. /root/backfill/runner.pyinitrun <시작일> <끝일> <주인> 을 만들어 구간의 파티션을 세어 daily 에 넣게 하세요.
  3. run 이 파티션을 통째로 치환하게 고치고, 통째로 돌린 답과 하루씩 돌린 답과 재실행 결과를 /root/backfill/split.json 에 적으세요.
  4. rollup-add <시작일> <끝일>rollup 을 더하고, 백필 뒤에 두 방식이 얼마나 벌어지는지를 /root/backfill/double.json 에 적으세요.
  5. claim <시작일> <끝일> <주인>release <시작일> <끝일> <주인> 을 더해 구간을 예약하게 하세요.
  6. run 이 남이 잡고 있는 파티션을 건너뛰고 skipped 로 보고하게 하세요. 건너뛴 것이 있으면 종료 코드는 5 입니다.
  7. seal <시작일> <끝일>amend <날짜> <사유> 를 더하세요. 봉인된 파티션은 run 이 건드리지 않고, 늦게 온 전표는 corrections 에 정정으로 남습니다.
  8. /root/backfill/backfill_report.json/root/backfill/backfill_report.md 로 한 장을 남기세요.

참고

열나흘치 파티션 만들기

/root/backfill/gen_events.py 를 만들어 실행해 /root/backfill/events 아래 dt=2026-02-01.jsonl 부터 dt=2026-02-14.jsonl 까지 만드세요. 하루가 파일 하나이고 환불 줄의 amount 는 음수입니다.

날짜는 datetime.date.fromisoformattimedelta 로 늘리면 됩니다. 하루마다 줄 수를 다르게 두면 나중에 파티션별 합계가 서로 구별됩니다. 환불 줄을 몇 개 섞어 금액이 단순한 누적이 아니게 만드세요.

구간을 받아 돌리는 실행기 만들기

/root/backfill/runner.pyinitrun <시작일> <끝일> <주인> 을 만드세요. run 은 구간의 파티션마다 줄 수와 금액을 세어 daily 에 넣고, 원본 파일이 없는 날짜는 skippedno_data 로 남깁니다.

정기 실행은 구간이 하루인 경우일 뿐입니다. 그러니 함수는 하나만 만드세요. 표는 init 에서 CREATE TABLE IF NOT EXISTS 로 만들고, daily 에는 기본 키를 걸지 않습니다. no_data 는 종료 코드를 5 로 만들지 않습니다.

파티션 단위로 치환하기

run 이 한 파티션을 다시 돌릴 때 그 파티션의 옛 결과를 지우고 넣게 고치세요. 그다음 통째로 돌린 답과 하루씩 돌린 답과 재실행 결과를 /root/backfill/split.jsonwhole·by_day·rerun_changed 로 적고, /root/backfill/state.dbdaily 도 날짜마다 한 줄이 되게 다시 채우세요.

치환은 그 날짜의 행을 지우고 다시 넣는 것입니다. 통째로 돌린 답과 하루씩 돌린 답을 견주려면 저장소를 두 번 새로 만들어야 하는데, BACKFILL_DB 환경 변수로 임시 저장소를 가리키면 본 저장소를 건드리지 않고 비교할 수 있습니다.

누적 집계가 두 배가 되는 것 재현하기

rollup-add <시작일> <끝일>rollup 을 더하세요. 그다음 임시 저장소에서 전 구간을 돌려 누적에 더하고, 일부 구간을 백필한 뒤 그 구간을 다시 더해, 파생시킨 값과 얼마나 벌어지는지를 /root/backfill/double.jsonbackfill_range·true_total·add_after_backfill·gap 으로 적으세요.

rollup-add 는 구간 합계를 기존 값에 더하고, rollupdaily 전체를 다시 세어 덮어씁니다. 벌어진 폭은 정확히 백필 구간의 금액 합계와 같아야 합니다 — 그 구간이 누적에 두 번 들어갔기 때문입니다.

구간을 먼저 예약하기

claim <시작일> <끝일> <주인>release <시작일> <끝일> <주인> 을 더하세요. 남이 잡고 있는 날짜는 denied 에 주인과 함께 담고 종료 코드는 4 입니다. release 는 자기 예약만 풉니다.

claims 표의 기본 키를 날짜로 두면 한 날짜를 두 주인이 잡을 수 없습니다. 자기가 이미 잡은 날짜를 다시 잡는 것은 거절이 아니라 성공으로 두세요 — 재시도가 실패로 보이면 아무도 재시도하지 않습니다.

남의 구간은 건너뛰고 보고하기

run 이 남이 잡고 있는 파티션을 건드리지 않고 skippedclaimed_by:<주인> 으로 남기게 하세요. 건너뛴 것이 있으면 종료 코드는 5 입니다. 자기가 잡은 파티션은 그대로 돌립니다.

기다리지도 덮어쓰지도 않는 것이 요점입니다. 건너뛰고 말하면 호출한 쪽이 다시 부를지 사람을 부를지 정할 수 있습니다. 예약 목록은 구간을 돌기 전에 한 번만 읽으면 됩니다.

이미 내보낸 구간은 정정으로 남기기

seal <시작일> <끝일>amend <날짜> <사유> 를 더하세요. 봉인된 파티션은 runsealed 로 건너뜁니다. 그다음 2026-02-02 파티션에 늦게 온 전표 한 줄을 더하고 그 날짜를 봉인한 뒤 amend 로 정정을 남기세요. 정정의 before 와 after 는 달라야 합니다.

amenddaily 를 고치지 않습니다. 원본에서 다시 센 값과 daily 에 남아 있는 값을 나란히 corrections 에 적을 뿐입니다. 봉인되지 않은 날짜에 amend 를 부르면 종료 코드 6 으로 거절하세요 — 그런 날짜는 그냥 run 하면 됩니다.

재실행 한 판을 한 장으로 보고하기

/root/backfill/backfill_report.jsonpartitions·orders·amount·sealed·corrections·rollup·double_gap 을 적고, /root/backfill/backfill_report.md## 무엇을 다시 돌렸나 ## 정기 실행과 어떻게 부딪혔나 ## 누적 집계는 왜 두 배가 되나 ## 되돌리면 안 되는 자리 네 절로 쓰세요.

partitions·orders·amountdaily 표에서, sealedseals 에서, corrections 는 줄 수로, rollup 은 파생시킨 누적에서 읽습니다. double_gap 은 4단계에서 잰 값을 그대로 씁니다. 보고서에는 벌어진 금액을 숫자로 적으세요.