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>
189 lines
15 KiB
XML
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 'rm -rf "$D"' 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=""
|
|
while IFS= read -r line; do
|
|
coord=${line%%=*}; confs=${line#*=}
|
|
case ",$confs," in *",runtimeClassp</tspan><tspan x="24" dy="22">ath,"*) ;; *) continue ;; esac
|
|
group=${coord%%:*}; rest=${coord#*:}; name=${rest%%:*}; ver=${rest##*:}
|
|
jar=$(find "$</tspan><tspan x="24" dy="22">CACHE/$group/$name/$ver" -name '*.jar' ! -name '*sources*' ! -name '*javadoc*' 2>/dev/null | head -1)
|
|
[ -n "$jar" ] &&</tspan><tspan x="24" dy="22"> cp="$cp:$jar"
|
|
done < <(grep -E '^[a-zA-Z0-9._-]+:[^=]+=' "$H/gradle.lockfile")
|
|
CP="$JAR${cp}"
|
|
java -version 2>&1 | head</tspan><tspan x="24" dy="22"> -1
|
|
cat > "$D/BufferReleaseProbe.java" <<'JAVA'
|
|
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<NettyDataBuffer> buffers(int count) {
|
|
List<NettyDataBuffer> made = new Arra</tspan><tspan x="24" dy="22">yList<>(count);
|
|
for (int i = 0; i < 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<NettyDataBuffer> made) {
|
|
return</tspan><tspan x="24" dy="22"> made.stream().filter(b -> b.getNativeBuffer().refCnt() > 0).count();
|
|
}
|
|
|
|
/** The chain the class assembles, minus it</tspan><tspan x="24" dy="22">s own doOnDiscard. */
|
|
private static Flux<DataBuffer> withoutDiscard(Flux<DataBuffer> source) {
|
|
return source.doOn</tspan><tspan x="24" dy="22">Next(
|
|
buffer -> {
|
|
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"><DataBuffer> withoutTheTwo(Flux<DataBuffer> 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<DataBuffer> asCalled(
|
|
Flux<DataBuffer> source, Function<Flux<DataBuffer>, Flux<DataBuffer>> 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<DataBuffer> chain, List<NettyDataBuffer> made, int take) {
|
|
Lis</tspan><tspan x="24" dy="22">t<DataBuffer> delivered = new ArrayList<>();
|
|
try {
|
|
(take > 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(
|
|
" %-46</tspan><tspan x="24" dy="22">s 전달 %d, 상류가 쥐고 있던 것 중 미회수 %d / %d%n",
|
|
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("[취소] bound</tspan><tspan x="24" dy="22"> 만 구독한다 — 다섯 중 하나만 받고 취소");
|
|
for (String[] variant : new String[][] {{"있는 그대로", "shipped"}, {"두</tspan><tspan x="24" dy="22"> 연산자 제거", "two"}, {"doOnDiscard 까지 제거", "none"}}) {
|
|
List<NettyDataBuffer> made = buffers(5);
|
|
Flux<D</tspan><tspan x="24" dy="22">ataBuffer> source = Flux.fromIterable(new ArrayList<DataBuffer>(made));
|
|
run(variant[0], chain(variant[1], source),</tspan><tspan x="24" dy="22"> made, 1);
|
|
}
|
|
System.out.println();
|
|
|
|
System.out.println("[취소] 부르는 쪽이 105 에서 같은 폐기 연산자를 한 번</tspan><tspan x="24" dy="22"> 더 건다");
|
|
for (String[] variant : new String[][] {{"있는 그대로", "shipped"}, {"doOnDiscard 까지 제거", "none"}}) </tspan><tspan x="24" dy="22">{
|
|
List<NettyDataBuffer> made = buffers(5);
|
|
Flux<DataBuffer> source = Flux.fromIterable(new ArrayList<DataBuf</tspan><tspan x="24" dy="22">fer>(made));
|
|
run(variant[0], asCalled(source, s -> chain(variant[1], s)), made, 1);
|
|
}
|
|
System.out.println()</tspan><tspan x="24" dy="22">;
|
|
|
|
System.out.println("[오류] 상류가 셋을 쥔 채 던진다");
|
|
for (String[] variant : new String[][] {{"있는 그대로"</tspan><tspan x="24" dy="22">, "shipped"}, {"doOnDiscard 까지 제거", "none"}}) {
|
|
List<NettyDataBuffer> made = buffers(5);
|
|
List<DataBuffer</tspan><tspan x="24" dy="22">> firstTwo = new ArrayList<>(made.subList(0, 2));
|
|
Flux<DataBuffer> source =
|
|
Flux.concat(Flux.fromIterabl</tspan><tspan x="24" dy="22">e(firstTwo), Flux.error(new IllegalStateException("upstream failed")));
|
|
run(variant[0], chain(variant[1], source),</tspan><tspan x="24" dy="22"> made, 0);
|
|
}
|
|
System.out.println();
|
|
|
|
System.out.println("[취소] 상류가 폐기 규약을 지키지 않는다");
|
|
for (</tspan><tspan x="24" dy="22">String[] variant : new String[][] {{"있는 그대로", "shipped"}, {"doOnDiscard 까지 제거", "none"}}) {
|
|
List<NettyDat</tspan><tspan x="24" dy="22">aBuffer> made = buffers(5);
|
|
Flux<DataBuffer> 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<DataBuffer> nonParticipating(
|
|
List<NettyDataBuf</tspan><tspan x="24" dy="22">fer> made) {
|
|
return subscriber ->
|
|
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 < 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<DataBuf</tspan><tspan x="24" dy="22">fer> chain(String variant, Flux<DataBuffer> source) {
|
|
return switch (variant) {
|
|
case "shipped" -> BoundedDataB</tspan><tspan x="24" dy="22">ufferFlux.bound(source, LIMITER, GUARD);
|
|
case "two" -> withoutTheTwo(source);
|
|
default -> withoutDiscard(sour</tspan><tspan x="24" dy="22">ce);
|
|
};
|
|
}
|
|
}
|
|
JAVA
|
|
javac -encoding UTF-8 -cp "$CP" -d "$D" "$D/BufferReleaseProbe.java"
|
|
# slf4j 는 제공자가 없다고만</tspan><tspan x="24" dy="22"> 알린다. 측정과 무관하므로 그 줄만 걸러 낸다.
|
|
java -Dstdout.encoding=UTF-8 -cp "$CP:$D" BufferReleaseProbe 2>&1 | grep -</tspan><tspan x="24" dy="22">v "^SLF4J(W):"
|
|
</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 "21.0.12" 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>
|