소개
Nightjar Print Studio는 워크숍 참가 등록을 받고 다운로드할 수 있는 입장권을 준비합니다. 워커가 멈춰 있는 동안 등록은 안전하게 기다릴 수 있지만 현재 워커는 전달을 너무 일찍 확인합니다. 나중에 완료된 작업을 다시 시도하면 중복 표가 생길 수도 있습니다.
기본 터미널 사용과 짧은 Python 함수, 들여쓰기, 사전 조회를 읽는 능력이 필요합니다. 아래의 짧은 연결 설명에서 이를 복습하고 SQL과 트랜잭션을 소개합니다. RabbitMQ, Python, SQLite 데이터베이스와 완성된 사이트가 준비되어 있으며 handler.py만 수정합니다. 설치, 외부 계정, 결제, 이메일은 필요 없습니다.
실제 등록과 입장권 페이지에는 Nightjar, 명령에는 Workstation을 사용합니다. 각 단원은 독립적으로 시작하며 사이트와 실제 인터페이스 이름은 영어로 유지됩니다. 한 대의 시스템에서 워커 중단을 연습하는 것으로, 고가용성이나 정확히 한 번 전달을 보장하지 않습니다.
등록에서 입장권까지 따라가기
이 단계는 Nightjar에서 시작합니다. 워커를 변경하기 전에 정상 업무가 어떻게 작동하는지 봅니다.
Explore the evening을 열어 내용을 읽고 Plan your visit을 따릅니다. The evening으로 돌아옵니다. Guest name에 Normal guest를 입력하고 Register for the evening을 선택합니다. 새 등록 페이지에는 R-0002 같은 ID와 Your ticket is being prepared가 표시됩니다. 등록 저장은 발권 완료와 다릅니다. Your registrations에는 Mina Patel의 기존 표도 있으며 반드시 보존해야 합니다.

