docs(keycloak-session-store): import the session-storage lab as a new project
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
+117
@@ -0,0 +1,117 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: a-replay-store-with-no-eviction-path
|
||||
title: 재생 저장소에 제거 경로가 없고, 형제 맵의 상한은 세션 경계 하나다
|
||||
topic: retention-and-unbounded-growth
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:a-replay-store-with-no-eviction-path
|
||||
evidenceCapturedOn: 2026-09-02
|
||||
assets:
|
||||
- key: a-replay-store-with-no-eviction-path
|
||||
file: ../../../final/evidence/rendered/a-replay-store-with-no-eviction-path.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/a-replay-store-with-no-eviction-path.txt
|
||||
source:
|
||||
- 분석 문서는 grpc-policy 편 §17.3 과 grpc-advanced-streaming 편 §17.1 이다. 앞의 절이 재생 저장소의 제거 경로 부재를 P2 로 판정하고, 두 모듈의 대조가 "저쪽은 풀었고 이쪽은 안 풀었다"가 아니라는 정정도 그 절에 있다. 뒤의 절이 형제 맵의 증가를 P3 으로 기록하고, 필요한 창이 좁다는 근거를 함께 적는다.
|
||||
- 같은 절이 이 저장소가 커밋될 때마다 항목이 쌓인다고 적는데, 이 클래스를 잡는 프로덕션 코드는 아직 없다. 위 터미널 출력의 참조 수가 그것이다.
|
||||
---
|
||||
|
||||
# 재생 저장소에 제거 경로가 없고, 형제 맵의 상한은 세션 경계 하나다
|
||||
|
||||
결과 재생 저장소가 항목을 넣기만 하고 지우지 않는다. 형제 모듈의 중복 제거기가 같은 형태이고, 그 클래스는 같은 종류의 무제한 증가를 비판하는 javadoc 을 갖고 있다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **bounded 와 unbounded 오버로드를 나란히 둔 포트**
|
||||
같은 Topic 의 보존 경계 문제다.
|
||||
- **같은 안전 규칙은 하나의 공식과 하나의 강제 시점을 갖는다**
|
||||
보존 경계를 어디서 정하는지에 대한 규칙이다.
|
||||
- **runtime_memberships를 먼저 읽고 심각도를 정한다**
|
||||
이 사례가 오늘의 사고가 아닌 이유를 판정하는 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
정확히 한 번을 보장하려면 과거를 기억해야 한다. 같은 요청이 다시 오면 저장된 결과를 돌려주고, 같은 메시지가 다시 오면 건너뛴다.
|
||||
|
||||
그 기억이 자료구조다. 요청 식별자나 메시지 식별자를 키로, 결과나 체크포인트를 값으로 갖는다.
|
||||
|
||||
## 결론
|
||||
|
||||
결과 재생 저장소에는 넣는 경로만 있다. 항목 하나의 크기는 제한하고 개수는 제한하지 않는다. 제거도 비우기도 축출도 만료도 없다.
|
||||
|
||||
형제 모듈의 중복 제거기는 같은 형태에 상한 하나를 갖는다. 세션을 끝낼 때 그 세션 키를 지운다. 그래서 이쪽의 증가는 세션 수명에 묶이고, 저장소 쪽은 프로세스 수명에 묶인다.
|
||||
|
||||
두 모듈의 대조는 한쪽이 풀고 한쪽이 안 푼 것이 아니다. 같은 형태의 무제한 증가를 둘 다 갖고 있고, 한쪽만 부분적 상한을 갖는다. 무거운 쪽은 저장소다.
|
||||
|
||||
이 종류의 자료구조에서 보존 경계는 기능이 아니라 전제다. 무엇을 언제까지 기억하는지가 정해지지 않으면 그 보장은 메모리가 버티는 동안만 성립한다.
|
||||
|
||||
두 모듈 모두 런타임 소속이 비어 있고, 저장소 쪽은 이 클래스를 잡는 프로덕션 코드도 없다. 지금 도는 배포에서 일어나는 문제는 아니다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
Gradle : 9.0.0
|
||||
확인 방식 : 두 자료구조의 삽입 경로와 제거 경로 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 재생 저장소의 맵 선언과 항목을 넣는 메서드를 읽는다.
|
||||
2. 같은 파일에서 제거·비우기·축출·만료를 검색하고, 이 클래스를 잡는 프로덕션 코드를 센다.
|
||||
3. 개수를 노출하는 메서드가 있는지, 그 값을 읽는 프로덕션 코드가 있는지 센다.
|
||||
4. 중복 제거기의 클래스 javadoc 과 두 맵의 증가·감소 지점을 확인한다.
|
||||
5. 중복 제거 키의 형식을 확인해 세션 종료가 실제로 그 세션 항목만 지우는지 본다.
|
||||
6. 두 모듈의 런타임 소속을 모듈 레지스트리에서 읽는다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
## 크기는 제한하고 개수는 제한하지 않는다
|
||||
|
||||
:::evidence key="a-replay-store-with-no-eviction-path" alt="코드베이스에서 결과 재생 저장소의 맵 선언과 항목 크기 제한, 제거·비우기·축출·만료 매치 수, 개수를 노출하는 메서드와 그 값을 읽는 코드 수, 이 클래스를 잡는 코드 수, javadoc 이 저장소를 부르는 이름을 뽑은 출력. 이어서 형제 모듈 중복 제거기의 클래스 javadoc 과 두 맵의 선언, 항목이 쌓이는 지점, 재생 판정이 보는 값, 이 파일의 제거 매치 수와 그 둘이 있는 자리, 중복 제거 키의 형식, 그리고 두 모듈의 런타임 소속이 나온다. 저장소 쪽 제거 매치가 0 이고 형제 맵 쪽은 세션 종료 하나뿐이라는 것이 그 출력에 보인다." caption="저장소: 크기 제한만 · 제거 0 · 잡는 코드 0 / 형제 맵: 메시지마다 한 항목 · 제거는 세션 종료 하나 · 키는 세션 접두 — 54줄" zoom="true"
|
||||
:::
|
||||
|
||||
결과 재생 저장소는 결과 참조를 키로, 직렬화된 응답을 값으로 갖는 맵이다.
|
||||
|
||||
넣는 메서드가 크기를 검사한다. 인라인 한도를 넘는 응답은 예외로 거절하고, 객체 참조 뒤에 두라고 메시지에 적는다.
|
||||
|
||||
개수를 검사하는 코드는 없다. 제거도 비우기도 축출도 만료도 파일 전체에서 0 이다.
|
||||
|
||||
이 클래스를 잡는 프로덕션 코드도 아직 0 이다. 배선되면 멱등 키가 필요한 커밋 하나당 항목 하나가 프로세스 수명 동안 남는 형태다.
|
||||
|
||||
## 개수를 볼 수는 있는데 보는 곳이 없다
|
||||
|
||||
개수를 돌려주는 메서드가 하나 있다. 그것을 부르는 프로덕션 코드가 0 이고, 부르는 것은 테스트 하나다.
|
||||
|
||||
javadoc 은 이 저장소를 배포가 고를 수 있는 선택지 중 하나로 소개하면서 작은 인라인 저장소라고 부른다. 작게 유지하는 장치는 그 안에 없다.
|
||||
|
||||
## 형제 모듈이 같은 문제를 문장으로 적어 두었다
|
||||
|
||||
중복 제거기의 클래스 javadoc 이 설계 선택을 설명한다. 본 키를 모으는 집합을 거부한 이유가 셋인데, 첫 번째가 집합은 세션 수명 동안 무제한으로 증가한다는 것이다.
|
||||
|
||||
체크포인트 맵은 그 비판을 지킨다. 세션당 항목 하나이고 순번만 앞으로 간다.
|
||||
|
||||
형제 맵은 지키지 않는다. 적용된 메시지마다 결과 참조를 하나씩 넣는다.
|
||||
|
||||
같은 javadoc 에는 그 맵에 대해 재생 결과가 체크포인트 뒤의 좁은 창 동안만 유지된다고 한 줄 적혀 있다. 창을 닫는 코드가 없다 — 체크포인트가 그 순번을 지나가도 항목은 남는다.
|
||||
|
||||
필요한 창이 실제로 좁다는 것도 코드에 있다. 재생 판정은 체크포인트의 순번 비교가 내리고, 재생 응답에 쓰이는 것은 그 순번의 결과 하나다. 체크포인트보다 한참 뒤처진 순번의 결과가 필요한 구간은 재개 직후뿐이다.
|
||||
|
||||
## 줄어드는 지점은 세션 종료 하나다
|
||||
|
||||
이 파일에서 무언가를 지우는 지점은 둘이고 둘 다 같은 메서드 안에 있다. 세션을 끝내면서 체크포인트를 지우고, 재생 결과 맵에서 그 세션 접두사를 가진 키를 지운다.
|
||||
|
||||
중복 제거 키는 세션 값과 세대와 순번을 막대로 이어 만든다. 접두사 제거가 그 세션의 모든 세대를 함께 지운다.
|
||||
|
||||
그래서 이쪽의 증가는 세션 수명에 묶인다. 저장소 쪽은 프로세스 수명에 묶인다.
|
||||
|
||||
두 모듈 모두 모듈 레지스트리의 런타임 소속이 비어 있다. 배선 경로가 없으니 오늘 이것으로 무너지는 배포는 없다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
오래 돌려 증가를 확인해 보지는 않았다. 배선 경로가 없어 실행 대상이 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: the-cleanup-that-causes-the-outage-it-prevents
|
||||
title: cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다
|
||||
topic: retention-and-unbounded-growth
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:the-cleanup-that-causes-the-outage-it-prevents
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: the-cleanup-that-causes-the-outage-it-prevents
|
||||
file: ../../../final/evidence/rendered/the-cleanup-that-causes-the-outage-it-prevents.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/the-cleanup-that-causes-the-outage-it-prevents.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-inbox-jdbc-postgresql.md §17 P1 · analysis/messaging/messaging-outbox-jdbc-postgresql.md §17 P1 이다.
|
||||
---
|
||||
|
||||
# cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다
|
||||
|
||||
inbox 와 outbox 의 정리 잡이 무제한 삭제 오버로드를 부른다. 배치 제한 구현은 두 리프 모두에 있고 호출 지점이 0이다. 잡의 javadoc 이 실행되는 코드의 동작을 그대로 서술한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **bounded 와 unbounded 오버로드를 나란히 둔 포트**
|
||||
이 사례가 속한 구조다.
|
||||
- **두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다**
|
||||
이 사례가 만든 규칙이다.
|
||||
- **그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다**
|
||||
회귀 테스트가 성립하지 않는 이유다.
|
||||
|
||||
## 문제
|
||||
|
||||
inbox 와 outbox 는 처리된 행을 보존 기간 동안 남긴다. 그 기간이 지나면 정리 잡이 지운다.
|
||||
|
||||
한 번에 얼마나 지우는지가 이 잡의 안전을 정한다. 몇 주 동안 쌓인 테이블에서 조건에 맞는 행 전부를 한 문장으로 지우면, 그 문장이 끝날 때까지 락을 잡고 백로그 크기에 비례해 WAL 을 쓴다.
|
||||
|
||||
## 결론
|
||||
|
||||
두 정리 잡이 무제한 오버로드를 부른다.
|
||||
|
||||
배치 제한 구현은 두 리프 모두에 있다. 건수 제한과 잠긴 행 건너뛰기를 쓰는 SQL 이고, 저장소 전체에서 그 시그니처가 등장하는 곳은 선언 둘, 구현 둘, 테스트 대역 다섯이며 호출 지점이 0이다.
|
||||
|
||||
inbox 정리 잡의 javadoc 이 실행되는 코드의 동작을 그대로 서술한다.
|
||||
|
||||
> A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent.
|
||||
|
||||
정리가 막으려던 장애를 정리가 일으킨다는 문장이고, 그것이 지금 호출되는 형태다.
|
||||
|
||||
두 리프 다 출하 애플리케이션 소속이고 두 잡 다 스타터 빈이다. 다만 스케줄러가 등록되지 않으며 그것은 의도된 설계다. 그래서 상시 결함이 아니라 잠재 결함이고, 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 첫 스윕에서 발현한다.
|
||||
|
||||
회귀 테스트를 쓰려면 대역도 함께 고쳐야 한다. 현재 대역의 배치 제한 구현은 무제한 결과와 제한값 중 작은 쪽을 돌려주는 형태다. 전부 지우고 숫자만 깎으므로 두 형태의 차이를 재현하지 못한다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
Gradle : 9.0.0
|
||||
확인 방식 : 두 오버로드의 선언과 구현 위치 확인, 저장소 전역 호출 지점 검색
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 final/evidence/raw/294-bounded-purge-never-called.txt 에 있다.
|
||||
|
||||
1. 두 포트에서 정리 연산의 두 오버로드 선언을 확인한다.
|
||||
2. 각 구현의 SQL 에서 건수 제한과 잠금 건너뛰기 유무를 확인한다.
|
||||
3. 배치 제한 시그니처를 저장소 전역에서 검색해 호출 지점을 센다.
|
||||
4. 두 정리 잡이 어느 오버로드를 부르는지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
두 cleanup 잡이 무제한 오버로드를 부른다. bounded 구현은 `LIMIT` + `FOR UPDATE SKIP LOCKED` 로 두 리프 모두에 존재하고 호출 지점이 0 이다.
|
||||
|
||||
## InboxCleanupJob 참조 위치
|
||||
|
||||
:::evidence key="the-cleanup-that-causes-the-outage-it-prevents" alt="코드베이스에서 InboxCleanupJob 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="InboxCleanupJob 코드베이스 검색 — 3줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## javadoc 이 실행되는 코드의 동작을 그대로 서술한다
|
||||
|
||||
"A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent." bounded 쪽 javadoc 은 한 발 더 나가, 배치 크기로 제한된다는 잡의 자기 서술을 참으로 만드는 것이 바로 이 파라미터라고 적는다. 그 파라미터를 아무도 넘기지 않는다.
|
||||
|
||||
## 상시 결함이 아니라 잠재 결함이다
|
||||
|
||||
두 리프 다 `app-bootstrap` 소속이고 두 잡 다 스타터 빈이지만 스케줄러가 등록되지 않는다 — 그것이 의도된 설계다. 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 첫 스윕에서 발현한다.
|
||||
|
||||
## 회귀 테스트가 성립하려면 대역도 고쳐야 한다
|
||||
|
||||
현재 대역의 bounded 구현은 전부 지우고 숫자만 깎는 형태라 차이를 재현하지 못한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
백로그가 쌓인 실제 테이블에서 두 형태의 락 보유 시간을 측정하지 않았다. 호출 지점 부재와 두 SQL 의 형태로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
Reference in New Issue
Block a user