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>
167 lines
16 KiB
XML
167 lines
16 KiB
XML
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1866" viewBox="0 0 1200 1866" role="img">
|
|
<title>terminal evidence</title>
|
|
<desc>Terminal-style rendering generated from retained command output. Sensitive-looking values are redacted in the visual asset.</desc>
|
|
<rect x="1" y="1" width="1198" height="1864" rx="14" fill="#0d1117" stroke="#30363d"/>
|
|
<rect x="1" y="1" width="1198" height="44" rx="14" fill="#161b22"/>
|
|
<rect x="1" y="30" width="1198" height="14" fill="#161b22"/>
|
|
<circle cx="24" cy="22" r="6" fill="#ff5f57"/>
|
|
<circle cx="44" cy="22" r="6" fill="#febc2e"/>
|
|
<circle cx="64" cy="22" r="6" fill="#28c840"/>
|
|
<text x="92" y="27" fill="#8b949e" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">terminal evidence</text>
|
|
<text x="24" y="68" fill="#c9d1d9" font-size="15" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">$ set -e
|
|
set -o pipefail
|
|
D=$(mktemp -d); trap 'rm -rf "$D"' EXIT
|
|
C=/shared/codebase/clean-architecture-backend-template
|
|
</tspan><tspan x="24" dy="22">N=$C/src/adapter/outbound/notification
|
|
CORE=$C/src/application-core
|
|
java -version 2>&1 | head -1
|
|
CP=$(find "$N/build/lib</tspan><tspan x="24" dy="22">s" "$CORE/build/libs" -name "*+21234e38cdb9.jar" 2>/dev/null | tr '\n' ':')
|
|
echo "$CP" | tr ':' '\n' | grep -c jar | sed</tspan><tspan x="24" dy="22"> "s/^/고정 리비전 jar : /"
|
|
cat > "$D/AdminSequenceProbe.java" <<'JAVA'
|
|
import dev.caskeleton.adapter.outbound.notificati</tspan><tspan x="24" dy="22">on.platform.dispatch.ProviderAttemptLimiter;
|
|
import dev.caskeleton.adapter.outbound.notification.platform.dispatch.Provi</tspan><tspan x="24" dy="22">derRuntimeRegistry;
|
|
import dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationHealthReporter;</tspan><tspan x="24" dy="22">
|
|
import dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationServingThresholds;
|
|
import dev.cask</tspan><tspan x="24" dy="22">eleton.application.notification.platform.observation.NotificationServingState;
|
|
import dev.caskeleton.adapter.outbound.no</tspan><tspan x="24" dy="22">tification.platform.dispatch.ProviderRuntime;
|
|
import dev.caskeleton.application.notification.platform.api.ProviderId;
|
|
im</tspan><tspan x="24" dy="22">port dev.caskeleton.application.notification.platform.api.ProviderProfileId;
|
|
import dev.caskeleton.application.notificat</tspan><tspan x="24" dy="22">ion.platform.api.routing.Channel;
|
|
import dev.caskeleton.application.notification.platform.provider.NotificationProviderA</tspan><tspan x="24" dy="22">dapter;
|
|
import dev.caskeleton.application.notification.platform.provider.ProviderCapabilities;
|
|
import dev.caskeleton.app</tspan><tspan x="24" dy="22">lication.notification.platform.provider.ProviderProfileSnapshot;
|
|
import dev.caskeleton.application.notification.platform</tspan><tspan x="24" dy="22">.provider.ProviderRuntimeState;
|
|
import dev.caskeleton.application.notification.platform.provider.ProviderSubmission;
|
|
imp</tspan><tspan x="24" dy="22">ort dev.caskeleton.application.notification.platform.provider.ProviderSubmissionResult;
|
|
import java.time.Clock;
|
|
import j</tspan><tspan x="24" dy="22">ava.time.Duration;
|
|
import java.time.Instant;
|
|
import java.time.ZoneOffset;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
im</tspan><tspan x="24" dy="22">port java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.CompletionStage;
|
|
|
|
public final class AdminSeque</tspan><tspan x="24" dy="22">nceProbe {
|
|
|
|
private static final Clock CLOCK = Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC);
|
|
|
|
</tspan><tspan x="24" dy="22">private static ProviderRuntime runtime() {
|
|
return new ProviderRuntime(
|
|
new ProviderProfileSnapshot(
|
|
</tspan><tspan x="24" dy="22"> new ProviderProfileId("apns-main"),
|
|
new ProviderId("apns"),
|
|
Channel.PUSH,
|
|
"PRODU</tspan><tspan x="24" dy="22">CTION",
|
|
1L,
|
|
new ProviderCapabilities(
|
|
false, false, false, false, false, false, </tspan><tspan x="24" dy="22">false, true, 1, 4096L, Duration.ofHours(1)),
|
|
Map.of("topic", "com.example.app")),
|
|
new StubAdapter(),</tspan><tspan x="24" dy="22">
|
|
new ProviderAttemptLimiter(1, 1_000_000, CLOCK));
|
|
}
|
|
|
|
private static NotificationHealthReporter reporter;
|
|
|
|
</tspan><tspan x="24" dy="22"> private static void show(String step, ProviderRuntime runtime, String returned) {
|
|
String indicator = reporter == nu</tspan><tspan x="24" dy="22">ll ? "-" : String.valueOf(reporter.snapshot().healthy());
|
|
System.out.printf(" %-34s 반환 %-6s 상태 %-22s 사유 %-19s</tspan><tspan x="24" dy="22"> 헬스 %s%n",
|
|
step, returned, runtime.health().state(), runtime.health().reason().orElse("(없음)"), indicator);
|
|
</tspan><tspan x="24" dy="22"> }
|
|
|
|
private static NotificationHealthReporter reporterFor(ProviderRuntime runtime) {
|
|
ProviderRuntimeRegistry regis</tspan><tspan x="24" dy="22">try = new ProviderRuntimeRegistry();
|
|
registry.register(runtime);
|
|
return new NotificationHealthReporter(
|
|
</tspan><tspan x="24" dy="22">registry,
|
|
java.util.List.of(new ProviderProfileId("apns-main")),
|
|
() -> NotificationServingState.IDLE,
|
|
</tspan><tspan x="24" dy="22"> Set.of(Channel.PUSH),
|
|
NotificationServingThresholds.DEFAULT);
|
|
}
|
|
|
|
public static void main(String[] args</tspan><tspan x="24" dy="22">) {
|
|
System.out.println("[관리자 포트가 부르는 여섯 전이가 현재 상태를 읽는가]");
|
|
for (String[] row : new String[</tspan><tspan x="24" dy="22">][] {
|
|
{"resumeHealthy", "읽는다 — AUTHENTICATION_FAILED 면 그대로 둔다"},
|
|
{"markDegraded", "읽는다 — H</tspan><tspan x="24" dy="22">EALTHY 일 때만 바꾼다"},
|
|
{"markThrottled", "읽는다 — HEALTHY 일 때만 바꾼다"},
|
|
{"markAuthenticationFail</tspan><tspan x="24" dy="22">ed", "set 으로 덮어쓴다"},
|
|
{"markDraining", "읽지 않는다 — 무조건 덮어쓴다"},
|
|
{"markDisabled", "읽지 않</tspan><tspan x="24" dy="22">는다 — 무조건 덮어쓴다"}}) {
|
|
System.out.printf(" %-26s %s%n", row[0], row[1]);
|
|
}
|
|
System.out.println();
|
|
|
|
</tspan><tspan x="24" dy="22"> System.out.println("[네 단계 시퀀스]");
|
|
ProviderRuntime runtime = runtime();
|
|
reporter = reporterFor(runtime)</tspan><tspan x="24" dy="22">;
|
|
show("0. 시작", runtime, "-");
|
|
runtime.markAuthenticationFailed("INVALID_CREDENTIAL");
|
|
show("1. 제공자가 </tspan><tspan x="24" dy="22">자격증명 거부", runtime, "-");
|
|
show("2. 운영자가 HEALTHY 요청", runtime, String.valueOf(runtime.resumeHealthy()));
|
|
</tspan><tspan x="24" dy="22"> show("3. 운영자가 DRAINING 요청", runtime, String.valueOf(runtime.markDraining()));
|
|
show("4. 운영자가 다시 HEALTH</tspan><tspan x="24" dy="22">Y 요청", runtime, String.valueOf(runtime.resumeHealthy()));
|
|
System.out.println();
|
|
|
|
System.out.println("[DISABLED</tspan><tspan x="24" dy="22"> 로도 같은지]");
|
|
ProviderRuntime other = runtime();
|
|
reporter = reporterFor(other);
|
|
other.markAuthenticationF</tspan><tspan x="24" dy="22">ailed("INVALID_CREDENTIAL");
|
|
show("3'. 운영자가 DISABLED 요청", other, String.valueOf(other.markDisabled()));
|
|
sh</tspan><tspan x="24" dy="22">ow("4'. 운영자가 HEALTHY 요청", other, String.valueOf(other.resumeHealthy()));
|
|
System.out.println();
|
|
|
|
System.out</tspan><tspan x="24" dy="22">.println("[비교] 가드가 있는 전이로는 통과하지 못한다");
|
|
ProviderRuntime guarded = runtime();
|
|
reporter = reporte</tspan><tspan x="24" dy="22">rFor(guarded);
|
|
guarded.markAuthenticationFailed("INVALID_CREDENTIAL");
|
|
show("3\". 운영자가 DEGRADED 요청", guard</tspan><tspan x="24" dy="22">ed, String.valueOf(guarded.markDegraded("MAINTENANCE")));
|
|
show("4\". 운영자가 HEALTHY 요청", guarded, String.valueOf</tspan><tspan x="24" dy="22">(guarded.resumeHealthy()));
|
|
}
|
|
|
|
private static final class StubAdapter implements NotificationProviderAdapter {
|
|
p</tspan><tspan x="24" dy="22">ublic ProviderId providerId() { return new ProviderId("apns"); }
|
|
public Set<Channel> channels() { return Set.of(Chan</tspan><tspan x="24" dy="22">nel.PUSH); }
|
|
public ProviderCapabilities capabilities() {
|
|
return new ProviderCapabilities(
|
|
false, fa</tspan><tspan x="24" dy="22">lse, false, false, false, false, false, true, 1, 4096L, Duration.ofHours(1));
|
|
}
|
|
public CompletionStage<ProviderS</tspan><tspan x="24" dy="22">ubmissionResult> submit(ProviderSubmission submission) {
|
|
return CompletableFuture.failedFuture(new IllegalStateExc</tspan><tspan x="24" dy="22">eption("not invoked"));
|
|
}
|
|
}
|
|
}
|
|
JAVA
|
|
javac -encoding UTF-8 -cp "$CP" -d "$D" "$D/AdminSequenceProbe.java"
|
|
java -Dstd</tspan><tspan x="24" dy="22">out.encoding=UTF-8 -cp "$CP$D" AdminSequenceProbe
|
|
</tspan></text>
|
|
<text x="24" y="1234" fill="#8b949e" font-size="13" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">cwd: /shared/codebase/clean-architecture-backend-template</tspan></text>
|
|
<text x="24" y="1256" fill="#8b949e" font-size="13" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">time: 2026-09-02T14:53:15+00:00 · exit 0</tspan></text>
|
|
<line x1="24" y1="1270" x2="1176" y2="1270" stroke="#30363d"/>
|
|
<text x="24" y="1310" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">openjdk version "21.0.12" 2026-07-21</tspan></text>
|
|
<text x="24" y="1332" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">고정 리비전 jar : 2</tspan></text>
|
|
<text x="24" y="1354" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[관리자 포트가 부르는 여섯 전이가 현재 상태를 읽는가]</tspan></text>
|
|
<text x="24" y="1376" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> resumeHealthy 읽는다 — AUTHENTICATION_FAILED 면 그대로 둔다</tspan></text>
|
|
<text x="24" y="1398" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> markDegraded 읽는다 — HEALTHY 일 때만 바꾼다</tspan></text>
|
|
<text x="24" y="1420" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> markThrottled 읽는다 — HEALTHY 일 때만 바꾼다</tspan></text>
|
|
<text x="24" y="1442" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> markAuthenticationFailed set 으로 덮어쓴다</tspan></text>
|
|
<text x="24" y="1464" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> markDraining 읽지 않는다 — 무조건 덮어쓴다</tspan></text>
|
|
<text x="24" y="1486" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> markDisabled 읽지 않는다 — 무조건 덮어쓴다</tspan></text>
|
|
<text x="24" y="1508" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
|
|
<text x="24" y="1530" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[네 단계 시퀀스]</tspan></text>
|
|
<text x="24" y="1552" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 0. 시작 반환 - 상태 HEALTHY 사유 (없음) 헬스 true</tspan></text>
|
|
<text x="24" y="1574" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 1. 제공자가 자격증명 거부 반환 - 상태 AUTHENTICATION_FAILED 사유 INVALID_CREDENTIAL 헬스 false</tspan></text>
|
|
<text x="24" y="1596" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 2. 운영자가 HEALTHY 요청 반환 false 상태 AUTHENTICATION_FAILED 사유 INVALID_CREDENTIAL 헬스 false</tspan></text>
|
|
<text x="24" y="1618" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 3. 운영자가 DRAINING 요청 반환 true 상태 DRAINING 사유 DRAINING 헬스 true</tspan></text>
|
|
<text x="24" y="1640" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 4. 운영자가 다시 HEALTHY 요청 반환 true 상태 HEALTHY 사유 (없음) 헬스 true</tspan></text>
|
|
<text x="24" y="1662" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
|
|
<text x="24" y="1684" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[DISABLED 로도 같은지]</tspan></text>
|
|
<text x="24" y="1706" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 3'. 운영자가 DISABLED 요청 반환 true 상태 DISABLED 사유 DISABLED 헬스 false</tspan></text>
|
|
<text x="24" y="1728" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 4'. 운영자가 HEALTHY 요청 반환 true 상태 HEALTHY 사유 (없음) 헬스 true</tspan></text>
|
|
<text x="24" y="1750" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
|
|
<text x="24" y="1772" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[비교] 가드가 있는 전이로는 통과하지 못한다</tspan></text>
|
|
<text x="24" y="1794" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 3". 운영자가 DEGRADED 요청 반환 false 상태 AUTHENTICATION_FAILED 사유 INVALID_CREDENTIAL 헬스 false</tspan></text>
|
|
<text x="24" y="1816" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 4". 운영자가 HEALTHY 요청 반환 false 상태 AUTHENTICATION_FAILED 사유 INVALID_CREDENTIAL 헬스 false</tspan></text>
|
|
</svg>
|