둘 다 "예약됐습니다" — await 하나가 가르는 것
7 분예약 도구에서 제일 나쁜 실패는 예약이 안 되는 게 아니다.
두 사람에게 다 "예약되셨습니다"를 보내는 것이다. 안 되면 다시 하면 되는데, 둘 다 됐다고 하면 그 다음은 사람이 전화로 해결한다.
두 처리기, 같은 설명
이 샘플에는 슬롯을 잡는 처리기가 둘 있다. 말로 설명하면 완전히 같다 — 비었는지 보고, 비었으면 잡는다.
한쪽은 이렇다.
// 1. Look.
final free = slot.takenBy == null;
// 2. Anything at all in here — a database round trip, a payment check, a
// log write, a lookup of the customer's name — hands control to
// whatever else was in flight.
await Future<void>.delayed(Duration.zero);
// 3. Write, using what was true in step 1.
if (free) { slot.takenBy = who; ... }다른 쪽은 이렇다.
if (slot.takenBy != null) {
return '$at is already taken by ${slot.takenBy}';
}
slot.takenBy = who;
slot.confirmations.add(who);
await Future<void>.delayed(Duration.zero); // the slow part, after the claim같은 await 가 위치만 다르다. 앞엣것은 확인과 기록 사이에 있고, 뒤엣것은 기록 뒤에 있다.
그리고 그 위치가 이중 예약을 만든다.
실제로 일어나게 만든다
unsafe -> Smith: "CONFIRMED 10:30 for Smith"
unsafe -> Jones: "CONFIRMED 10:30 for Jones"
unsafe result: 1 slot confirmed to more than one person
화면이 정확히 현실의 증상을 보여준다. 슬롯 주인은 Jones 한 명인데, "예약됐다"는 말을 들은 사람은 둘이다.
이게 실제 가게에서 벌어지는 모양이다. 장부는 한 명이고, 문 앞에는 두 명이 온다.
그래서 서버가 확정을 보낸 사람을 다 기억한다.
/// Everybody who has ever been told they got this slot. In a correct
/// booking system this never has more than one name in it, which is exactly
/// why it is worth keeping.
final confirmations = <String>[];정상이면 항상 한 명짜리 목록이다. 항상 한 명이어야 하는 목록이야말로 세어 볼 값어치가 있다.
그리고 안 일어나게 만든다
같은 두 사람, 같은 시간, 틈만 닫고.
--- cleared, same two people, same slot, gap closed ---
safe -> Smith: "CONFIRMED 10:30 for Smith"
safe -> Jones: "10:30 is already taken by Smith"
safe result: nobody was told yes twice
거절이 "실패했습니다"가 아니라 "Smith 님이 잡으셨습니다"다. 두 번째 사람이 알아야 하는 건 자기가 실패했다는 사실이 아니라 그 자리가 나갔다는 사실이고, 그래야 다음 시간을 고른다.
좋은 경우만 검사하면 통과한다
이 샘플의 검증이 특이하다. 버그가 일어났는지를 검사한다.
unsafe = s.call("book.stampede", {"at": "10:30", "first": "Smith", "second": "Jones", "mode": "unsafe"})
assert unsafe["doubleCount"] == 1, unsafe # the race must actually happen, or this run proves nothing
safe = s.call("book.stampede", {"at": "10:30", "first": "Smith", "second": "Jones", "mode": "safe"})
assert safe["doubleCount"] == 0, safe이유는 단순하다. 예약 테스트가 "한 명만 확정됐다"만 검사하면, 버그가 살아 있는 서버에서도 통과한다. 경합이 실제로 안 일어나면 순서대로 처리되고 결과는 정상이기 때문이다.
그래서 순서가 이렇다.
- 안전하지 않은 쪽으로 경합이 실제로 일어났음을 확인하고
- 안전한 쪽으로 같은 경합에서 안 일어남을 확인한다
첫 번째가 없으면 두 번째는 아무 의미가 없다.
하나 더 있다. 틈이 사라지면 실패한다.
# The two handlers must stay near-identical apart from where the await sits.
# If somebody "fixes" the unsafe one, this sample stops teaching anything.
grep -q "await Future<void>.delayed(Duration.zero);" booking_server/bin/server.dart \
|| { echo " the gap in the unsafe handler is gone — nothing is demonstrated"; exit 1; }만들면서 경합이 안 일어났다
첫 판에는 버그가 안 나왔다.
하니스에서 도구 호출 두 개를 동시에 쏘았는데 결과가 정상이었다.
take_unsafe -> Smith: "CONFIRMED 10:30 for Smith"
take_unsafe -> Jones: "10:30 is already taken by Smith"
take_unsafe result: nobody was told yes twice전송 계층이 직렬화하고 있었다. stdio 는 요청 한 줄을 읽고, 그 처리기를 끝까지 돌리고, 그 다음 줄을 읽는다. 겹칠 구간이 아예 없다.
여기서 두 갈래였다. 하나는 "이 전송에서는 안 나니까 문제 없다"고 넘기는 것. 다른 하나는 겹침을 만들어서 확인하는 것.
후자를 택했다. HTTP 나 SSE 로 바꾸면 그 겹침은 아무도 안 만들어도 도착하기 때문이다. 전송이 우연히 지켜 주고 있는 것은 지켜진 게 아니다.
// The overlap is made here, inside the server, and that is on purpose.
// ...
// On an HTTP or SSE transport the overlap arrives by itself and nobody
// has to arrange it. So this tool arranges what that transport would have
// handed the server anyway.이건 이 시리즈에서 처음 있는 종류의 발견이다. 앞선 편들의 결함은 전부 내 코드에 있었는데, 이건 환경이 결함을 가려 준 경우다. 그리고 가려 주는 환경은 언제든 바뀐다.
이 샘플의 범위
겹침을 내가 만들었다. 서버 안에서 두 처리를 동시에 띄운다. 진짜 동시 요청이 전송을 타고 들어와서 나는 경합이 아니다. stdio 에서는 그 경합이 재현되지 않는다는 것이 이 편이 알아낸 사실이고, 그러니 이 샘플은 "HTTP 였다면 이랬을 것"을 보이는 것이다. HTTP 전송에서 실제로 확인한 것은 아니다.
단일 프로세스 안의 경합만 다뤘다. 서버가 두 대로 늘어나면 이 해결책은 통째로 무효다. 그때는 데이터베이스의 유일 제약이나 잠금이 필요하고, "확인하고 기록하는 사이에 아무것도 없다"를 프로세스 밖에서 보장하는 일은 훨씬 어렵다. 안 다뤘다.
취소도 대기도 없다. 잡은 걸 놓는 일, 놓인 자리를 기다리던 사람에게 넘기는 일이 없다. 실제 예약 도구의 절반이 그쪽이다.
결제가 없다. 결제가 붙으면 "자리는 잡았는데 결제가 실패한" 상태가 생기고, 그 상태를 얼마나 붙잡고 있을지가 새 문제가 된다.
남는 것
이 편의 코드는 열 줄이 안 된다. 그런데 그 열 줄이 말로 설명하면 구별이 안 된다.
"비었는지 보고 비었으면 잡는다" — 두 처리기 다 그렇게 설명된다. 코드 리뷰에서도 통과할 것이고, 명세서에 적어도 똑같이 적힐 것이다.
보이는 곳은 실행뿐이다. 그래서 이런 종류의 결함은 읽어서 못 찾고, 일어나게 만들어서 찾는다.
그리고 일어나게 만들어 놓으면, 그게 그대로 검사가 된다.
확인 과제
같은 예약을 연달아 두 번 보내 보세요. 두 번째가 예약을 하나 더 만들지 않는다는 것을 보이고, 어느 쪽이 그것을 보장했는지 설명하세요.