# 승인과 해제의 본문 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 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 *

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 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 *

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 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