Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/a20-f007-grpcstreamadmission.txt
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

113 lines
7.6 KiB
Plaintext

# 승인과 해제의 본문
GrpcStreamAdmission.java:38 public boolean tryAdmit(String callerFingerprint) {
GrpcStreamAdmission.java:39 if (callerFingerprint == null || callerFingerprint.isBlank()) {
GrpcStreamAdmission.java:40 throw new IllegalArgumentException("stream admission needs a caller fingerprint");
GrpcStreamAdmission.java:41 }
GrpcStreamAdmission.java:42 AtomicInteger callerCount =
GrpcStreamAdmission.java:43 perCaller.computeIfAbsent(callerFingerprint, key -> new AtomicInteger());
GrpcStreamAdmission.java:44 if (callerCount.get() >= maxStreamsPerCaller) {
GrpcStreamAdmission.java:45 return false;
GrpcStreamAdmission.java:46 }
GrpcStreamAdmission.java:47 if (openStreams.get() >= maxConcurrentStreams) {
GrpcStreamAdmission.java:48 return false;
GrpcStreamAdmission.java:49 }
GrpcStreamAdmission.java:50 callerCount.incrementAndGet();
GrpcStreamAdmission.java:51 openStreams.incrementAndGet();
GrpcStreamAdmission.java:52 return true;
GrpcStreamAdmission.java:53 }
GrpcStreamAdmission.java:54
GrpcStreamAdmission.java:55 /** Releases a stream's slot. */
GrpcStreamAdmission.java:56 public void release(String callerFingerprint) {
GrpcStreamAdmission.java:57 AtomicInteger callerCount = perCaller.get(callerFingerprint);
GrpcStreamAdmission.java:58 if (callerCount != null && callerCount.get() > 0) {
GrpcStreamAdmission.java:59 callerCount.decrementAndGet();
GrpcStreamAdmission.java:60 }
GrpcStreamAdmission.java:61 if (openStreams.get() > 0) {
GrpcStreamAdmission.java:62 openStreams.decrementAndGet();
GrpcStreamAdmission.java:63 }
GrpcStreamAdmission.java:64 }
필드 넷 :
GrpcStreamAdmission.java:14 private final int maxConcurrentStreams;
GrpcStreamAdmission.java:15 private final int maxStreamsPerCaller;
GrpcStreamAdmission.java:16 private final AtomicInteger openStreams = new AtomicInteger();
GrpcStreamAdmission.java:17 private final java.util.concurrent.ConcurrentMap<String, AtomicInteger> perCaller =
GrpcStreamAdmission.java:18 new java.util.concurrent.ConcurrentHashMap<>();
# 자바독이 서술하는 실패 시나리오
GrpcStreamAdmission.java:5 /**
GrpcStreamAdmission.java:6 * Bounds how many streams a server holds open at once.
GrpcStreamAdmission.java:7 *
GrpcStreamAdmission.java:8 * <p>Streams are not requests: they occupy a connection, a queue and a producer for their whole
GrpcStreamAdmission.java:9 * lifetime, so the number of them is a capacity number in a way that request rate is not. Without a
GrpcStreamAdmission.java:10 * bound, a client that reconnects on every error opens streams faster than the old ones close.
GrpcStreamAdmission.java:11 */
GrpcStreamAdmission.java:12 public final class GrpcStreamAdmission {
# perCaller 가 나오는 줄 전부와 그 소속
GrpcStreamAdmission.java:17 private final java.util.concurrent.ConcurrentMap<String, AtomicInteger> perCaller =
GrpcStreamAdmission.java:43 perCaller.computeIfAbsent(callerFingerprint, key -> new AtomicInteger());
GrpcStreamAdmission.java:57 AtomicInteger callerCount = perCaller.get(callerFingerprint);
GrpcStreamAdmission.java:73 AtomicInteger callerCount = perCaller.get(callerFingerprint);
:43 은 tryAdmit · :57 은 release · :73 은 openStreamsFor 안이다
제거 계열 호출 (remove·clear·removeIf·compute·merge·keySet·entrySet) :
43: perCaller.computeIfAbsent(callerFingerprint, key -> new AtomicInteger());
# 이 타입을 참조하는 자리 — 경로 제한 없이 저장소 전체
$ git grep -l GrpcStreamAdmission (pathspec 없음)
docs/2026-08-13-grpc-type-safe-rpc-platform-implementation-plan.md
src/grpc/grpc-policy/src/main/java/dev/caskeleton/grpc/streaming/GrpcStreamAdmission.java
src/grpc/grpc-policy/src/test/java/dev/caskeleton/grpc/streaming/GrpcFlowControlPolicyTest.java
$ git grep -l GrpcAdmissionController (대조군)
docs/2026-08-13-grpc-type-safe-rpc-platform-implementation-plan.md
docs/runbooks/grpc-platform-operations.md
src/grpc/grpc-admin/src/main/java/dev/caskeleton/grpc/admin/GrpcDrainCoordinator.java
src/grpc/grpc-admin/src/test/java/dev/caskeleton/grpc/admin/GrpcDrainCoordinatorTest.java
src/grpc/grpc-server/src/main/java/dev/caskeleton/grpc/server/GrpcAdmissionController.java
src/grpc/grpc-server/src/test/java/dev/caskeleton/grpc/server/GrpcServerProfileTest.java
src/grpc/grpc-spring-boot-starter/src/main/java/dev/caskeleton/grpc/boot/GrpcPlatformAutoConfiguration.java
두 타입이 실린 배포 아티팩트가 있는가 :
modules.json 의 grpc leaf 18 개 중 runtime_memberships 가 빈 것 : 18 개
자원 파일·자동설정·리플렉션 경로 :
0 건
Class.forName 0 건
# 같은 가족이 무제한 증가를 막는 다른 자리
src/grpc/grpc-observability/src/main/java/dev/caskeleton/grpc/observability/GrpcMetricCardinalityPolicy.java
GrpcMetricCardinalityPolicy.java:12 * <p>An allowlist, because the failure is not a bad tag — it is an unbounded one. A tag whose value
GrpcMetricCardinalityPolicy.java:13 * space grows with traffic multiplies every time series by that space: one tenant id turns a
GrpcMetricCardinalityPolicy.java:14 * hundred series into a hundred thousand, and the metric backend either drops the data or bills for
GrpcMetricCardinalityPolicy.java:15 * it. By the time anyone notices, the dashboards built on those metrics are already gone.
ALLOWED_TAGS 선언 범위와 원소 :
GrpcMetricCardinalityPolicy.java:24 private static final Set<String> ALLOWED_TAGS =
GrpcMetricCardinalityPolicy.java:25 Set.of(
GrpcMetricCardinalityPolicy.java:26 "grpc.service",
GrpcMetricCardinalityPolicy.java:27 "grpc.method",
GrpcMetricCardinalityPolicy.java:28 "grpc.rpc_type",
GrpcMetricCardinalityPolicy.java:29 "grpc.status",
GrpcMetricCardinalityPolicy.java:30 "grpc.channel_profile",
GrpcMetricCardinalityPolicy.java:31 "grpc.completion_outcome",
GrpcMetricCardinalityPolicy.java:32 "grpc.retry_bucket",
GrpcMetricCardinalityPolicy.java:33 "grpc.stream_termination_reason");
# 프로브 — 아래 소스를 그 자리에서 컴파일해 돌린다
GrpcStreamAdmission a = new GrpcStreamAdmission(200000, 200000);
for (int i = 0; i < n; i++) { a.tryAdmit("fp-" + i); a.release("fp-" + i); }
int after = mapSize(a);
for (int i = 0; i < n; i++) { a.tryAdmit("fp-" + i); a.release("fp-" + i); }
int again = mapSize(a);
a.tryAdmit("new-one"); a.release("new-one");
n, a.openStreams(), after, again, mapSize(a));
GrpcStreamAdmission a = new GrpcStreamAdmission(1, 1);
boolean held = a.tryAdmit("holder");
for (int i = 0; i < n; i++) if (!a.tryAdmit("fp-" + i)) refusedCount++;
System.out.printf(" openStreams=%d (상한 1) · 맵 %d%n", a.openStreams(), mapSize(a));
가. 열고 모두 닫았을 때 맵이 줄어드는가
호출자 3 개를 열고 모두 닫음 : openStreams=0 · 맵 3 · 같은 지문 반복 후 3 · 새 지문 하나 후 4
호출자 50000 개를 열고 모두 닫음 : openStreams=0 · 맵 50000 · 같은 지문 반복 후 50000 · 새 지문 하나 후 50001
나. 상한에 걸려 거절된 승인도 항목을 만드는가
상한 (1,1) 에서 먼저 하나 승인=true · 이후 3 번 시도 중 거절 3
openStreams=1 (상한 1) · 맵 4
상한 (1,1) 에서 먼저 하나 승인=true · 이후 50000 번 시도 중 거절 50000
openStreams=1 (상한 1) · 맵 50001