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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
+185
@@ -0,0 +1,185 @@
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user