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