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>
196 lines
12 KiB
XML
196 lines
12 KiB
XML
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1624" viewBox="0 0 1200 1624" 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="1622" 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 'rm -rf "$D"' EXIT
|
|
|
|
cat > "$D/FlamingockFenceProbe.java" <<'JAVA'
|
|
import dev.caskeleton.ada</tspan><tspan x="24" dy="22">pter.outbound.mongo.security.admin.MongoAdminAuthorization;
|
|
import dev.caskeleton.adapter.outbound.mongo.security.admin.</tspan><tspan x="24" dy="22">MongoAdminGateway;
|
|
import dev.caskeleton.adapter.outbound.mongo.security.admin.MongoAdminOperation;
|
|
import dev.caskeleto</tspan><tspan x="24" dy="22">n.adapter.outbound.mongo.security.admin.MongoAdminRuntimeGuard;
|
|
import dev.caskeleton.adapter.outbound.mongo.migration.M</tspan><tspan x="24" dy="22">ongoMigration;
|
|
import dev.caskeleton.adapter.outbound.mongo.migration.MongoMigrationChecksum;
|
|
import dev.caskeleton.adap</tspan><tspan x="24" dy="22">ter.outbound.mongo.migration.MongoMigrationContext;
|
|
import dev.caskeleton.adapter.outbound.mongo.migration.MongoMigratio</tspan><tspan x="24" dy="22">nId;
|
|
import dev.caskeleton.adapter.outbound.mongo.migration.MongoMigrationLedger;
|
|
import dev.caskeleton.adapter.outbound</tspan><tspan x="24" dy="22">.mongo.migration.MongoMigrationLock;
|
|
import dev.caskeleton.adapter.outbound.mongo.migration.MongoMigrationPostcondition;</tspan><tspan x="24" dy="22">
|
|
import dev.caskeleton.adapter.outbound.mongo.migration.MongoMigrationPrecondition;
|
|
import dev.caskeleton.adapter.outbou</tspan><tspan x="24" dy="22">nd.mongo.migration.MongoMigrationResult;
|
|
import dev.caskeleton.adapter.outbound.mongo.migration.MongoMigrationRunner;
|
|
im</tspan><tspan x="24" dy="22">port dev.caskeleton.adapter.outbound.mongo.migration.flamingock.FlamingockLockAdapter;
|
|
import java.time.Clock;
|
|
import ja</tspan><tspan x="24" dy="22">va.time.Duration;
|
|
import java.time.Instant;
|
|
import java.time.ZoneOffset;
|
|
import java.util.List;
|
|
import java.util.Optiona</tspan><tspan x="24" dy="22">l;
|
|
import java.util.Set;
|
|
|
|
public final class FlamingockFenceProbe {
|
|
|
|
private static final MongoMigrationId ID = new Mo</tspan><tspan x="24" dy="22">ngoMigrationId("20260829-001");
|
|
|
|
public static void main(String[] args) {
|
|
FlamingockLockAdapter flamingock =
|
|
</tspan><tspan x="24" dy="22"> new FlamingockLockAdapter(() -> true, extension -> {}, () -> {});
|
|
System.out.println("FlamingockLockAdapter.held(</tspan><tspan x="24" dy="22">) = " + flamingock.held());
|
|
System.out.println("FlamingockLockAdapter.fence() = " + flamingock.fence()
|
|
+ " </tspan><tspan x="24" dy="22"> (UNFENCED = " + MongoMigrationLock.UNFENCED + ")");
|
|
System.out.println();
|
|
|
|
MongoMigrationRunner runner = new M</tspan><tspan x="24" dy="22">ongoMigrationRunner(new EmptyLedger());
|
|
System.out.println("[검사가 마이그레이션을 보기 전에 걸린다]");
|
|
apply("</tspan><tspan x="24" dy="22">마이그레이션 0건", runner, List.of(), flamingock);
|
|
apply("재개 불가 1건", runner, List.of(nonResumable()), flamingoc</tspan><tspan x="24" dy="22">k);
|
|
System.out.println();
|
|
|
|
System.out.println("[같은 마이그레이션, 펜스가 있는 리스]");
|
|
apply("재개 불가 1건</tspan><tspan x="24" dy="22">", runner, List.of(nonResumable()), new FencedLock(7L));
|
|
System.exit(0);
|
|
}
|
|
|
|
private static void apply(
|
|
Str</tspan><tspan x="24" dy="22">ing what, MongoMigrationRunner runner, List<MongoMigration> migrations,
|
|
MongoMigrationLock lock) {
|
|
try {
|
|
</tspan><tspan x="24" dy="22"> List<MongoMigrationResult> results = runner.apply(migrations, lock, template());
|
|
System.out.println(" " + what +</tspan><tspan x="24" dy="22"> " -> 실행, 결과 " + results.size() + "건"
|
|
+ (results.isEmpty() ? "" : " " + results.get(0).status()));
|
|
} </tspan><tspan x="24" dy="22">catch (RuntimeException refused) {
|
|
System.out.println(" " + what + " -> 거절, " + refused.getClass().getSimpleNam</tspan><tspan x="24" dy="22">e());
|
|
System.out.println(" " + refused.getMessage().split(" \\[")[0]);
|
|
}
|
|
}
|
|
|
|
/** Completed in one call</tspan><tspan x="24" dy="22">, so it never produces a checkpoint: not a resumable migration. */
|
|
private static MongoMigration nonResumable() {
|
|
</tspan><tspan x="24" dy="22">return new MongoMigration() {
|
|
@Override
|
|
public MongoMigrationId id() {
|
|
return ID;
|
|
}
|
|
|
|
@Ov</tspan><tspan x="24" dy="22">erride
|
|
public MongoMigrationChecksum checksum() {
|
|
return new MongoMigrationChecksum("abc");
|
|
}
|
|
|
|
</tspan><tspan x="24" dy="22"> @Override
|
|
public MongoMigrationPrecondition precondition() {
|
|
return context -> true;
|
|
}
|
|
|
|
@Over</tspan><tspan x="24" dy="22">ride
|
|
public MongoMigrationResult execute(MongoMigrationContext context) {
|
|
return MongoMigrationResult.comp</tspan><tspan x="24" dy="22">leted(1L);
|
|
}
|
|
|
|
@Override
|
|
public MongoMigrationPostcondition postcondition() {
|
|
return (context, </tspan><tspan x="24" dy="22">result) -> true;
|
|
}
|
|
};
|
|
}
|
|
|
|
private static MongoMigrationContext template() {
|
|
MongoAdminGateway gateway =</tspan><tspan x="24" dy="22">
|
|
new MongoAdminGateway(
|
|
new MongoAdminRuntimeGuard(true, "app-credential", "migration-credential"),
|
|
</tspan><tspan x="24" dy="22"> MongoAdminAuthorization.routine(Set.of(MongoAdminOperation.APPLY_MIGRATION)),
|
|
record -> {},
|
|
</tspan><tspan x="24" dy="22"> Clock.fixed(Instant.EPOCH, ZoneOffset.UTC));
|
|
return MongoMigrationContext.unleased(
|
|
ID, gateway, nul</tspan><tspan x="24" dy="22">l, 100, Duration.ZERO, Duration.ofMinutes(1), false, "release-engineer");
|
|
}
|
|
|
|
private static final class FencedLock i</tspan><tspan x="24" dy="22">mplements MongoMigrationLock {
|
|
|
|
private final long fence;
|
|
|
|
private FencedLock(long fence) {
|
|
this.fence = f</tspan><tspan x="24" dy="22">ence;
|
|
}
|
|
|
|
@Override
|
|
public boolean held() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public long fence() {</tspan><tspan x="24" dy="22">
|
|
return fence;
|
|
}
|
|
|
|
@Override
|
|
public void refresh(Duration extension) {}
|
|
|
|
@Override
|
|
public void </tspan><tspan x="24" dy="22">close() {}
|
|
}
|
|
|
|
private static final class EmptyLedger implements MongoMigrationLedger {
|
|
|
|
@Override
|
|
public Opt</tspan><tspan x="24" dy="22">ional<AppliedMigration> find(MongoMigrationId migrationId) {
|
|
return Optional.empty();
|
|
}
|
|
|
|
@Override
|
|
pub</tspan><tspan x="24" dy="22">lic void recordApplied(
|
|
MongoMigrationId migrationId, MongoMigrationChecksum checksum, String operator,
|
|
</tspan><tspan x="24" dy="22">Instant appliedAt, long fence) {}
|
|
|
|
@Override
|
|
public Optional<dev.caskeleton.adapter.outbound.mongo.migration.Mon</tspan><tspan x="24" dy="22">goMigrationCheckpoint>
|
|
findCheckpoint(MongoMigrationId migrationId) {
|
|
return Optional.empty();
|
|
}
|
|
|
|
</tspan><tspan x="24" dy="22">@Override
|
|
public void saveCheckpoint(
|
|
dev.caskeleton.adapter.outbound.mongo.migration.MongoMigrationCheckpoi</tspan><tspan x="24" dy="22">nt checkpoint,
|
|
long fence) {}
|
|
}
|
|
}
|
|
JAVA
|
|
|
|
CACHE=/root/.gradle/caches/modules-2/files-2.1
|
|
LOCK=src/adapter/outbou</tspan><tspan x="24" dy="22">nd/persistence-mongo/gradle.lockfile
|
|
CP=src/adapter/outbound/persistence-mongo/build/classes/java/main
|
|
while IFS= read -</tspan><tspan x="24" dy="22">r line; do
|
|
coord=${line%%=*}; confs=${line#*=}
|
|
case ",$confs," in *",testRuntimeClasspath,"*) ;; *) continue ;; esac</tspan><tspan x="24" dy="22">
|
|
g=${coord%%:*}; r=${coord#*:}; n=${r%%:*}; v=${r##*:}
|
|
jar=$(find "$CACHE/$g/$n/$v" -name '*.jar' ! -name '*sources*</tspan><tspan x="24" dy="22">' ! -name '*javadoc*' 2>/dev/null | head -1)
|
|
[ -n "$jar" ] && CP="$CP:$jar"
|
|
done < <(grep -E '^[a-zA-Z0-9._-]+:[^=]+='</tspan><tspan x="24" dy="22"> "$LOCK")
|
|
|
|
javac -encoding UTF-8 -cp "$CP" -d "$D" "$D/FlamingockFenceProbe.java"
|
|
java -Dstdout.encoding=UTF-8 -cp "$D:$</tspan><tspan x="24" dy="22">CP" FlamingockFenceProbe 2>&1 \
|
|
| grep -vE '^[0-9]{2}:[0-9]{2}:[0-9]{2}[.][0-9]{3} ' </tspan></text>
|
|
<text x="24" y="1234" 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="1256" 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-02T07:44:36+00:00 · exit 0</tspan></text>
|
|
<line x1="24" y1="1270" x2="1176" y2="1270" stroke="#30363d"/>
|
|
<text x="24" y="1310" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">FlamingockLockAdapter.held() = true</tspan></text>
|
|
<text x="24" y="1332" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">FlamingockLockAdapter.fence() = -1 (UNFENCED = -1)</tspan></text>
|
|
<text x="24" y="1354" 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="1376" 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="1398" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 마이그레이션 0건 -> 거절, MongoOperationRejectedException</tspan></text>
|
|
<text x="24" y="1420" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> this migration lease exposes no fencing token, so a stalled runner cannot be excluded; use a fenced lease or run the migra</tspan><tspan x="24" dy="22">tion inside a maintenance window with the application stopped</tspan></text>
|
|
<text x="24" y="1464" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 재개 불가 1건 -> 거절, MongoOperationRejectedException</tspan></text>
|
|
<text x="24" y="1486" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> this migration lease exposes no fencing token, so a stalled runner cannot be excluded; use a fenced lease or run the migra</tspan><tspan x="24" dy="22">tion inside a maintenance window with the application stopped</tspan></text>
|
|
<text x="24" y="1530" 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="1552" 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="1574" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 재개 불가 1건 -> 실행, 결과 1건 COMPLETED</tspan></text>
|
|
</svg>
|