refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
@@ -30,7 +30,7 @@ outbox 리스가 만료 시각만 담고 최종 상태 쓰기가 메시지 식
- **fenced lease — 만료 시각만으로는 부족한 이유**
이 사례가 만든 개념이다.
- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다**
결함의 수정 형태를 규칙으로 옮긴 것이다.
사건에서 사용한 owner token·revision 조건을 재사용 가능한 CAS 규칙으로 정리한다.
- **만료된 claim과 만료된 실행은 다르게 다뤄야 한다**
만료된 claim 과 만료된 실행을 갈라 다루는 판단이다.
@@ -80,7 +80,7 @@ AMBIGUOUS 는 청구 가능한 상태다. 확인된 메시지가 다시 발행
:::evidence key="a-lease-without-an-owner" alt="코드베이스에서 V2 마이그레이션이 더한 컬럼과 토큰 제약, 다섯 상태에서 여섯으로 바뀐 CHECK, 청구문이 소유자와 토큰을 쓰는 SET 절과 그 청구가 읽는 술어와 그에 맞춘 부분 인덱스, 최종 쓰기의 펜싱 술어와 0행을 보고한다는 주석, 릴레이가 부르는 다섯 전이, 펜싱 없는 옛 메서드의 선언과 그 자신의 javadoc 과 신세대 javadoc 의 폐기 문장과 @Deprecated 매치 수와 호출 수, 두 세대가 AMBIGUOUS 에서 남기는 컬럼의 차이, 그리고 파일서버 마이그레이션이 같은 결함을 다른 곳에서 지목하는 줄을 뽑은 출력. 옛 메서드의 javadoc 이 아직 안전을 주장하고 신세대 javadoc 이 그것을 폐기라 적는다는 것이 나란히 보인다." caption="V2 컬럼과 제약 · 청구 술어와 부분 인덱스 · 펜싱 술어와 0행 보고 · 옛 경로의 두 javadoc · AMBIGUOUS 에서의 세대 차이" zoom="true"
:::
토큰에는 음수가 아니라는 제약이 붙는다. 백필은 하지 않는다 — 기본값이 0 이고 첫 청구가 그것을 올리므로 정확성에 필요하지 않으며, 제약은 코드가 의존하는 불변식을 스키마에 남기려는 것이다.
토큰에는 음수가 아니라는 제약이 붙는다. 기본값이 0이고 첫 claim이 값을 증가시키므로 별도 backfill은 하지 않는다. 제약은 코드가 의존하는 불변식을 스키마에서도 검사하게 한다.
청구문이 토큰을 `o.lease_token + 1` 로 올린다. 청구를 내주는 바로 그 문장 안에서, 서버가 올린다. 그래서 같은 행을 두고 경쟁한 두 릴레이가 같은 번호를 받을 수 없다.
@@ -90,7 +90,7 @@ AMBIGUOUS 는 청구 가능한 상태다. 확인된 메시지가 다시 발행
최종 쓰기의 술어에 `lease_owner``lease_token` 이 들어간다. 지나간 획득의 쓰기는 걸릴 행이 없다.
그 자리 javadoc 이 0행을 어떻게 다루는지 적는다. 0행은 삼키지 않고 보고한다 — 지나간 쓰기가 있었다는 것은 이 작업자가 중복 발행을 만들었을 수 있다는 뜻이고, 그것이 운영자가 봐야 하는 사실이라는 것이다.
최종 갱신 메서드의 javadoc은 update count가 0일 때 이를 삼키지 않고 보고하도록 요구한다. 소유권을 잃은 뒤 쓰기를 시도했다면 중복 발행 가능성을 조사해야 하기 때문이다.
술어를 붙이는 SQL 문자열은 두 개이고, 그 둘을 만드는 헬퍼 둘이 다섯 전이의 최종 쓰기를 전부 처리한다.
@@ -104,11 +104,11 @@ AMBIGUOUS 는 청구 가능한 상태다. 확인된 메시지가 다시 발행
이 저장소의 프로덕션 코드는 청구도 전이 넷도 전부 신세대만 부른다. 아래는 지금 일어나는 일이 아니라 포트가 두 형태를 나란히 둔 결과다.
청구 메서드 인터페이스에 그대로 있다. 그 메서드 자신의 javadoc 아직 이렇게 적는다 — 단순 조회가 아니라 리스를 잡는 것이 여러 릴레이를 안전하게 만들고, 한 릴레이가 청구한 레코드는 리스가 만료될 때까지 다른 릴레이에 보이지 않으므로 같은 메시지가 두 프로세스에서 동시에 발행되지 않는다는 것이다.
claim 메서드 인터페이스에 남아 있다. 해당 javadoc은 단순 조회 대신 lease를 획득해, 한 relay가 claim한 레코드를 lease 만료 전까지 다른 relay가 다시 claim하지 못하게 한다고 설명한다.
이 사례가 반증한 문장이 그대로 있다.
폐기를 적은 것은 신세대 쪽 javadoc 이다. 옛 메서드는 토큰 없는 레코드를 돌려주므로 호출자가 자기 쓰기가 자기 청구에 속한다는 것을 증명할 수 없고, 조사 경로용으로 남기며 릴레이가 쓰기에는 폐기되었다는 것이다.
신규 API javadoc은 옛 메서드를 relay 경로에서 사용하지 말라고 명시한다. 옛 메서드는 fencing token 없는 레코드를 반환하므로 후속 쓰기가 현재 claim에 속하는지 증명할 수 없고, 조사 용도로만 남긴다.
그 문장은 산문이다. messaging 트리 전체에 `@Deprecated` 가 하나도 없다.
@@ -0,0 +1,73 @@
---
kind: CASE
slug: a-mark-that-meant-seen-not-projected
title: 「본 적 있는 위치」를 「투영이 끝난 위치」로 쓴 mark 가 재전달된 이벤트를 삼켰다
topic: owner-safe-state-machines
topicName: owner-safe 상태 기계 — 소유권을 SQL에 적기
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:a-mark-that-meant-seen-not-projected
source:
- final/document.md#8-1
- final/document.md#a06
- final/document.md#4-2
- final/document.md#8-1 항목 9
- final/document.md#a06 §67
---
# 「본 적 있는 위치」를 「투영이 끝난 위치」로 쓴 mark 가 재전달된 이벤트를 삼켰다
이벤트 위치를 “관측했다”는 시점에 mark를 전진시키면 projection이 끝나기 전에 failover가 발생했을 때 재전달 이벤트를 이미 처리한 것으로 오인할 수 있다. 기존 분석은 한 번의 failover probe에서 이 손실 경로를 재현했다.
## 관계
- **CAS tuple과 update count**
상태 전이가 완료됐다는 조건을 owner와 revision으로 함께 검사하는 개념이다.
- **lease에는 owner가 있어야 한다**
worker가 바뀌는 경계에서 이전 소유자의 진행 상태를 그대로 신뢰하지 않는 사례다.
## 문제
mark가 이벤트를 읽은 직후 전진하고 실제 projection 반영은 그 뒤에 실행되면 두 상태 사이에 공백이 생긴다.
worker가 그 공백에서 중단되고 다른 worker가 mark부터 읽으면, 새 worker는 해당 위치를 이미 완료한 것으로 판단해 이벤트를 건너뛸 수 있다.
## 결론
“마지막으로 본 위치”와 “projection을 완료한 위치”는 같은 상태가 아니다. 완료 mark는 projection이 성공적으로 반영된 뒤에만 전진해야 한다.
기존 probe는 worker 하나와 한 번의 평범한 failover만 다뤘다. 여러 worker나 반복 resume에서 손실 폭이 어떻게 달라지는지는 확인하지 않았다.
## 검증 환경
sourceRevision : 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
확인 방식 : SSOT에 기록된 failover probe 결과
현재 source repository 재대조 : UNVERIFIABLE
## 재현 조건
1. 이벤트 위치 mark가 갱신되는 시점과 projection 반영 시점을 분리해 확인한다.
2. mark 전진 뒤 projection 완료 전에 worker를 중단한다.
3. 다른 worker가 같은 stream을 resume했을 때 해당 이벤트가 다시 처리되는지 확인한다.
4. 이번 검토에서는 기존 SSOT probe 결과만 사용했고 새 실행은 하지 않았다.
## 본문
<!-- body:start -->
## 필요한 상태는 세 개다
아직 보지 않은 위치, 본 적은 있지만 projection이 끝나지 않은 위치, projection까지 완료한 위치를 구분해야 한다.
두 상태만 두고 “봤다”를 “끝났다”로 사용하면 failover 중간 상태를 표현할 수 없다.
## mark는 완료 뒤에 전진한다
projection 반영과 mark 갱신이 하나의 owner-safe 전이로 묶이거나, 적어도 완료가 증명된 뒤 mark를 갱신해야 한다. 새 worker는 완료 mark만 resume 기준으로 사용한다.
## 확인하지 못한 것
기존 probe는 단일 worker의 한 번 failover다. 반복 failover와 다중 worker에서의 손실 폭은 측정하지 않았다.
<!-- body:end -->
@@ -58,7 +58,7 @@ IdempotencyCapabilityGuard 의 클래스 자바독(:12~:21)이 무엇이 틀렸
OpenJDK : 21.0.12
Spring Boot : 4.0.8
근거 : 저장소의 자바독이 사후 기록으로 남긴 회귀
확인 방식 : 가드 자바독의 사후 기록 확인, 필드와 생성자의 널 검사 유무 확인, 전제 검사 메서드의 세 검사와 실패 메시지 확인, 데이터소스를 정하는 메서드와 그 자바독 확인, 저장소를 만드는 자리 전수와 각각이 넘기는 값 확인, JdbcOperations 구현과 JdbcTemplate 하위 클래스 검색, 세 번째 검사의 메시지를 단언하는 시험 검색, 가드의 두 메서드를 부르는 자리 전수, 형제 어댑터 둘의 데이터소스 주입과 세 검사 형태 대조
확인 방식 : 가드 javadoc의 사후 기록 확인, 필드와 생성자의 null 검사 확인, 전제 검사 메서드의 세 검사와 실패 메시지 확인, DataSource 추론 메서드 확인, 저장소 생성 지점과 전달 값 확인, JdbcOperations 구현 검색, 세 번째 검사 테스트 검색, 가드 호출 지점 전수 확인, 형제 어댑터의 DataSource 주입 방식과 검사 비교
소스 수정 : x
## 재현 조건
@@ -68,26 +68,26 @@ Spring Boot : 4.0.8
3. 전제를 검사하는 메서드의 본문을 끝까지 읽고 검사가 몇 개인지, 각각 어떤 메시지로 실패하는지 적는다.
4. 데이터소스 검사에 붙은 조건을 읽고 그 값이 어디서 오는지 거슬러 올라간다.
5. 그 값을 정하는 메서드와 자바독을 읽는다.
6. 이 저장소를 만드는 자리를 전부 찾고 각각이 무엇을 넘기는지 확인한다.
6. 이 저장소를 생성하는 코드를 전부 찾고 각 생성자가 무엇을 넘기는지 확인한다.
7. 저장소에 그 인터페이스의 다른 구현이 있는지 찾는다.
8. 세 번째 검사가 던지는 메시지를 저장소에서 찾아 그것을 단언하는 시험이 있는지 본다.
9. 가드의 두 메서드를 부르는 자리를 전부 찾는다.
9. 가드의 두 메서드를 호출하는 코드를 전부 찾는다.
10. 형제인 outbox 와 inbox 어댑터가 데이터소스를 어떻게 받고 같은 세 검사를 어떤 형태로 거는지 확인한다.
## 본문
<!-- body:start -->
`IdempotencyCapabilityGuard` 는 멱등성 저장소가 SQL 돌리기 전에 만족해야 할 전제를 모아 둔 타입이다. 클래스 자바독이 이 타입이 따로 생긴 이유를 적는데, 전제가 서로 다른 세 질문이고 저장소가 세 번째를 틀리게 답했다는 것이다.
`IdempotencyCapabilityGuard`는 멱등성 저장소가 SQL을 실행하기 전에 세 전제를 검사한다. 클래스 javadoc은 기존 저장소가 그중 DataSource 소유권 질문을 잘못 검사해 별도 guard로 분리했다고 기록한다.
## 자바독이 남긴 사후 기록
:::evidence key="an-active-transaction-check-that-asked-the-wrong-question" alt="저장소 루트에서 돌린 정적 검색 출력 149줄. IdempotencyCapabilityGuard 의 클래스 자바독이 9번부터 23번 줄까지 원문 그대로 실려 세 질문과 틀린 답과 데이터소스가 둘일 때의 결과가 나온다. 이어서 필드 셋과 생성자가 28번부터 39번 줄까지 실리는데 jdbc 와 activeCapabilitySql 은 requireNonNull 을 지나고 dataSource 만 그대로 대입된다. requirePrimaryWriteTransaction 의 본문이 87번부터 109번 줄까지 나와 세 검사와 각각의 메시지가 보이고, 마지막 검사가 dataSource 가 널이 아닐 때만 hasResource 를 평가한다. 그 값을 정하는 dataSourceOf 가 JdbcTemplate 일 때만 getDataSource 를 돌려주고 아니면 널이라는 것과, 그 절충을 인정하는 자바독이 함께 나온다. 이 저장소를 만드는 자리 셋이 소스 세트별로 나오는데 프로덕션은 하나이고 그 자바독이 가드가 데이터소스를 식별하므로 다른 데이터소스의 트랜잭션은 통과할 수 없다고 약속한다. 유닛 시험은 mock 을 넘긴다. 저장소에 JdbcOperations 구현이나 JdbcTemplate 하위 클래스는 0 건이다. 세 번째 검사의 메시지를 찾으면 던지는 줄 하나만 나오고 시험은 없으며, 전제 시험 셋이 무엇을 단언하는지 이름으로 나온다. 마지막으로 가드를 부르는 열일곱 줄과 형제 어댑터 둘이 데이터소스를 생성자로 받아 requireNonNull 하는 것과 같은 세 검사를 거는 형태가 나온다." caption="세 질문과 틀린 답을 적은 자바독 · dataSource 만 널 검사를 지나지 않는 생성자 · 지금의 세 검사와 마지막에 붙은 널 조건 · 그 값을 정하는 dataSourceOf · 조립 자리 셋과 프로덕션 자바독의 약속 · JdbcOperations 구현 0 · 세 번째 검사를 덮는 시험 0 · 형제 둘의 생성자 주입과 세 검사 — 149줄 · exit 0" zoom="true"
:::evidence key="an-active-transaction-check-that-asked-the-wrong-question" alt="저장소 루트에서 돌린 정적 검색 출력 149줄. IdempotencyCapabilityGuard 의 클래스 자바독이 9번부터 23번 줄까지 원문 그대로 실려 세 질문과 틀린 답과 데이터소스가 둘일 때의 결과가 나온다. 이어서 필드 셋과 생성자가 28번부터 39번 줄까지 실리는데 jdbc 와 activeCapabilitySql 은 requireNonNull 을 지나고 dataSource 만 그대로 대입된다. requirePrimaryWriteTransaction 의 본문이 87번부터 109번 줄까지 나와 세 검사와 각각의 메시지가 보이고, 마지막 검사가 dataSource 가 널이 아닐 때만 hasResource 를 평가한다. 그 값을 정하는 dataSourceOf 가 JdbcTemplate 일 때만 getDataSource 를 돌려주고 아니면 널이라는 것과, 그 절충을 인정하는 자바독이 함께 나온다. 이 저장소의 생성 지점 셋이 소스 세트별로 나오는데 프로덕션은 하나이고 그 자바독이 가드가 데이터소스를 식별하므로 다른 데이터소스의 트랜잭션은 통과할 수 없다고 약속한다. 유닛 시험은 mock 을 넘긴다. 저장소에 JdbcOperations 구현이나 JdbcTemplate 하위 클래스는 0 건이다. 세 번째 검사의 메시지를 찾으면 던지는 줄 하나만 나오고 시험은 없으며, 전제 시험 셋이 무엇을 단언하는지 이름으로 나온다. 마지막으로 가드를 부르는 열일곱 줄과 형제 어댑터 둘이 데이터소스를 생성자로 받아 requireNonNull 하는 것과 같은 세 검사를 거는 형태가 나온다." caption="세 질문과 틀린 답을 적은 자바독 · dataSource 만 널 검사를 지나지 않는 생성자 · 지금의 세 검사와 마지막에 붙은 널 조건 · 그 값을 정하는 dataSourceOf · 조립 자리 셋과 프로덕션 자바독의 약속 · JdbcOperations 구현 0 · 세 번째 검사를 덮는 시험 0 · 형제 둘의 생성자 주입과 세 검사 — 149줄 · exit 0" zoom="true"
:::
자바독 `:12`\~`:16` 은 세 질문을 나열한다. 스키마가 승인되었는가, 트랜잭션이 있는가, 그것이 이 저장소의 트랜잭션인가.
이어서 저장소가 세 번째를 틀리게 답했다고 적는다. 스레드에 활성 읽기 쓰기 트랜잭션이 있는지만 확인했는데 그 조건은 어느 데이터소스에서든 트랜잭션이 열려 있으면 참이고, 형제인 outboxinbox 어댑터는 `hasResource(dataSource)` 를 확인하며 그것이 실제로 중요한 질문이라는 것이다.
기존 코드는 스레드에 활성 read-write transaction이 있는지만 확인했다. 이 조건은 다른 DataSource의 transaction에도 참이 될 수 있다. 형제인 outbox·inbox 어댑터는 `hasResource(dataSource)`까지 검사해 현재 transaction이 같은 DataSource를 사용하고 있는지 확인한다.
`:18`\~`:21` 이 결과를 적는다. 데이터소스가 둘인 애플리케이션에서 다른 쪽의 트랜잭션 안에서 발행된 변경이 옛 검사를 통과했고, 이 저장소의 커넥션에서 트랜잭션 없이 실행됐으며, 원자적이어야 할 작업과 독립적으로 커밋됐다.
@@ -109,11 +109,11 @@ Spring Boot : 4.0.8
넣는 쪽은 `PostgreSqlOwnerSafeIdempotencyStore:91` 이다. 생성자가 `dataSourceOf(jdbc)` 로 값을 만드는데, `:105`\~`:109` 의 그 메서드는 `jdbc``JdbcTemplate` 이면 `template.getDataSource()` 를 돌려주고 아니면 널을 돌려준다.
그 자바독(`:98`\~`:104`)이 절충을 인정한다. `JdbcTemplate` 은 자기 데이터소스를 알지만 손으로 만든 `JdbcOperations` 는 모를 수 있고, 가드는 알 수 없는 데이터소스를 "검사할 수 없음" 으로 다루는데 그것은 outbox 어댑터의 정확한 검사보다 약하고 이전보다는 강하며 협력자를 정말로 식별할 수 없을 때의 정직한 답이라는 것이다.
해당 javadoc(`:98`\~`:104`) `JdbcTemplate`에서는 DataSource를 식별할 수 있지만 임의의 `JdbcOperations` 구현에서는 식별하지 못할 수 있음을 적는다. guard는 DataSource를 알 수 없을 때 이 검사를 생략하므로 outbox 어댑터보다 약한 보장이다.
## 그 널 경로가 실제로 열리는 곳
## DataSource 추론이 null이 될 수 있는 생성 경로
이 저장소를 만드는 자리는 셋이다.
이 저장소의 생성 지점은 셋이다.
프로덕션은 `PostgreSqlIdempotencyProviderConfig:60` 하나이고 `JdbcOperations` 빈을 받는다. 그 메서드의 자바독 `:55`\~`:56` 은 저장소의 트랜잭션 가드가 그것으로부터 자기 데이터소스를 식별하므로 다른 데이터소스에서 연 트랜잭션은 통과할 수 없다고 적는다.
@@ -135,11 +135,11 @@ Spring Boot : 4.0.8
갈리는 것은 데이터소스를 얻는 방법이다. 형제 둘은 생성자가 `DataSource` 를 직접 받고 `:158``:210``Objects.requireNonNull` 로 거른다. 널일 수 없으므로 널 가드가 필요 없다. 가드는 `JdbcOperations` 에서 추론하고, 추론이 실패하면 널이 된다.
## 가드를 부르는 자리
## 가드를 호출하는 메서드
`PostgreSqlOwnerSafeIdempotencyStore` 의 여섯 자리`:122`, `:176`, `:211`, `:255`, `:303`, `:360` 같은 클래스의 private `requirePrimaryWriteTransaction`(`:506`\~`:507`)을 부르고, 그 메서드가 `guard.requirePrimaryWriteTransaction` 으로 넘긴다. `requireActiveCapability` 를 부르는 자리`:123` 하나다.
`PostgreSqlOwnerSafeIdempotencyStore`의 여섯 호출 지점`:122`, `:176`, `:211`, `:255`, `:303`, `:360` 같은 클래스의 private `requirePrimaryWriteTransaction`(`:506`\~`:507`)을 부르고, 그 메서드가 `guard.requirePrimaryWriteTransaction`으로 넘긴다. `requireActiveCapability``:123`에서 한 번 호출한다.
## 원문과 갈리는 자리
## 원문과 다른 검사 순서
원문은 이 수정이 세 번째 질문을 형제와 같은 형태로 바꾼 것이라고 적었다. 바꾼 것이 아니라 더한 것이다. `:93` 의 옛 검사가 그대로 첫 줄에 있다.
@@ -37,9 +37,9 @@ source:
독립 Flyway 스트림 설계의 접착제다. 각 capability 스트림의 V1이 세 단계를 밟는다 — (1) 선행조건 검사(`DO $$ ... RAISE EXCEPTION`으로 core epoch가 ACTIVE인지), (2) 테이블 생성, (3) **자기를 `INSTALLED_INACTIVE`로 등록**.
## 적용과 사용 승인이 갈리는 자리
## 설치 상태와 사용 승인을 분리한다
:::evidence key="capability-schema-registry-diagram" alt="선행조건 검사에서 테이블 생성으로 core epoch 확인이 건너가고 테이블 생성에서 레지스트리 등록으로 INSTALLED_INACTIVE 가 건너간다" caption="적용과 사용 승인이 갈리는 자리" zoom="false"
:::evidence key="capability-schema-registry-diagram" alt="선행조건 검사에서 테이블 생성으로 core epoch 확인이 건너가고 테이블 생성에서 레지스트리 등록으로 INSTALLED_INACTIVE 가 건너간다" caption="설치 상태와 사용 승인 분리" zoom="false"
:::
## 어댑터가 런타임에 다시 묻는다
@@ -100,7 +100,7 @@ update idempotency_record
전이마다 리비전이 오른다. 그래서 같은 소유자라도 자기가 본 리비전이 아니면 갱신이 0 건이 된다. 소유권만으로는 부족하고 시점까지 맞아야 한다.
## 한 자리에 모으는 이유
## CAS 조건을 공통 헬퍼에 모으는 이유
```java
/**
@@ -32,8 +32,8 @@ verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지
4. 전이마다 상태 리비전을 올린다
소유권만으로는 부족하다. 자기가 본 시점까지 맞아야 한다.
5. 같은 가드를 쓰는 문장을 한 자리에 모은
흩어져 있으면 그중 하나가 조건을 짧게 쓰는 것을 막을다.
5. 같은 CAS 가드는 공통 SQL 생성 코드에서 만든
전이마다 WHERE 조건을 따로 작성하면 한 전이만 owner token이나 revision을 빠뜨릴다.
## 적용 조건
@@ -12,7 +12,7 @@ verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지
# 시간은 DB에서, 그리고 행을 잠근 다음에 읽는다
애플리케이션 시계로 리스 만료를 판단하거나 잠그기 전의 시각으로 판단해, 서로 다른 노드가 같은 행에 대해 다른 답을 내는 것을 막는다. 시각은 데이터베이스에서 읽고, 행을 잠근 다음에 읽으며, 판정과 갱신을 한 문장 안에 둔다.
애플리케이션 시계로 리스 만료를 판단하거나 lock wait 이전의 시각으로 판단해 서로 다른 노드가 같은 행에 대해 다른 답을 내는 것을 막는다. 이 규칙에서는 행 lock을 획득한 뒤 PostgreSQL `clock_timestamp()`를 읽는다. transaction 시작 시각인 `now()`·`CURRENT_TIMESTAMP`·`transaction_timestamp()`는 lock wait 이후의 실제 시각을 반영하지 않으므로 이 용도의 대체재로 쓰지 않는다.
## 목적
@@ -20,11 +20,11 @@ verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지
## 규칙
1. 시각은 데이터베이스에서 읽는다
여러 노드의 시계는 서로 다르다. 리스 만료 판정의 기준 시각이 노드마다 다르면 두 노드가 동시에 소유자가 될 수 있다.
1. 행 lock 뒤 `clock_timestamp()` 읽는다
여러 노드의 애플리케이션 시계 대신 DB 시계를 쓰고, lock wait가 끝난 뒤 호출 시점의 실제 시각을 읽는다. `now()``CURRENT_TIMESTAMP`는 transaction start time이라 이 용도에는 맞지 않는다.
2. 행을 잠근 다음에 읽는다
그기 전의 시각으로 판단하면 잠금을 기다리는 동안 리스가 만료될 수 있다.
2. lock 이전에 읽은 시각을 재사용하지 않는다
잠금을 기다리는 동안 lease가 만료될 수 있으므로 판정에 쓰는 시각은 lock 획득 후 다시 얻는다.
3. 판정과 갱신을 한 문장 안에 둔다
시각 비교를 where 절에 넣으면 판정과 갱신 사이에 시간이 흐르지 않는다.