Files
document-haness/docs/clean-architecture-backend-template/final/evidence/rendered/a-replay-store-with-no-eviction-path.svg
T
DongHyeonkaandClaude Opus 5 b2963105a8 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>
2026-09-04 22:51:59 +09:00

70 lines
16 KiB
XML

<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1712" viewBox="0 0 1200 1712" 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="1710" 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">$ R=grpc/grpc-policy/src/main/java/dev/caskeleton/grpc/idempotency/GrpcOutcomeReplay.java; D=grpc-advanced/grpc-advanced</tspan><tspan x="24" dy="22">-streaming/src/main/java/dev/caskeleton/grpc/advanced/streaming/GrpcClientMessageDeduplicator.java; S=grpc-advanced/grpc</tspan><tspan x="24" dy="22">-advanced-streaming/src/main/java/dev/caskeleton/grpc/advanced/streaming/GrpcClientStreamSessionId.java; echo &#x27;# 결과 재</tspan><tspan x="24" dy="22">생 저장소 — 항목 하나의 크기만 제한한다&#x27;; sed -n &#x27;18,19p;41,49p&#x27; $R; echo -n &#x27;# remove / clear / evict / expire 매치:</tspan><tspan x="24" dy="22"> &#x27;; grep -c &#x27;remove\|clear\|evict\|expire&#x27; $R; echo &#x27;# 개수를 볼 수 있는 유일한 창구&#x27;; sed -n &#x27;59,61p&#x27; $R; echo -n &#x27;# </tspan><tspan x="24" dy="22"> 그 값을 읽는 main 코드: &#x27;; grep -rn &#x27;replay.size()\|Replay.size()&#x27; --include=*.java . | grep -c &#x27;/main/&#x27;; echo -n &#x27;# </tspan><tspan x="24" dy="22"> 이 클래스를 잡는 main 코드: &#x27;; grep -rn &#x27;GrpcOutcomeReplay&#x27; --include=*.java . | grep &#x27;/main/&#x27; | grep -vc &#x27;GrpcOutcomeR</tspan><tspan x="24" dy="22">eplay.java&#x27;; echo &#x27;# javadoc 이 이 저장소를 부르는 이름&#x27;; sed -n &#x27;13,14p&#x27; $R; echo; echo &#x27;# 형제 모듈의 중복 제거기 —</tspan><tspan x="24" dy="22"> 같은 형태를 자기 javadoc 이 비판한다&#x27;; sed -n &#x27;11,17p&#x27; $D; echo &#x27;# 비판을 지키는 쪽: 세션당 항목 하나&#x27;; sed -n &#x27;21,23</tspan><tspan x="24" dy="22">p&#x27; $D; echo &#x27;# 지키지 않는 쪽: 적용된 메시지마다 한 항목&#x27;; sed -n &#x27;74,77p&#x27; $D; echo &#x27;# 재생 판정이 실제로 보는 것&#x27;; </tspan><tspan x="24" dy="22">grep -n &#x27;alreadyApplied(message.sequence())&#x27; $D; echo -n &#x27;# 이 파일의 remove / clear / removeIf 매치: &#x27;; grep -c &#x27;remo</tspan><tspan x="24" dy="22">ve\|clear\|evict\|expire&#x27; $D; echo &#x27;# 그 둘이 있는 자리&#x27;; sed -n &#x27;109,113p&#x27; $D; echo &#x27;# 그 접두사가 실제로 무엇인가&#x27;</tspan><tspan x="24" dy="22">; sed -n &#x27;40p;44p&#x27; $S; echo; echo -n &#x27;# 두 모듈의 런타임 소속: &#x27;; python3 -c &quot;import json;m=json.load(open(&#x27;config/archi</tspan><tspan x="24" dy="22">tecture/modules.json&#x27;));ls=[x for x in m.get(&#x27;modules&#x27;,m) if isinstance(x,dict)];print(&#x27; &#x27;.join(f\&quot;{x[&#x27;id&#x27;]}={x.get(&#x27;run</tspan><tspan x="24" dy="22">time_memberships&#x27;)}\&quot; for x in ls if x.get(&#x27;id&#x27;,&#x27;&#x27;).endswith(&#x27;grpc-policy&#x27;) or x.get(&#x27;id&#x27;,&#x27;&#x27;).endswith(&#x27;grpc-advanced-st</tspan><tspan x="24" dy="22">reaming&#x27;)))&quot;</tspan></text>
<text x="24" y="420" 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/src</tspan></text>
<text x="24" y="442" 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-02T01:58:32+00:00 · exit 0</tspan></text>
<line x1="24" y1="456" x2="1176" y2="456" stroke="#30363d"/>
<text x="24" y="496" 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="518" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> private final ConcurrentMap&lt;String, byte[]&gt; storedOutcomes = new ConcurrentHashMap&lt;&gt;();</tspan></text>
<text x="24" y="540" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> private final int maxInlineBytes;</tspan></text>
<text x="24" y="562" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> if (serializedResponse.length &gt; maxInlineBytes) {</tspan></text>
<text x="24" y="584" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> throw new IllegalArgumentException(</tspan></text>
<text x="24" y="606" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> &quot;response of &quot;</tspan></text>
<text x="24" y="628" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> + serializedResponse.length</tspan></text>
<text x="24" y="650" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> + &quot; bytes exceeds the inline outcome limit of &quot;</tspan></text>
<text x="24" y="672" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> + maxInlineBytes</tspan></text>
<text x="24" y="694" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> + &quot;; store it behind an object reference instead&quot;);</tspan></text>
<text x="24" y="716" 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="738" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> storedOutcomes.put(outcomeReference, serializedResponse.clone());</tspan></text>
<text x="24" y="760" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"># remove / clear / evict / expire 매치: 0</tspan></text>
<text x="24" y="782" 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="804" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> public int size() {</tspan></text>
<text x="24" y="826" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> return storedOutcomes.size();</tspan></text>
<text x="24" y="848" 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="870" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"># 그 값을 읽는 main 코드: 0</tspan></text>
<text x="24" y="892" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"># 이 클래스를 잡는 main 코드: 0</tspan></text>
<text x="24" y="914" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"># javadoc 이 이 저장소를 부르는 이름</tspan></text>
<text x="24" y="936" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> * against whatever the deployment chose — a small inline store, an object store, or a re-read of</tspan></text>
<text x="24" y="958" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> * the committed resource.</tspan></text>
<text x="24" y="980" 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="1002" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"># 형제 모듈의 중복 제거기 — 같은 형태를 자기 javadoc 이 비판한다</tspan></text>
<text x="24" y="1024" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> * &lt;p&gt;Checkpoint-based rather than a set of seen keys. A set grows without bound for the life of a</tspan></text>
<text x="24" y="1046" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> * session and answers &quot;have I seen this&quot; — which is not quite the question. The question is &quot;has</tspan></text>
<text x="24" y="1068" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> * this been applied&quot;, and a monotonic applied-sequence answers it in constant space and survives</tspan></text>
<text x="24" y="1090" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> * the process restart that a set does not.</tspan></text>
<text x="24" y="1112" 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="1134" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> * &lt;p&gt;Replayed outcomes are kept for the small window after the checkpoint, so a duplicate that</tspan></text>
<text x="24" y="1156" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> * arrives before the checkpoint advances gets the original answer rather than being reapplied.</tspan></text>
<text x="24" y="1178" 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="1200" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> private final ConcurrentMap&lt;String, GrpcClientStreamCheckpoint&gt; checkpoints =</tspan></text>
<text x="24" y="1222" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> new ConcurrentHashMap&lt;&gt;();</tspan></text>
<text x="24" y="1244" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> private final ConcurrentMap&lt;String, String&gt; replayableOutcomes = new ConcurrentHashMap&lt;&gt;();</tspan></text>
<text x="24" y="1266" 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="1288" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> if (outcomeReference != null &amp;&amp; !outcomeReference.isBlank()) {</tspan></text>
<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"> replayableOutcomes.put(message.dedupKey(), outcomeReference);</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"> }</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"># 재생 판정이 실제로 보는 것</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">54: if (checkpoint.alreadyApplied(message.sequence())) {</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"># 이 파일의 remove / clear / removeIf 매치: 2</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"># 그 둘이 있는 자리</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"> /** Forgets a finished session. */</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"> public void endSession(GrpcClientStreamSessionId sessionId) {</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"> checkpoints.remove(sessionId.value());</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"> replayableOutcomes.keySet().removeIf(key -&gt; key.startsWith(sessionId.value() + &quot;|&quot;));</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"> }</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"># 그 접두사가 실제로 무엇인가</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"> public String dedupKey(long sequence) {</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"> return value + &quot;|&quot; + generation + &quot;|&quot; + sequence;</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"></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"># 두 모듈의 런타임 소속: grpc-policy=[] grpc-advanced-streaming=[]</tspan></text>
</svg>