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