The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
123 lines
5.2 KiB
Markdown
123 lines
5.2 KiB
Markdown
---
|
|
kind: CONCEPT
|
|
slug: fenced-lease
|
|
title: fenced lease — 만료 시각만으로는 부족한 이유
|
|
topic: owner-safe-state-machines
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
rootTreeNode: concept:fenced-lease
|
|
evidenceCapturedOn: 2026-09-01
|
|
assets:
|
|
- key: fenced-lease
|
|
file: ../../../final/evidence/rendered/fenced-lease.svg
|
|
evidence:
|
|
- ../../../final/evidence/raw/fenced-lease.txt
|
|
source:
|
|
- 원본 분석 절은 final/document.md#4-1, #4-3 · analysis/05 §10 · analysis/19 §7.3 이다.
|
|
---
|
|
|
|
# fenced lease — 만료 시각만으로는 부족한 이유
|
|
|
|
리스에 만료 시각만 기록하면 언제 끝나는지는 알아도 누가 들고 있는지는 모른다. 소유자와 증가하는 토큰을 함께 기록하면 만료된 작업자의 쓰기가 아무 행에도 맞지 않게 된다.
|
|
|
|
## 관계
|
|
|
|
- **lease가 만료 시각만 기록하고 소유자를 기록하지 않아 terminal state가 되돌려졌다**
|
|
이 개념이 필요해진 사례다.
|
|
- **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다**
|
|
펜싱 토큰을 실제로 강제하는 방법이다.
|
|
- **만료된 claim과 만료된 실행은 다르게 다뤄야 한다**
|
|
리스가 만료됐을 때의 처리를 갈라야 하는 이유다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
lease가 "언제 끝나는가"만 기록하고 "누가 들고 있는가"를 기록하지 않으면 만료를 지난 worker가 여전히 쓸 수 있다. V2 마이그레이션 헤더가 그 시나리오를 3단계로 적는다 — relay A가 claim하고 브로커를 부름 / lease 만료, relay B가 재claim하고 발행하고 PUBLISHED 기록 / relay A가 타임아웃 후 그 위에 AMBIGUOUS를 씀.
|
|
|
|
## V2 헤더가 적은 3단계 시나리오
|
|
|
|
:::evidence key="fenced-lease" alt="분석 문서 final/document.md 에서 이 기록의 근거 절을 그대로 잘라낸 18줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="final/document.md 발췌 — 18줄" zoom="true"
|
|
:::
|
|
|
|
## lease를 늘리는 것이 해법이 아닌 이유
|
|
|
|
"**Making the lease longer than the publish timeout lowers the odds; it does not turn a GC pause, a scheduler stall**..."
|
|
|
|
## 해법과 그 반복
|
|
|
|
소유자와 fencing token을 행에 기록하고 terminal write가 그 튜플로 매칭하는 것이다. 같은 결함이 이 저장소에서 최소 세 곳(messaging outbox·fileserver cleanup·notification dispatcher)에 나타났다.
|
|
|
|
:::note
|
|
|
|
없음 — 마이그레이션과 claim SQL을 코드로 확인했다
|
|
|
|
:::
|
|
|
|
## 만료 시각만 있을 때 일어나는 일
|
|
|
|
메시징 outbox 의 V1 스키마는 리스 만료 시각만 기록했다. 릴레이의 최종 상태 쓰기는 메시지 식별자만으로 행을 찾았다.
|
|
|
|
마이그레이션 헤더가 그 결과를 순서대로 적는다.
|
|
|
|
```text
|
|
relay A claims the row and calls the broker
|
|
the lease expires; relay B reclaims it, publishes, and records PUBLISHED
|
|
relay A finally times out and records AMBIGUOUS over the top
|
|
```
|
|
|
|
세 줄이 끝나면 행은 다시 청구 가능한 상태가 되고 메시지는 두 번째로 발행된다.
|
|
|
|
## 리스를 늘리는 것은 해법이 아니다
|
|
|
|
```text
|
|
Making the lease longer than the publish timeout lowers the odds; it does not turn a GC pause, a
|
|
scheduler stall or a slow broker into a data constraint.
|
|
```
|
|
|
|
확률을 낮추는 것과 불변식을 만드는 것은 다르다. GC 정지나 스케줄러 지연이나 느린 브로커는 시간 여유로 없앨 수 있는 것이 아니다.
|
|
|
|
## 토큰이 하는 일
|
|
|
|
```text
|
|
A token does: every terminal write names the acquisition it belongs to, and a superseded worker's
|
|
write matches nothing.
|
|
```
|
|
|
|
모든 최종 쓰기가 자기가 속한 획득을 지목한다. 밀려난 작업자의 쓰기는 어떤 행에도 맞지 않는다. 실패가 아니라 갱신 건수 0 이 되고, 그것이 답이 된다.
|
|
|
|
## 스키마가 담는 것
|
|
|
|
```sql
|
|
ALTER TABLE messaging_outbox
|
|
ADD COLUMN lease_owner VARCHAR(160),
|
|
ADD COLUMN lease_token BIGINT NOT NULL DEFAULT 0,
|
|
ADD COLUMN next_attempt_at TIMESTAMPTZ;
|
|
|
|
ALTER TABLE messaging_outbox
|
|
ADD CONSTRAINT ck_messaging_outbox_lease_token CHECK (lease_token >= 0);
|
|
```
|
|
|
|
백필이 정확성에 필요하지 않다는 것도 헤더가 적는다. 기본값이 0 이고 첫 청구가 그것을 올린다. 제약은 코드가 의존하는 불변식을 문장으로 남기기 위한 것이다.
|
|
|
|
## 같은 마이그레이션이 함께 고친 것
|
|
|
|
```sql
|
|
-- EXHAUSTED is a new terminal state: the attempt budget ran out without any confirmation, which is
|
|
-- not the same as the broker rejecting the message. V1's CHECK listed five states, so writing the
|
|
-- sixth failed at the constraint rather than at review.
|
|
```
|
|
|
|
:::note
|
|
|
|
상태 목록을 CHECK 제약으로 닫아 두면, 새 상태를 추가하는 변경이 리뷰가 아니라 제약에서 실패한다. 그것이 의도된 동작이다 — 상태 어휘의 확장이 조용히 일어나지 않는다.
|
|
|
|
:::
|
|
|
|
## 같은 형태가 다른 곳에도 있다
|
|
|
|
파일서버 리프의 V3 마이그레이션이 같은 문제를 같은 방식으로 푼다. 리스 소유자와 펜스를 함께 기록한다.
|
|
|
|
<!-- body:end -->
|