Files
document-haness/docs/clean-architecture-backend-template/final/evidence/rendered/a05-f029-for-update-skip-locked-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

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 &quot;$CT&quot; &gt;/dev/null 2&gt;&amp;1 || true
docker run -d --name &quot;$CT&quot; -</tspan><tspan x="24" dy="22">-network &quot;container:workmachine&quot; \
-e POSTGRES_PASSWORD=probe -e POSTGRES_USER=probe -e POSTGRES_DB=probe \
postgres</tspan><tspan x="24" dy="22">:16-alpine -p 55432 &gt;/dev/null
for i in $(seq 1 40); do
docker inspect -f &#x27;{{.State.Running}}&#x27; &quot;$CT&quot; | grep -q true ||</tspan><tspan x="24" dy="22"> { echo &#x27;컨테이너가 뜨지 않았다&#x27;; exit 1; }
docker exec &quot;$CT&quot; pg_isready -U probe -p 55432 &gt;/dev/null 2&gt;&amp;1 &amp;&amp; break
</tspan><tspan x="24" dy="22">sleep 1
done
docker exec &quot;$CT&quot; psql -U probe -p 55432 -d probe -tAc &#x27;select version()&#x27; | 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 &quot;$M/$d&quot;/V*.sql | sort -V); do
docker exec -i &quot;$CT&quot; psql -U probe -p 55432 -d probe -q -v ON_ERROR_STOP=1 &lt; &quot;$f&quot; &gt;/</tspan><tspan x="24" dy="22">dev/null 2&gt;&amp;1 || true
done
done
echo -n &#x27;# 상위 표는 마이그레이션이 세운다: &#x27;
docker exec &quot;$CT&quot; psql -U probe -p 554</tspan><tspan x="24" dy="22">32 -d probe -tAc \
&quot;select string_agg(table_name, &#x27; &#x27; order by table_name) from information_schema.tables
where t</tspan><tspan x="24" dy="22">able_schema=&#x27;public&#x27; and table_name like &#x27;notification_re%&#x27;&quot;
echo
echo &#x27;# 저장소의 청구 문장을 두 연결이 차례로 실행한</tspan><tspan x="24" dy="22">다. 사이에 정산은 없다.&#x27;
echo &#x27;# 상위 행 사슬을 만들지 않으려고 외래 키 하나만 뗐다. 청구 문장과 색인은 그대로다.&#x27;
cat</tspan><tspan x="24" dy="22"> &gt; /tmp/probe/cp.sh &lt;&lt;&#x27;CPSH&#x27;
# 락파일의 한 구성에 실린 좌표 전부를 캐시 경로로 바꾼다. 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=&quot;&quot;
while IFS= read -r line; do
coord=${line%%=*}; confs=${line#*=}
case &quot;,$conf</tspan><tspan x="24" dy="22">s,&quot; in *&quot;,$CONF,&quot;*) ;; *) continue ;; esac
group=${coord%%:*}; rest=${coord#*:}; name=${rest%%:*}; ver=${rest##*:}
j</tspan><tspan x="24" dy="22">ar=$(find &quot;$CACHE/$group/$name/$ver&quot; -name &#x27;*.jar&#x27; ! -name &#x27;*sources*&#x27; ! -name &#x27;*javadoc*&#x27; 2&gt;/dev/null | head -1)
[ -n</tspan><tspan x="24" dy="22"> &quot;$jar&quot; ] &amp;&amp; cp=&quot;$cp:$jar&quot;
done &lt; &lt;(grep -E &#x27;^[a-zA-Z0-9._-]+:[^=]+=&#x27; &quot;$LOCK&quot;)
echo &quot;${cp#:}&quot;
CPSH
cat &gt; /tmp/probe/Skip</tspan><tspan x="24" dy="22">LockedProbe.java &lt;&lt;&#x27;JAVA&#x27;
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 =
&quot;&quot;&quot;
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 &lt;= ?
ORDER BY next_check</tspan><tspan x="24" dy="22">_at, id
LIMIT ?
FOR UPDATE SKIP LOCKED
&quot;&quot;&quot;;
public static void main(String[] args) throws Exception</tspan><tspan x="24" dy="22"> {
String url = &quot;jdbc:postgresql://127.0.0.1:55432/probe&quot;;
UUID job = UUID.randomUUID();
try (Connection set</tspan><tspan x="24" dy="22">up = DriverManager.getConnection(url, &quot;probe&quot;, &quot;probe&quot;);
Statement s = setup.createStatement()) {
// 탐침 </tspan><tspan x="24" dy="22">설정: 상위 행 사슬을 만들지 않으려고 외래 키 하나만 뗀다.
// 표는 마이그레이션이 모두 세운다. 청구 문장과 색인은 </tspan><tspan x="24" dy="22">그대로다.
s.execute(&quot;alter table notification_reconciliation_job &quot;
+ &quot;drop constraint if exists notifica</tspan><tspan x="24" dy="22">tion_reconciliation_job_attempt_id_fkey&quot;);
s.execute(&quot;delete from notification_reconciliation_job&quot;);
s.execu</tspan><tspan x="24" dy="22">te(&quot;insert into notification_reconciliation_job &quot;
+ &quot;(id, attempt_id, provider_profile_id, next_check_at, atte</tspan><tspan x="24" dy="22">mpts, created_at, updated_at)&quot;
+ &quot; values (&#x27;&quot; + job + &quot;&#x27;, &#x27;&quot; + UUID.randomUUID() + &quot;&#x27;, &#x27;p1&#x27;,&quot;
+ &quot; no</tspan><tspan x="24" dy="22">w() - interval &#x27;1 minute&#x27;, 0, now(), now())&quot;);
}
System.out.println(&quot;만기 작업 하나를 넣는다: &quot; + job);
Con</tspan><tspan x="24" dy="22">nection a = DriverManager.getConnection(url, &quot;probe&quot;, &quot;probe&quot;);
Connection b = DriverManager.getConnection(url, &quot;pro</tspan><tspan x="24" dy="22">be&quot;, &quot;probe&quot;);
System.out.println(&quot;두 연결의 autoCommit : A=&quot; + a.getAutoCommit() + &quot; B=&quot; + b.getAutoCommit());
</tspan><tspan x="24" dy="22"> String gotA = claim(a);
System.out.println(&quot;작업자 A 가 받은 작업 : &quot; + gotA);
String gotB = claim(b);
Syst</tspan><tspan x="24" dy="22">em.out.println(&quot;작업자 B 가 받은 작업 : &quot; + gotB);
System.out.println(&quot;같은 작업인가 : &quot; + gotA.equals(gotB)</tspan><tspan x="24" dy="22">);
try (Statement s = a.createStatement();
ResultSet r = s.executeQuery(
&quot;select attempts, last</tspan><tspan x="24" dy="22">_result, next_check_at from notification_reconciliation_job&quot;)) {
r.next();
System.out.println(&quot;행 상태 </tspan><tspan x="24" dy="22"> : attempts=&quot; + r.getInt(1)
+ &quot; last_result=&quot; + r.getString(2) + &quot; next_check_at 그대로&quot;);
}
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(&quot;id&quot;) : &quot;없음&quot;;
}
}
}
}
J</tspan><tspan x="24" dy="22">AVA
CP=&quot;$(bash /tmp/probe/cp.sh adapter/outbound/persistence-jpa/gradle.lockfile postgresqlIntegrationTestRuntimeClasspa</tspan><tspan x="24" dy="22">th)&quot;
javac -encoding UTF-8 -nowarn -cp &quot;$CP&quot; -d /tmp/probe /tmp/probe/SkipLockedProbe.java
java -Dstdout.encoding=UTF-8 </tspan><tspan x="24" dy="22">-cp &quot;$CP:/tmp/probe&quot; SkipLockedProbe
docker rm -f &quot;$CT&quot; &gt;/dev/null 2&gt;&amp;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>