Files
document-haness/docs/clean-architecture-backend-template/final/evidence/rendered/a11-f006-boundeddatabufferflux-release.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

189 lines
15 KiB
XML

<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1822" viewBox="0 0 1200 1822" 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="1820" 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 &#x27;rm -rf &quot;$D&quot;&#x27; EXIT
H=/shared/codebase/clean-architecture-backend-template/</tspan><tspan x="24" dy="22">src/adapter/outbound/httpclient
JAR=$H/build/libs/httpclient-0.0.1+21234e38cdb9.jar
CACHE=/root/.gradle/caches/modules-2</tspan><tspan x="24" dy="22">/files-2.1
cp=&quot;&quot;
while IFS= read -r line; do
coord=${line%%=*}; confs=${line#*=}
case &quot;,$confs,&quot; in *&quot;,runtimeClassp</tspan><tspan x="24" dy="22">ath,&quot;*) ;; *) continue ;; esac
group=${coord%%:*}; rest=${coord#*:}; name=${rest%%:*}; ver=${rest##*:}
jar=$(find &quot;$</tspan><tspan x="24" dy="22">CACHE/$group/$name/$ver&quot; -name &#x27;*.jar&#x27; ! -name &#x27;*sources*&#x27; ! -name &#x27;*javadoc*&#x27; 2&gt;/dev/null | head -1)
[ -n &quot;$jar&quot; ] &amp;&amp;</tspan><tspan x="24" dy="22"> cp=&quot;$cp:$jar&quot;
done &lt; &lt;(grep -E &#x27;^[a-zA-Z0-9._-]+:[^=]+=&#x27; &quot;$H/gradle.lockfile&quot;)
CP=&quot;$JAR${cp}&quot;
java -version 2&gt;&amp;1 | head</tspan><tspan x="24" dy="22"> -1
cat &gt; &quot;$D/BufferReleaseProbe.java&quot; &lt;&lt;&#x27;JAVA&#x27;
import dev.caskeleton.adapter.outbound.httpclient.restclient.ResponseSiz</tspan><tspan x="24" dy="22">eLimiter;
import dev.caskeleton.adapter.outbound.httpclient.webclient.BoundedDataBufferFlux;
import dev.caskeleton.adapt</tspan><tspan x="24" dy="22">er.outbound.httpclient.webclient.FirstByteDeliveryGuard;
import io.netty.buffer.UnpooledByteBufAllocator;
import java.ut</tspan><tspan x="24" dy="22">il.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.springframework.core.io.buffer.DataB</tspan><tspan x="24" dy="22">uffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBuf</tspan><tspan x="24" dy="22">fer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import reactor.core.publisher.Flux;
import reacto</tspan><tspan x="24" dy="22">r.core.publisher.Mono;
public final class BufferReleaseProbe {
private static final NettyDataBufferFactory FACTORY =</tspan><tspan x="24" dy="22">
new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
private static final ResponseSizeLimiter LIMIT</tspan><tspan x="24" dy="22">ER = new ResponseSizeLimiter(1_000_000L, 1_000_000L);
private static final FirstByteDeliveryGuard GUARD = new FirstByt</tspan><tspan x="24" dy="22">eDeliveryGuard();
private static List&lt;NettyDataBuffer&gt; buffers(int count) {
List&lt;NettyDataBuffer&gt; made = new Arra</tspan><tspan x="24" dy="22">yList&lt;&gt;(count);
for (int i = 0; i &lt; count; i++) {
made.add((NettyDataBuffer) FACTORY.allocateBuffer(16).write(</tspan><tspan x="24" dy="22">new byte[] {(byte) i}));
}
return made;
}
private static long held(List&lt;NettyDataBuffer&gt; made) {
return</tspan><tspan x="24" dy="22"> made.stream().filter(b -&gt; b.getNativeBuffer().refCnt() &gt; 0).count();
}
/** The chain the class assembles, minus it</tspan><tspan x="24" dy="22">s own doOnDiscard. */
private static Flux&lt;DataBuffer&gt; withoutDiscard(Flux&lt;DataBuffer&gt; source) {
return source.doOn</tspan><tspan x="24" dy="22">Next(
buffer -&gt; {
LIMITER.recordWireBytes(buffer.readableByteCount());
GUARD.markDelivered()</tspan><tspan x="24" dy="22">;
});
}
/** The chain the class assembles, minus the two operators under question. */
private static Flux</tspan><tspan x="24" dy="22">&lt;DataBuffer&gt; withoutTheTwo(Flux&lt;DataBuffer&gt; source) {
return withoutDiscard(source).doOnDiscard(DataBuffer.class, Da</tspan><tspan x="24" dy="22">taBufferUtils::release);
}
/** What ReactiveStreamingGateway:102-105 actually composes around the call. */
privat</tspan><tspan x="24" dy="22">e static Flux&lt;DataBuffer&gt; asCalled(
Flux&lt;DataBuffer&gt; source, Function&lt;Flux&lt;DataBuffer&gt;, Flux&lt;DataBuffer&gt;&gt; inner) {</tspan><tspan x="24" dy="22">
return Mono.just(source)
.flatMapMany(inner)
.doOnDiscard(DataBuffer.class, DataBufferUtils::releas</tspan><tspan x="24" dy="22">e);
}
private static void run(String label, Flux&lt;DataBuffer&gt; chain, List&lt;NettyDataBuffer&gt; made, int take) {
Lis</tspan><tspan x="24" dy="22">t&lt;DataBuffer&gt; delivered = new ArrayList&lt;&gt;();
try {
(take &gt; 0 ? chain.take(take) : chain).doOnNext(delivered::a</tspan><tspan x="24" dy="22">dd).blockLast();
} catch (RuntimeException expected) {
// An error scenario ends here by design.
}
lon</tspan><tspan x="24" dy="22">g beforeOwnRelease = held(made);
delivered.forEach(DataBufferUtils::release);
System.out.printf(
&quot; %-46</tspan><tspan x="24" dy="22">s 전달 %d, 상류가 쥐고 있던 것 중 미회수 %d / %d%n&quot;,
label, delivered.size(), beforeOwnRelease - delivered.size(</tspan><tspan x="24" dy="22">), made.size() - delivered.size());
}
public static void main(String[] args) {
System.out.println(&quot;[취소] bound</tspan><tspan x="24" dy="22"> 만 구독한다 — 다섯 중 하나만 받고 취소&quot;);
for (String[] variant : new String[][] {{&quot;있는 그대로&quot;, &quot;shipped&quot;}, {&quot;</tspan><tspan x="24" dy="22"> 연산자 제거&quot;, &quot;two&quot;}, {&quot;doOnDiscard 까지 제거&quot;, &quot;none&quot;}}) {
List&lt;NettyDataBuffer&gt; made = buffers(5);
Flux&lt;D</tspan><tspan x="24" dy="22">ataBuffer&gt; source = Flux.fromIterable(new ArrayList&lt;DataBuffer&gt;(made));
run(variant[0], chain(variant[1], source),</tspan><tspan x="24" dy="22"> made, 1);
}
System.out.println();
System.out.println(&quot;[취소] 부르는 쪽이 105 에서 같은 폐기 연산자를 한 번</tspan><tspan x="24" dy="22"> 더 건다&quot;);
for (String[] variant : new String[][] {{&quot;있는 그대로&quot;, &quot;shipped&quot;}, {&quot;doOnDiscard 까지 제거&quot;, &quot;none&quot;}}) </tspan><tspan x="24" dy="22">{
List&lt;NettyDataBuffer&gt; made = buffers(5);
Flux&lt;DataBuffer&gt; source = Flux.fromIterable(new ArrayList&lt;DataBuf</tspan><tspan x="24" dy="22">fer&gt;(made));
run(variant[0], asCalled(source, s -&gt; chain(variant[1], s)), made, 1);
}
System.out.println()</tspan><tspan x="24" dy="22">;
System.out.println(&quot;[오류] 상류가 셋을 쥔 채 던진다&quot;);
for (String[] variant : new String[][] {{&quot;있는 그대로&quot;</tspan><tspan x="24" dy="22">, &quot;shipped&quot;}, {&quot;doOnDiscard 까지 제거&quot;, &quot;none&quot;}}) {
List&lt;NettyDataBuffer&gt; made = buffers(5);
List&lt;DataBuffer</tspan><tspan x="24" dy="22">&gt; firstTwo = new ArrayList&lt;&gt;(made.subList(0, 2));
Flux&lt;DataBuffer&gt; source =
Flux.concat(Flux.fromIterabl</tspan><tspan x="24" dy="22">e(firstTwo), Flux.error(new IllegalStateException(&quot;upstream failed&quot;)));
run(variant[0], chain(variant[1], source),</tspan><tspan x="24" dy="22"> made, 0);
}
System.out.println();
System.out.println(&quot;[취소] 상류가 폐기 규약을 지키지 않는다&quot;);
for (</tspan><tspan x="24" dy="22">String[] variant : new String[][] {{&quot;있는 그대로&quot;, &quot;shipped&quot;}, {&quot;doOnDiscard 까지 제거&quot;, &quot;none&quot;}}) {
List&lt;NettyDat</tspan><tspan x="24" dy="22">aBuffer&gt; made = buffers(5);
Flux&lt;DataBuffer&gt; source = Flux.from(nonParticipating(made));
run(variant[0], cha</tspan><tspan x="24" dy="22">in(variant[1], source), made, 1);
}
}
/** A publisher that pushes everything and does nothing on cancel — no d</tspan><tspan x="24" dy="22">iscard protocol. */
private static org.reactivestreams.Publisher&lt;DataBuffer&gt; nonParticipating(
List&lt;NettyDataBuf</tspan><tspan x="24" dy="22">fer&gt; made) {
return subscriber -&gt;
subscriber.onSubscribe(
new org.reactivestreams.Subscription()</tspan><tspan x="24" dy="22"> {
private int index;
@Override
public void request(long n) {
</tspan><tspan x="24" dy="22"> while (index &lt; made.size()) {
subscriber.onNext(made.get(index++));
}
</tspan><tspan x="24" dy="22"> subscriber.onComplete();
}
@Override
public void cancel() {
</tspan><tspan x="24" dy="22">// Nothing. The remaining buffers are simply dropped.
}
});
}
private static Flux&lt;DataBuf</tspan><tspan x="24" dy="22">fer&gt; chain(String variant, Flux&lt;DataBuffer&gt; source) {
return switch (variant) {
case &quot;shipped&quot; -&gt; BoundedDataB</tspan><tspan x="24" dy="22">ufferFlux.bound(source, LIMITER, GUARD);
case &quot;two&quot; -&gt; withoutTheTwo(source);
default -&gt; withoutDiscard(sour</tspan><tspan x="24" dy="22">ce);
};
}
}
JAVA
javac -encoding UTF-8 -cp &quot;$CP&quot; -d &quot;$D&quot; &quot;$D/BufferReleaseProbe.java&quot;
# slf4j 는 제공자가 없다고만</tspan><tspan x="24" dy="22"> 알린다. 측정과 무관하므로 그 줄만 걸러 낸다.
java -Dstdout.encoding=UTF-8 -cp &quot;$CP:$D&quot; BufferReleaseProbe 2&gt;&amp;1 | grep -</tspan><tspan x="24" dy="22">v &quot;^SLF4J(W):&quot;
</tspan></text>
<text x="24" y="1344" 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/adapter/outbound/httpclient</tspan></text>
<text x="24" y="1366" 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-02T13:51:07+00:00 · exit 0</tspan></text>
<line x1="24" y1="1380" x2="1176" y2="1380" stroke="#30363d"/>
<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">openjdk version &quot;21.0.12&quot; 2026-07-21</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">[취소] bound 만 구독한다 — 다섯 중 하나만 받고 취소</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"> 있는 그대로 전달 1, 상류가 쥐고 있던 것 중 미회수 0 / 4</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"> 두 연산자 제거 전달 1, 상류가 쥐고 있던 것 중 미회수 0 / 4</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"> doOnDiscard 까지 제거 전달 1, 상류가 쥐고 있던 것 중 미회수 4 / 4</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">[취소] 부르는 쪽이 105 에서 같은 폐기 연산자를 한 번 더 건다</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, 상류가 쥐고 있던 것 중 미회수 0 / 4</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"> doOnDiscard 까지 제거 전달 1, 상류가 쥐고 있던 것 중 미회수 0 / 4</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"></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"> 있는 그대로 전달 2, 상류가 쥐고 있던 것 중 미회수 3 / 3</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"> doOnDiscard 까지 제거 전달 2, 상류가 쥐고 있던 것 중 미회수 3 / 3</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"></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">[취소] 상류가 폐기 규약을 지키지 않는다</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"> 있는 그대로 전달 1, 상류가 쥐고 있던 것 중 미회수 4 / 4</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"> doOnDiscard 까지 제거 전달 1, 상류가 쥐고 있던 것 중 미회수 4 / 4</tspan></text>
</svg>