LabHub
배우기 러닝패스 코스

ACK 전에 꺼진 간식 자판기 · ACK 직후 꺼진 전달자를 되살린다 · 이론

ACK 직후 꺼진 전달자를 되살린다

LabHub 에서 이어서 보기

한 줄 요약

미완료 항목은 확인 가능한 ACK를 받은 뒤에만 완료 표시하고, 실패하면 이미 끝난 항목을 보존한 채 같은 업무 ID로 다시 보냅니다.

왜 이게 필요했나

접수 DB와 창고 DB를 각각 안전하게 만들었어도 전달 루프의 한 줄이 복구를 망칠 수 있습니다. 요청을 보내기 전에 sent=1을 저장하면 전달 도중 죽은 항목이 다음 조회에서 사라집니다. 보낸 뒤 표시하면 창고 반영과 표식 사이의 종료 때문에 재전송이 생깁니다. 어느 쪽도 전송을 한 번으로 만들지는 않습니다. 이 실습은 재전송을 허용하고 수신 효과를 보호하는 쪽을 선택합니다.

운영자는 “이 요청이 성공했나요?”보다 “어느 상태까지 증거가 있나요?”를 물어야 합니다. orders에 있음, outbox에 미완료로 있음, 창고 inbox에 있음, 발행자에 ACK가 돌아옴, sent가 저장됨은 서로 다른 관측입니다. 한 단계가 다음 단계를 자동으로 증명하지 않습니다. 장애 대응에서 이 차이를 표로 정리하면 불확실성을 지우기 위해 데이터를 삭제하는 실수를 줄일 수 있습니다.

어떻게 동작하나

작은 배치를 순서대로 처리한다

pending은 sent=0인 항목을 outbox의 삽입 순서 seq로 읽습니다. ID의 사전순이나 초 단위 시각으로 정렬하지 않습니다. ID z가 a보다 먼저 접수될 수 있고 같은 시각의 항목이 여럿일 수 있습니다. 한 번에 1–16개만 반환하며 조회 자체는 상태를 바꾸지 않습니다. 조회하자마자 완료로 바꾸면 아직 전송하지 않은 항목을 잃습니다.

dispatch는 선택한 배치의 각 항목에 send를 호출합니다. 정확한 True ACK를 받으면 mark_sent를 호출합니다. 첫 실패에서는 멈추고 원래 오류를 전달합니다. 앞서 끝난 항목은 그대로 완료이며, 실패 항목과 아직 시도하지 않은 뒤 항목은 미완료로 남습니다. 재실행하면 그 지점부터 이어집니다. 이 실습은 전달 루프 하나의 순서를 다루며, 여러 작업자가 같은 배치를 동시에 가져가는 claim·lease·fencing은 구현하지 않습니다.

예를 들어 A·B·C 중 A의 ACK 뒤 완료 표식을 저장했고 B의 응답이 사라졌다면, 다음 배치에는 B·C가 남습니다. B가 창고에 이미 적용됐어도 같은 ID로 재전송하므로 inbox가 중복 효과를 막습니다. C는 아직 보내지 않았습니다. B의 실패를 건너뛰고 C부터 완료시키는 정책도 어떤 시스템에서는 가능하지만, 순서가 중요한 업무라면 다른 의미가 됩니다. 이 실습의 stop-on-first-failure는 명시적으로 선택한 계약입니다.

불필요한 잠금과 인자 변경을 막는다

전송 전에 pending 조회를 끝내고 SQLite 쓰기 트랜잭션을 닫습니다. send가 느리거나 응답을 못 받는 동안 새 주문 접수까지 잠그지 않도록 합니다. 시험에서는 send 콜백 안에서 별도 DB 연결이 BEGIN IMMEDIATE를 시작할 수 있는지 확인합니다. 단순히 con.in_transaction 값만 읽는 것보다 실제 경쟁 연결이 쓰기 경계에 들어갈 수 있는지를 보는 것입니다.

