ACK 전에 꺼진 간식 자판기 · ACK 직후 꺼진 전달자를 되살린다 · 실습
간식 주문은 저장됐는데 창고는 몰랐다
목표
업무와 전송 의도, 수신 ID와 업무 효과를 각각 원자적으로 저장하고 실제 HTTP 재전송·프로세스 종료 후 복구합니다.
왜 중요한가
응답이 없다는 이유만으로 업무가 실행되지 않았다고 단정하면 중복 반영을 만듭니다. 반대로 전송 전에 완료 표시하면 아직 전달하지 않은 업무를 잃습니다. Python과 SQLite로 두 경계를 분리해 구현하며 정리 코드가 실행되지 않는 프로세스 종료에서도 남는 상태를 확인합니다.
예상 85분입니다. 기본 세션보다 길므로 만료 전에 +시간을 눌러 연장하세요. 세션 종료 후 파일이 사라집니다. 필요한 코드는 별도로 보관하세요. Python 함수·예외·SQLite 트랜잭션과 앞 모듈의 ACK 복구를 알고 시작합니다.
데이터 계약
모든 제출 코드는 /root/outbox/worker.py입니다. 아래 함수가 받은 con은 올바른 역할의 열린 연결이며 호출 전에 트랜잭션이 없습니다. open_store를 제외한 함수는 빌린 연결을 닫지 않습니다. 모든 성공·실패 뒤 열린 트랜잭션을 남기지 않습니다. source와 sink는 반드시 별개 파일로 시험하며 운영 DB에는 접속하지 않습니다.
source 스키마:
CREATE TABLE orders (id TEXT PRIMARY KEY, qty INTEGER NOT NULL);CREATE TABLE outbox (seq INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL UNIQUE, qty INTEGER NOT NULL, sent INTEGER NOT NULL CHECK(sent IN (0,1)));sink 스키마:
CREATE TABLE inbox (id TEXT PRIMARY KEY, qty INTEGER NOT NULL);CREATE TABLE stock (id INTEGER PRIMARY KEY CHECK(id=1), total INTEGER NOT NULL);stock 초기 행은 (1,0)입니다. 각 역할의 초기화는 한 트랜잭션이며 기존 내용을 보존합니다. 검사기가 임시 DB·loopback HTTP 서버·자식 프로세스를 만들고 정리하므로 DB 경로나 고정 포트를 코드에 넣지 마세요.
단계
1. 전송 ID와 수량의 계약을 만든다 — worker.py에 Exception 하위 Conflict·DeliveryError와 validate_event(event)를 구현하세요. event는 정확히 id·qty 두 키인 dict입니다. id는 ASCII 영문·숫자·밑줄·하이픈 1–64자, qty는 bool 제외 int 1–1000입니다. 잘못되면 ValueError, 정상이면 입력을 바꾸지 않고 새 dict 사본을 반환합니다.
2. 서로 다른 두 저장소를 초기화한다 — open_store(path, role)는 source 또는 sink 역할의 sqlite3.Connection을 반환합니다. isolation_level=None·busy timeout 1초로 열고 아래 스키마를 없을 때만 만드세요. sink의 stock 초기 행 (1,0)은 없을 때만 넣습니다. 초기화는 한 트랜잭션이며 실패에는 연결을 닫고 오류를 전달합니다. 기존 기록을 초기값으로 덮어쓰지 않습니다. 다른 role은 연결 전에 ValueError입니다.
3. 주문과 전송 의도를 함께 남긴다 — enqueue(con,event,fault=None)는 validate_event 후 BEGIN IMMEDIATE에서 처리합니다. 새 ID는 orders 삽입→fault('after-order')→outbox(sent=0) 삽입→fault('after-outbox')→COMMIT→fault('after-commit') 순서로 진행하고 True를 반환합니다. fault는 있을 때만 호출합니다. 기존 ID·같은 qty는 변경 없이 False, 다른 qty는 Conflict입니다. 커밋 전 오류는 전부 롤백하고 원래 오류를 전달하며, 커밋 후 오류는 확정된 기록을 지우지 않습니다.
4. 완료하지 않은 배치를 순서대로 읽는다 — pending(con,limit)는 sent=0인 outbox를 seq 오름차순으로 최대 limit개 반환합니다. 각 원소는 id·qty만 가진 새 dict입니다. limit는 bool 제외 int 1–16이며 다른 값은 ValueError입니다. 조회는 DB나 입력을 변경하지 않습니다.
5. 중복 수신의 효과를 한 번으로 제한한다 — receive(con,event,fault=None)는 sink 연결에서 동작합니다. 검증 뒤 BEGIN IMMEDIATE에서 새 ID의 inbox 삽입→fault('after-inbox')→stock.total에 qty 더하기→fault('after-total')→COMMIT→fault('after-commit') 순서로 True를 반환합니다. 같은 ID·qty는 False, 같은 ID·다른 qty는 Conflict입니다. 다른 ID·같은 qty는 별개 업무입니다. 커밋 전 오류는 전체 롤백, 커밋 후 오류는 확정 상태 보존이며 원래 오류를 전달합니다.
6. 확인한 항목에만 완료 표식을 남긴다 — mark_sent(con,event)는 검증한 id·qty와 일치하는 source outbox의 sent만 1로 바꿉니다. 없는 ID는 ValueError, 다른 qty는 Conflict이며 실패에 DB 상태를 바꾸지 않습니다. 새 표식이면 True, 이미 완료면 False입니다. orders와 outbox 행을 삭제하지 않고 트랜잭션을 남기지 않습니다.
7. ACK 뒤 표시하고 첫 실패에서 멈춘다 — dispatch(con,send,limit=8,fault=None)는 pending의 유한 배치만 순서대로 처리합니다. send에 event 사본을 주고 정확한 True ACK를 받은 뒤 fault('after-delivery'), mark_sent를 호출합니다. False·None·1·문자열은 DeliveryError이고 send/fault의 예외는 원래대로 전달합니다. 첫 실패에서 멈추며 앞선 완료는 보존합니다. 반환값은 이번에 완료 표시한 개수입니다. 전송 중 쓰기 트랜잭션을 잡지 않으며 콜백이 인자를 바꿔도 원래 선택한 항목만 완료 표시합니다.
8. HTTP와 실제 프로세스 재시작으로 복구한다 — send_http(url,event,timeout=1)를 구현하세요. validate_event 후 HTTP POST JSON을 보내고 상태 200, 1024바이트 이하 JSON dict의 정확한 두 키 id·accepted, 요청과 같은 id, accepted is True일 때만 True를 반환합니다. 잘못된 ACK는 DeliveryError, HTTP·연결 오류는 전달하며 응답을 닫습니다. timeout은 bool 제외 양의 유한 int/float 5초 이하이고 아니면 ValueError입니다. URL은 http://127.0.0.1:포트/events만 허용하며 명시적 포트 1–65535, 사용자정보·쿼리·fragment 없음이어야 합니다. 자동 프록시와 리다이렉트를 사용하지 않습니다. 최종 검사는 수신 커밋 뒤 발행자를 실제 종료하고 새 프로세스가 같은 ID를 재전송해 재고를 한 번만 늘리는지 확인합니다.
참고
- 최종 진단 명령: python3 -B /opt/fixtures/outbox/check.py 8 /root/outbox/worker.py. 앞 단계는 8을 해당 단계 번호로 바꿉니다. 제출 파일을 고치지 않고 임시 저장소에서 실행하며 채점은 12초로 제한합니다.
- 기본 라이브러리만 쓰고 인터넷·설치·추가 capability가 필요 없습니다. 제공 HTTP 서버는 loopback 전용이며 인증·TLS가 없는 교육용입니다. send_http의 timeout은 소켓의 블로킹 동작별 대기 상한이지 전체 요청 총 기한이 아닙니다.
- 단일 전달 루프의 유한 배치와 같은 디스크의 프로세스 재시작을 검증합니다. 자동 재시도 스케줄러·다중 작업자 claim/lease·디스크 소실·외부 결제 원자성·대규모 성능은 검증하지 않습니다.
- 수신 커밋 뒤 응답이 사라져도 기존 업무를 지우지 마세요. 같은 ID·내용으로 다시 전달하고 효과가 한 번인지 독립 연결로 확인합니다.
단계 8개
- 전송 ID와 수량의 계약을 만든다
- 서로 다른 두 저장소를 초기화한다
- 주문과 전송 의도를 함께 남긴다
- 완료하지 않은 배치를 순서대로 읽는다
- 중복 수신의 효과를 한 번으로 제한한다
- 확인한 항목에만 완료 표식을 남긴다
- ACK 뒤 표시하고 첫 실패에서 멈춘다
- HTTP와 실제 프로세스 재시작으로 복구한다