Files
document-haness/docs/clean-architecture-backend-template/final/evidence/rendered/a06-f003-change-streams-true-probe.svg
T
DongHyeonkaandClaude Opus 5 b2963105a8 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>
2026-09-04 22:51:59 +09:00

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 &#x27;docker rm -f cs-standalone &gt;/dev/null 2&gt;&amp;1; rm -rf &quot;$D&quot;&#x27; EXIT
docker rm -f cs-standalone </tspan><tspan x="24" dy="22">&gt;/dev/null 2&gt;&amp;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 &gt;/dev/null
for i in $(seq 1 60); do
docker exec cs-standalone mongosh --port 57017 --quiet \
</tspan><tspan x="24" dy="22">--eval &#x27;db.runCommand({ping:1}).ok&#x27; 2&gt;/dev/null | grep -q 1 &amp;&amp; break
sleep 1
done
cat &gt; &quot;$D/ChangeStreamProbe.java&quot; &lt;</tspan><tspan x="24" dy="22">&lt;&#x27;JAVA&#x27;
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 = &quot;ca-skeleton.persistence-mongo.platform&quot;;
private static final MongoCrede</tspan><tspan x="24" dy="22">ntialReference RUNTIME =
new MongoCredentialReference(&quot;secret:[REDACTED], MongoPrincipalRole.APP_WRITE);
privat</tspan><tspan x="24" dy="22">e static final MongoCredentialReference ADMIN =
new MongoCredentialReference(&quot;secret:[REDACTED], MongoPrincipalRol</tspan><tspan x="24" dy="22">e.DBA);
public static void main(String[] args) {
System.out.println(&quot;[바인딩] 같은 컴팩트 생성자에 들어가는 세 입</tspan><tspan x="24" dy="22">&quot;);
show(&quot;transactions&quot;, &quot;true&quot;);
show(&quot;change-streams&quot;, &quot;true&quot;);
show(&quot;required-secondaries&quot;, &quot;-1&quot;);
</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]), &quot;probe&quot;));
Document hello = template.executeCommand(new Doc</tspan><tspan x="24" dy="22">ument(&quot;hello&quot;, 1)).block();
MongoTopology observed =
hello.containsKey(&quot;setName&quot;)
? MongoTopolog</tspan><tspan x="24" dy="22">y.REPLICA_SET
: &quot;isdbgrid&quot;.equals(hello.getString(&quot;msg&quot;))
? MongoTopology.SHARDED
</tspan><tspan x="24" dy="22"> : MongoTopology.STANDALONE;
String version =
template.executeCommand(new Document(&quot;buildInfo&quot;, 1)).blo</tspan><tspan x="24" dy="22">ck().getString(&quot;version&quot;);
MongoTopologyProbe probe = new MongoTopologyProbe(observed, version, 0);
MongoCapabi</tspan><tspan x="24" dy="22">litySet capabilities = probe.capabilities();
System.out.println(&quot;서버 : mongod &quot; + version + &quot;, 관측 토</tspan><tspan x="24" dy="22">폴로지 &quot; + observed);
System.out.println(
&quot;CHANGE_STREAM : isStable=&quot; + capabilities.isStable(MongoCapab</tspan><tspan x="24" dy="22">ility.CHANGE_STREAM));
System.out.println(
&quot; 사유 : &quot; + 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 + &quot;.change-streams&quot;, &quot;true&quot;)))
.bind(PREFIX, MongoPlatformSettings.class)
.get();
</tspan><tspan x="24" dy="22"> System.out.println();
System.out.println(&quot;[출하 배선] MongoStartupValidator(..., properties.changeStreams())&quot;);
</tspan><tspan x="24" dy="22"> System.out.println(&quot; validate() -&gt; &quot; + outcome(validator(bound, probe, bound.changeStreams())));
System.out.print</tspan><tspan x="24" dy="22">ln(&quot;[시험 배선] MongoStartupValidator(..., true)&quot;);
System.out.println(&quot; validate() -&gt; &quot; + outcome(validator(bound,</tspan><tspan x="24" dy="22"> probe, true)));
template.getCollection(&quot;events&quot;).flatMap(c -&gt; reactor.core.publisher.Mono.from(
c.inse</tspan><tspan x="24" dy="22">rtOne(new Document(&quot;seed&quot;, 1)))).block();
MongoCollectionProfileRegistry collections =
MongoCollectionProfil</tspan><tspan x="24" dy="22">eRegistry.builder().register(&quot;events&quot;, &quot;events&quot;).build();
System.out.println();
System.out.println(&quot;[source 직접</tspan><tspan x="24" dy="22"> 생성] SpringReactiveChangeStreamSource.open()&quot;);
try {
new SpringReactiveChangeStreamSource(template, collect</tspan><tspan x="24" dy="22">ions)
.open(MongoChangeStreamSubscription.of(&quot;probe&quot;, &quot;events&quot;), Optional.empty())
.blockFirst(Durat</tspan><tspan x="24" dy="22">ion.ofSeconds(15));
System.out.println(&quot; -&gt; 커서 열림&quot;);
} 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(&quot; -&gt; &quot; + root.getClass().getSimpleName());
System.out.println(&quot; &quot; + root.getMessage().split(&quot;\n&quot;)[0]);
</tspan><tspan x="24" dy="22"> if (root instanceof MongoException driverFailure) {
System.out.println();
System.out.println(&quot;[복구 정</tspan><tspan x="24" dy="22">책] MongoChangeStreamRecoveryPolicy.onFailure(...)&quot;);
System.out.println(
&quot; -&gt; &quot;
+ </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 + &quot;.&quot; + key, value)))
</tspan><tspan x="24" dy="22"> .bind(PREFIX, MongoPlatformSettings.class)
.get();
System.out.println(
&quot; &quot;
</tspan><tspan x="24" dy="22"> + (key + &quot;=&quot; + value + &quot; &quot;).substring(0, 28)
+ &quot;바인딩 성공, transactions=&quot;
</tspan><tspan x="24" dy="22"> + bound.transactions()
+ &quot; changeStreams=&quot;
+ bound.changeStreams()
+ &quot; req</tspan><tspan x="24" dy="22">uiredSecondaries=&quot;
+ 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(
&quot; &quot;
+ (key + &quot;=&quot; + value + &quot; &quot;).substring(0, 28)
+ &quot;바인딩</tspan><tspan x="24" dy="22"> 실패, &quot;
+ root.getClass().getSimpleName()
+ &quot;: &quot;
+ root.getMessage().split(&quot; </tspan><tspan x="24" dy="22">\\[&quot;)[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 &quot;통과&quot;;
} catch (RuntimeException rejected) {
r</tspan><tspan x="24" dy="22">eturn rejected.getClass().getSimpleName() + &quot;: &quot; + rejected.getMessage().split(&quot; \\[&quot;)[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 &quot;,$co</tspan><tspan x="24" dy="22">nfs,&quot; in *&quot;,testRuntimeClasspath,&quot;*) ;; *) continue ;; esac
g=${coord%%:*}; r=${coord#*:}; n=${r%%:*}; v=${r##*:}
ja</tspan><tspan x="24" dy="22">r=$(find &quot;$CACHE/$g/$n/$v&quot; -name &#x27;*.jar&#x27; ! -name &#x27;*sources*&#x27; ! -name &#x27;*javadoc*&#x27; 2&gt;/dev/null | head -1)
[ -n &quot;$jar&quot; ] </tspan><tspan x="24" dy="22">&amp;&amp; CP=&quot;$CP:$jar&quot;
done &lt; &lt;(grep -E &#x27;^[a-zA-Z0-9._-]+:[^=]+=&#x27; &quot;$LOCK&quot;)
javac -encoding UTF-8 -cp &quot;$CP&quot; -d &quot;$D&quot; &quot;$D/Change</tspan><tspan x="24" dy="22">StreamProbe.java&quot;
java -Dstdout.encoding=UTF-8 -cp &quot;$D:$CP&quot; ChangeStreamProbe mongodb://127.0.0.1:57017/probe 2&gt;&amp;1 \
|</tspan><tspan x="24" dy="22"> grep -vE &#x27;^[0-9]{2}:[0-9]{2}:[0-9]{2}[.][0-9]{3} &#x27; </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() -&gt; 통과</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() -&gt; 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"> -&gt; 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): &#x27;The $changeStream stage is only supported on </tspan><tspan x="24" dy="22">replica sets&#x27; on server 127.0.0.1:57017. The full response is {&quot;ok&quot;: 0.0, &quot;errmsg&quot;: &quot;The $changeStream stage is only supported o</tspan><tspan x="24" dy="22">n replica sets&quot;, &quot;code&quot;: 40573, &quot;codeName&quot;: &quot;Location40573&quot;}</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"> -&gt; MongoChangeStreamRecoveryDecision[state=FAILED, autoResume=false, requiredRunbook=docs/mongodb/runbooks/failover.md]</tspan></text>
</svg>