통합과 배포 · 받아 오는 동안에도 원본은 바뀐다 · 실습
매일 받아 오는데 매번 몇 건씩 빈다
목표
계속 바뀌는 원본을 페이지로 나눠 받는 동기화를 만든다. 오프셋 방식이 왜 조용히 행을 빠뜨리는지 직접 재현하고, (updated_at, id) 커서로 옮기고, 워터마크로 중단 지점부터 재개하고, 마지막에 원본과 사본을 대조한다.
왜 중요한가
60만 건을 1000건씩 600번에 나눠 받는 동안 원본도 계속 바뀐다. 이미 지나온 페이지의 행이 갱신되어 정렬 순서에서 맨 뒤로 가면, 뒤에 있던 행들이 앞으로 당겨지고 그 사이의 행은 아무도 읽지 않은 채 지나간다.
이 사고는 오류가 나지 않고, 건수는 대충 맞고, 매번 다른 행이 빠진다. 그래서 재현이 안 되고 몇 달 뒤 신고로 돌아온다.
고치는 방법은 자리를 번호가 아니라 값으로 잡는 것이다. 다만 정렬 키가 유일하지 않으면 또 다른 함정이 기다린다 — updated_at 하나로 잡으면 같은 시각의 행을 잃거나 같은 자리를 무한히 돈다. 그래서 커서는 유일해질 때까지 칸을 늘린다.
채점기는 여러분의 문장을 믿지 않는다. 원본 서버를 채점기가 고른 포트에 직접 띄우고, 채점기가 원본을 바꿔 가며 여러분의 동기화를 실제로 돌려 무엇이 빠지고 무엇이 겹쳤는지 센다.
단계
1. /root/sync/source.py 를 만들어 포트 8019 에 띄우고, 전체를 /root/sync/snapshot.json 에 뜨세요.
2. /root/sync/offset_sync.py 를 만들어 오프셋으로 순회하되 도중에 원본을 바꾸어, 누락과 중복이 함께 생기는 것을 숫자로 보이세요.
3. /root/sync/cursor_sync.py 를 만들어 (updated_at, id) 커서로 순회해 같은 상황에서 누락이 0 이 되게 하세요.
4. cursor_sync.py 에 --state 를 붙여 워터마크를 파일에 남기고, 두 번째 실행이 바뀐 것만 받아 오게 하세요.
5. 페이지가 같은 updated_at 을 가진 행들의 한가운데를 가르도록 만들어도 누락도 중복도 없음을 /root/sync/tie_report.json 에 적으세요.
6. 중단된 동기화를 이어받아 /root/sync/sink.jsonl 에 전체를 모으세요.
7. /root/sync/reconcile.py 로 원본과 사본을 대조해 /root/sync/sync_result.json 을 만드세요.
8. /root/sync/sync_report.md 에 네 절로 보고하세요.
참고
- 원본 서버 실행 계약:
python3 /root/sync/source.py --port <포트>./health는{"ok": true, "rows": 60},/all은 전체를(updated_at, id)순으로,/rows?offset=&limit=은 오프셋 방식,/rows?since=&since_id=&limit=은 커서 방식으로 냅니다./mutate?ids=R-0001,R-0002는 그 행들의updated_at을 지금 가장 큰 값 뒤로 밀고value를 1 올립니다. - 행은 60개이고
id·updated_at·value세 칸입니다. 21번부터 25번까지 다섯 행은updated_at이 같습니다 — 5번 단계의 경계가 여기서 생깁니다. - 순회기 실행 계약(둘 공통):
--base <URL> --limit <n> [--snapshot <파일>] [--mutate-after <페이지>] [--mutate-ids <a,b,c>] [--out <jsonl>]. 출력은{"pages": ..., "fetched": ..., "unique": ..., "missing": [...], "duplicated": [...]}이고, cursor_sync.py 는 여기에watermark가 더 붙으며--state <파일>과--max-pages <k>를 더 받습니다. pages는 한 행 이상을 받아 온 요청의 수입니다.missing은--snapshot을 주었을 때만 채우고, 안 주면 빈 리스트입니다.--out은 받은 행을 한 줄에 하나씩 JSON 으로 덧붙여 씁니다.--mutate-after <페이지>는 그 페이지를 받은 직후에--mutate-ids의 행들을 원본에서 갱신합니다. 도는 동안 원본이 바뀌는 상황을 우리가 흉내 내는 장치입니다.- 대조기 실행 계약:
python3 reconcile.py --snapshot <원본 JSON> --sink <사본 JSONL> --out <결과 JSON>은{"source_rows": ..., "sink_rows": ..., "unique": ..., "missing": [...], "extra": [...], "value_mismatch": [...], "match": ...}를 냅니다.sink_rows는 사본 파일의 줄 수,unique는 서로 다른 id 의 수입니다. 같은 id 가 여러 번 있으면updated_at이 가장 큰 것을 씁니다. - 경계 보고 형식:
{"limit": ..., "pages": ..., "fetched": ..., "missing": [...], "duplicated": [...], "tie_updated_at": ..., "tie_ids": [...]}. - 흔한 실수: 커서를
updated_at하나로만 잡기, 워터마크를 처리 전에 저장하기, 중복을 버그로 보고 없애려 하기(적어도 한 번이 정상입니다), 건수만 맞춰 보고 값은 안 맞춰 보기. - 서버는 백그라운드로 띄우고
/health가 200 이 될 때까지 기다린 뒤 다음으로 갑니다. 채점기는 여러분이 띄워 둔 프로세스를 보지 않고 스크립트를 직접 다시 띄웁니다.
단계 8개
- 계속 바뀌는 원본 띄우기
- 오프셋 순회가 삼킨 세 행
- 자리를 번호가 아니라 값으로
- 다음 실행이 어디서부터인지 기억하기
- 같은 시각을 가진 다섯 행
- 중단된 동기화를 이어받기
- 다 받았는지 세어 본다
- 동기화 점검 보고서