{ "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 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 — 탐침 데이터만, 자격증명 없음" }