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>
186 lines
10 KiB
Plaintext
186 lines
10 KiB
Plaintext
adapter-inbound-grpc — bounded scope manifest and probes
|
|
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
generatedAt=2026-08-30T08:18:53+00:00
|
|
|
|
=== OWNED FILES ===
|
|
df8de3ec8bfc53ef761bbeb9f4727ce148bd4f9b CLAUDE.md 81
|
|
0ae9758763c0413fa0e5dc67d8fa4d349e8469c2 README.md 94
|
|
a43fffb399ffd727426fe254687c2e4ab3d4c6c0 build.gradle 48
|
|
25d3aff7d9c48c628d0bb2ee28f61f5fab367789 gradle.lockfile 176
|
|
e808c723ef7d8d6f963d53b06960e93364c1f0ae src/main/java/dev/caskeleton/adapter/inbound/grpc/ApiErrorException.java 38
|
|
d167e2f11d20ca2b5a3e8c4b5949179f036988f8 src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcAuthenticationInterceptor.java 42
|
|
b6d4b8bcbbd77aed5ad9354cdf77e5e97e0c2f4c src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcAuthenticationPolicy.java 17
|
|
559d1f500e9e4dbae49d9b94a560e50c7a3aad82 src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcExceptionHandlingInterceptor.java 131
|
|
ae2081a132978493283b7f4add97381b92f28c8d src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcServerConfig.java 57
|
|
0a306682112e2d267c2e242b4e875b97176410b3 src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcServerProperties.java 114
|
|
918516bbbbe562f725b7bf48873c13853bab0c7e src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcServerRunner.java 146
|
|
07efccf35fce03b90ca73a77eacc8d396a0dc660 src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcStatusMapper.java 57
|
|
e69571c2d3d605b9aec6269050df3b979612d41c src/test/java/dev/caskeleton/adapter/inbound/grpc/ApiErrorExceptionTest.java 25
|
|
a38d902a09cae66b28df3841993b54d843a97463 src/test/java/dev/caskeleton/adapter/inbound/grpc/GrpcExceptionHandlingInterceptorTest.java 132
|
|
343da8d0ea529ab4d8d70bab97173d32400ae31f src/test/java/dev/caskeleton/adapter/inbound/grpc/GrpcP1BoundaryWireTest.java 355
|
|
7a65db1d5d2d57a0a916ae8879d0805905f962b5 src/test/java/dev/caskeleton/adapter/inbound/grpc/GrpcSafeActivationTest.java 111
|
|
4c97503068ad9e3a1345c287f9d661a1bfc0ed23 src/test/java/dev/caskeleton/adapter/inbound/grpc/GrpcServerRunnerBootTest.java 104
|
|
c32c0f1e591a4b0694014fcfa9f70cdb281e77a5 src/test/java/dev/caskeleton/adapter/inbound/grpc/GrpcStatusMapperTest.java 55
|
|
file_count=18
|
|
|
|
=== 8.1 도달성: feature 서비스와 정책 구현이 존재하는가 ===
|
|
$ grep -rn 'implements BindableService\|BindableService\|GrpcAuthenticationPolicy' --include=*.java . | grep -v 'adapter/inbound/grpc/' | wc -l
|
|
0
|
|
exit=0
|
|
|
|
$ grep -rn 'ca-skeleton.grpc' --include=*.yml --include=*.yaml . | wc -l
|
|
0
|
|
exit=0
|
|
|
|
$ grep -rn 'inbound.grpc' --include=*.java app-bootstrap/src/main sample-portfolio/src/main
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java:71: "dev.caskeleton.adapter.inbound.grpc",
|
|
exit=0
|
|
|
|
=== 8.2 조건 형제 비교: cause chain 순회 관용구 두 가지 (저장소 전체) ===
|
|
$ grep -rn 'getCause() == current' --include=*.java adapter | sed 's|adapter/||'
|
|
inbound/grpc/src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcExceptionHandlingInterceptor.java:124: if (current.getCause() == current) {
|
|
inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/MvcDisconnectDetector.java:61: if (current.getCause() == current) {
|
|
inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/WebFluxDisconnectDetector.java:66: if (current.getCause() == current) {
|
|
outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionRetryClassifier.java:19: if (current.getCause() == current) {
|
|
exit=0
|
|
|
|
$ grep -rn 'depth < \|depth <= \|MAXIMUM_DEPTH' --include=*.java adapter | grep -i 'cause\|depth <' | grep -v test | sed 's|adapter/||' | head
|
|
inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/auth/JwtDecoderConfig.java:63: for (int depth = 0; current != null && depth < 32; depth++) {
|
|
inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/cost/GraphQlDocumentShape.java:28: if (depth < 0
|
|
inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/observation/GraphQlRequestObservationConvention.java:169: if (depth <= 3) {
|
|
inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/observation/GraphQlRequestObservationConvention.java:172: if (depth <= 6) {
|
|
inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/observation/GraphQlRequestObservationConvention.java:175: if (depth <= 12) {
|
|
outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/imperative/bulk/SpringDataBulkFailureExtractor.java:30: for (int depth = 0; current != null && depth < MAXIMUM_DEPTH; depth++) {
|
|
outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/failure/MongoFailureExtractor.java:41: for (int depth = 0; current != null && depth < MAXIMUM_DEPTH; depth++) {
|
|
outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/NotificationSchedulerWorker.java:124: for (int depth = 0; current != null && depth < 8; depth++) {
|
|
outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/http/JdkNotificationHttpGateway.java:98: for (int depth = 0; cause != null && depth < 10; depth++, cause = cause.getCause()) {
|
|
exit=0
|
|
|
|
$ sed -n '93,99p' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/http/JdkNotificationHttpGateway.java
|
|
private static boolean bodyWasLikelyCommitted(IOException failure) {
|
|
// Depth-bounded rather than cycle-detecting: a cause chain can be circular (two exceptions
|
|
// each initCause'd to the other), and an unbounded walk over one hangs the dispatch thread.
|
|
// Ten is far deeper than any real transport wrapping.
|
|
Throwable cause = failure;
|
|
for (int depth = 0; cause != null && depth < 10; depth++, cause = cause.getCause()) {
|
|
if (cause instanceof java.net.ConnectException
|
|
exit=0
|
|
|
|
$ sed -n '118,130p' adapter/inbound/grpc/src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcExceptionHandlingInterceptor.java
|
|
private static ApiErrorCode errorCodeOf(Throwable throwable) {
|
|
Throwable current = throwable;
|
|
while (current != null) {
|
|
if (current instanceof ApiErrorCarrier carrier) {
|
|
return carrier.errorCode();
|
|
}
|
|
if (current.getCause() == current) {
|
|
break;
|
|
}
|
|
current = current.getCause();
|
|
}
|
|
return null;
|
|
}
|
|
exit=0
|
|
|
|
=== 8.3 에러 계약: 모든 non-OK close 가 재작성되는가 ===
|
|
$ sed -n '90,116p' adapter/inbound/grpc/src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcExceptionHandlingInterceptor.java
|
|
private <REQT, RESPT> ServerCall<REQT, RESPT> sanitizingCall(
|
|
ServerCall<REQT, RESPT> delegate, AtomicBoolean closed) {
|
|
return new SimpleForwardingServerCall<>(delegate) {
|
|
@Override
|
|
public void close(Status status, Metadata trailers) {
|
|
if (!closed.compareAndSet(false, true)) {
|
|
return;
|
|
}
|
|
if (status.isOk()) {
|
|
super.close(status, trailers);
|
|
return;
|
|
}
|
|
|
|
ApiErrorCode code = errorCodeOf(status.getCause());
|
|
if (code == null) {
|
|
code = OperationalError.INTERNAL_ERROR;
|
|
}
|
|
super.close(
|
|
statusMapper.toStatus(code.category()).withDescription(code.code()),
|
|
statusMapper.trailersFor(code));
|
|
}
|
|
};
|
|
}
|
|
|
|
private static void closeWithError(ServerCall<?, ?> call, RuntimeException exception) {
|
|
call.close(Status.fromThrowable(exception).withCause(exception), new Metadata());
|
|
}
|
|
exit=0
|
|
|
|
$ grep -n 'closeWithError(sanitizingCall' adapter/inbound/grpc/src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcExceptionHandlingInterceptor.java
|
|
48: closeWithError(sanitizingCall, e);
|
|
84: closeWithError(sanitizingCall, e);
|
|
exit=0
|
|
|
|
=== 8.4 설정 게이트와 검증 ===
|
|
$ grep -n 'ConditionalOnProperty' -A 5 adapter/inbound/grpc/src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcServerConfig.java | head -8
|
|
6:import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
7-import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
8-import org.springframework.context.annotation.Bean;
|
|
9-import org.springframework.context.annotation.Configuration;
|
|
10-
|
|
11-/**
|
|
--
|
|
18:@ConditionalOnProperty(
|
|
exit=0
|
|
|
|
$ grep -n 'AssertTrue' -A 4 adapter/inbound/grpc/src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcServerProperties.java
|
|
3:import jakarta.validation.constraints.AssertTrue;
|
|
4-import jakarta.validation.constraints.Max;
|
|
5-import jakarta.validation.constraints.Min;
|
|
6-import jakarta.validation.constraints.NotBlank;
|
|
7-import java.net.InetAddress;
|
|
--
|
|
90: @AssertTrue(
|
|
91- message = "insecure gRPC requires allow-insecure-local=true and a loopback bind address")
|
|
92- public boolean isInsecureLocalConfigurationValid() {
|
|
93- return !enabled || (allowInsecureLocal && isLoopbackBindAddress());
|
|
94- }
|
|
exit=0
|
|
|
|
$ grep -n 'ignoreUnknownFields\|@Validated' adapter/inbound/grpc/src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcServerProperties.java
|
|
17:@ConfigurationProperties(prefix = "ca-skeleton.grpc", ignoreUnknownFields = false)
|
|
18:@Validated
|
|
exit=0
|
|
|
|
$ sed -n '69,73p' app-bootstrap/src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java
|
|
"dev.caskeleton.bootstrap.settings",
|
|
"dev.caskeleton.bootstrap.tracing",
|
|
"dev.caskeleton.adapter.inbound.grpc",
|
|
"dev.caskeleton.adapter.inbound.web",
|
|
"dev.caskeleton.adapter.inbound.websocket",
|
|
exit=0
|
|
|
|
$ sed -n '73,77p' app-bootstrap/src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java
|
|
"dev.caskeleton.adapter.inbound.websocket",
|
|
"dev.caskeleton.adapter.outbound.cache.redis",
|
|
"dev.caskeleton.adapter.outbound.fileserver",
|
|
"dev.caskeleton.adapter.outbound.objectstorage",
|
|
"dev.caskeleton.application",
|
|
exit=0
|
|
|
|
=== 8.4b health 상태를 언제 SERVING 으로 두는가 ===
|
|
$ grep -n 'setStatus\|builder.build().start()' adapter/inbound/grpc/src/main/java/dev/caskeleton/adapter/inbound/grpc/GrpcServerRunner.java
|
|
86: healthStatusManager.setStatus(
|
|
94: server = builder.build().start();
|
|
exit=0
|
|
|
|
=== 실행 검증 ===
|
|
$ tail -8 /tmp/grpc_test.log
|
|
|
|
> Task :adapter:inbound:grpc:grpcTransportQualificationTestEvidence
|
|
grpcTransportQualificationTest: 15 tests, 0 skipped
|
|
|
|
BUILD SUCCESSFUL in 12s
|
|
17 actionable tasks: 6 executed, 11 up-to-date
|
|
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.0.0/userguide/configuration_cache_enabling.html
|
|
GRADLE_EXIT=0
|
|
exit=0
|
|
|