ACK 전에 꺼진 간식 자판기 · 늦게 돌아온 전달자의 도장을 거절한다 · 이론
늦게 돌아온 전달자의 도장을 거절한다
한 줄 요약
임대는 잠깐 맡을 권리이고, 선점 토큰은 어느 번의 권리인지를 구별합니다. 오래된 작업자는 살아 돌아와도 새 작업자의 기록을 바꿀 수 없어야 합니다.
왜 이게 필요했나
전달자 A가 간식 주문을 가져간 뒤 멈췄습니다. 큐에서 주문을 완전히 삭제하고 A의 메모리에만 두었다면 주문도 같이 사라집니다. 반대로 큐에 그대로 보이게 두면 B도 동시에 가져갈 수 있습니다. 그래서 leased 상태를 저장하고 일정 시간 동안 A에게 맡겼다는 표식을 둡니다. 시간이 지났는데 완료하지 못하면 다른 작업자가 다시 가져갈 수 있습니다.
문제는 만료가 A를 종료시켜 주지 않는다는 것입니다. A가 네트워크 응답을 오래 기다리고 있었을 뿐이라면, B가 새로 선점한 뒤 A도 성공 응답을 받을 수 있습니다. 그때 A가 id 하나만 조건으로 완료 표시하면 B의 현재 작업을 가로챕니다. 실행 중인 사람의 이름만 검사해도 충분하지 않습니다. 같은 이름의 작업자가 재시작했을 때 이전 실행과 새 실행을 구별할 수 없기 때문입니다.
어떻게 동작하나
jobs 행에는 owner, lease_until, token, attempts가 있습니다. claim이 성공할 때 attempts와 token을 각각 1 올립니다. owner는 설명 가능한 작업자 이름이고 token은 해당 업무의 선점 세대를 구별하는 단조 증가 번호입니다. 하나의 claim 결과에는 id·qty·owner·token·attempt·lease_until이 들어갑니다. finish는 전달받은 표식과 현재 행이 모두 일치하고, 아직 임대와 업무 마감이 남아 있을 때만 결과를 반영합니다.
0ms A가 선점: token=1, lease_until=10001000ms B가 재선점: token=21001ms A가 token=1로 완료 요청 → False, 현재 상태 유지경계는 now < lease_until일 때만 유효합니다. now가 만료 시각과 같으면 이미 권리가 끝났습니다. 새 선점은 반대로 lease_until <= now인 일을 가져올 수 있습니다. 두 조건을 다르게 해석하면 같은 순간 두 사람이 유효하다고 생각하거나, 아무도 맡지 못하는 빈틈이 생깁니다. 시험은 만료 전·정확한 만료 시각·만료 후를 각각 넣습니다.
선점은 조회와 갱신을 한 쓰기 트랜잭션으로 묶습니다. 먼저 가능한 일을 읽고 트랜잭션을 나중에 시작하면 A와 B가 같은 pending 행을 읽을 수 있습니다. 이번 큐는 SQLite의 짧은 BEGIN IMMEDIATE 트랜잭션으로 선택과 토큰 갱신을 직렬화합니다. 읽기 시점 스냅샷을 오래 유지할 이유가 없는 작업 큐라 rollback journal을 사용합니다. 앞 실습의 WAL 스냅샷 내보내기와 요구하는 동시성 형태가 다릅니다.
선점을 커밋한 뒤에는 잠금을 풀고 전송합니다. DB 트랜잭션 안에서 네트워크 응답까지 기다리면 이 일과 무관한 새 접수와 다른 작업자의 선점도 막힙니다. 시험의 send 콜백 안에서 별도 연결이 다른 업무를 실제로 접수하도록 하여 잠금이 남았는지 확인합니다. 외부 작업을 하지 않는 짧은 트랜잭션이라는 의도가 실제 실행에서 지켜지는지가 채점 대상입니다.
현장에서 만나는 모습
SQS의 visibility timeout도 받은 메시지를 일정 기간 다른 소비자에게 보이지 않게 하는 관련 개념입니다. 다만 그 제품의 전달 보장과 이번 로컬 SQLite 큐의 계약은 동일하지 않습니다. 이 실습은 SQS를 구현하거나 호출하지 않습니다. 공식 설명에서도 visibility timeout이 중복 전달 가능성을 완전히 없애 주는 보장은 아니므로 수신 업무의 멱등성은 별도로 생각해야 합니다.
여기서 선점 토큰은 큐 안의 오래된 완료 표식을 거절합니다. 수신 서버가 토큰을 검사하는 외부 자원 fencing까지 구현한 것은 아닙니다. A의 요청이 이미 수신 서버에 도착했다면 큐가 A의 finish를 거절해도 그 외부 효과가 되돌아가지는 않습니다. 그래서 종합 시험의 수신 서버는 업무 ID와 수량을 함께 기록하고 같은 ID의 재전송에는 효과를 다시 더하지 않습니다. 큐의 소유권과 수신 효과의 경계가 각각 필요한 이유입니다.
임대를 아주 길게 주면 죽은 작업자의 일을 다시 맡기는 시간이 늘고, 아주 짧게 주면 아직 정상인 작업자의 일을 불필요하게 다시 보냅니다. 이번 구현은 임대를 갱신하는 heartbeat를 만들지 않습니다. 장기 작업을 운영할 때는 처리 시간, 갱신 주기, 안전한 취소, 수신 멱등성을 함께 설계해야 합니다. 단순히 lease_ms를 크게 바꾸는 것은 완결된 해결책이 아닙니다.
원래 업무 마감보다 임대가 길어지지도 않게 합니다. 예를 들어 deadline=100인데 now=0, lease_ms=1000이면 실제 lease_until은 100입니다. 큐에 저장된 임대가 업무 예산과 모순되지 않아야 합니다. 시도가 소진된 만료 업무는 새 토큰을 주기 전에 격리하며, 이미 유효한 임대를 가진 일을 반복 조회만으로 다른 작업자에게 넘기지 않습니다.
다음 확인에서 할 것
이어지는 퀴즈에서는 이름·업무 ID·선점 번호가 각각 구별하는 대상을 판단합니다. 뒤의 종합 실습에서 실제 자식 프로세스 두 개를 동시에 출발시키고 선점 결과가 하나만 있는지 봅니다. 완료 경계의 실패 메시지에는 현재 결과와 기대 결과가 함께 나오며, 같은 작업자 이름으로 재선점해도 이전 토큰이 거절되는지 확인합니다.
참고: [SQLite 트랜잭션](https://www.sqlite.org/lang_transaction.html), [SQS visibility timeout](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html). 숫자 토큰은 접근 권한이나 인증 수단이 아닙니다.