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>
173 lines
8.7 KiB
Plaintext
173 lines
8.7 KiB
Plaintext
# 열거형이 세운 규칙과 상수
|
|
/**
|
|
* The web capabilities that are not part of Stable, each behind its own flag.
|
|
*
|
|
* <p>One flag per capability, not one for "advanced". They have nothing in common operationally:
|
|
* virtual threads change how every request is scheduled, streaming changes how long a response
|
|
* holds a connection, XML adds a parser with a decades-long history of entity-expansion attacks. A
|
|
* single switch would make those one decision, and a deployment that wanted the first would be
|
|
* given the third.
|
|
*
|
|
* <p>Every constant is off unless named.
|
|
*/
|
|
선언된 상수 수 : 12
|
|
24: MVC_VIRTUAL_THREADS,
|
|
27: WEBFLUX_BLOCKING_BRIDGE,
|
|
30: JSON_MERGE_PATCH,
|
|
33: JSON_PATCH,
|
|
36: SSE,
|
|
39: NDJSON,
|
|
42: JSON_SEQUENCE,
|
|
45: FUNCTIONAL_WEBFLUX,
|
|
48: CBOR,
|
|
51: XML,
|
|
54: OPENAPI_32,
|
|
57: RATELIMIT_DRAFT_HEADERS;
|
|
각 상수가 발행하는 속성 이름 :
|
|
/** The property that turns this on. */
|
|
public String propertyName() {
|
|
return "backend.web.advanced." + name().toLowerCase(Locale.ROOT).replace('_', '-') + ".enabled";
|
|
}
|
|
|
|
|
|
# main 에 그 접두사가 나오는 자리 전부. 만드는 쪽과 읽는 쪽이 섞여 있다
|
|
VirtualThreadProfile.java:76 return "backend.web.advanced.virtual-threads.enabled";
|
|
MvcStreamingExecutorConfiguration.java:37 prefix = "backend.web.advanced.ndjson",
|
|
VirtualThreadSettings.java:10 * Settings bound from {@code backend.web.advanced.mvc-virtual-threads.*}.
|
|
VirtualThreadSettings.java:17 @ConfigurationProperties(prefix = "backend.web.advanced.mvc-virtual-threads")
|
|
VirtualThreadMvcConfiguration.java:33 prefix = "backend.web.advanced.mvc-virtual-threads",
|
|
WebAdvancedFeature.java:61 return "backend.web.advanced." + name().toLowerCase(Locale.ROOT).replace('_', '-') + ".enabled";
|
|
그중 게이트나 바인딩인 셋 :
|
|
17:@ConfigurationProperties(prefix = "backend.web.advanced.mvc-virtual-threads")
|
|
33: prefix = "backend.web.advanced.mvc-virtual-threads",
|
|
34- name = "enabled",
|
|
35- havingValue = "true")
|
|
36-public class VirtualThreadMvcConfiguration {
|
|
37: prefix = "backend.web.advanced.ndjson",
|
|
38- name = "enabled",
|
|
39- havingValue = "true")
|
|
40-public class MvcStreamingExecutorConfiguration {
|
|
이름을 만들기만 하는 둘 :
|
|
public String propertyName() {
|
|
return "backend.web.advanced." + name().toLowerCase(Locale.ROOT).replace('_', '-') + ".enabled";
|
|
/** The property that turns this on. */
|
|
public static String propertyName() {
|
|
return "backend.web.advanced.virtual-threads.enabled";
|
|
}
|
|
|
|
# 두 스위치를 켜면 각각 무엇이 생기는가
|
|
* <p>Named {@code applicationTaskExecutor} because that is the bean Spring Boot's MVC async
|
|
* support looks for. A differently named bean is created, is never used, and leaves the container
|
|
* default in place — which is the failure mode where the whole capability is switched on and
|
|
* nothing changes.
|
|
*/
|
|
@Bean("applicationTaskExecutor")
|
|
@ConditionalOnMissingBean(name = "applicationTaskExecutor")
|
|
그런데 그 이름을 출하 조립 경로가 이미 쥐고 있다 :
|
|
16: * with a bounded pool; naming the bean {@value #EXECUTOR_BEAN_NAME} backs off Boot's auto-config
|
|
19:@Configuration
|
|
25: public static final String EXECUTOR_BEAN_NAME = "applicationTaskExecutor";
|
|
40: @Bean(name = EXECUTOR_BEAN_NAME)
|
|
41: @Primary
|
|
42: ThreadPoolTaskExecutor applicationTaskExecutor(
|
|
52: executor.setRejectedExecutionHandler(new LoggingAbortPolicy(EXECUTOR_BEAN_NAME, metrics));
|
|
57: EXECUTOR_BEAN_NAME, () -> executor.getThreadPoolExecutor().getQueue().size());
|
|
컴포넌트 스캔 제외 정규식에 bootstrap.async 가 있는가 : 0
|
|
@Configuration(proxyBeanMethods = false)
|
|
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
|
@ConditionalOnProperty(
|
|
prefix = "backend.web.advanced.ndjson",
|
|
name = "enabled",
|
|
havingValue = "true")
|
|
public class MvcStreamingExecutorConfiguration {
|
|
72: return new MvcStreamWriter(encoder, StreamFraming.NDJSON);
|
|
84: return new MvcStreamWriter(encoder, StreamFraming.JSON_SEQUENCE);
|
|
그 두 미디어 타입을 내는 main 라우트 : 0
|
|
MvcStreamWriter 를 advanced 밖에서 부르는 main 코드 : 0
|
|
|
|
# 레인이 무엇을 고르고 그 안에서 무엇을 단언하는가
|
|
tasks.register('webAdvancedTest', Test) {
|
|
group = 'verification'
|
|
description = 'Runs every web Advanced capability contract.'
|
|
testClassesDirs = sourceSets.test.output.classesDirs
|
|
classpath = sourceSets.test.runtimeClasspath
|
|
useJUnitPlatform {
|
|
includeTags 'web-advanced'
|
|
}
|
|
failOnNoDiscoveredTests = true
|
|
그 태그를 단 시험 클래스 수 : 22
|
|
그 파일에서 속성을 참으로 두는 자리. 셋은 만들어지는 빈을, 둘은 기동 실패를 단언한다 :
|
|
43: "backend.web.advanced.mvc-virtual-threads.enabled=true",
|
|
72: .withPropertyValues("backend.web.advanced.ndjson.enabled=true")
|
|
92: "backend.web.advanced.mvc-virtual-threads.enabled=true",
|
|
117: "backend.web.advanced.mvc-virtual-threads.enabled=true",
|
|
136: .withPropertyValues("backend.web.advanced.mvc-virtual-threads.enabled=true")
|
|
그중 켠 뒤 빈을 확인하는 자리 :
|
|
.withPropertyValues("backend.web.advanced.ndjson.enabled=true")
|
|
.withUserConfiguration(MvcStreamingExecutorConfiguration.class)
|
|
.run(
|
|
context ->
|
|
assertThat(context)
|
|
.hasNotFailed()
|
|
.hasSingleBean(WebStreamPolicy.class)
|
|
.hasSingleBean(WebStreamRegistry.class)
|
|
.hasSingleBean(StreamRecordEncoder.class)
|
|
.hasBean("mvcNdjsonWriter")
|
|
.hasBean("mvcJsonSequenceWriter"));
|
|
|
|
# 롤백 시험 일곱이 무엇을 단언하는가
|
|
49: void nothingIsCreatedWithNoFlags() {
|
|
67: void explicitFalseCreatesNothing() {
|
|
86: void flagsReportStablePreservation() {
|
|
103: void codecsOffLeaveOnlyJson() {
|
|
119: void draftHeadersOffWriteNothing() {
|
|
134: void functionalRoutingOffServesNothing() {
|
|
146: void everyGateDemandsRollbackEvidence() {
|
|
속성 상태를 다루는 둘 — 하나는 값을 두지 않고, 하나는 거짓으로 둔다 :
|
|
void nothingIsCreatedWithNoFlags() {
|
|
// No property values at all — the shape a Stable deployment actually has.
|
|
runner
|
|
.withUserConfiguration(
|
|
.withPropertyValues(
|
|
"backend.web.advanced.ndjson.enabled=false",
|
|
"backend.web.advanced.mvc-virtual-threads.enabled=false")
|
|
.withUserConfiguration(
|
|
플래그 값을 쓰는 유일한 시험 전문 :
|
|
void flagsReportStablePreservation() {
|
|
assertThat(
|
|
dev.caskeleton.adapter.inbound.web.advanced.WebAdvancedFeatureFlags.none()
|
|
.stableBehaviourPreserved())
|
|
.isTrue();
|
|
for (WebAdvancedFeature feature : WebAdvancedFeature.values()) {
|
|
boolean preserved =
|
|
dev.caskeleton.adapter.inbound.web.advanced.WebAdvancedFeatureFlags.of(feature)
|
|
.stableBehaviourPreserved();
|
|
assertThat(preserved)
|
|
.as("%s claims to leave unrelated requests alone", feature)
|
|
.isEqualTo(!feature.affectsUnrelatedRequests());
|
|
}
|
|
}
|
|
|
|
44: public boolean stableBehaviourPreserved() {
|
|
45- return enabled.stream().noneMatch(WebAdvancedFeature::affectsUnrelatedRequests);
|
|
46- }
|
|
|
|
# 롤백 검증이 대상으로 삼는 값
|
|
22: public static WebAdvancedFeatureFlags none() {
|
|
27: public static WebAdvancedFeatureFlags of(WebAdvancedFeature... features) {
|
|
그 타입을 부르는 자리 전부 :
|
|
test · WebAdvancedReleaseTest.java:21 WebAdvancedFeatureFlags flags = WebAdvancedFeatureFlags.none();
|
|
test · WebAdvancedReleaseTest.java:37 assertThat(WebAdvancedFeatureFlags.of(WebAdvancedFeature.CBOR).stableBehaviourPreserved())
|
|
test · WebAdvancedReleaseTest.java:40 WebAdvancedFeatureFlags.of(WebAdvancedFeature.MVC_VIRTUAL_THREADS)
|
|
test · WebAdvancedRollbackIT.java:88 dev.caskeleton.adapter.inbound.web.advanced.WebAdvancedFeatureFlags.none()
|
|
test · WebAdvancedRollbackIT.java:93 dev.caskeleton.adapter.inbound.web.advanced.WebAdvancedFeatureFlags.of(feature)
|
|
|
|
# 그 값을 돌리는 레인과, 속성 이름을 단언하는 시험
|
|
.github/workflows/web-advanced-nightly.yml:46: :adapter:inbound:web:webAdvancedTest
|
|
.github/workflows/web-advanced-release.yml:53: :adapter:inbound:web:webAdvancedTest
|
|
test · ResumeTokenTest.java:187 assertThat(WebSocketAdvancedCapability.RESUME.propertyName())
|
|
main · WebSocketAdvancedCapability.java:75 public String propertyName() {
|
|
test · WebAdvancedReleaseTest.java:48 assertThat(WebAdvancedFeature.JSON_MERGE_PATCH.propertyName())
|
|
main · VirtualThreadProfile.java:75 public static String propertyName() {
|
|
main · WebAdvancedFeature.java:60 public String propertyName() {
|