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

15 lines
7.7 KiB
JSON

{
"assetKey": "a11-f006-boundeddatabufferflux-release",
"kind": "terminal",
"command": "set -e\nset -o pipefail\nD=$(mktemp -d); trap 'rm -rf \"$D\"' EXIT\nH=/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/httpclient\nJAR=$H/build/libs/httpclient-0.0.1+21234e38cdb9.jar\nCACHE=/root/.gradle/caches/modules-2/files-2.1\ncp=\"\"\nwhile IFS= read -r line; do\n coord=${line%%=*}; confs=${line#*=}\n case \",$confs,\" in *\",runtimeClasspath,\"*) ;; *) continue ;; esac\n group=${coord%%:*}; rest=${coord#*:}; name=${rest%%:*}; ver=${rest##*:}\n jar=$(find \"$CACHE/$group/$name/$ver\" -name '*.jar' ! -name '*sources*' ! -name '*javadoc*' 2>/dev/null | head -1)\n [ -n \"$jar\" ] && cp=\"$cp:$jar\"\ndone < <(grep -E '^[a-zA-Z0-9._-]+:[^=]+=' \"$H/gradle.lockfile\")\nCP=\"$JAR${cp}\"\njava -version 2>&1 | head -1\ncat > \"$D/BufferReleaseProbe.java\" <<'JAVA'\nimport dev.caskeleton.adapter.outbound.httpclient.restclient.ResponseSizeLimiter;\nimport dev.caskeleton.adapter.outbound.httpclient.webclient.BoundedDataBufferFlux;\nimport dev.caskeleton.adapter.outbound.httpclient.webclient.FirstByteDeliveryGuard;\nimport io.netty.buffer.UnpooledByteBufAllocator;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.function.Function;\nimport org.springframework.core.io.buffer.DataBuffer;\nimport org.springframework.core.io.buffer.DataBufferUtils;\nimport org.springframework.core.io.buffer.NettyDataBuffer;\nimport org.springframework.core.io.buffer.NettyDataBufferFactory;\nimport reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\npublic final class BufferReleaseProbe {\n\n private static final NettyDataBufferFactory FACTORY =\n new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));\n private static final ResponseSizeLimiter LIMITER = new ResponseSizeLimiter(1_000_000L, 1_000_000L);\n private static final FirstByteDeliveryGuard GUARD = new FirstByteDeliveryGuard();\n\n private static List<NettyDataBuffer> buffers(int count) {\n List<NettyDataBuffer> made = new ArrayList<>(count);\n for (int i = 0; i < count; i++) {\n made.add((NettyDataBuffer) FACTORY.allocateBuffer(16).write(new byte[] {(byte) i}));\n }\n return made;\n }\n\n private static long held(List<NettyDataBuffer> made) {\n return made.stream().filter(b -> b.getNativeBuffer().refCnt() > 0).count();\n }\n\n /** The chain the class assembles, minus its own doOnDiscard. */\n private static Flux<DataBuffer> withoutDiscard(Flux<DataBuffer> source) {\n return source.doOnNext(\n buffer -> {\n LIMITER.recordWireBytes(buffer.readableByteCount());\n GUARD.markDelivered();\n });\n }\n\n /** The chain the class assembles, minus the two operators under question. */\n private static Flux<DataBuffer> withoutTheTwo(Flux<DataBuffer> source) {\n return withoutDiscard(source).doOnDiscard(DataBuffer.class, DataBufferUtils::release);\n }\n\n /** What ReactiveStreamingGateway:102-105 actually composes around the call. */\n private static Flux<DataBuffer> asCalled(\n Flux<DataBuffer> source, Function<Flux<DataBuffer>, Flux<DataBuffer>> inner) {\n return Mono.just(source)\n .flatMapMany(inner)\n .doOnDiscard(DataBuffer.class, DataBufferUtils::release);\n }\n\n private static void run(String label, Flux<DataBuffer> chain, List<NettyDataBuffer> made, int take) {\n List<DataBuffer> delivered = new ArrayList<>();\n try {\n (take > 0 ? chain.take(take) : chain).doOnNext(delivered::add).blockLast();\n } catch (RuntimeException expected) {\n // An error scenario ends here by design.\n }\n long beforeOwnRelease = held(made);\n delivered.forEach(DataBufferUtils::release);\n System.out.printf(\n \" %-46s 전달 %d, 상류가 쥐고 있던 것 중 미회수 %d / %d%n\",\n label, delivered.size(), beforeOwnRelease - delivered.size(), made.size() - delivered.size());\n }\n\n public static void main(String[] args) {\n System.out.println(\"[취소] bound 만 구독한다 — 다섯 중 하나만 받고 취소\");\n for (String[] variant : new String[][] {{\"있는 그대로\", \"shipped\"}, {\"두 연산자 제거\", \"two\"}, {\"doOnDiscard 까지 제거\", \"none\"}}) {\n List<NettyDataBuffer> made = buffers(5);\n Flux<DataBuffer> source = Flux.fromIterable(new ArrayList<DataBuffer>(made));\n run(variant[0], chain(variant[1], source), made, 1);\n }\n System.out.println();\n\n System.out.println(\"[취소] 부르는 쪽이 105 에서 같은 폐기 연산자를 한 번 더 건다\");\n for (String[] variant : new String[][] {{\"있는 그대로\", \"shipped\"}, {\"doOnDiscard 까지 제거\", \"none\"}}) {\n List<NettyDataBuffer> made = buffers(5);\n Flux<DataBuffer> source = Flux.fromIterable(new ArrayList<DataBuffer>(made));\n run(variant[0], asCalled(source, s -> chain(variant[1], s)), made, 1);\n }\n System.out.println();\n\n System.out.println(\"[오류] 상류가 셋을 쥔 채 던진다\");\n for (String[] variant : new String[][] {{\"있는 그대로\", \"shipped\"}, {\"doOnDiscard 까지 제거\", \"none\"}}) {\n List<NettyDataBuffer> made = buffers(5);\n List<DataBuffer> firstTwo = new ArrayList<>(made.subList(0, 2));\n Flux<DataBuffer> source =\n Flux.concat(Flux.fromIterable(firstTwo), Flux.error(new IllegalStateException(\"upstream failed\")));\n run(variant[0], chain(variant[1], source), made, 0);\n }\n System.out.println();\n\n System.out.println(\"[취소] 상류가 폐기 규약을 지키지 않는다\");\n for (String[] variant : new String[][] {{\"있는 그대로\", \"shipped\"}, {\"doOnDiscard 까지 제거\", \"none\"}}) {\n List<NettyDataBuffer> made = buffers(5);\n Flux<DataBuffer> source = Flux.from(nonParticipating(made));\n run(variant[0], chain(variant[1], source), made, 1);\n }\n }\n\n /** A publisher that pushes everything and does nothing on cancel — no discard protocol. */\n private static org.reactivestreams.Publisher<DataBuffer> nonParticipating(\n List<NettyDataBuffer> made) {\n return subscriber ->\n subscriber.onSubscribe(\n new org.reactivestreams.Subscription() {\n private int index;\n\n @Override\n public void request(long n) {\n while (index < made.size()) {\n subscriber.onNext(made.get(index++));\n }\n subscriber.onComplete();\n }\n\n @Override\n public void cancel() {\n // Nothing. The remaining buffers are simply dropped.\n }\n });\n }\n\n private static Flux<DataBuffer> chain(String variant, Flux<DataBuffer> source) {\n return switch (variant) {\n case \"shipped\" -> BoundedDataBufferFlux.bound(source, LIMITER, GUARD);\n case \"two\" -> withoutTheTwo(source);\n default -> withoutDiscard(source);\n };\n }\n}\nJAVA\njavac -encoding UTF-8 -cp \"$CP\" -d \"$D\" \"$D/BufferReleaseProbe.java\"\n# slf4j 는 제공자가 없다고만 알린다. 측정과 무관하므로 그 줄만 걸러 낸다.\njava -Dstdout.encoding=UTF-8 -cp \"$CP:$D\" BufferReleaseProbe 2>&1 | grep -v \"^SLF4J(W):\"\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/httpclient",
"exitCode": 0,
"executedAt": "2026-09-02T13:51:07+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a11-f006-boundeddatabufferflux-release.txt",
"svg": "evidence/rendered/a11-f006-boundeddatabufferflux-release.svg",
"rawSha256": "a63d1101f05224fee8fcbc6eb01c59b47ef486dc1343c9e2eb565434944de61a",
"lines": 17,
"redaction": "none — 밀폐 탐침 실행 출력, 자격증명 없음"
}