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

15 lines
6.2 KiB
JSON

{
"assetKey": "a05-f029-for-update-skip-locked-probe",
"kind": "terminal",
"command": "set -e\nmkdir -p /tmp/probe\nCT=skiplocked-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/notification-platform; 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\necho -n '# 상위 표는 마이그레이션이 세운다: '\ndocker exec \"$CT\" psql -U probe -p 55432 -d probe -tAc \\\n \"select string_agg(table_name, ' ' order by table_name) from information_schema.tables\n where table_schema='public' and table_name like 'notification_re%'\"\n\necho\necho '# 저장소의 청구 문장을 두 연결이 차례로 실행한다. 사이에 정산은 없다.'\necho '# 상위 행 사슬을 만들지 않으려고 외래 키 하나만 뗐다. 청구 문장과 색인은 그대로다.'\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/SkipLockedProbe.java <<'JAVA'\nimport java.sql.Connection;\nimport java.sql.DriverManager;\nimport java.sql.ResultSet;\nimport java.sql.Statement;\nimport java.sql.Timestamp;\nimport java.time.Instant;\nimport java.util.UUID;\n\n/** 두 작업자가 저장소의 청구 문장을 차례로 실행한다. 사이에 정산은 없다. */\npublic class SkipLockedProbe {\n\n private static final String CLAIM_DUE =\n \"\"\"\n SELECT id, attempt_id, provider_profile_id, next_check_at, attempts, last_result,\n created_at, updated_at\n FROM notification_reconciliation_job\n WHERE next_check_at <= ?\n ORDER BY next_check_at, id\n LIMIT ?\n FOR UPDATE SKIP LOCKED\n \"\"\";\n\n public static void main(String[] args) throws Exception {\n String url = \"jdbc:postgresql://127.0.0.1:55432/probe\";\n UUID job = UUID.randomUUID();\n try (Connection setup = DriverManager.getConnection(url, \"probe\", \"probe\");\n Statement s = setup.createStatement()) {\n // 탐침 설정: 상위 행 사슬을 만들지 않으려고 외래 키 하나만 뗀다.\n // 표는 마이그레이션이 모두 세운다. 청구 문장과 색인은 그대로다.\n s.execute(\"alter table notification_reconciliation_job \"\n + \"drop constraint if exists notification_reconciliation_job_attempt_id_fkey\");\n s.execute(\"delete from notification_reconciliation_job\");\n s.execute(\"insert into notification_reconciliation_job \"\n + \"(id, attempt_id, provider_profile_id, next_check_at, attempts, created_at, updated_at)\"\n + \" values ('\" + job + \"', '\" + UUID.randomUUID() + \"', 'p1',\"\n + \" now() - interval '1 minute', 0, now(), now())\");\n }\n System.out.println(\"만기 작업 하나를 넣는다: \" + job);\n\n Connection a = DriverManager.getConnection(url, \"probe\", \"probe\");\n Connection b = DriverManager.getConnection(url, \"probe\", \"probe\");\n System.out.println(\"두 연결의 autoCommit : A=\" + a.getAutoCommit() + \" B=\" + b.getAutoCommit());\n\n String gotA = claim(a);\n System.out.println(\"작업자 A 가 받은 작업 : \" + gotA);\n String gotB = claim(b);\n System.out.println(\"작업자 B 가 받은 작업 : \" + gotB);\n System.out.println(\"같은 작업인가 : \" + gotA.equals(gotB));\n\n try (Statement s = a.createStatement();\n ResultSet r = s.executeQuery(\n \"select attempts, last_result, next_check_at from notification_reconciliation_job\")) {\n r.next();\n System.out.println(\"행 상태 : attempts=\" + r.getInt(1)\n + \" last_result=\" + r.getString(2) + \" next_check_at 그대로\");\n }\n a.close();\n b.close();\n }\n\n private static String claim(Connection db) throws Exception {\n try (var claim = db.prepareStatement(CLAIM_DUE)) {\n claim.setTimestamp(1, Timestamp.from(Instant.now()));\n claim.setInt(2, 10);\n try (ResultSet r = claim.executeQuery()) {\n return r.next() ? r.getString(\"id\") : \"없음\";\n }\n }\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/SkipLockedProbe.java\njava -Dstdout.encoding=UTF-8 -cp \"$CP:/tmp/probe\" SkipLockedProbe\n\ndocker rm -f \"$CT\" >/dev/null 2>&1 || true",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T06:03:12+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f029-for-update-skip-locked-probe.txt",
"svg": "evidence/rendered/a05-f029-for-update-skip-locked-probe.svg",
"rawSha256": "472d60d7184d4889c14210cd457bb9955bdaf3c58b7eec223ec557bea6298703",
"lines": 11,
"redaction": "none — 탐침 데이터만, 자격증명 없음"
}