Architecture

두 번 오는 웹훅을 전제로 설계한다

결제 이벤트의 두 번째 사본은 새 일이 아니다. 고유 키, 보낼 상자, 과거를 고치지 않는 무효.

Design for the webhook that arrives twice

위험한 요청은 첫 번째가 아닙니다. 보내는 쪽이 첫 답을 듣지 못했거나, 듣고도 다시 시도해서 같은 요청이 또 오는 것입니다. 결제 제공자가 그렇게 합니다. 큐가 그렇게 합니다. 사람이 버튼을 두 번 누르면 브라우저도 그렇게 합니다. 시스템이 두 번째 사본을 새 일로 다루면, 두 번 청구하고, 두 번 보내고, 하나의 판돈에 장부 두 줄을 씁니다.

중복이 온다고 보고

두 번째 전달이 아무것도 하지 않는 성공이 되도록 처리기를 설계합니다. 첫 번째는 일을 하고 키를 저장합니다. 제공자의 이벤트 id이거나, 클라이언트가 보낸 키이며, 데이터베이스에서 유일합니다. 두 번째는 그 키를 찾고 같은 결과를 돌려줍니다. 두 번째 청구는 없습니다. 두 번째 메일도 없습니다. 두 번째 행도 없습니다.

제가 믿는 부분은 고유 제약입니다. 삽입 직전에 애플리케이션 코드에서 하는 검사는, worker 둘이 함께 깨면 경주에서 집니다. 데이터베이스는 그 경주에서 지지 않습니다. 진 쪽은 충돌을 받고, 그것을 “이미 됨”으로 바꿉니다.

첫 도착두 번째 도착
조회키가 새것이다키는 이미 저장됨
쓰기행 하나, 부작용 하나새 행 없음
답장수락됨같은 것, 이미 수락됨
프로세스가 중간에 죽으면보낼 상자는 아직 그 메시지를 빚지고 있다두 번째는 빚지지 않는다

메시지는 쓰기와 함께 나가거나, 아예 나가지 않는다

요청 안에서, commit 전에 메일이나 큐 메시지를 보내면, 롤백된 거래의 메시지가 남습니다. 나가는 메시지는 업무 행과 같은 트랜잭션에 쓰고, 별도의 worker가 게시합니다. 프로세스가 죽으면 행과 메시지는 둘 다 있거나 둘 다 없습니다. worker는 게시를 다시 시도할 수 있습니다. 처리기는 청구를 다시 시도하면 안 됩니다.

무효는 새 분개이지, 수정이 아니다

되돌려야 할 때 원래 줄을 덮어쓰지 않습니다. 상쇄 기록은 무엇이 바뀌었는지 말하고, 첫 줄을 읽을 수 있게 둡니다. 일주일 뒤에도 14:02에 무엇을 믿었는지 답할 수 있습니다. 행을 고치면 지금 무엇을 믿는지밖에 답하지 않으며, 증인도 없습니다.

재시도가 작업을 두 번 일어나게 할 수 있다면, 그 작업은 끝나지 않았다.

핸들러를 쓰기 전에 키의 이름을 정한다

재시도 사이에 보내는 쪽이 무엇을 바꾸는지 확인하지 않았다면, 키는 “요청 전체의 해시”가 아닙니다. 결제 제공자는 같은 이벤트 id를 새 전달 시각과 함께 다시 보냅니다. 그 시각이 키 안에 있으면 두 번째 전달이 새것으로 보이고, 막으려던 버그를 만들게 됩니다. 보내는 쪽이 안정하다고 약속한 id를 씁니다. 약속이 없으면 호출하는 쪽이 하나를 보내게 하고, 그것 없이 온 호출은 거절합니다.

그다음 실패를 성공과 같은 곳에 적습니다. 충돌은 보내는 쪽의 오류 페이지가 아닙니다. 재시도에 대한 예상된 답입니다. 보내는 쪽은 멈출 수 있어야 합니다. 500을 돌려주면 세 번째, 네 번째를 시도하고, 사건은 같은 일의 대기열이 됩니다.

행복한 경로가 200을 돌려줬다고 끝난 것으로 보지 않습니다. 테스트는 같은 페이로드를 다시 보내고 부작용이 하나임을 확인합니다. 그다음 commit과 게시 사이에 worker를 죽이고, 메시지가 여전히 한 번 나가는지 확인합니다. 이 두 테스트는 그것이 대신하는 사건보다 짧습니다. 거기서 시작합니다.