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>
223 lines
18 KiB
XML
223 lines
18 KiB
XML
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="2262" viewBox="0 0 1200 2262" role="img">
|
|
<title>terminal evidence</title>
|
|
<desc>Terminal-style rendering generated from retained command output. Sensitive-looking values are redacted in the visual asset.</desc>
|
|
<rect x="1" y="1" width="1198" height="2260" rx="14" fill="#0d1117" stroke="#30363d"/>
|
|
<rect x="1" y="1" width="1198" height="44" rx="14" fill="#161b22"/>
|
|
<rect x="1" y="30" width="1198" height="14" fill="#161b22"/>
|
|
<circle cx="24" cy="22" r="6" fill="#ff5f57"/>
|
|
<circle cx="44" cy="22" r="6" fill="#febc2e"/>
|
|
<circle cx="64" cy="22" r="6" fill="#28c840"/>
|
|
<text x="92" y="27" fill="#8b949e" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">terminal evidence</text>
|
|
<text x="24" y="68" fill="#c9d1d9" font-size="15" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">$ set -e
|
|
D=$(mktemp -d)
|
|
trap 'docker rm -f cs-standalone >/dev/null 2>&1; rm -rf "$D"' EXIT
|
|
|
|
docker rm -f cs-standalone </tspan><tspan x="24" dy="22">>/dev/null 2>&1 || true
|
|
docker run -d --name cs-standalone --network container:workmachine \
|
|
mongo:8.0.16 --port 57017</tspan><tspan x="24" dy="22"> --bind_ip 127.0.0.1 >/dev/null
|
|
for i in $(seq 1 60); do
|
|
docker exec cs-standalone mongosh --port 57017 --quiet \
|
|
</tspan><tspan x="24" dy="22">--eval 'db.runCommand({ping:1}).ok' 2>/dev/null | grep -q 1 && break
|
|
sleep 1
|
|
done
|
|
|
|
cat > "$D/ChangeStreamProbe.java" <</tspan><tspan x="24" dy="22"><'JAVA'
|
|
import com.mongodb.MongoException;
|
|
import com.mongodb.reactivestreams.client.MongoClients;
|
|
import dev.caskeleton</tspan><tspan x="24" dy="22">.adapter.outbound.mongo.api.capability.MongoCapability;
|
|
import dev.caskeleton.adapter.outbound.mongo.api.capability.Mong</tspan><tspan x="24" dy="22">oCapabilitySet;
|
|
import dev.caskeleton.adapter.outbound.mongo.api.profile.MongoTopology;
|
|
import dev.caskeleton.adapter.ou</tspan><tspan x="24" dy="22">tbound.mongo.api.schema.MongoSchemaVersionRange;
|
|
import dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoPlatfor</tspan><tspan x="24" dy="22">mSettings;
|
|
import dev.caskeleton.adapter.outbound.mongo.autoconfigure.MongoStartupValidator;
|
|
import dev.caskeleton.adapt</tspan><tspan x="24" dy="22">er.outbound.mongo.autoconfigure.MongoTopologyProbe;
|
|
import dev.caskeleton.adapter.outbound.mongo.changestream.MongoChang</tspan><tspan x="24" dy="22">eStreamSubscription;
|
|
import dev.caskeleton.adapter.outbound.mongo.changestream.consumer.SpringReactiveChangeStreamSource</tspan><tspan x="24" dy="22">;
|
|
import dev.caskeleton.adapter.outbound.mongo.changestream.recovery.MongoChangeStreamRecoveryPolicy;
|
|
import dev.caskele</tspan><tspan x="24" dy="22">ton.adapter.outbound.mongo.failure.MongoDriverFailureView;
|
|
import dev.caskeleton.adapter.outbound.mongo.imperative.Mongo</tspan><tspan x="24" dy="22">CollectionProfileRegistry;
|
|
import dev.caskeleton.adapter.outbound.mongo.security.MongoCredentialReference;
|
|
import dev.ca</tspan><tspan x="24" dy="22">skeleton.adapter.outbound.mongo.security.MongoPrincipalRole;
|
|
import dev.caskeleton.adapter.outbound.mongo.security.Mongo</tspan><tspan x="24" dy="22">SecurityProfile;
|
|
import java.time.Duration;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
impor</tspan><tspan x="24" dy="22">t org.bson.Document;
|
|
import org.springframework.boot.context.properties.bind.Binder;
|
|
import org.springframework.boot.con</tspan><tspan x="24" dy="22">text.properties.source.MapConfigurationPropertySource;
|
|
import org.springframework.data.mongodb.core.ReactiveMongoTemplat</tspan><tspan x="24" dy="22">e;
|
|
import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory;
|
|
|
|
public final class ChangeStreamProb</tspan><tspan x="24" dy="22">e {
|
|
|
|
private static final String PREFIX = "ca-skeleton.persistence-mongo.platform";
|
|
|
|
private static final MongoCrede</tspan><tspan x="24" dy="22">ntialReference RUNTIME =
|
|
new MongoCredentialReference("secret:[REDACTED], MongoPrincipalRole.APP_WRITE);
|
|
|
|
privat</tspan><tspan x="24" dy="22">e static final MongoCredentialReference ADMIN =
|
|
new MongoCredentialReference("secret:[REDACTED], MongoPrincipalRol</tspan><tspan x="24" dy="22">e.DBA);
|
|
|
|
public static void main(String[] args) {
|
|
System.out.println("[바인딩] 같은 컴팩트 생성자에 들어가는 세 입</tspan><tspan x="24" dy="22">력");
|
|
show("transactions", "true");
|
|
show("change-streams", "true");
|
|
show("required-secondaries", "-1");
|
|
</tspan><tspan x="24" dy="22">System.out.println();
|
|
|
|
ReactiveMongoTemplate template =
|
|
new ReactiveMongoTemplate(
|
|
new SimpleRea</tspan><tspan x="24" dy="22">ctiveMongoDatabaseFactory(MongoClients.create(args[0]), "probe"));
|
|
|
|
Document hello = template.executeCommand(new Doc</tspan><tspan x="24" dy="22">ument("hello", 1)).block();
|
|
MongoTopology observed =
|
|
hello.containsKey("setName")
|
|
? MongoTopolog</tspan><tspan x="24" dy="22">y.REPLICA_SET
|
|
: "isdbgrid".equals(hello.getString("msg"))
|
|
? MongoTopology.SHARDED
|
|
</tspan><tspan x="24" dy="22"> : MongoTopology.STANDALONE;
|
|
String version =
|
|
template.executeCommand(new Document("buildInfo", 1)).blo</tspan><tspan x="24" dy="22">ck().getString("version");
|
|
|
|
MongoTopologyProbe probe = new MongoTopologyProbe(observed, version, 0);
|
|
MongoCapabi</tspan><tspan x="24" dy="22">litySet capabilities = probe.capabilities();
|
|
System.out.println("서버 : mongod " + version + ", 관측 토</tspan><tspan x="24" dy="22">폴로지 " + observed);
|
|
System.out.println(
|
|
"CHANGE_STREAM : isStable=" + capabilities.isStable(MongoCapab</tspan><tspan x="24" dy="22">ility.CHANGE_STREAM));
|
|
System.out.println(
|
|
" 사유 : " + capabilities.require(MongoCapability.CHA</tspan><tspan x="24" dy="22">NGE_STREAM).constraints());
|
|
|
|
MongoPlatformSettings bound =
|
|
new Binder(new MapConfigurationPropertySource(Map</tspan><tspan x="24" dy="22">.of(PREFIX + ".change-streams", "true")))
|
|
.bind(PREFIX, MongoPlatformSettings.class)
|
|
.get();
|
|
|
|
</tspan><tspan x="24" dy="22"> System.out.println();
|
|
System.out.println("[출하 배선] MongoStartupValidator(..., properties.changeStreams())");
|
|
</tspan><tspan x="24" dy="22"> System.out.println(" validate() -> " + outcome(validator(bound, probe, bound.changeStreams())));
|
|
System.out.print</tspan><tspan x="24" dy="22">ln("[시험 배선] MongoStartupValidator(..., true)");
|
|
System.out.println(" validate() -> " + outcome(validator(bound,</tspan><tspan x="24" dy="22"> probe, true)));
|
|
|
|
template.getCollection("events").flatMap(c -> reactor.core.publisher.Mono.from(
|
|
c.inse</tspan><tspan x="24" dy="22">rtOne(new Document("seed", 1)))).block();
|
|
MongoCollectionProfileRegistry collections =
|
|
MongoCollectionProfil</tspan><tspan x="24" dy="22">eRegistry.builder().register("events", "events").build();
|
|
System.out.println();
|
|
System.out.println("[source 직접</tspan><tspan x="24" dy="22"> 생성] SpringReactiveChangeStreamSource.open()");
|
|
try {
|
|
new SpringReactiveChangeStreamSource(template, collect</tspan><tspan x="24" dy="22">ions)
|
|
.open(MongoChangeStreamSubscription.of("probe", "events"), Optional.empty())
|
|
.blockFirst(Durat</tspan><tspan x="24" dy="22">ion.ofSeconds(15));
|
|
System.out.println(" -> 커서 열림");
|
|
} catch (RuntimeException failure) {
|
|
Throwable</tspan><tspan x="24" dy="22"> root = failure;
|
|
while (root.getCause() != null) {
|
|
root = root.getCause();
|
|
}
|
|
System.out.printl</tspan><tspan x="24" dy="22">n(" -> " + root.getClass().getSimpleName());
|
|
System.out.println(" " + root.getMessage().split("\n")[0]);
|
|
</tspan><tspan x="24" dy="22"> if (root instanceof MongoException driverFailure) {
|
|
System.out.println();
|
|
System.out.println("[복구 정</tspan><tspan x="24" dy="22">책] MongoChangeStreamRecoveryPolicy.onFailure(...)");
|
|
System.out.println(
|
|
" -> "
|
|
+ </tspan><tspan x="24" dy="22">new MongoChangeStreamRecoveryPolicy()
|
|
.onFailure(MongoDriverFailureView.from(driverFailure)));
|
|
</tspan><tspan x="24" dy="22"> }
|
|
}
|
|
System.exit(0);
|
|
}
|
|
|
|
private static void show(String key, String value) {
|
|
try {
|
|
MongoPlatformSe</tspan><tspan x="24" dy="22">ttings bound =
|
|
new Binder(new MapConfigurationPropertySource(Map.of(PREFIX + "." + key, value)))
|
|
</tspan><tspan x="24" dy="22"> .bind(PREFIX, MongoPlatformSettings.class)
|
|
.get();
|
|
System.out.println(
|
|
" "
|
|
</tspan><tspan x="24" dy="22"> + (key + "=" + value + " ").substring(0, 28)
|
|
+ "바인딩 성공, transactions="
|
|
</tspan><tspan x="24" dy="22"> + bound.transactions()
|
|
+ " changeStreams="
|
|
+ bound.changeStreams()
|
|
+ " req</tspan><tspan x="24" dy="22">uiredSecondaries="
|
|
+ bound.requiredSecondaries());
|
|
} catch (RuntimeException failure) {
|
|
Throwabl</tspan><tspan x="24" dy="22">e root = failure;
|
|
while (root.getCause() != null) {
|
|
root = root.getCause();
|
|
}
|
|
System.out.print</tspan><tspan x="24" dy="22">ln(
|
|
" "
|
|
+ (key + "=" + value + " ").substring(0, 28)
|
|
+ "바인딩</tspan><tspan x="24" dy="22"> 실패, "
|
|
+ root.getClass().getSimpleName()
|
|
+ ": "
|
|
+ root.getMessage().split(" </tspan><tspan x="24" dy="22">\\[")[0]);
|
|
}
|
|
}
|
|
|
|
private static MongoStartupValidator validator(
|
|
MongoPlatformSettings settings, MongoTopol</tspan><tspan x="24" dy="22">ogyProbe probe, boolean changeStreamsEnabled) {
|
|
return new MongoStartupValidator(
|
|
settings,
|
|
probe,
|
|
</tspan><tspan x="24" dy="22"> MongoSecurityProfile.production(RUNTIME, Set.of()),
|
|
ADMIN,
|
|
settings.transactions(),
|
|
chang</tspan><tspan x="24" dy="22">eStreamsEnabled,
|
|
MongoSchemaVersionRange.of(1, 3));
|
|
}
|
|
|
|
private static String outcome(MongoStartupValidator v</tspan><tspan x="24" dy="22">alidator) {
|
|
try {
|
|
validator.validate();
|
|
return "통과";
|
|
} catch (RuntimeException rejected) {
|
|
r</tspan><tspan x="24" dy="22">eturn rejected.getClass().getSimpleName() + ": " + rejected.getMessage().split(" \\[")[0];
|
|
}
|
|
}
|
|
}
|
|
JAVA
|
|
|
|
CACHE=/root</tspan><tspan x="24" dy="22">/.gradle/caches/modules-2/files-2.1
|
|
LOCK=src/adapter/outbound/persistence-mongo/gradle.lockfile
|
|
CP=src/adapter/outbound/</tspan><tspan x="24" dy="22">persistence-mongo/build/classes/java/main
|
|
while IFS= read -r line; do
|
|
coord=${line%%=*}; confs=${line#*=}
|
|
case ",$co</tspan><tspan x="24" dy="22">nfs," in *",testRuntimeClasspath,"*) ;; *) continue ;; esac
|
|
g=${coord%%:*}; r=${coord#*:}; n=${r%%:*}; v=${r##*:}
|
|
ja</tspan><tspan x="24" dy="22">r=$(find "$CACHE/$g/$n/$v" -name '*.jar' ! -name '*sources*' ! -name '*javadoc*' 2>/dev/null | head -1)
|
|
[ -n "$jar" ] </tspan><tspan x="24" dy="22">&& CP="$CP:$jar"
|
|
done < <(grep -E '^[a-zA-Z0-9._-]+:[^=]+=' "$LOCK")
|
|
|
|
javac -encoding UTF-8 -cp "$CP" -d "$D" "$D/Change</tspan><tspan x="24" dy="22">StreamProbe.java"
|
|
java -Dstdout.encoding=UTF-8 -cp "$D:$CP" ChangeStreamProbe mongodb://127.0.0.1:57017/probe 2>&1 \
|
|
|</tspan><tspan x="24" dy="22"> grep -vE '^[0-9]{2}:[0-9]{2}:[0-9]{2}[.][0-9]{3} ' </tspan></text>
|
|
<text x="24" y="1630" fill="#8b949e" font-size="13" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">cwd: /shared/codebase/clean-architecture-backend-template</tspan></text>
|
|
<text x="24" y="1652" fill="#8b949e" font-size="13" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">time: 2026-09-02T06:56:08+00:00 · exit 0</tspan></text>
|
|
<line x1="24" y1="1666" x2="1176" y2="1666" stroke="#30363d"/>
|
|
<text x="24" y="1706" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[바인딩] 같은 컴팩트 생성자에 들어가는 세 입력</tspan></text>
|
|
<text x="24" y="1728" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> transactions=true 바인딩 성공, transactions=true changeStreams=false requiredSecondaries=2</tspan></text>
|
|
<text x="24" y="1750" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> change-streams=true 바인딩 성공, transactions=false changeStreams=false requiredSecondaries=2</tspan></text>
|
|
<text x="24" y="1772" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> required-secondaries=-1 바인딩 실패, MongoOperationRejectedException: the required secondary count must not be negative; -</tspan><tspan x="24" dy="22">1 is</tspan></text>
|
|
<text x="24" y="1816" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
|
|
<text x="24" y="1838" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">서버 : mongod 8.0.16, 관측 토폴로지 STANDALONE</tspan></text>
|
|
<text x="24" y="1860" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">CHANGE_STREAM : isStable=false</tspan></text>
|
|
<text x="24" y="1882" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 사유 : {reason=topology is STANDALONE, which has no oplog}</tspan></text>
|
|
<text x="24" y="1904" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
|
|
<text x="24" y="1926" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[출하 배선] MongoStartupValidator(..., properties.changeStreams())</tspan></text>
|
|
<text x="24" y="1948" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> validate() -> 통과</tspan></text>
|
|
<text x="24" y="1970" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[시험 배선] MongoStartupValidator(..., true)</tspan></text>
|
|
<text x="24" y="1992" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> validate() -> MongoOperationRejectedException: change streams are enabled but unavailable here: {reason=topology is STANDALONE</tspan><tspan x="24" dy="22">, which has no oplog}; a REPLICA_SET, SHARDED or ATLAS topology is required</tspan></text>
|
|
<text x="24" y="2036" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
|
|
<text x="24" y="2058" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[source 직접 생성] SpringReactiveChangeStreamSource.open()</tspan></text>
|
|
<text x="24" y="2080" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> -> MongoCommandException</tspan></text>
|
|
<text x="24" y="2102" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> Command execution failed on MongoDB server with error 40573 (Location40573): 'The $changeStream stage is only supported on </tspan><tspan x="24" dy="22">replica sets' on server 127.0.0.1:57017. The full response is {"ok": 0.0, "errmsg": "The $changeStream stage is only supported o</tspan><tspan x="24" dy="22">n replica sets", "code": 40573, "codeName": "Location40573"}</tspan></text>
|
|
<text x="24" y="2168" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
|
|
<text x="24" y="2190" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[복구 정책] MongoChangeStreamRecoveryPolicy.onFailure(...)</tspan></text>
|
|
<text x="24" y="2212" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> -> MongoChangeStreamRecoveryDecision[state=FAILED, autoResume=false, requiredRunbook=docs/mongodb/runbooks/failover.md]</tspan></text>
|
|
</svg>
|