통합과 배포 · 200 이라고 다 들어간 것은 아니다 · 실습
우리 로그는 전부 성공인데 상대에겐 열두 건이 없다
목표
묶어 보낸 요청에서 몇 건만 실패하는 상황을 다룬다. 건별 결과를 우리 쪽에 기록하고, 다시 보낼 실패와 고쳐야 할 실패를 가르고, 실패분만 재전송하고, 마지막에 보낸 쪽과 받은 쪽의 건수와 합계를 맞춰 보는 대사표를 만든다.
왜 중요한가
묶음 전송 API 는 요청 하나에 200 을 주면서 본문 안에 건별 결과를 담는다. HTTP 의 관점에서 200 은 옳다 — 2xx 는 요청을 받아 수락했다는 뜻이지 요청 안의 모든 항목이 성공했다는 뜻이 아니다.
그래서 호출한 쪽이 상태 코드만 보면 실패한 몇 건은 아무 데도 남지 않는다. 오류도 경보도 없이 며칠이 쌓이고, 상대 쪽 숫자와 맞춰 보다가 발견된다.
고치는 순서는 넷이다. 건별로 기록하고, 실패를 두 갈래로 가르고, 실패분만 다시 보내고, 양쪽 수를 맞춰 본다. 특히 세 번째가 중요하다 — 묶음 전체를 다시 보내면 이미 들어간 건이 두 번 들어간다. 재시도의 단위는 묶음이 아니라 건이다.
채점기는 여러분의 문장을 믿지 않는다. 파트너 서버를 채점기가 고른 포트에 새로 띄우고, 여러분의 발신함과 전송기를 채점기의 데이터베이스에 대고 실제로 돌려 상대 원장과 대조한다.
단계
1. /root/recon/receiver.py 를 만들어 포트 8010 에 띄우고, /root/recon/gen_outbox.py 로 보낼 것 120건을 /root/recon/outbox.db 에 만드세요.
2. /root/recon/send.py 의 --naive 로 상태 코드만 보고 보낸 뒤, 우리 기록과 상대 원장의 차이를 /root/recon/naive.json 에 적으세요.
3. send.py 가 건별 결과를 읽어 sent 표에 status·reason·attempts 로 기록하게 하세요.
4. /root/recon/classify.py 를 만들어 실패 이유를 다시 보낼 것과 고칠 것으로 가르게 하세요.
5. send.py 의 --retry 가 다시 보내도 되는 건만 골라 재전송하게 하세요.
6. /root/recon/recon.py 로 보낸 쪽과 받은 쪽을 대조해 /root/recon/recon_result.json 을 만드세요.
7. 남은 건의 처리 계획을 /root/recon/unresolved.json 에 적으세요.
8. /root/recon/recon_report.md 에 네 절로 보고하세요.
참고
- 파트너 실행 계약:
python3 /root/recon/receiver.py --port <포트>.POST /batch는{"batch_id": ..., "items": [{"item_id", "account", "amount"}]}를 받아 200 과 함께{"batch_id", "accepted", "rejected", "results": [{"item_id", "status", "reason"}]}를 냅니다.GET /ledger는{"rows": [...], "count": n, "total": m}입니다. - 파트너의 거절 이유는 넷입니다.
invalid_amount(금액이 양의 정수가 아님) ·unknown_account(아는 계좌가 AC-01 부터 AC-08 까지뿐임) ·duplicate(이미 원장에 있음) ·temporary_hold(금액이 97의 배수인 건을 처음 한 번만 잡아 둠). 판정 순서는 이 차례입니다. - 발신함 실행 계약:
python3 gen_outbox.py [--db <경로>]는outbox(item_id, account, amount, batch_id)120건과 빈sent(item_id, status, reason, attempts)를 만듭니다. 120건 가운데 계좌가 틀린 것 5건, 금액이 0 인 것 3건, 97의 배수인 것 4건이 섞여 있어야 하고 묶음은 30건씩 4개입니다. - 전송기 실행 계약:
python3 send.py --base <URL> --db <sqlite> [--batch-size 30] [--retry] [--naive]는{"sent": n, "accepted": n, "rejected": n, "by_reason": {...}, "http_ok": n}를 냅니다. 기본은 아직 보내지 않은 건을,--retry는 앞서 거절된 건 가운데 다시 보내도 되는 것만 보냅니다.--naive는 건별 결과를 읽지 않고 상태 코드만 보며sent표에 아무것도 적지 않습니다. - 분류기 실행 계약:
python3 classify.py --reason <이유>는{"retryable": true|false, "action": "requeue"|"fix_data"|"ignore"}를 냅니다. 모르는 이유는 안전한 쪽(다시 보내지 않음)으로 답합니다. - 대조기 실행 계약:
python3 recon.py --base <URL> --db <sqlite> --out <결과 JSON>은outbox_items·accepted·rejected·not_sent·partner_rows·partner_total·our_accepted_total·only_ours·only_theirs·by_reason·unresolved·balanced를 냅니다.balanced는 보내지 않은 건이 없고 양쪽에만 있는 건도 없으며 건수와 합계가 모두 맞을 때 true 입니다. - 남은 건 형식:
{"total": n, "by_action": {...}, "items": [{"item_id", "reason", "action", "owner"}]}.owner는 사람이나 팀 이름입니다 — 주인이 없으면 그 건은 영원히 남습니다. - 흔한 실수: 상태 코드만 보기, 실패한 것만 기록하기(보낸 적 없는 것과 구별이 안 됩니다), 묶음 전체를 다시 보내기(중복이 생깁니다), 건수만 맞춰 보고 합계는 안 보기.
- 서버는 백그라운드로 띄우고
/health가 200 이 될 때까지 기다린 뒤 다음으로 갑니다. 채점기는 여러분이 띄워 둔 프로세스를 보지 않고 스크립트를 직접 다시 띄웁니다.
단계 8개
- 묶음 수신 파트너와 보낼 것 120건
- 상태 코드만 보면 무엇을 놓치나
- 건별 결과를 우리 쪽에 남기기
- 다시 보낼 것과 고칠 것
- 묶음이 아니라 건 단위로 다시
- 보낸 쪽과 받은 쪽을 맞춰 본다
- 남은 건에는 주인이 있어야 한다
- 대사 보고서