Files
document-haness/docs/clean-architecture-backend-template/final/evidence/meta/a05-f021-complete-replayttl-postgres.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.3 KiB
JSON

{
"assetKey": "a05-f021-complete-replayttl-postgres",
"kind": "terminal",
"command": "set -e\nmkdir -p /tmp/probe\nCT=complete-ttl-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 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 < \"$f\" >/dev/null 2>&1 || true\n done\ndone\n\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/CompleteTtlProbe.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.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 CompleteTtlProbe {\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(\"b\".repeat(64), 1, \"PROBE\");\n var fingerprint = RequestFingerprint.ofSha256(\"probe-request\".getBytes());\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.ofHours(1), \"json.v1\", 2);\n var response = new StoredResponse(\"{\\\"v\\\":\\\"SAME\\\"}\");\n\n transactions.executeWithoutResult(status -> {\n var owner = ((IdempotencyClaimOutcome.Acquired) store.claim(request)).owner();\n var started = store.markExecutionStarted(owner, new OperationId(\"op-1\")).owner().orElseThrow();\n System.out.println(\"첫 완료 (재생 창 1시간) : \"\n + store.complete(started, response, Duration.ofHours(1), new OperationId(\"op-1\")));\n });\n String first = jdbc.queryForObject(\n \"select extract(epoch from (replay_until - updated_at))::int::text from idempotency_record\",\n String.class);\n String digestFirst = jdbc.queryForObject(\n \"select last_transition_result_digest from idempotency_record\", String.class);\n System.out.println(\"저장된 재생 창(초) : \" + first);\n\n var owner2 = new IdempotencyOwner(scope, \"1\".repeat(64),\n jdbc.queryForObject(\"select attempt from idempotency_record\", Long.class),\n jdbc.queryForObject(\"select state_revision from idempotency_record\", Long.class),\n new OperationId(\"op-1\"));\n transactions.executeWithoutResult(status ->\n System.out.println(\"둘째 완료 (재생 창 9시간) : \"\n + store.complete(owner2, response, Duration.ofHours(9), new OperationId(\"op-1\"))));\n String second = jdbc.queryForObject(\n \"select extract(epoch from (replay_until - updated_at))::int::text from idempotency_record\",\n String.class);\n String digestSecond = jdbc.queryForObject(\n \"select last_transition_result_digest from idempotency_record\", String.class);\n System.out.println(\"저장된 재생 창(초) : \" + second);\n System.out.println(\"전이 다이제스트 그대로인가 : \" + digestFirst.equals(digestSecond));\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/CompleteTtlProbe.java\njava -Dstdout.encoding=UTF-8 -cp \"$CP:/tmp/probe\" CompleteTtlProbe 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:43:45+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f021-complete-replayttl-postgres.txt",
"svg": "evidence/rendered/a05-f021-complete-replayttl-postgres.svg",
"rawSha256": "996f5ee6c79f20d571d5ebb41f830425b68afdff38d88b2d7bb2fb60e7a0c96b",
"lines": 8,
"redaction": "none — 탐침 데이터만, 자격증명 없음"
}