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>
15 lines
7.2 KiB
JSON
15 lines
7.2 KiB
JSON
{
|
|
"assetKey": "a13-f002-authentication-failed-resumehealthy-sequence",
|
|
"kind": "terminal",
|
|
"command": "set -e\nset -o pipefail\nD=$(mktemp -d); trap 'rm -rf \"$D\"' EXIT\nC=/shared/codebase/clean-architecture-backend-template\nN=$C/src/adapter/outbound/notification\nCORE=$C/src/application-core\njava -version 2>&1 | head -1\nCP=$(find \"$N/build/libs\" \"$CORE/build/libs\" -name \"*+21234e38cdb9.jar\" 2>/dev/null | tr '\\n' ':')\necho \"$CP\" | tr ':' '\\n' | grep -c jar | sed \"s/^/고정 리비전 jar : /\"\ncat > \"$D/AdminSequenceProbe.java\" <<'JAVA'\nimport dev.caskeleton.adapter.outbound.notification.platform.dispatch.ProviderAttemptLimiter;\nimport dev.caskeleton.adapter.outbound.notification.platform.dispatch.ProviderRuntimeRegistry;\nimport dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationHealthReporter;\nimport dev.caskeleton.adapter.outbound.notification.platform.observation.NotificationServingThresholds;\nimport dev.caskeleton.application.notification.platform.observation.NotificationServingState;\nimport dev.caskeleton.adapter.outbound.notification.platform.dispatch.ProviderRuntime;\nimport dev.caskeleton.application.notification.platform.api.ProviderId;\nimport dev.caskeleton.application.notification.platform.api.ProviderProfileId;\nimport dev.caskeleton.application.notification.platform.api.routing.Channel;\nimport dev.caskeleton.application.notification.platform.provider.NotificationProviderAdapter;\nimport dev.caskeleton.application.notification.platform.provider.ProviderCapabilities;\nimport dev.caskeleton.application.notification.platform.provider.ProviderProfileSnapshot;\nimport dev.caskeleton.application.notification.platform.provider.ProviderRuntimeState;\nimport dev.caskeleton.application.notification.platform.provider.ProviderSubmission;\nimport dev.caskeleton.application.notification.platform.provider.ProviderSubmissionResult;\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.time.ZoneOffset;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionStage;\n\npublic final class AdminSequenceProbe {\n\n private static final Clock CLOCK = Clock.fixed(Instant.parse(\"2026-01-01T00:00:00Z\"), ZoneOffset.UTC);\n\n private static ProviderRuntime runtime() {\n return new ProviderRuntime(\n new ProviderProfileSnapshot(\n new ProviderProfileId(\"apns-main\"),\n new ProviderId(\"apns\"),\n Channel.PUSH,\n \"PRODUCTION\",\n 1L,\n new ProviderCapabilities(\n false, false, false, false, false, false, false, true, 1, 4096L, Duration.ofHours(1)),\n Map.of(\"topic\", \"com.example.app\")),\n new StubAdapter(),\n new ProviderAttemptLimiter(1, 1_000_000, CLOCK));\n }\n\n private static NotificationHealthReporter reporter;\n\n private static void show(String step, ProviderRuntime runtime, String returned) {\n String indicator = reporter == null ? \"-\" : String.valueOf(reporter.snapshot().healthy());\n System.out.printf(\" %-34s 반환 %-6s 상태 %-22s 사유 %-19s 헬스 %s%n\",\n step, returned, runtime.health().state(), runtime.health().reason().orElse(\"(없음)\"), indicator);\n }\n\n private static NotificationHealthReporter reporterFor(ProviderRuntime runtime) {\n ProviderRuntimeRegistry registry = new ProviderRuntimeRegistry();\n registry.register(runtime);\n return new NotificationHealthReporter(\n registry,\n java.util.List.of(new ProviderProfileId(\"apns-main\")),\n () -> NotificationServingState.IDLE,\n Set.of(Channel.PUSH),\n NotificationServingThresholds.DEFAULT);\n }\n\n public static void main(String[] args) {\n System.out.println(\"[관리자 포트가 부르는 여섯 전이가 현재 상태를 읽는가]\");\n for (String[] row : new String[][] {\n {\"resumeHealthy\", \"읽는다 — AUTHENTICATION_FAILED 면 그대로 둔다\"},\n {\"markDegraded\", \"읽는다 — HEALTHY 일 때만 바꾼다\"},\n {\"markThrottled\", \"읽는다 — HEALTHY 일 때만 바꾼다\"},\n {\"markAuthenticationFailed\", \"set 으로 덮어쓴다\"},\n {\"markDraining\", \"읽지 않는다 — 무조건 덮어쓴다\"},\n {\"markDisabled\", \"읽지 않는다 — 무조건 덮어쓴다\"}}) {\n System.out.printf(\" %-26s %s%n\", row[0], row[1]);\n }\n System.out.println();\n\n System.out.println(\"[네 단계 시퀀스]\");\n ProviderRuntime runtime = runtime();\n reporter = reporterFor(runtime);\n show(\"0. 시작\", runtime, \"-\");\n runtime.markAuthenticationFailed(\"INVALID_CREDENTIAL\");\n show(\"1. 제공자가 자격증명 거부\", runtime, \"-\");\n show(\"2. 운영자가 HEALTHY 요청\", runtime, String.valueOf(runtime.resumeHealthy()));\n show(\"3. 운영자가 DRAINING 요청\", runtime, String.valueOf(runtime.markDraining()));\n show(\"4. 운영자가 다시 HEALTHY 요청\", runtime, String.valueOf(runtime.resumeHealthy()));\n System.out.println();\n\n System.out.println(\"[DISABLED 로도 같은지]\");\n ProviderRuntime other = runtime();\n reporter = reporterFor(other);\n other.markAuthenticationFailed(\"INVALID_CREDENTIAL\");\n show(\"3'. 운영자가 DISABLED 요청\", other, String.valueOf(other.markDisabled()));\n show(\"4'. 운영자가 HEALTHY 요청\", other, String.valueOf(other.resumeHealthy()));\n System.out.println();\n\n System.out.println(\"[비교] 가드가 있는 전이로는 통과하지 못한다\");\n ProviderRuntime guarded = runtime();\n reporter = reporterFor(guarded);\n guarded.markAuthenticationFailed(\"INVALID_CREDENTIAL\");\n show(\"3\\\". 운영자가 DEGRADED 요청\", guarded, String.valueOf(guarded.markDegraded(\"MAINTENANCE\")));\n show(\"4\\\". 운영자가 HEALTHY 요청\", guarded, String.valueOf(guarded.resumeHealthy()));\n }\n\n private static final class StubAdapter implements NotificationProviderAdapter {\n public ProviderId providerId() { return new ProviderId(\"apns\"); }\n public Set<Channel> channels() { return Set.of(Channel.PUSH); }\n public ProviderCapabilities capabilities() {\n return new ProviderCapabilities(\n false, false, false, false, false, false, false, true, 1, 4096L, Duration.ofHours(1));\n }\n public CompletionStage<ProviderSubmissionResult> submit(ProviderSubmission submission) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"not invoked\"));\n }\n }\n}\nJAVA\njavac -encoding UTF-8 -cp \"$CP\" -d \"$D\" \"$D/AdminSequenceProbe.java\"\njava -Dstdout.encoding=UTF-8 -cp \"$CP$D\" AdminSequenceProbe\n",
|
|
"cwd": "/shared/codebase/clean-architecture-backend-template",
|
|
"exitCode": 0,
|
|
"executedAt": "2026-09-02T14:53:15+00:00",
|
|
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
|
|
"raw": "evidence/raw/a13-f002-authentication-failed-resumehealthy-sequence.txt",
|
|
"svg": "evidence/rendered/a13-f002-authentication-failed-resumehealthy-sequence.svg",
|
|
"rawSha256": "35b11c278699e896b8e311b14cc20c978ff7a3696dca879fc15a57e04059e6ea",
|
|
"lines": 24,
|
|
"redaction": "none — 밀폐 탐침 실행 출력, 자격증명 없음"
|
|
}
|