Guest name 양식은 워크숍 페이지에 있고 등록 상태와 입장권은 별도 페이지입니다.
Workstation으로 전환합니다. pwd는 현재 디렉터리를, cat은 텍스트 파일을 표시합니다. 디렉터리는 /home/labex/project/ticket-desk여야 합니다.
pwd
cat handler.py
큐는 처리할 메시지를 저장합니다. RabbitMQ는 실제 큐 broker입니다. 워커는 등록 ID와 이름을 담은 메시지를 받아 표를 기록하고 전달을 확인합니다. 확인은 RabbitMQ에 그 전달을 제거해도 된다고 알립니다. 확인 자체가 표를 만들지는 않습니다.
Python의 def handle(...)은 함수를 정의하고 들여쓴 줄은 함수 본문입니다. 준비된 워커가 메시지마다 호출합니다. job은 사전이며 job['id']와 job['name']은 값을 조회합니다. ch.basic_ack(delivery.delivery_tag)는 메시지를 받은 채널에서 해당 전달을 확인합니다. checkpoint는 제공된 교육용 일시 중지 함수입니다. 평소에는 아무 일도 하지 않으며 일치하는 --pause 옵션이 있으면 Enter 또는 Ctrl+C를 기다립니다. 업무 처리나 broker 이벤트를 흉내 내지 않습니다.
데이터베이스의 테이블은 행의 모음입니다. db.execute(...)는 SQL을 실행하고 INSERT INTO tickets...는 입장권 행을 추가합니다. 각 ?에는 다음 튜플의 값이 들어가므로 이름을 SQL 명령이 아닌 데이터로 취급합니다. with db:는 들여쓴 블록이 정상 종료하면 기록을 커밋하고 예외가 밖으로 나오면 롤백합니다. 커밋은 기록을 영구 저장합니다. 기존 코드는 작업 전에 확인하므로 다음 단계에서 순서를 고칩니다.
python3 worker.py는 준비된 워커를 한 번 실행합니다. 준비된 메시지 하나를 받아 핸들러를 호출하고 끝납니다. 지금은 중단 없이 실행합니다.
python3 worker.py
Received R-0002 redelivered=False와 Handler returned.를 예상합니다. 추가 등록을 했다면 ID가 달라질 수 있습니다. redelivered는 입장권 데이터베이스가 아니라 RabbitMQ에서 옵니다. 순서가 위험해도 정상 실행은 실제 표를 만듭니다.
Nightjar로 돌아와 Your registrations, Normal guest, Open ticket을 차례로 엽니다. Download ticket을 눌러 텍스트 파일의 등록 ID와 이름이 같은지 읽습니다. Back to registration으로 돌아갑니다. Mina Patel의 표도 열려야 합니다. 수정 후에도 이 고객 경로를 보존해야 합니다.
No ready registrations가 나오면 다시 시도하기 전에 목록을 확인합니다. 이전 실행이 이미 처리했을 수 있습니다. 이름 오타는 요구한 이름으로 다시 등록할 수 있으며 각 ID는 독립적으로 유지됩니다. 결과를 억지로 만들려고 데이터베이스를 수정하지 마세요.
완료된 작업만 확인하기
이 단계는 Workstation에서 시작합니다. 확인을 트랜잭션 뒤로 옮긴 다음 미완료 작업을 중단하여 RabbitMQ가 돌려주는 모습을 봅니다.
먼저 확인하고 장애가 나면 RabbitMQ에는 재시도할 미확인 전달이 남지 않습니다. 커밋 후 확인하면 표가 존재할 때까지 전달을 미확인 상태로 둡니다. 이는 워커 중단을 보호하지만 RabbitMQ와 SQLite를 하나의 트랜잭션으로 만들지는 않습니다.
아래 블록으로 handler.py만 바꿉니다. cat > handler.py는 파일을 덮어쓰고 <<'PYTHON'은 내용을 그대로 보존하는 here-document를 시작합니다. 마지막 PYTHON을 단독 줄에 두고 블록 전체를 붙여 넣으며 네 칸 들여쓰기를 유지합니다. 트랜잭션은 같고 확인 위치만 뒤로 갑니다.
cat > handler.py <<'PYTHON'
from common import checkpoint
def handle(ch, delivery, job, db, pause):
checkpoint(pause, 'before-write')
with db:
db.execute('INSERT INTO tickets(registration_id,name) VALUES (?,?)',
(job['id'], job['name']))
checkpoint(pause, 'after-commit')
ch.basic_ack(delivery.delivery_tag)
PYTHON
Nightjar의 The evening에서 Retry guest를 등록하고 그 페이지를 열어 둡니다. Workstation으로 돌아옵니다. --pause before-write는 데이터베이스 쓰기 직전의 지점에서 멈춥니다.
python3 worker.py --pause before-write
**Paused before-write.**를 기다리고 터미널에서 Ctrl+C를 누릅니다. 의도적인 중단은 상태 130으로 끝납니다. 바로 $?로 직전 명령의 종료 상태를 출력합니다.
echo $?
130을 예상합니다. Nightjar에서 Retry guest 등록을 새로 고침하면 아직 표가 없습니다. Workstation으로 돌아와 중지 옵션 없이 실행합니다.
python3 worker.py
이제 redelivered=True를 예상합니다. 이전 연결이 닫힐 때 RabbitMQ가 실제로 미확인 전달을 돌려주었습니다. Nightjar에서 등록을 새로 고침하고 표를 열어 다운로드합니다. 복구된 표는 같은 등록 ID에 속합니다.
멈추지 않으면 옵션 철자와 cat handler.py를 확인합니다. 실수로 Enter를 눌렀다면 정상 완료된 것이므로 다른 Retry guest를 등록해 중단을 반복합니다. 이전 코드로 확인한 뒤 중단했다면 python3 desk.py status로 ID를 찾고 python3 desk.py retry R-0003의 ID를 실제 값으로 바꿔 기존 등록의 재전달을 요청합니다. 재시도는 학습자가 명시적으로 하는 복구이며 채점은 하지 않습니다. 다음 단계에서 중복 방지를 추가하기 전에는 완료된 표를 다시 시도하지 마세요.
데이터베이스 결과를 안전하게 반복하기
이 단계는 Workstation에서 시작합니다. 두 번째 장애 구간이 남았습니다. SQLite가 표를 커밋한 다음 RabbitMQ 확인 전에 워커가 멈출 수 있습니다. 현재 코드라면 재전달 시 표를 하나 더 만듭니다.
여기서 멱등성은 같은 등록 ID를 다시 처리해도 업무 결과가 하나로 남는다는 뜻입니다. 이름은 등록 식별자가 아닙니다. Same name이라는 두 사람도 각자 표가 필요합니다. 준비된 completed 테이블의 기본 키 id는 같은 ID의 두 행을 막습니다.
INSERT OR IGNORE는 ID 삽입을 시도합니다. rowcount는 새 행이면 1, 이미 존재하는 ID면 0입니다. (job['id'],)는 값 하나의 튜플이며 쉼표가 중요합니다. if added:는 새 ID일 때만 들여쓴 표 삽입을 실행합니다. 두 쓰기는 같은 with db: 블록 안에 있어야 합니다. 그렇지 않으면 완료 키만 저장되고 표가 없는 상태에서 중단되어 정상 등록을 영구히 건너뛸 수 있습니다.
아래의 짧은 전체 구현으로 함수를 바꿉니다. 롤백을 관찰하도록 트랜잭션 내부 중지 지점을 두 쓰기 사이에 의도적으로 놓았습니다.
cat > handler.py <<'PYTHON'
from common import checkpoint
def handle(ch, delivery, job, db, pause):
checkpoint(pause, 'before-write')
with db:
added = db.execute('INSERT OR IGNORE INTO completed VALUES (?)',
(job['id'],)).rowcount
checkpoint(pause, 'inside-transaction')
if added:
db.execute('INSERT INTO tickets(registration_id,name) VALUES (?,?)',
(job['id'], job['name']))
checkpoint(pause, 'after-commit')
ch.basic_ack(delivery.delivery_tag)
PYTHON
Nightjar에서 Same name을 두 번 등록합니다. 보통 R-0004와 R-0005라는 서로 다른 ID를 받습니다. Workstation으로 돌아와 첫 작업을 트랜잭션 안에서 멈춥니다.
python3 worker.py --pause inside-transaction
**Paused inside-transaction.**에서 Ctrl+C를 누릅니다. Nightjar에서 등록을 새로 고침하면 표가 없고 완료 키도 남아서는 안 됩니다. Workstation에서 같은 ID를 다시 시도하되 이번에는 커밋 뒤에서 멈춥니다.
python3 worker.py --pause after-commit
**Paused after-commit.**에서 Nightjar 등록을 열면 표 한 장이 이미 있습니다. Workstation으로 돌아와 Ctrl+C를 누르고 미확인 전달을 다시 처리합니다.
python3 worker.py
redelivered=True가 보여도 Nightjar에서 새로 고친 등록에는 정확히 한 장만 있어야 합니다. 중복 완료 키 삽입이 0을 반환했으므로 핸들러는 두 번째 표를 생략하면서 전달은 확인합니다.
Workstation으로 돌아옵니다. --drain은 준비된 메시지가 없어질 때까지 같은 단일 메시지 처리를 반복합니다.
python3 worker.py --drain
두 번째 Same name 등록도 자기 표를 받습니다. desk.py status는 실제 행을 읽습니다.
python3 desk.py status
명시적 중복 전달을 시험하려면 출력에서 첫 Same name ID를 사용합니다. 정상 순서에서는 R-0004이며 다르면 실제 ID로 바꿉니다.
python3 desk.py retry R-0004
python3 worker.py --drain
Nightjar에서 두 Same name ID가 다운로드 가능한 표를 각각 한 장 가지며 Mina Patel, Normal guest, Retry guest도 보존됩니다. 대기 등록이 없어야 합니다. No ready registrations만으로는 충분하지 않습니다. 작업을 잃어도 큐는 빌 수 있습니다.
Python이 들여쓰기나 구문 오류를 보고하면 함수를 다시 복사하고 재시도합니다. 미확인 작업은 RabbitMQ로 돌아갑니다. 두 쓰기를 함께 유지하고 잘못된 방법을 올바르게 보이게 하려고 기존 표나 완료 키를 삭제하지 마세요.
이전의 비멱등 코드로 완료된 ID를 실수로 처리했다면 먼저 handler.py를 고칩니다. 제공된 repair 동작은 다른 참가자를 초기화하지 않는 국소 복구를 제공합니다. python3 desk.py status로 대상 ID를 찾습니다. 예를 들어 Normal guest가 R-0002라면 미리 봅니다.
python3 desk.py repair R-0002
유지할 가장 이른 원본 표, 제거할 추가 표 번호, 이 ID만의 완료 기록을 보여 줍니다. 아직 변경하지 않습니다. --apply는 계획을 하나의 데이터베이스 트랜잭션에서 명시적으로 적용합니다.
python3 desk.py repair R-0002 --apply
영향받은 ID로 바꾸세요. 도구는 알 수 없는 ID, 표가 없는 ID, 등록과 표 이름이 다른 경우를 거부합니다. 가장 이른 표의 번호와 내용, 다른 등록과 표를 보존합니다. 수정된 ID를 완료로 기록하여 고친 핸들러가 안전하게 건너뛸 수 있게 합니다. 큐 메시지를 소비하거나 확인하지 않습니다. Nightjar에서 보존한 표를 열고 python3 worker.py --drain으로 고친 워커가 나머지 전달을 처리하게 합니다. 결과 복구만으로 안전하지 않은 핸들러가 격리 검사를 통과할 수는 없습니다.
채점은 실제 고객 결과를 읽고 수정하지 않은 핸들러를 별도 RabbitMQ broker와 일회성 데이터베이스에서 실행합니다. 실제 작업을 소비, 확인, 복구하지 않고 중단, 실제 재전달, 롤백을 시험합니다. 반복 가능한 데이터베이스 업무 효과를 안전하게 만드는 것이며 정확히 한 번 전달 보장이 아닙니다. 외부 이메일이나 결제 부작용은 별도 설계가 필요합니다.
요약
큐의 발권 작업을 복구하고 전달과 업무 완료를 구분했으며 이름이 같은 별도 등록을 보존했습니다. 커밋 후 확인하면 재시도할 수 있고 같은 트랜잭션의 완료 키와 표 쓰기는 이 데이터베이스 결과에 대해 재시도를 안전하게 합니다. 정확히 한 번 전달을 보장하지 않으며 외부 이메일, 결제, broker 장애나 모든 운영 장애를 다루지 않습니다.


