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
+158
@@ -0,0 +1,158 @@
|
||||
---
|
||||
kind: CONCEPT
|
||||
slug: transport-failure-stage-and-category
|
||||
title: 전송 실패의 단계와 범주 — AttemptStage와 FailureCategory
|
||||
topic: http-failure-classification
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: concept:transport-failure-stage-and-category
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: transport-failure-stage-and-category
|
||||
file: ../../../final/evidence/rendered/transport-failure-stage-and-category.svg
|
||||
- key: transport-failure-stage-and-category-diagram
|
||||
file: ../../../final/assets/diagrams/transport-failure-stage-and-category.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/transport-failure-stage-and-category.txt
|
||||
source:
|
||||
- 원본 분석 절은 final/document.md#8-1 · analysis/11 §51 이다.
|
||||
---
|
||||
|
||||
# 전송 실패의 단계와 범주 — AttemptStage와 FailureCategory
|
||||
|
||||
HTTP 전송 실패를 "어디까지 갔는가"와 "무엇이 실패했는가" 두 축으로 나눠 기록하고, 그 둘이 함께 재시도 안전성 판정의 입력이 된다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **붉은 테스트를 제품 결함으로 읽은 오진**
|
||||
이 두 축이 실제로 어떻게 채워지는지, 그리고 축을 채우는 정보가 사라지면 어떻게 되는지 보여 주는 사례다.
|
||||
- **분류기는 엔진이 남긴 것만 볼 수 있다**
|
||||
이 모델의 입력 한계를 규칙으로 옮긴 것이다.
|
||||
- **재시도 가능성은 멱등성과 실패 범주를 함께 봐야 정해진다**
|
||||
이 모델의 출력이 재시도 결정으로 이어지는 규칙이다.
|
||||
- **재시도 안전성은 증거에 기반해 판정한다**
|
||||
이 모델을 채택한 프로젝트 결정이다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
HTTP 전송 실패를 "어디까지 갔는가"(`AttemptStage`: pool·DNS·CONNECT·TLS·전송·응답)와 "무엇이 실패했는가"(`FailureCategory`)의 두 축으로 모델링하는 구조의 설명이다.
|
||||
|
||||
## 두 축이 만드는 판정
|
||||
|
||||
:::evidence key="transport-failure-stage-and-category-diagram" alt="단계 축과 범주 축의 조합에서 notSent 와 maybeSent 두 갈래가 나온다" caption="두 축이 만드는 판정" zoom="false"
|
||||
:::
|
||||
|
||||
두 축이 함께 `notSent`/`maybeSent` 판정을 만들고, 그것이 재시도 결정표의 입력이 된다 — `TLS_PERMANENT`는 `permanent()`로 절대 차단되고 `CONNECT`는 멱등성과 무관하게 재시도된다.
|
||||
|
||||
## AttemptStage 참조 위치
|
||||
|
||||
:::evidence key="transport-failure-stage-and-category" alt="코드베이스에서 AttemptStage 를 검색한 출력 23줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="AttemptStage 코드베이스 검색 — 23줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 분류가 곧 재시도 안전성이다
|
||||
|
||||
분류의 입력은 엔진이 남긴 예외 사슬이므로, 사슬 순회 순서와 **사슬에 무엇이 남았는가**가 함께 그 분류를 정한다.
|
||||
|
||||
:::note
|
||||
|
||||
없음 — 분류기와 결정표를 코드로 확인했다
|
||||
|
||||
:::
|
||||
|
||||
## 두 축
|
||||
|
||||
`AttemptStage`는 한 번의 물리적 시도가 어디까지 진행했는지를 담는 단조 진행 표식이다.
|
||||
|
||||
```java
|
||||
public enum AttemptStage {
|
||||
VALIDATION(0, true),
|
||||
AUTHENTICATION(1, true),
|
||||
POOL_ACQUIRE(2, true),
|
||||
DNS(3, true),
|
||||
CONNECT(4, true),
|
||||
TLS_HANDSHAKE(5, true),
|
||||
PROXY_CONNECT(6, true),
|
||||
REQUEST_HEADERS(7, false),
|
||||
REQUEST_BODY(8, false),
|
||||
RESPONSE_HEADERS(9, false),
|
||||
RESPONSE_BODY(10, false),
|
||||
COMPLETE(11, false);
|
||||
```
|
||||
|
||||
두 번째 인자가 `provesNotSent`다. `REQUEST_HEADERS` 앞의 일곱 단계는 전부 `true`이고, 요청 쓰기가 시작된 뒤로는 전부 `false`다. 첫 인자인 `order`는 명시적 진행 순위이며, javadoc이 그 이유를 적는다 — "the progress tracker forbids regression and the evidence classifier reads the rank, so neither depends on enum declaration ordinals."
|
||||
|
||||
`FailureCategory`는 무엇이 실패했는지를 담는 전송 중립 어휘 23개다. `permanent()`가 그중 여덟을 영구로 표시한다.
|
||||
|
||||
```java
|
||||
public boolean permanent() {
|
||||
return this == CONFIGURATION
|
||||
|| this == TARGET_REJECTED
|
||||
|| this == TLS_PERMANENT
|
||||
|| this == SERIALIZATION
|
||||
|| this == RESPONSE_TOO_LARGE
|
||||
|| this == REDIRECT_REJECTED
|
||||
|| this == DEADLINE_EXCEEDED
|
||||
|| this == CANCELLED;
|
||||
}
|
||||
```
|
||||
|
||||
## 세 번째 값: 증거
|
||||
|
||||
두 축의 결과는 `TransportFailure` 레코드로 합쳐지고, 거기에 `ExecutionEvidence`가 함께 실린다.
|
||||
|
||||
```java
|
||||
public record TransportFailure(
|
||||
AttemptStage stage, ExecutionEvidence evidence, FailureCategory category, String safeReason) {
|
||||
|
||||
public static TransportFailure notSent(
|
||||
AttemptStage stage, FailureCategory category, String safeReason) {
|
||||
return new TransportFailure(stage, ExecutionEvidence.NOT_SENT, category, safeReason);
|
||||
}
|
||||
|
||||
public static TransportFailure sentNoResponse(
|
||||
AttemptStage stage, FailureCategory category, String safeReason) {
|
||||
return new TransportFailure(stage, ExecutionEvidence.SENT_NO_RESPONSE, category, safeReason);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ExecutionEvidence`는 `NOT_SENT`, `SENT_NO_RESPONSE`, `RESPONSE_RECEIVED`, `PARTIAL_RESPONSE` 넷이고, javadoc이 승격을 금지한다 — "NOT_SENT is only used when a stage failure proves the request never reached the server. A generic engine I/O failure is never upgraded to NOT_SENT."
|
||||
|
||||
`safeReason`은 고정 어휘 토큰이며 엔진 메시지가 아니다. 이유도 javadoc에 있다 — 엔진 메시지에는 전체 URL이나 해석된 주소가 들어갈 수 있다.
|
||||
|
||||
## 왜 분류가 곧 재시도 안전성인가
|
||||
|
||||
`DefaultRetryEligibilityEngine`이 이 세 값을 그대로 읽는다.
|
||||
|
||||
```java
|
||||
if (context.failureCategory().permanent()) {
|
||||
return RetryDenied.permanentFailure(context.failureCategory().name());
|
||||
}
|
||||
...
|
||||
case CONNECT -> RetryAllowed.of("CONNECT");
|
||||
```
|
||||
|
||||
같은 실패가 `TLS_PERMANENT`로 분류되면 절대 재시도되지 않고, `CONNECT`로 분류되면 멱등성과 무관하게 재시도된다. 두 경로 사이에 정책이 개입할 여지가 없다. 분류가 곧 결정이다.
|
||||
|
||||
## 이 모델의 입력 한계
|
||||
|
||||
분류의 입력은 엔진이 호출자에게 남긴 예외 사슬이다. `ApacheFailureClassifier.classify`는 사슬 전체를 훑는다.
|
||||
|
||||
```java
|
||||
for (Throwable cause : chain(failure)) {
|
||||
TransportFailure recognized = recognize(cause, lastObservedStage);
|
||||
if (recognized != null) {
|
||||
return recognized;
|
||||
}
|
||||
}
|
||||
return conservative(lastObservedStage, FailureCategory.UNKNOWN, "TRANSPORT_FAILURE");
|
||||
```
|
||||
|
||||
주석이 그렇게 하는 이유를 적는다 — Spring이 엔진 예외를 감싸고, 감싸인 `ConnectException`도 요청이 전송되지 않았음을 증명하므로 바깥 타입만 보면 증명 가능한 `NOT_SENT`를 모호한 `SENT_NO_RESPONSE`로 낮추게 된다.
|
||||
|
||||
그러나 사슬 순회는 사슬에 있는 것만 볼 수 있다. 엔진이 원인을 버리면 이 모델은 그 원인을 분류할 수 없고, 그때는 순회 순서를 어떻게 짜도 결과가 달라지지 않는다. 그 경계가 실제로 어떻게 드러났는지는 관련 Case에 있다.
|
||||
|
||||
<!-- body:end -->
|
||||
Reference in New Issue
Block a user