Files
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.7 KiB
JSON

{
"assetKey": "a05-f020-inspect-claim-postgres",
"kind": "terminal",
"command": "set -e\nmkdir -p /tmp/probe\nCT=idem-replay-probe\n\necho '# 실제 PostgreSQL 을 세우고 이 리프의 마이그레이션을 적용한다'\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 docker exec \"$CT\" pg_isready -U probe -p 55432 >/dev/null 2>&1 && break; sleep 1; done\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/idempotency; do\n for f in \"$M/$d\"/V*.sql; 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 where table_schema='public'\"\n\necho\necho '# 재생 유효 기간 2초로 완료하고, 만료 전후로 조회와 청구를 부른다'\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/ReplayExpiryProbe.java <<'JAVA'\nimport dev.caskeleton.application.idempotency.RequestFingerprint;\nimport dev.caskeleton.application.idempotency.StoredResponse;\nimport dev.caskeleton.application.idempotency.v2.*;\nimport dev.caskeleton.application.transaction.OperationId;\nimport dev.caskeleton.adapter.outbound.persistence.postgresql.idempotency.PostgreSqlOwnerSafeIdempotencyStore;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport org.springframework.jdbc.core.JdbcTemplate;\nimport org.springframework.jdbc.datasource.DataSourceTransactionManager;\nimport org.springframework.jdbc.datasource.SimpleDriverDataSource;\nimport org.springframework.transaction.support.TransactionTemplate;\n\n/** 재생 유효 기간이 지난 완료 행을 조회와 청구에 각각 물어본다. */\npublic class ReplayExpiryProbe {\n\n public static void main(String[] args) throws Exception {\n var dataSource = new SimpleDriverDataSource(\n (java.sql.Driver) Class.forName(\"org.postgresql.Driver\").getDeclaredConstructor().newInstance(),\n \"jdbc:postgresql://127.0.0.1:55432/probe\", \"probe\", \"probe\");\n var jdbc = new JdbcTemplate(dataSource);\n var transactions = new TransactionTemplate(new DataSourceTransactionManager(dataSource));\n var store = new PostgreSqlOwnerSafeIdempotencyStore(jdbc);\n\n jdbc.update(\"update capability_schema_registry set lifecycle_state = 'ACTIVE' \"\n + \"where capability_id = 'jpa-idempotency-owner-safe-v2'\");\n jdbc.update(\"delete from idempotency_record\");\n\n var scope = new IdempotencyScopeDigest(\"a\".repeat(64), 1, \"PROBE\");\n var fingerprint = RequestFingerprint.ofSha256(\"probe-request\".getBytes(StandardCharsets.UTF_8));\n var attempt = new IdempotencyClaimAttempt(\"1\".repeat(64), new OperationId(\"op-1\"));\n var request = new IdempotencyClaimRequest(\n scope, fingerprint, attempt, Duration.ofMinutes(5), Duration.ofSeconds(2), \"json.v1\", 2);\n\n transactions.executeWithoutResult(status -> {\n IdempotencyClaimOutcome claimed = store.claim(request);\n IdempotencyOwner owner = ((IdempotencyClaimOutcome.Acquired) claimed).owner();\n IdempotencyMutationResult<IdempotencyStartOutcome> start =\n store.markExecutionStarted(owner, new OperationId(\"op-1\"));\n IdempotencyOwner started = start.owner().orElseThrow();\n var completed = store.complete(started, new StoredResponse(\"{\\\"v\\\":\\\"OLD-RESPONSE\\\"}\"),\n Duration.ofSeconds(2), new OperationId(\"op-1\"));\n System.out.println(\"완료 : \" + completed);\n });\n\n System.out.println(\"replay_until : \" + jdbc.queryForObject(\n \"select replay_until from idempotency_record\", String.class));\n System.out.println(\"지금 : \" + jdbc.queryForObject(\n \"select clock_timestamp()::text\", String.class));\n\n var inspection = new IdempotencyInspectionRequest(scope, fingerprint, attempt);\n System.out.println(\"만료 전 조회 : \" + store.inspect(inspection).outcome());\n\n Thread.sleep(3500);\n\n System.out.println(\"지금 : \" + jdbc.queryForObject(\n \"select clock_timestamp()::text\", String.class));\n IdempotencyInspection after = store.inspect(inspection);\n System.out.println(\"만료 후 조회 : \" + after.outcome()\n + \" 응답=\" + after.response().map(StoredResponse::payload).orElse(\"없음\"));\n\n transactions.executeWithoutResult(status -> {\n var again = new IdempotencyClaimRequest(scope, fingerprint,\n new IdempotencyClaimAttempt(\"2\".repeat(64), new OperationId(\"op-2\")),\n Duration.ofMinutes(5), Duration.ofSeconds(2), \"json.v1\", 2);\n System.out.println(\"만료 후 청구 : \" + store.claim(again).getClass().getSimpleName());\n });\n }\n}\nJAVA\nCP=\"adapter/outbound/persistence-jpa/build/classes/java/main:application-core/build/classes/java/main:$(bash /tmp/probe/cp.sh adapter/outbound/persistence-jpa/gradle.lockfile postgresqlIntegrationTestRuntimeClasspath)\"\njavac -encoding UTF-8 -nowarn -cp \"$CP\" -d /tmp/probe /tmp/probe/ReplayExpiryProbe.java\njava -Dstdout.encoding=UTF-8 -cp \"$CP:/tmp/probe\" ReplayExpiryProbe 2>&1 | grep -v SLF4J\n\ndocker rm -f \"$CT\" >/dev/null 2>&1 || true",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T04:35:46+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f020-inspect-claim-postgres.txt",
"svg": "evidence/rendered/a05-f020-inspect-claim-postgres.svg",
"rawSha256": "16dc0e147f4f9f7c5e59ffc62953c079c7508fe5b1dd506a04fa36381373aa89",
"lines": 12,
"redaction": "none — 탐침 데이터만, 자격증명 없음"
}