{ "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 buffers(int count) {\n List 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 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 withoutDiscard(Flux 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 withoutTheTwo(Flux 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 asCalled(\n Flux source, Function, Flux> 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 chain, List made, int take) {\n List 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 made = buffers(5);\n Flux source = Flux.fromIterable(new ArrayList(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 made = buffers(5);\n Flux source = Flux.fromIterable(new ArrayList(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 made = buffers(5);\n List firstTwo = new ArrayList<>(made.subList(0, 2));\n Flux 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 made = buffers(5);\n Flux 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 nonParticipating(\n List 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 chain(String variant, Flux 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 — 밀폐 탐침 실행 출력, 자격증명 없음" }