merge: integrate JPA production capability

# Conflicts:
#	.github/ci-gate-matrix.yml
#	.github/scripts/verify-gate-matrix.sh
#	.github/workflows/ci-quality-gates.yml
#	src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
This commit is contained in:
donghyeon-ka
2026-07-31 23:57:29 +09:00
142 changed files with 13206 additions and 325 deletions
@@ -98,6 +98,8 @@ failure translation contract 다. 여기서는 SPI 구조와 fallback 근거만
기여한다. vendor 별 row(`40P01`, `25P03`, `57014` 등 PostgreSQL)는 `adapter-persistence-postgresql`
가 추가 `SqlStateErrorMapping` 빈으로 기여한다.
- 서로 다른 contributor가 같은 exact SQLState를 등록하면 code가 같더라도 startup construction을
실패시킨다. last-writer-wins merge는 mapping ownership drift를 숨기므로 허용하지 않는다.
- `08*` connection-class prefix → `DB_UNAVAILABLE` 규칙은 맵 엔트리가 아니라 translator 가 직접
처리한다. 따라서 core 매핑 맵에는 `08*` 가 없다.
- **Fallback:** 기여된 어떤 row 에도 없는 SQLState — 또는 cause chain 에
@@ -205,6 +207,120 @@ retention 보다 오래된 PUBLISHED row 를 주기적으로 비워 테이블
cadence 측정 필요). retention 한 값은 reaper-local 이라 `@Value` 로 받지만, canonical 6-property
문서는 app-bootstrap `OutboxSettings` / `application.yml` 에 있다.
## JPA production capability candidate
`src/config/jpa/readiness-cards.yaml`이 15개 capability와 7개 독립 schema stream의
machine-readable SSOT다. `selected` base card와 `implemented-candidate` reliability card를
구분하며, 실제 PostgreSQL 테스트 통과만으로 immutable 운영 evidence가 필요한 R2를 주장하지
않는다.
독립 Flyway stream은 broad `classpath:db/migration`으로 함께 실행하지 않는다. 각 stream은
자기 location/history table을 사용하고 non-empty schema adoption 때 version 0 baseline을 명시한
뒤 V1부터 실행한다.
| Capability | Location | History table | 상태 |
|---|---|---|---|
| core/adoption | `db/migration/jpa/core` | `flyway_jpa_core_history` | selected candidate |
| idempotency V2 | `db/migration/jpa/idempotency` | `flyway_jpa_idempotency_history` | implemented-candidate |
| outbox storage V2 | `db/migration/jpa/outbox-storage` | `flyway_jpa_outbox_storage_history` | implemented-candidate |
| polling delivery V2 | `db/migration/jpa/outbox-polling` | `flyway_jpa_outbox_polling_history` | implemented-candidate |
| inbox V1 | `db/migration/jpa/inbox` | `flyway_jpa_inbox_history` | implemented-candidate |
### owner-safe idempotency V2
`PostgreSqlOwnerSafeIdempotencyStore`는 row lock을 얻은 뒤 `clock_timestamp()`를 평가한다.
claim takeover와 start/renew/complete/fail/release는 scope/state/owner/attempt/claim operation/
state revision을 SQL predicate로 다시 검증한다. expired `CLAIMED`만 takeover하며 expired
`EXECUTING``ABANDONED`로 닫고 reconciliation을 요구한다. raw client key는 저장하지 않고
versioned HMAC scope digest만 쓴다.
### immutable outbox storage와 polling delivery V2
`PostgreSqlImmutableOutboxAppendAdapter`는 publication control을 `FOR SHARE`로 잠근 상태에서
compact global identity guard와 partitioned immutable envelope를 같은 business transaction에
기록한다. cutover는 control `FOR UPDATE`와 충돌하므로 시작된 append를 추월하지 못하며, target
authority 활성화 뒤 legacy V1 writer trigger가 실패한다.
polling mode일 때 database trigger가 initial `outbox_delivery_v2` row를 같은 transaction에
생성한다. `PostgreSqlPollingDeliveryAdapter`는 bounded `FOR UPDATE SKIP LOCKED` claim,
aggregate version/ordinal strict-order head gate, owner/token/attempt/version/epoch completion
CAS를 사용한다. broker 호출은 transaction 밖이고 duplicate publish 가능성은 stable event ID로
consumer inbox에서 처리한다.
### same-store inbox
`PostgreSqlSameStoreInboxAdapter`의 transactional claim은 business mutation/outgoing outbox/
completion과 caller의 한 primary write transaction에 참여한다. received lease expiry는 takeover할
수 있지만 processing lease expiry는 blind retry하지 않고 recovery-required terminal state로
보낸다. broker ACK는 commit 이후에만 실행한다.
### 실제 PostgreSQL task
base 6개 task 외에 다음 candidate task가 Docker 부재 시 skip이 아니라 실패하도록 등록돼 있다.
```text
postgresqlIdempotencyIntegrationTest
postgresqlOutboxStorageIntegrationTest
postgresqlOutboxPollingIntegrationTest
postgresqlInboxIntegrationTest
```
### evidence manifest와 R2 gate
`readiness-cards.yaml``evidence.scenarios``evidence.task-claims`가 required evidence를 실제
JUnit selector/Gradle task에 연결한다. `verifyJpaReadinessRegistryContract`는 unknown claim,
duplicate selector와 다른 card task 차용을 mutation test로 거절한다.
```bash
./gradlew :adapter:outbound:persistence-jpa:verifyJpaCandidateEvidence --console=plain
```
위 task는 active card 11개의 producer를 실행하고 JUnit XML에서 exact selector와
executed/skipped/failure/error 수를 읽는다. 각 manifest는 source revision/dirty digest,
prerequisite manifest ID, PostgreSQL image digest, pgjdbc/Hibernate/Flyway version, topology와
migration/dispatch metadata를 담고 다음 위치에 canonical JSON SHA-256 이름으로 생성된다.
```text
build/jpa-evidence/manifests/<card-id>/<sha256>.json
```
후보 검증은 zero-skip, schema, content hash와 prerequisite link가 맞으면 성공하지만
`attainedReadiness=R1`을 유지한다. 로컬 후보 lane은 다음 E2/E3 동작을 실제 PostgreSQL에서
검증한다.
- bounded pool saturation과 shutdown 뒤 connection 거부
- runtime/migration role 분리, trusted namespace, TLS `verify-full`의 정상·hostname mismatch·
untrusted CA·expired certificate 경로
- persistence failure의 HTTP/log/span redaction
- fresh/legacy adoption, interrupted migration forward recovery, N/N-1 additive rolling shape
- serialization/deadlock/lock/statement timeout, pool exhaustion, commit transport 단절
- idempotency/outbox/inbox 독립 stream의 disabled/first-enable/disable/re-enable/interrupted
lifecycle
각 manifest는 그래도 candidate profile, dirty source와 아직 R2가 아닌 prerequisite를
`readinessBlockers`에 보존하므로 후보 통과를 R2로 오인할 수 없다.
실제 aggregation gate는 별도 명령이다.
```bash
./gradlew \
:adapter:outbound:persistence-jpa:verifyJpaPrimaryFoundationEvidence \
-PjpaEvidenceProfile=r2 \
--console=plain
```
이 task는 clean revision, `JPA_EVIDENCE_CI_JOB`,
`JPA_EVIDENCE_ARTIFACT_LOCATION`, immutable PostgreSQL image digest, 모든 required evidence와
R2 prerequisite DAG가 있어야만 성공한다. `.github/workflows/ci-quality-gates.yml`의 candidate
job은 PR에서 zero-skip manifest를 보존하고, `.github/workflows/jpa-r2-evidence.yml`은 명시적으로
실행하는 production-profile lane이다. 로컬 dirty worktree 또는 unpublished 실행은
`worktree-is-dirty`/CI provenance blocker를 보고 실패하는 것이 정식 동작이다. R2 승격은 clean
revision에서 workflow를 실행하고 보존된 manifest artifact를 검토한 뒤에만 가능하다.
stream migration이 중단되면 history/registry/object 상태를 먼저 확인하고 기존 migration을
임의 수정하거나 history를 바로 `repair`하지 않는다. 장애를 수정한 forward migration으로
복구하는 운영 절차는 `docs/runbooks/migration-failed.md`를 따른다.
## lock — 분산 락
provider 선택표(flag → bean → registry)의 SSOT 는 CLAUDE.md(또는 app-bootstrap 와이어링)다.