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>
15 lines
6.3 KiB
JSON
15 lines
6.3 KiB
JSON
{
|
|
"assetKey": "a05-f026-enqueue-race",
|
|
"kind": "terminal",
|
|
"command": "set -e\nmkdir -p /tmp/probe\nCT=recovery-race-probe\ndocker rm -f \"$CT\" >/dev/null 2>&1 || true\ndocker run -d --name \"$CT\" --network \"container:workmachine\" \\\n -e POSTGRES_PASSWORD=probe -e POSTGRES_USER=probe -e POSTGRES_DB=probe \\\n postgres:16-alpine -p 55432 >/dev/null\nfor i in $(seq 1 40); do\n docker inspect -f '{{.State.Running}}' \"$CT\" | grep -q true || { echo '컨테이너가 뜨지 않았다'; exit 1; }\n docker exec \"$CT\" pg_isready -U probe -p 55432 >/dev/null 2>&1 && break\n sleep 1\ndone\ndocker exec \"$CT\" psql -U probe -p 55432 -d probe -tAc 'select version()' | cut -c1-24\nM=adapter/outbound/persistence-jpa/src/main/resources/db/migration\nfor d in postgresql jpa/core jpa/fileserver; do\n for f in $(ls \"$M/$d\"/V*.sql | sort -V); do\n docker exec -i \"$CT\" psql -U probe -p 55432 -d probe -q -v ON_ERROR_STOP=1 < \"$f\" >/dev/null 2>&1 || true\n done\ndone\n\necho\necho '# 열린 항목이 없는 상태에서 두 연결을 장벽으로 맞춰 동시에 푼다. 여섯 번 반복.'\necho '# 시계 출처만 DB 로 바꿨다. 조건과 열은 저장소 질의 그대로다.'\ncat > /tmp/probe/cp.sh <<'CPSH'\n# 락파일의 한 구성에 실린 좌표 전부를 캐시 경로로 바꾼다. Gradle 실행 없이 재현된다.\nLOCK=${1:-adapter/outbound/persistence-jpa/gradle.lockfile}\nCONF=${2:-postgresqlIntegrationTestRuntimeClasspath}\nCACHE=/root/.gradle/caches/modules-2/files-2.1\ncp=\"\"\nwhile IFS= read -r line; do\n coord=${line%%=*}; confs=${line#*=}\n case \",$confs,\" in *\",$CONF,\"*) ;; *) continue ;; esac\n group=${coord%%:*}; rest=${coord#*:}; name=${rest%%:*}; ver=${rest##*:}\n jar=$(find \"$CACHE/$group/$name/$ver\" -name '*.jar' ! -name '*sources*' ! -name '*javadoc*' 2>/dev/null | head -1)\n [ -n \"$jar\" ] && cp=\"$cp:$jar\"\ndone < <(grep -E '^[a-zA-Z0-9._-]+:[^=]+=' \"$LOCK\")\necho \"${cp#:}\"\nCPSH\ncat > /tmp/probe/RecoveryRaceProbe.java <<'JAVA'\nimport java.sql.Connection;\nimport java.sql.DriverManager;\nimport java.sql.ResultSet;\nimport java.sql.Statement;\nimport java.util.UUID;\nimport java.util.concurrent.CountDownLatch;\n\n/** 두 호출자가 동시에 enqueue 를 부르는 상황을 그 구조 그대로 돌린다. */\npublic class RecoveryRaceProbe {\n\n private static final String REFRESH =\n \"update fs_recovery_item set reason_code = ?, attempt = attempt + 1, updated_at = now()\"\n + \" where file_id = ? and status = 'PENDING'\";\n\n private static final String INSERT =\n \"insert into fs_recovery_item (recovery_id, file_id, reason_code, status, attempt,\"\n + \" created_at, updated_at) values (?, ?, ?, 'PENDING', 0, now(), now())\";\n\n public static void main(String[] args) throws Exception {\n int wins = 0;\n int violations = 0;\n int extraRows = 0;\n for (int round = 0; round < 6; round++) {\n runOnce(round);\n }\n }\n\n private static void runOnce(int round) throws Exception {\n String url = \"jdbc:postgresql://127.0.0.1:55432/probe\";\n UUID file = UUID.randomUUID();\n try (Connection setup = DriverManager.getConnection(url, \"probe\", \"probe\");\n Statement s = setup.createStatement()) {\n s.execute(\"delete from fs_recovery_item\");\n s.execute(\"delete from fs_file\");\n s.execute(\"insert into fs_file (file_id, namespace, state, original_name, created_at,\"\n + \" updated_at) values ('\" + file + \"', 'probe', 'CREATED', 'probe.txt', now(), now())\");\n }\n\n var started = new CountDownLatch(2);\n var go = new CountDownLatch(1);\n String[] result = new String[2];\n\n Runnable enqueue = () -> {\n int slot = Integer.parseInt(Thread.currentThread().getName());\n try (Connection db = DriverManager.getConnection(url, \"probe\", \"probe\")) {\n db.setAutoCommit(false);\n started.countDown();\n go.await();\n int updated;\n try (var refresh = db.prepareStatement(REFRESH)) {\n refresh.setString(1, \"reason-\" + slot);\n refresh.setObject(2, file);\n updated = refresh.executeUpdate();\n }\n if (updated == 0) {\n try (var insert = db.prepareStatement(INSERT)) {\n insert.setObject(1, UUID.randomUUID());\n insert.setObject(2, file);\n insert.setString(3, \"reason-\" + slot);\n insert.executeUpdate();\n }\n }\n db.commit();\n result[slot] = updated > 0 ? \"갱신으로 흡수\" : \"삽입 성공\";\n } catch (Exception failure) {\n result[slot] = failure.getClass().getSimpleName() + \": \"\n + String.valueOf(failure.getMessage()).split(\"\\n\")[0];\n }\n };\n\n Thread a = new Thread(enqueue, \"0\");\n Thread b = new Thread(enqueue, \"1\");\n a.start();\n b.start();\n started.await();\n go.countDown();\n a.join();\n b.join();\n\n int rows;\n try (Connection db = DriverManager.getConnection(url, \"probe\", \"probe\");\n Statement s = db.createStatement();\n ResultSet r = s.executeQuery(\"select count(*) from fs_recovery_item\")) {\n r.next();\n rows = r.getInt(1);\n }\n String winner = result[0].startsWith(\"삽입\") ? \"A\" : \"B\";\n String loser = winner.equals(\"A\") ? result[1] : result[0];\n System.out.printf(\"%d 회차: 이긴 쪽 %s · 진 쪽 %s · 남은 행 %d%n\",\n round + 1, winner, loser.replaceFirst(\"^PSQLException: ERROR: \", \"\"), rows);\n }\n}\nJAVA\nCP=\"$(bash /tmp/probe/cp.sh adapter/outbound/persistence-jpa/gradle.lockfile postgresqlIntegrationTestRuntimeClasspath)\"\njavac -encoding UTF-8 -nowarn -cp \"$CP\" -d /tmp/probe /tmp/probe/RecoveryRaceProbe.java\njava -Dstdout.encoding=UTF-8 -cp \"$CP:/tmp/probe\" RecoveryRaceProbe\n\ndocker rm -f \"$CT\" >/dev/null 2>&1 || true",
|
|
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
|
|
"exitCode": 0,
|
|
"executedAt": "2026-09-02T05:03:15+00:00",
|
|
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
|
|
"raw": "evidence/raw/a05-f026-enqueue-race.txt",
|
|
"svg": "evidence/rendered/a05-f026-enqueue-race.svg",
|
|
"rawSha256": "34e5cc0ec2bac51e03ab43d12e09fb18cda5ccfc3b9bf004adbe6930421524bb",
|
|
"lines": 10,
|
|
"redaction": "none — 탐침 데이터만, 자격증명 없음"
|
|
}
|