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>
113 lines
7.6 KiB
Plaintext
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
|