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>
87 lines
4.3 KiB
Plaintext
87 lines
4.3 KiB
Plaintext
httpclient 모듈 완료 검증 — lane 실행과 실패 진단
|
|
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
generatedAt=2026-08-30T05:40:58+00:00
|
|
|
|
=== 1. :check 는 archive-hygiene 게이트에서 실패한다 (소스와 무관) ===
|
|
$ ./gradlew :adapter:outbound:httpclient:check → BUILD FAILED
|
|
원인: build/libs 에 이전 리비전 JAR 이 남아 있음
|
|
$ ls src/adapter/outbound/httpclient/build/libs
|
|
httpclient-0.0.1+0137263441f6.jar
|
|
httpclient-0.0.1+a24ece9cf797.jar
|
|
httpclient-0.0.1+e98b56eb03ec.jar
|
|
exit=0
|
|
|
|
게이트 정의: src/gradle/archive-hygiene.gradle — 저장소가 remedy 태스크 cleanStaleTraceableJars 를 제공한다.
|
|
이 분석은 사용자 워크스페이스의 빌드 산출물을 삭제하지 않았다.
|
|
|
|
=== 2. 다섯 lane 개별 실행 ===
|
|
$ ./gradlew :adapter:outbound:httpclient:test :…:httpClientStableContractTest :…:httpClientSecurityTest :…:httpClientBlockHoundTest :…:spring62ApiSurfaceScan
|
|
GRADLE_EXIT=1 — :test 에서 283 tests completed, 3 failed
|
|
|
|
=== 3. JUnit 집계 ===
|
|
test: classes=61 tests=283 failures=3 errors=0 skipped=0
|
|
httpClientStableContractTest: classes=7 tests=24 failures=0 errors=0 skipped=0
|
|
httpClientSecurityTest: classes=2 tests=9 failures=0 errors=0 skipped=0
|
|
httpClientBlockHoundTest: classes=1 tests=3 failures=0 errors=0 skipped=0
|
|
spring62ApiSurfaceScan: classes=1 tests=6 failures=0 errors=0 skipped=0
|
|
|
|
=== 4. 실패 3건의 정확한 내용 ===
|
|
anUntrustedAuthorityIsAPermanentTlsFailure() -> org.opentest4j.AssertionFailedError: expected: TLS_HANDSHAKE but was: CONNECT
|
|
anExpiredCertificateIsAPermanentTlsFailure() -> org.opentest4j.AssertionFailedError: expected: TLS_HANDSHAKE but was: CONNECT
|
|
aHostnameMismatchIsAPermanentTlsFailure() -> org.opentest4j.AssertionFailedError: expected: TLS_HANDSHAKE but was: CONNECT
|
|
|
|
=== 5. 원인: 원인 사슬을 바깥부터 훑고 CONNECT 분기가 TLS 분기보다 앞에 있다 ===
|
|
$ sed -n '30,42p' src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/apache/ApacheFailureClassifier.java
|
|
|
|
@Override
|
|
public TransportFailure classify(Throwable failure, AttemptStage lastObservedStage) {
|
|
// The whole cause chain is inspected, not just the outermost throwable: Spring wraps engine
|
|
// exceptions, and a wrapped ConnectException still proves the request was never sent. Matching
|
|
// only the outer type would downgrade a provable NOT_SENT to an ambiguous SENT_NO_RESPONSE.
|
|
for (Throwable cause : chain(failure)) {
|
|
TransportFailure recognized = recognize(cause, lastObservedStage);
|
|
if (recognized != null) {
|
|
return recognized;
|
|
}
|
|
}
|
|
return conservative(lastObservedStage, FailureCategory.UNKNOWN, "TRANSPORT_FAILURE");
|
|
exit=0
|
|
|
|
$ sed -n '58,73p' src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/apache/ApacheFailureClassifier.java
|
|
if (cause instanceof ConnectTimeoutException
|
|
|| cause instanceof HttpHostConnectException
|
|
|| cause instanceof ConnectException
|
|
|| cause instanceof NoRouteToHostException) {
|
|
return TransportFailure.notSent(
|
|
AttemptStage.CONNECT, FailureCategory.CONNECT, "CONNECT_FAILED");
|
|
}
|
|
if (cause instanceof SSLPeerUnverifiedException || cause instanceof CertificateException) {
|
|
return TransportFailure.notSent(
|
|
AttemptStage.TLS_HANDSHAKE, FailureCategory.TLS_PERMANENT, "TLS_TRUST_FAILED");
|
|
}
|
|
if (cause instanceof SSLHandshakeException) {
|
|
return TransportFailure.notSent(
|
|
AttemptStage.TLS_HANDSHAKE, FailureCategory.TLS_PERMANENT, "TLS_HANDSHAKE_FAILED");
|
|
}
|
|
if (cause instanceof SSLException
|
|
exit=0
|
|
|
|
--- 재시도 결과 대비 ---
|
|
$ grep -n 'case CONNECT\|case TLS_TRANSIENT\|permanent()' src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/resilience/DefaultRetryEligibilityEngine.java
|
|
37: if (context.failureCategory().permanent()) {
|
|
107: case CONNECT -> RetryAllowed.of("CONNECT");
|
|
109: case TLS_TRANSIENT -> RetryAllowed.of("TLS_TRANSIENT");
|
|
exit=0
|
|
|
|
$ grep -n 'TLS_PERMANENT' src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/api/operation/FailureCategory.java
|
|
18: TLS_PERMANENT,
|
|
38: || this == TLS_PERMANENT
|
|
exit=0
|
|
|
|
|
|
=== 6. 작업 트리 ===
|
|
$ git status --short | wc -l
|
|
0
|
|
exit=0
|
|
|