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>
145 lines
12 KiB
XML
145 lines
12 KiB
XML
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1382" viewBox="0 0 1200 1382" 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="1380" 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
|
|
mkdir -p /tmp/probe
|
|
CT=skiplocked-probe
|
|
docker rm -f "$CT" >/dev/null 2>&1 || true
|
|
docker run -d --name "$CT" -</tspan><tspan x="24" dy="22">-network "container:workmachine" \
|
|
-e POSTGRES_PASSWORD=probe -e POSTGRES_USER=probe -e POSTGRES_DB=probe \
|
|
postgres</tspan><tspan x="24" dy="22">:16-alpine -p 55432 >/dev/null
|
|
for i in $(seq 1 40); do
|
|
docker inspect -f '{{.State.Running}}' "$CT" | grep -q true ||</tspan><tspan x="24" dy="22"> { echo '컨테이너가 뜨지 않았다'; exit 1; }
|
|
docker exec "$CT" pg_isready -U probe -p 55432 >/dev/null 2>&1 && break
|
|
</tspan><tspan x="24" dy="22">sleep 1
|
|
done
|
|
docker exec "$CT" psql -U probe -p 55432 -d probe -tAc 'select version()' | cut -c1-24
|
|
M=adapter/outbound/p</tspan><tspan x="24" dy="22">ersistence-jpa/src/main/resources/db/migration
|
|
for d in postgresql jpa/core jpa/notification-platform; do
|
|
for f in $(l</tspan><tspan x="24" dy="22">s "$M/$d"/V*.sql | sort -V); do
|
|
docker exec -i "$CT" psql -U probe -p 55432 -d probe -q -v ON_ERROR_STOP=1 < "$f" >/</tspan><tspan x="24" dy="22">dev/null 2>&1 || true
|
|
done
|
|
done
|
|
echo -n '# 상위 표는 마이그레이션이 세운다: '
|
|
docker exec "$CT" psql -U probe -p 554</tspan><tspan x="24" dy="22">32 -d probe -tAc \
|
|
"select string_agg(table_name, ' ' order by table_name) from information_schema.tables
|
|
where t</tspan><tspan x="24" dy="22">able_schema='public' and table_name like 'notification_re%'"
|
|
|
|
echo
|
|
echo '# 저장소의 청구 문장을 두 연결이 차례로 실행한</tspan><tspan x="24" dy="22">다. 사이에 정산은 없다.'
|
|
echo '# 상위 행 사슬을 만들지 않으려고 외래 키 하나만 뗐다. 청구 문장과 색인은 그대로다.'
|
|
cat</tspan><tspan x="24" dy="22"> > /tmp/probe/cp.sh <<'CPSH'
|
|
# 락파일의 한 구성에 실린 좌표 전부를 캐시 경로로 바꾼다. Gradle 실행 없이 재현된다.
|
|
LOCK=$</tspan><tspan x="24" dy="22">{1:-adapter/outbound/persistence-jpa/gradle.lockfile}
|
|
CONF=${2:-postgresqlIntegrationTestRuntimeClasspath}
|
|
CACHE=/root/.</tspan><tspan x="24" dy="22">gradle/caches/modules-2/files-2.1
|
|
cp=""
|
|
while IFS= read -r line; do
|
|
coord=${line%%=*}; confs=${line#*=}
|
|
case ",$conf</tspan><tspan x="24" dy="22">s," in *",$CONF,"*) ;; *) continue ;; esac
|
|
group=${coord%%:*}; rest=${coord#*:}; name=${rest%%:*}; ver=${rest##*:}
|
|
j</tspan><tspan x="24" dy="22">ar=$(find "$CACHE/$group/$name/$ver" -name '*.jar' ! -name '*sources*' ! -name '*javadoc*' 2>/dev/null | head -1)
|
|
[ -n</tspan><tspan x="24" dy="22"> "$jar" ] && cp="$cp:$jar"
|
|
done < <(grep -E '^[a-zA-Z0-9._-]+:[^=]+=' "$LOCK")
|
|
echo "${cp#:}"
|
|
CPSH
|
|
cat > /tmp/probe/Skip</tspan><tspan x="24" dy="22">LockedProbe.java <<'JAVA'
|
|
import java.sql.Connection;
|
|
import java.sql.DriverManager;
|
|
import java.sql.ResultSet;
|
|
import j</tspan><tspan x="24" dy="22">ava.sql.Statement;
|
|
import java.sql.Timestamp;
|
|
import java.time.Instant;
|
|
import java.util.UUID;
|
|
|
|
/** 두 작업자가 저장소의</tspan><tspan x="24" dy="22"> 청구 문장을 차례로 실행한다. 사이에 정산은 없다. */
|
|
public class SkipLockedProbe {
|
|
|
|
private static final String CLAIM</tspan><tspan x="24" dy="22">_DUE =
|
|
"""
|
|
SELECT id, attempt_id, provider_profile_id, next_check_at, attempts, last_result,
|
|
cr</tspan><tspan x="24" dy="22">eated_at, updated_at
|
|
FROM notification_reconciliation_job
|
|
WHERE next_check_at <= ?
|
|
ORDER BY next_check</tspan><tspan x="24" dy="22">_at, id
|
|
LIMIT ?
|
|
FOR UPDATE SKIP LOCKED
|
|
""";
|
|
|
|
public static void main(String[] args) throws Exception</tspan><tspan x="24" dy="22"> {
|
|
String url = "jdbc:postgresql://127.0.0.1:55432/probe";
|
|
UUID job = UUID.randomUUID();
|
|
try (Connection set</tspan><tspan x="24" dy="22">up = DriverManager.getConnection(url, "probe", "probe");
|
|
Statement s = setup.createStatement()) {
|
|
// 탐침 </tspan><tspan x="24" dy="22">설정: 상위 행 사슬을 만들지 않으려고 외래 키 하나만 뗀다.
|
|
// 표는 마이그레이션이 모두 세운다. 청구 문장과 색인은 </tspan><tspan x="24" dy="22">그대로다.
|
|
s.execute("alter table notification_reconciliation_job "
|
|
+ "drop constraint if exists notifica</tspan><tspan x="24" dy="22">tion_reconciliation_job_attempt_id_fkey");
|
|
s.execute("delete from notification_reconciliation_job");
|
|
s.execu</tspan><tspan x="24" dy="22">te("insert into notification_reconciliation_job "
|
|
+ "(id, attempt_id, provider_profile_id, next_check_at, atte</tspan><tspan x="24" dy="22">mpts, created_at, updated_at)"
|
|
+ " values ('" + job + "', '" + UUID.randomUUID() + "', 'p1',"
|
|
+ " no</tspan><tspan x="24" dy="22">w() - interval '1 minute', 0, now(), now())");
|
|
}
|
|
System.out.println("만기 작업 하나를 넣는다: " + job);
|
|
|
|
Con</tspan><tspan x="24" dy="22">nection a = DriverManager.getConnection(url, "probe", "probe");
|
|
Connection b = DriverManager.getConnection(url, "pro</tspan><tspan x="24" dy="22">be", "probe");
|
|
System.out.println("두 연결의 autoCommit : A=" + a.getAutoCommit() + " B=" + b.getAutoCommit());
|
|
|
|
</tspan><tspan x="24" dy="22"> String gotA = claim(a);
|
|
System.out.println("작업자 A 가 받은 작업 : " + gotA);
|
|
String gotB = claim(b);
|
|
Syst</tspan><tspan x="24" dy="22">em.out.println("작업자 B 가 받은 작업 : " + gotB);
|
|
System.out.println("같은 작업인가 : " + gotA.equals(gotB)</tspan><tspan x="24" dy="22">);
|
|
|
|
try (Statement s = a.createStatement();
|
|
ResultSet r = s.executeQuery(
|
|
"select attempts, last</tspan><tspan x="24" dy="22">_result, next_check_at from notification_reconciliation_job")) {
|
|
r.next();
|
|
System.out.println("행 상태 </tspan><tspan x="24" dy="22"> : attempts=" + r.getInt(1)
|
|
+ " last_result=" + r.getString(2) + " next_check_at 그대로");
|
|
}
|
|
a</tspan><tspan x="24" dy="22">.close();
|
|
b.close();
|
|
}
|
|
|
|
private static String claim(Connection db) throws Exception {
|
|
try (var claim = db.pr</tspan><tspan x="24" dy="22">epareStatement(CLAIM_DUE)) {
|
|
claim.setTimestamp(1, Timestamp.from(Instant.now()));
|
|
claim.setInt(2, 10);
|
|
</tspan><tspan x="24" dy="22"> try (ResultSet r = claim.executeQuery()) {
|
|
return r.next() ? r.getString("id") : "없음";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
J</tspan><tspan x="24" dy="22">AVA
|
|
CP="$(bash /tmp/probe/cp.sh adapter/outbound/persistence-jpa/gradle.lockfile postgresqlIntegrationTestRuntimeClasspa</tspan><tspan x="24" dy="22">th)"
|
|
javac -encoding UTF-8 -nowarn -cp "$CP" -d /tmp/probe /tmp/probe/SkipLockedProbe.java
|
|
java -Dstdout.encoding=UTF-8 </tspan><tspan x="24" dy="22">-cp "$CP:/tmp/probe" SkipLockedProbe
|
|
|
|
docker rm -f "$CT" >/dev/null 2>&1 || true</tspan></text>
|
|
<text x="24" y="1036" 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/src</tspan></text>
|
|
<text x="24" y="1058" 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:03:12+00:00 · exit 0</tspan></text>
|
|
<line x1="24" y1="1072" x2="1176" y2="1072" stroke="#30363d"/>
|
|
<text x="24" y="1112" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">PostgreSQL 16.15 on x86_</tspan></text>
|
|
<text x="24" y="1134" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"># 상위 표는 마이그레이션이 세운다: notification_recipient_delivery notification_reconciliation_job notification_request</tspan></text>
|
|
<text x="24" y="1156" 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="1178" 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="1200" 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="1222" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">만기 작업 하나를 넣는다: 441a6b30-6574-4af2-a637-1caeef230678</tspan></text>
|
|
<text x="24" y="1244" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">두 연결의 autoCommit : A=true B=true</tspan></text>
|
|
<text x="24" y="1266" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">작업자 A 가 받은 작업 : 441a6b30-6574-4af2-a637-1caeef230678</tspan></text>
|
|
<text x="24" y="1288" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">작업자 B 가 받은 작업 : 441a6b30-6574-4af2-a637-1caeef230678</tspan></text>
|
|
<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">같은 작업인가 : 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">행 상태 : attempts=0 last_result=null next_check_at 그대로</tspan></text>
|
|
</svg>
|