콜백에 넘긴 dict를 상대가 바꿀 수 있다는 점도 생각해야 합니다. 선택한 항목이 B였는데 콜백이 인자의 id를 C로 바꾸면, 그 바뀐 값을 그대로 완료 표식에 사용해서 C를 잃을 수 있습니다. 교육용 event는 문자열과 정수만 있어 dict 사본이면 충분합니다. 원본 선택값으로 표식을 남기고 사본으로 전송합니다. 중첩 객체가 들어가는 다른 계약이라면 얕은 복사만으로 보호되지 않을 수 있습니다.

실제 장애 순서를 강제로 만든다

최종 검사는 임시 source DB에 snack-7을 넣고 별도 프로세스의 dispatch를 실행합니다. HTTP 서버는 별도의 sink DB에 수신 ID와 재고를 커밋합니다. 발행자는 성공 ACK를 확인한 직후 after-delivery 훅에서 os._exit(73)으로 종료됩니다. finally나 정상 close가 호출되지 않습니다. 첫 프로세스가 실제로 그 지점에서 종료했는지 종료 코드를 확인합니다.

부모 검사기는 source에는 미완료 항목이 남고 sink에는 재고 7이 남았는지 독립 연결로 읽습니다. 이어 새 발행 프로세스를 시작합니다. 서버가 관측한 요청은 같은 ID 두 건이어야 하고 최종 재고는 7이어야 합니다. 두 번째 프로세스에서 outbox가 완료로 바뀌어야 합니다. Python 객체의 상태만 새로 만드는 것이 아니라 서로 다른 프로세스가 같은 파일을 다시 여는 시험입니다.

다른 경우에는 서버가 수신 커밋 뒤 HTTP 응답을 보내지 않고 연결을 닫습니다. 이 경우 발행자는 오류를 받고 항목을 미완료로 남겨야 합니다. 다시 연결해 같은 항목을 보내면 서버는 유효한 중복으로 ACK하고 재고는 그대로입니다. 틀린 ID의 ACK, 과대 응답, 리다이렉트도 완료 증거로 받아들이지 않습니다. send_http는 이 실습의 loopback /events만 허용하고 프록시 환경 변수를 사용하지 않으며 리다이렉트를 따라가지 않습니다.

현장에서 만나는 모습

실제 운영에서는 전송 성공 횟수만으로 상태를 판단하지 않습니다. 미완료 항목 수, 가장 오래된 미완료의 나이, 시도 횟수, 수신 중복 수, 충돌 수를 나눠 봅니다. 앞 항목 하나가 영구적인 형식 오류라면 뒤 항목이 계속 막힐 수 있습니다. 무한 재시도 대신 경보·격리·수정·재조정 절차를 마련해야 합니다. 격리했다고 성공한 것은 아니며, 순서를 바꾸는 영향도 함께 기록해야 합니다.

재시도 대기에는 상한과 지터가 필요하지만 이번 dispatch는 호출 한 번의 유한 배치만 실행합니다. 자동 재시도 스케줄러나 고가용성 작업 분배를 구현했다고 설명하지 않습니다. source와 sink 파일도 LabHub 세션이 끝나면 사라집니다. 여기서 검증한 것은 프로세스 종료 후 같은 디스크를 다시 사용하는 경우입니다. 호스트 전원 차단, 디스크 고장, 백업 복구나 인터넷에서의 성능은 별도 검증입니다.

다음 실습에서 할 것

worker.py에 이벤트 검증, 저장소 초기화, 원자적 접수, 미완료 조회, 중복 수신 처리, 완료 표식, 유한 배치 전달, HTTP ACK 확인을 순서대로 구현합니다. 제공된 검사기는 임시 DB와 loopback 서버를 직접 만들고 정답 코드를 실행합니다. 빈 함수 틀은 문법만 유효하며 답을 대신하지 않습니다. 실습 뒤 퀴즈에서는 두 번 전달·한 번 적용의 결과가 입증하는 범위와 남은 운영 책임을 정리합니다.

공식 문서로 더 읽기