Our Log Says All Succeeded, Their Side Is Missing Twelve
한국어 원문으로 표시합니다.
목표
묶어 보낸 요청에서 몇 건만 실패하는 상황을 다룬다. 건별 결과를 우리 쪽에 기록하고, 다시 보낼 실패와 고쳐야 할 실패를 가르고, 실패분만 재전송하고, 마지막에 보낸 쪽과 받은 쪽의 건수와 합계를 맞춰 보는 대사표를 만든다.
왜 중요한가
묶음 전송 API 는 요청 하나에 200 을 주면서 본문 안에 건별 결과를 담는다. HTTP 의 관점에서 200 은 옳다 — 2xx 는 요청을 받아 수락했다는 뜻이지 요청 안의 모든 항목이 성공했다는 뜻이 아니다. 그래서 호출한 쪽이 상태 코드만 보면 실패한 몇 건은 아무 데도 남지 않는다. 오류도 경보도 없이 며칠이 쌓이고, 상대 쪽 숫자와 맞춰 보다가 발견된다. 고치는 순서는 넷이다. 건별로 기록하고, 실패를 두 갈래로 가르고, 실패분만 다시 보내고, 양쪽 수를 맞춰 본다. 특히 세 번째가 중요하다 — 묶음 전체를 다시 보내면 이미 들어간 건이 두 번 들어간다. 재시도의 단위는 묶음이 아니라 건이다. 채점기는 여러분의 문장을 믿지 않는다. 파트너 서버를 채점기가 고른 포트에 새로 띄우고, 여러분의 발신함과 전송기를 채점기의 데이터베이스에 대고 실제로 돌려 상대 원장과 대조한다.
단계
- /root/recon/receiver.py 를 만들어 포트 8010 에 띄우고, /root/recon/gen_outbox.py 로 보낼 것 120건을 /root/recon/outbox.db 에 만드세요.
- /root/recon/send.py 의
--naive로 상태 코드만 보고 보낸 뒤, 우리 기록과 상대 원장의 차이를 /root/recon/naive.json 에 적으세요. - send.py 가 건별 결과를 읽어
sent표에 status·reason·attempts 로 기록하게 하세요. - /root/recon/classify.py 를 만들어 실패 이유를 다시 보낼 것과 고칠 것으로 가르게 하세요.
- send.py 의
--retry가 다시 보내도 되는 건만 골라 재전송하게 하세요. - /root/recon/recon.py 로 보낸 쪽과 받은 쪽을 대조해 /root/recon/recon_result.json 을 만드세요.
- 남은 건의 처리 계획을 /root/recon/unresolved.json 에 적으세요.
- /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 이 될 때까지 기다린 뒤 다음으로 갑니다. 채점기는 여러분이 띄워 둔 프로세스를 보지 않고 스크립트를 직접 다시 띄웁니다.
묶음 수신 파트너와 보낼 것 120건
/root/recon/receiver.py 를 만들어 포트 8010 에 띄우고, /root/recon/gen_outbox.py 를 만들어 실행해 /root/recon/outbox.db 에 120건을 넣으세요. 계좌가 틀린 것 5건, 금액이 0 인 것 3건, 97의 배수인 것 4건이 섞여 있어야 합니다.
파트너는 요청 자체에는 늘 200 을 주고 실패를 본문의 results 안에 담습니다. 거절 판정은 금액·계좌·중복·일시 보류 순서로 봅니다. 발신함은 보낼 것과 보낸 결과를 나눠 두 표로 만드세요 — 한 표에 섞으면 '보낸 적 없는 것' 과 '보내서 실패한 것' 이 구별되지 않습니다.
상태 코드만 보면 무엇을 놓치나
/root/recon/send.py 에 --naive 를 만들어 상태 코드만 보고 전부 성공으로 세게 하세요. 그 결과와 상대 원장의 차이를 /root/recon/naive.json 에 batches·http_ok·assumed_sent·partner_rows·gap 으로 적으세요.
요청은 정말로 성공했습니다. 실패는 본문 안에 있습니다. 우리 기록이 120건 성공인데 상대 원장에는 몇 행이 있는지 세어 보면 차이가 그대로 드러납니다. 이 단계에서는 sent 표에 아무것도 적지 않습니다.
건별 결과를 우리 쪽에 남기기
send.py 가 응답 본문의 results 를 읽어 건마다 sent 표에 status·reason·attempts 를 남기게 하세요. 성공도 실패도 모두 기록해야 합니다. 두 번째 실행은 이미 보낸 건을 다시 보내지 않아야 합니다.
실패한 것만 적으면 '보낸 적이 없는 것' 과 '보내서 성공한 것' 이 구별되지 않습니다. 응답의 results 순서가 요청 순서와 같다고 가정하지 말고 item_id 로 맞춰 읽는 편이 안전합니다. 아직 보내지 않은 건은 outbox 에는 있고 sent 에는 없는 건입니다.
다시 보낼 것과 고칠 것
/root/recon/classify.py 를 만들어 --reason 으로 받은 이유를 {"retryable": ..., "action": ...} 로 판정하게 하세요. action 은 requeue·fix_data·ignore 셋이고, 모르는 이유는 다시 보내지 않는 쪽으로 답합니다.
일시 보류는 그대로 다시 보내면 풀립니다. 모르는 계좌와 잘못된 금액은 몇 번을 보내도 같은 답이 오므로 사람이 데이터를 고쳐야 합니다. 이미 상대 원장에 들어간 건은 다시 보낼 것도 고칠 것도 없습니다. 이 분류가 없으면 고칠 수 없는 건들이 큐에서 영원히 돕니다.
묶음이 아니라 건 단위로 다시
send.py 의 --retry 가 앞서 거절된 건 가운데 다시 보내도 되는 것만 골라 재전송하고 결과를 sent 에 갱신하게 하세요. 이미 성공한 건은 절대 다시 보내면 안 됩니다.
묶음 전체를 다시 보내면 이미 들어간 건이 두 번 들어갑니다. 상대가 중복을 막아 주면 다행이고 안 막아 주면 우리가 사고를 냅니다. 분류기를 불러 retryable 인 것만 고르고, attempts 를 올려 두어야 몇 번째 시도인지 나중에 압니다.
보낸 쪽과 받은 쪽을 맞춰 본다
/root/recon/recon.py 로 발신함·우리 기록·상대 원장을 대조해 /root/recon/recon_result.json 을 만드세요. 건수뿐 아니라 합계와 양쪽에만 있는 건까지 보아야 하고, 모두 맞을 때만 balanced 가 true 입니다.
건수만 맞춰 보면 한 건이 빠지고 다른 한 건이 두 번 들어간 경우를 놓칩니다. 우리가 성공으로 적은 id 집합과 상대 원장의 id 집합을 서로 빼 보면 어느 쪽에만 있는지 바로 나옵니다. 아직 보내지 않은 건이 있는지도 함께 셉니다.
남은 건에는 주인이 있어야 한다
/root/recon/unresolved.json 에 아직 해결되지 않은 건들을 total·by_action·items(item_id·reason·action·owner)로 적으세요. action 은 분류기의 답과 같아야 하고 owner 는 비워 둘 수 없습니다.
'고쳐야 함 8건' 을 만들어 놓고 누가 고칠지 안 적으면 그 8건은 영원히 남습니다. 대사표의 마지막 칸은 사람이나 팀 이름입니다. items 는 sent 표에서 거절로 남은 건들을 그대로 꺼내 만들면 됩니다.
대사 보고서
/root/recon/recon_report.md 에 ## 무엇을 놓치고 있었나 ## 건별 결과를 읽고 나서 ## 다시 보낼 것과 고칠 것 ## 대사표와 남은 것 네 절로 적으세요. naive.json 과 recon_result.json 의 숫자가 본문에 들어가야 합니다.
읽는 사람은 '우리 로그에는 다 성공인데요' 라고 말한 사람입니다. 그 로그가 거짓말이 아니라 요청의 상태 코드만 본 것이라는 점을 먼저 설명하세요. 그다음 이유별 건수와 대사표의 두 숫자를 보여 주면 됩니다.